← Files LegalQuants LitigationARCHIVED FILE
skills/wiki/scripts/wiki.py
155 KB · Oct 5, 2026 · 18:33 UTC
#!/usr/bin/env python3
"""wiki — the wiki engine for the /wiki skill (CODEX for Legal).
The wiki is an OKF v0.2 bundle: markdown notes with YAML frontmatter,
readable in any editor. This script is the deterministic half of the
skill: parsing, validation, history, and receipts. Judgment (distilling,
typing, linking) belongs to the model; this script refuses to guess.
Verbs (grouped; every verb prints a one-line receipt):
setup · init · wiki-list · wiki-use configure and find wikis
check · normalize · version validate and canonicalize
land · rename · delete · purge · merge mutate notes (gated)
accept · decline · verify-note markup review
verify · status · rollback · export-map history, receipts, map
gate · denylist-add method-not-matter gate
seed-scan · seed-emit · seed-land seeding pipeline
capture · capture-land legacy staged-import compatibility
maintain · tripwire maintenance and metrics
Run any verb with --help for its flags.
Conventions: stdlib only; UTF-8; findings to stdout, diagnostics to
stderr. Exit codes: 0 ok, 1 findings or failure, 2 usage/unreadable.
Spec references (§) are to the OKF v0.2 specification; the pinned
excerpts ship in references/okf-profile.md.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import platform
import re
import sys
import tempfile
import time
import uuid
from dataclasses import dataclass, field
from datetime import UTC, date, datetime
from pathlib import Path
from urllib.parse import quote, unquote, urlsplit
ENGINE_VERSION = "0.1.0"
SCHEMA_VERSION = 1
OKF_VERSION = "0.2"
NOTE_TYPES = ("Legal Insight", "Checklist", "Trap", "Position")
ORIGINS = ("seeded", "auto-built", "accepted")
STATUSES = ("draft", "stable", "disputed", "outdated", "deprecated")
SKIP_REASONS = (
"routine",
"no-new-rule",
"matter-specific",
"no-boundary-found",
"unreadable",
"over-cap",
)
PURGE_REASONS = (
"matter-data-removal",
"privacy-remediation",
"user-requested-erasure",
"corrupt-content",
)
# Canonical top-level key order: OKF standard keys in spec order, then the
# legal-profile extension keys. Unknown keys keep their source order after
# these (never dropped -- OKF §4.1).
CANONICAL_TOP = (
"type",
"title",
"description",
"tags",
"resource",
"generated",
"verified",
"status",
"stale_after",
"usage_window",
"runtime",
"parameters",
"computation",
"executor",
"attester",
"sources",
"practice_area",
"jurisdiction",
"document_kind",
"trigger",
"origin",
"pending",
)
# Canonical field order inside known record values.
RECORD_ORDER = {
"generated": ("by", "at"),
"verified": ("by", "at"),
"usage_window": ("from", "to"),
"executor": ("resource", "receipt"),
"attester": ("resource",),
"sources": (
"id",
"kind",
"resource",
"title",
"author",
"pinpoint",
"support",
"sha256",
"last_checked",
"usage_count",
"last_modified",
),
}
RESERVED_FILENAMES = ("index.md", "log.md")
SIDECAR_DIRNAME = ".wiki"
SEGMENT_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.\-]*$")
WINDOWS_FORBIDDEN = set(':*?"<>|\\')
KEY_RE = re.compile(r"^([A-Za-z0-9_.\-]+):(.*)$")
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}([T ].*)?$")
NUMERIC_RE = re.compile(r"^-?\d+(\.\d+)?$")
BOOL_NULL_RE = re.compile(r"^(true|false|yes|no|null|~)$", re.IGNORECASE)
DATE_HEADING_RE = re.compile(r"^## (\d{4}-\d{2}-\d{2})\s*$")
H2_RE = re.compile(r"^## ")
ROOTED_LINK_RE = re.compile(r"\]\(\s*/[^)]*\)")
MAX_FRONTMATTER_DEPTH = 40
class FrontmatterError(ValueError):
"""A frontmatter block that cannot be parsed under the pinned grammar."""
# ---------------------------------------------------------------------------
# Frontmatter: parsing
# ---------------------------------------------------------------------------
def split_document(text: str) -> tuple[dict | None, str]:
"""Split a note into (frontmatter mapping | None, body).
A file with no leading fence is all body (upstream behaviour).
An unterminated fence is an error; guessing would hide corruption.
"""
lines = text.replace("\r\n", "\n").split("\n")
if not lines or lines[0].strip() != "---":
return None, text
close = None
for i in range(1, len(lines)):
if lines[i].strip() in ("---", "..."):
close = i
break
if close is None:
raise FrontmatterError("unterminated frontmatter fence")
block = lines[1:close]
body = "\n".join(lines[close + 1 :])
mapping, pos = _parse_mapping(block, 0, _first_indent(block), 0)
rest = _next_content(block, pos)
if rest is not None:
raise FrontmatterError(f"unparseable frontmatter near: {block[rest].strip()!r}")
return mapping, body
def _first_indent(lines: list[str]) -> int:
i = _next_content(lines, 0)
return 0 if i is None else _indent_of(lines[i])
def _indent_of(line: str) -> int:
return len(line) - len(line.lstrip(" "))
def _next_content(lines: list[str], i: int) -> int | None:
while i < len(lines):
s = lines[i].strip()
if s and not s.startswith("#"):
return i
i += 1
return None
def _depth_guard(depth: int) -> None:
if depth > MAX_FRONTMATTER_DEPTH:
raise FrontmatterError(
f"frontmatter nesting exceeds {MAX_FRONTMATTER_DEPTH} levels"
)
def _parse_mapping(
lines: list[str], i: int, indent: int, depth: int
) -> tuple[dict, int]:
_depth_guard(depth)
out: dict = {}
while True:
j = _next_content(lines, i)
if j is None:
return out, len(lines)
line = lines[j]
cur = _indent_of(line)
if cur < indent:
return out, j
if cur > indent:
raise FrontmatterError(f"unexpected indent: {line.strip()!r}")
s = line.strip()
if s == "-" or s.startswith("- "):
raise FrontmatterError(f"list item where a key was expected: {s!r}")
m = KEY_RE.match(s)
if not m:
raise FrontmatterError(f"not a key-value line: {s!r}")
key, rest = m.group(1), m.group(2).strip()
rest = _strip_inline_comment(rest)
if rest:
out[key] = _parse_inline(rest, depth + 1)
i = j + 1
continue
k = _next_content(lines, j + 1)
if k is None or _indent_of(lines[k]) <= indent:
out[key] = ""
i = j + 1
continue
child_indent = _indent_of(lines[k])
child = lines[k].strip()
if child == "-" or child.startswith("- "):
out[key], i = _parse_list(lines, k, child_indent, depth + 1)
else:
out[key], i = _parse_mapping(lines, k, child_indent, depth + 1)
def _parse_list(lines: list[str], i: int, indent: int, depth: int) -> tuple[list, int]:
_depth_guard(depth)
items: list = []
while True:
j = _next_content(lines, i)
if j is None:
return items, len(lines)
line = lines[j]
cur = _indent_of(line)
if cur < indent:
return items, j
s = line.strip()
if cur > indent or not (s == "-" or s.startswith("- ")):
raise FrontmatterError(f"expected a list item: {s!r}")
after = _strip_inline_comment(s[1:].strip())
key_col = cur + 2
if not after:
k = _next_content(lines, j + 1)
if k is None or _indent_of(lines[k]) <= cur:
items.append("")
i = j + 1
continue
ci = _indent_of(lines[k])
child = lines[k].strip()
if child == "-" or child.startswith("- "):
value, i = _parse_list(lines, k, ci, depth + 1)
else:
value, i = _parse_mapping(lines, k, ci, depth + 1)
items.append(value)
continue
m = KEY_RE.match(after)
if m and not after.startswith(("{", "[", '"', "'")):
entry: dict = {}
first_key, first_rest = m.group(1), m.group(2).strip()
entry[first_key] = (
_parse_inline(first_rest, depth + 1) if first_rest else ""
)
more, i = _parse_mapping(lines, j + 1, key_col, depth + 1)
entry.update(more)
items.append(entry)
continue
items.append(_parse_inline(after, depth + 1))
i = j + 1
def _strip_inline_comment(s: str) -> str:
if not s or s.startswith(("{", "[", '"', "'")):
return s
depth = 0
for idx in range(len(s)):
ch = s[idx]
if ch in "{[":
depth += 1
elif ch in "}]":
depth -= 1
elif ch == "#" and depth == 0 and idx > 0 and s[idx - 1] == " ":
return s[:idx].rstrip()
return s
def _parse_inline(s: str, depth: int = 0):
_depth_guard(depth)
s = s.strip()
if s.startswith("{"):
return _parse_flow_mapping(s, depth + 1)
if s.startswith("["):
return _parse_flow_list(s, depth + 1)
return _parse_scalar(s)
def _parse_scalar(s: str) -> str:
if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
return s[1:-1].replace('\\"', '"').replace("\\\\", "\\")
if len(s) >= 2 and s[0] == "'" and s[-1] == "'":
return s[1:-1].replace("''", "'")
return s
def _split_flow(inner: str) -> list[str]:
parts: list[str] = []
depth = 0
quote: str | None = None
cur: list[str] = []
for ch in inner:
if quote:
cur.append(ch)
if ch == quote:
quote = None
continue
if ch in "\"'":
quote = ch
cur.append(ch)
elif ch in "{[":
depth += 1
cur.append(ch)
elif ch in "}]":
depth -= 1
cur.append(ch)
elif ch == "," and depth == 0:
parts.append("".join(cur).strip())
cur = []
else:
cur.append(ch)
tail = "".join(cur).strip()
if tail:
parts.append(tail)
return parts
def _parse_flow_mapping(s: str, depth: int) -> dict:
_depth_guard(depth)
if not s.endswith("}"):
raise FrontmatterError(f"unterminated flow mapping: {s!r}")
out: dict = {}
for part in _split_flow(s[1:-1]):
m = KEY_RE.match(part)
if not m:
raise FrontmatterError(f"bad flow-mapping entry: {part!r}")
out[m.group(1)] = _parse_inline(m.group(2).strip(), depth + 1)
return out
def _parse_flow_list(s: str, depth: int) -> list:
_depth_guard(depth)
if not s.endswith("]"):
raise FrontmatterError(f"unterminated flow list: {s!r}")
return [_parse_inline(p, depth + 1) for p in _split_flow(s[1:-1])]
# ---------------------------------------------------------------------------
# Frontmatter: canonical emission
# ---------------------------------------------------------------------------
def emit_document(fm: dict | None, body: str) -> str:
if fm is None:
return body
out = ["---"]
out.extend(_emit_mapping(_reorder_top(fm), 0))
out.append("---")
text = "\n".join(out)
return text + "\n" + body if body else text + "\n"
def _reorder_top(fm: dict) -> dict:
ordered: dict = {}
for key in CANONICAL_TOP:
if key in fm:
ordered[key] = fm[key]
for key, value in fm.items():
if key not in ordered:
ordered[key] = value
return ordered
def _reorder_record(key: str, record: dict) -> dict:
order = RECORD_ORDER.get(key, ())
ordered: dict = {}
for k in order:
if k in record:
ordered[k] = record[k]
for k, v in record.items():
if k not in ordered:
ordered[k] = v
return ordered
def _emit_mapping(mapping: dict, indent: int) -> list[str]:
pad = " " * indent
lines: list[str] = []
for key, value in mapping.items():
if isinstance(value, dict):
if key in ("generated", "verified", "usage_window"):
lines.append(f"{pad}{key}: {_emit_flow_mapping(key, value)}")
else:
lines.append(f"{pad}{key}:")
lines.extend(_emit_mapping(_reorder_record(key, value), indent + 2))
elif isinstance(value, list):
lines.extend(_emit_list(key, value, indent))
else:
lines.append(f"{pad}{key}: {_emit_scalar(value)}")
return lines
def _emit_list(key: str, items: list, indent: int) -> list[str]:
pad = " " * indent
if not items:
return [f"{pad}{key}: []"]
if all(not isinstance(i, dict | list) for i in items):
inner = ", ".join(_emit_scalar(i) for i in items)
return [f"{pad}{key}: [{inner}]"]
lines = [f"{pad}{key}:"]
for item in items:
if isinstance(item, dict):
if key == "verified":
lines.append(f"{pad} - {_emit_flow_mapping(key, item)}")
continue
entry = _reorder_record(key, item)
first = True
for k, v in entry.items():
lead = f"{pad} - " if first else f"{pad} "
first = False
if isinstance(v, dict | list):
sub = {k: v}
rendered = _emit_mapping(sub, 0)
lines.append(lead + rendered[0])
lines.extend(f"{pad} {extra}" for extra in rendered[1:])
else:
lines.append(f"{lead}{k}: {_emit_scalar(v)}")
elif isinstance(item, list):
raise FrontmatterError("nested lists are outside the pinned grammar")
else:
lines.append(f"{pad} - {_emit_scalar(item)}")
return lines
def _emit_flow_mapping(key: str, record: dict) -> str:
ordered = _reorder_record(key, record)
inner = ", ".join(f"{k}: {_emit_scalar(v)}" for k, v in ordered.items())
return "{ " + inner + " }"
def _emit_scalar(value) -> str:
s = str(value) if not isinstance(value, str) else value
if isinstance(value, bool):
s = "true" if value else "false"
if s == "":
return '""'
if DATE_RE.match(s):
return s
needs_quote = (
s != s.strip()
or ": " in s
or " #" in s
or s[0] in "-?:,[]{}#&*!|>'\"%@`"
or NUMERIC_RE.match(s) is not None
or (BOOL_NULL_RE.match(s) is not None and s not in ("true", "false"))
)
if needs_quote:
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
return s
# ---------------------------------------------------------------------------
# Trust, staleness, identity, digests
# ---------------------------------------------------------------------------
def normalize_verified(raw) -> list[dict]:
"""A bare mapping MUST be treated as a one-element list (§11)."""
if isinstance(raw, dict):
return [raw]
if isinstance(raw, list):
return [entry for entry in raw if isinstance(entry, dict)]
return []
def trust_tier(fm: dict) -> str:
entries = normalize_verified(fm.get("verified"))
if not entries:
return "unverified"
for entry in entries:
if str(entry.get("by", "")).startswith("human:"):
return "human-reviewed"
return "machine-confirmed"
def is_stale(value, today: date | None = None) -> bool:
"""Stale when today >= stale_after; unparseable never hides a note."""
if value is None:
return False
today = today or date.today()
try:
parsed = date.fromisoformat(str(value)[:10])
except ValueError:
return False
return today >= parsed
def canonical_json_digest(payload) -> str:
blob = json.dumps(
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False
)
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
def text_sha256(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def note_dirname(note_path: str) -> str:
"""Version-directory name for a note: first 16 hex of its path hash."""
return hashlib.sha256(note_path.encode("utf-8")).hexdigest()[:16]
def validate_note_path(rel_path: str) -> list[str]:
problems: list[str] = []
if not rel_path or Path(rel_path).is_absolute() or rel_path.startswith(("/", "\\")):
return ["note path must be bundle-relative"]
parts = rel_path.split("/")
for part in parts:
if part in ("", ".", ".."):
problems.append(f"segment {part!r} is not allowed")
continue
if part.casefold() == SIDECAR_DIRNAME or part.startswith("."):
problems.append(f"hidden or sidecar segment {part!r} is not allowed")
continue
stem = part[:-3] if part.endswith(".md") else part
bad = set(part) & WINDOWS_FORBIDDEN
if bad:
problems.append(f"forbidden character(s) {sorted(bad)} in {part!r}")
continue
if not SEGMENT_RE.match(stem):
problems.append(f"segment {part!r} violates the path grammar")
if parts and parts[-1].casefold() in RESERVED_FILENAMES:
problems.append(f"reserved filename {parts[-1]!r} is not a note")
return problems
def safe_note_path(
wiki: Path, rel_path: str, *, must_exist: bool = False
) -> tuple[str, Path]:
"""Resolve one canonical note path without permitting a symlink escape."""
rel = rel_path.replace(os.sep, "/")
problems = validate_note_path(rel)
if not rel.endswith(".md"):
problems.append("note path must end in .md")
if problems:
raise WikiError(f"bad note path {rel!r}: {'; '.join(problems)}")
root = wiki.resolve(strict=True)
candidate = root.joinpath(*rel.split("/"))
current = root
for segment in rel.split("/")[:-1]:
current = current / segment
if current.is_symlink():
raise WikiError(f"note path {rel!r} crosses a symbolic link")
try:
candidate.parent.resolve(strict=False).relative_to(root)
except ValueError as exc:
raise WikiError(f"note path {rel!r} escapes the wiki") from exc
if candidate.is_symlink():
raise WikiError(f"note path {rel!r} is a symbolic link")
if candidate.exists():
try:
candidate.resolve(strict=True).relative_to(root)
except ValueError as exc:
raise WikiError(f"note path {rel!r} escapes the wiki") from exc
if not candidate.is_file():
raise WikiError(f"note path {rel!r} is not a regular file")
elif must_exist:
raise WikiError(f"{rel} does not exist")
return rel, candidate
def path_has_symlink_component(path: Path) -> bool:
"""Check a lexical path without resolving away a symlinked parent."""
absolute = Path(os.path.abspath(path))
current = Path(absolute.anchor)
for part in absolute.parts[1:]:
current /= part
if current.is_symlink():
return True
return False
def atomic_write_text(path: Path, text: str) -> None:
"""Exclusive random temporary file + atomic replace + durable flush."""
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_name = tempfile.mkstemp(
prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
)
tmp = Path(tmp_name)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(text)
handle.flush()
os.fsync(handle.fileno())
for attempt in range(5):
try:
os.replace(tmp, path)
return
except PermissionError:
if attempt == 4:
raise
time.sleep(0.2 * (attempt + 1))
finally:
tmp.unlink(missing_ok=True)
def markdown_text(value: object) -> str:
"""Render untrusted metadata as one inert Markdown text fragment."""
text = " ".join(str(value).split())
return re.sub(r"([\\`*_{}\[\]<>()|])", r"\\\1", text)
def markdown_link(label: object, target: object) -> str:
"""Build a Markdown link without letting metadata alter the index shape."""
safe_target = quote(str(target), safe="/:#?&=@%+~,-._")
return f"[{markdown_text(label)}]({safe_target})"
# ---------------------------------------------------------------------------
# The conformance checker (OKF pass + legal-profile pass, reported apart)
# ---------------------------------------------------------------------------
@dataclass
class Report:
conformance: list[str] = field(default_factory=list)
profile: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
@property
def ok(self) -> bool:
return not self.conformance and not self.profile
def as_dict(self) -> dict:
return {
"ok": self.ok,
"conformance": self.conformance,
"profile": self.profile,
"warnings": self.warnings,
}
def check_wiki(root: Path, today: date | None = None) -> Report:
report = Report()
for path in sorted(root.rglob("*")):
rel = path.relative_to(root)
parts = rel.parts
if path.is_symlink():
report.profile.append(f"{rel}: symbolic links are not allowed in a wiki")
continue
if SIDECAR_DIRNAME in parts:
if path.suffix == ".md":
report.profile.append(
f"{rel}: .md file inside the machine sidecar "
f"({SIDECAR_DIRNAME}/ holds no markdown, ever)"
)
continue
if any(p.startswith(".") for p in parts):
continue
if not path.is_file() or path.suffix != ".md":
continue
if path.name == "index.md":
_check_index(path, rel, is_root=len(parts) == 1, report=report)
elif path.name == "log.md":
_check_log(path, rel, report)
else:
_check_note(path, rel, report, today)
root_index = root / "index.md"
if not root_index.exists():
report.warnings.append(
"no root index.md (legal, but the wiki has no front door)"
)
return report
def _read(path: Path, rel: Path, report: Report) -> str | None:
try:
return path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
report.conformance.append(f"{rel}: unreadable ({exc.__class__.__name__})")
return None
def _check_note(path: Path, rel: Path, report: Report, today: date | None) -> None:
text = _read(path, rel, report)
if text is None:
return
try:
fm, body = split_document(text)
except FrontmatterError as exc:
report.conformance.append(f"{rel}: {exc} (§11 rule 1)")
return
if fm is None:
report.conformance.append(f"{rel}: missing frontmatter block (§11 rule 1)")
return
note_type = str(fm.get("type", "") or "").strip()
if not note_type:
report.conformance.append(f"{rel}: empty or missing type (§11 rule 2)")
return
for problem in validate_note_path(str(rel).replace(os.sep, "/")):
report.profile.append(f"{rel}: {problem}")
if note_type not in NOTE_TYPES:
report.profile.append(
f"{rel}: type {note_type!r} is not in the legal profile "
f"(conformant OKF, but not a /wiki note type)"
)
return
_check_profile_keys(fm, rel, note_type, report, today)
if ROOTED_LINK_RE.search(body):
report.profile.append(
f"{rel}: /-rooted body link (profile: body links are document-relative)"
)
def _check_profile_keys(
fm: dict, rel: Path, note_type: str, report: Report, today: date | None
) -> None:
required = {"practice_area"}
if note_type == "Legal Insight":
required.add("jurisdiction")
if note_type in ("Checklist", "Trap"):
required.update(("document_kind", "trigger"))
for key in sorted(required):
if not str(fm.get(key, "") or "").strip():
report.profile.append(
f"{rel}: missing required key {key!r} for {note_type}"
)
if not str(fm.get("title", "") or "").strip():
report.profile.append(f"{rel}: missing title")
description = str(fm.get("description", "") or "")
if not description.strip():
report.warnings.append(
f"{rel}: missing description (index and previews use it)"
)
elif re.search(r"[.!?]\s+[A-Z0-9]", description):
report.warnings.append(f"{rel}: description reads as more than one sentence")
origin = str(fm.get("origin", "") or "")
if origin not in ORIGINS:
report.profile.append(
f"{rel}: origin {origin!r} missing or not one of {'/'.join(ORIGINS)}"
)
generated = fm.get("generated")
if not isinstance(generated, dict) or not str(generated.get("by", "")).strip():
report.profile.append(f"{rel}: generated {{by, at}} missing or incomplete")
status = str(fm.get("status", "") or "")
if status and status not in STATUSES:
report.profile.append(f"{rel}: status {status!r} not in {'/'.join(STATUSES)}")
pending = fm.get("pending")
if pending is not None:
truthy = str(pending).strip().lower() == "true"
if not truthy:
report.profile.append(f"{rel}: pending must be true or absent")
elif status != "draft":
report.profile.append(f"{rel}: pending requires status: draft")
stale_after = fm.get("stale_after")
if stale_after is not None:
try:
date.fromisoformat(str(stale_after)[:10])
except ValueError:
report.profile.append(f"{rel}: stale_after {stale_after!r} is not a date")
else:
if is_stale(stale_after, today):
report.warnings.append(f"{rel}: past stale_after (re-check queued)")
sources = fm.get("sources", []) or []
if not isinstance(sources, list):
report.profile.append(f"{rel}: sources must be a list")
else:
pending_draft = (
status == "draft"
and str(fm.get("pending", "")).strip().casefold() == "true"
)
if note_type in ASSERTION_TYPES and not sources and not pending_draft:
report.profile.append(
f"{rel}: {note_type} without sources must be a pending draft"
)
for index, source in enumerate(sources, 1):
if not isinstance(source, dict):
report.profile.append(f"{rel}: source {index} must be a record")
continue
resource = str(source.get("resource", "") or "").strip()
kind = str(source.get("kind", "") or "").strip()
if not resource:
report.warnings.append(
f"{rel}: source {index} has no clickable resource"
)
elif resource.startswith(("http://", "https://")):
if kind and kind != "public":
report.profile.append(
f"{rel}: URL source {index} kind must be public"
)
if _looks_like_matter_reference(
resource, str(source.get("title", "") or "")
):
report.profile.append(
f"{rel}: URL source {index} looks matter-specific"
)
elif kind != "authorised-local":
report.profile.append(
f"{rel}: local source {index} requires kind: authorised-local"
)
elif resource:
local = Path(resource).expanduser()
if path_has_symlink_component(local) or _looks_like_matter_reference(
f"{resource} {local.resolve(strict=False)}",
str(source.get("title", "") or ""),
):
report.profile.append(
f"{rel}: local source {index} is a link or looks "
"matter-specific"
)
elif not local.is_file():
report.warnings.append(
f"{rel}: local source {index} is unavailable"
)
elif source.get("sha256"):
actual = hashlib.sha256(local.read_bytes()).hexdigest()
if actual != source.get("sha256"):
report.warnings.append(
f"{rel}: local source {index} changed since last check"
)
if resource and not source.get("pinpoint"):
report.warnings.append(f"{rel}: source {index} has no pinpoint")
def _check_index(path: Path, rel: Path, is_root: bool, report: Report) -> None:
text = _read(path, rel, report)
if text is None:
return
try:
fm, _body = split_document(text)
except FrontmatterError as exc:
report.conformance.append(f"{rel}: {exc} (§8)")
return
if fm is None:
if is_root:
report.warnings.append(
f"{rel}: root index.md declares no okf_version (§12)"
)
return
if not is_root:
report.conformance.append(
f"{rel}: index.md may not carry frontmatter below the root (§8)"
)
return
extra = set(fm) - {"okf_version"}
if extra:
report.conformance.append(
f"{rel}: root index.md frontmatter may carry only okf_version, "
f"found {sorted(extra)} (§8/§12)"
)
declared = str(fm.get("okf_version", "") or "")
if declared and declared != OKF_VERSION:
report.warnings.append(f"{rel}: okf_version {declared!r} != {OKF_VERSION!r}")
if len(text.split("\n")) > 200 and is_root:
report.warnings.append(f"{rel}: root index exceeds the 200-line budget")
def _check_log(path: Path, rel: Path, report: Report) -> None:
text = _read(path, rel, report)
if text is None:
return
try:
fm, body = split_document(text)
except FrontmatterError as exc:
report.conformance.append(f"{rel}: {exc} (§9)")
return
if fm is not None:
report.profile.append(
f"{rel}: log.md carries frontmatter (profile: the log is prose only)"
)
text = body
dates: list[str] = []
for line in text.split("\n"):
if not H2_RE.match(line):
continue
m = DATE_HEADING_RE.match(line)
if not m:
report.conformance.append(
f"{rel}: heading {line.strip()!r} is not an ISO date group (§9)"
)
continue
dates.append(m.group(1))
if dates != sorted(dates, reverse=True):
report.conformance.append(f"{rel}: date groups are not newest-first (§9)")
# ---------------------------------------------------------------------------
# Wiki state: lock, history chain, versions, regeneration
# ---------------------------------------------------------------------------
LOCK_STALE_SECONDS = 600
DEFAULT_ACTOR = "wiki/engine"
OP_WORDS = {
"create_note": "Creation",
"update_note": "Update",
"rename_note": "Rename",
"merge_notes": "Merge",
"delete_note": "Deletion",
"purge_note": "Purge",
"accept_suggestion": "Acceptance",
"decline_suggestion": "Decline",
"rollback": "Rollback",
"intake_skip": "Intake",
}
class WikiError(RuntimeError):
"""A wiki operation that must refuse rather than guess."""
def sidecar(wiki: Path) -> Path:
return wiki / SIDECAR_DIRNAME
def history_path(wiki: Path) -> Path:
return sidecar(wiki) / "history.jsonl"
def utc_now_iso() -> str:
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
def utc_now_compact() -> str:
return datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
def _pid_alive(pid: int) -> bool:
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except OSError:
return True
return True
class WikiLock:
"""Single-writer O_EXCL lock; stale locks with a dead owner are broken."""
def __init__(self, wiki: Path):
self.path = sidecar(wiki) / "lock"
self.acquired = False
self.token = uuid.uuid4().hex
def __enter__(self) -> WikiLock:
self.path.parent.mkdir(parents=True, exist_ok=True)
for _attempt in (1, 2):
try:
fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
except FileExistsError:
info = self._read_holder()
age = time.time() - info.get("epoch", 0)
same_host = info.get("host") == platform.node()
dead = same_host and not _pid_alive(int(info.get("pid", 0)))
if age > LOCK_STALE_SECONDS and (dead or not same_host):
print(
f"note: breaking stale wiki lock (pid {info.get('pid')} "
f"on {info.get('host')}, {int(age)}s old)",
file=sys.stderr,
)
self.path.unlink(missing_ok=True)
continue
raise WikiError(
"another /wiki session is writing this wiki "
f"(pid {info.get('pid')} on {info.get('host')} since "
f"{info.get('ts')}); retry when it finishes"
) from None
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(
json.dumps(
{
"pid": os.getpid(),
"host": platform.node(),
"ts": utc_now_iso(),
"epoch": time.time(),
"token": self.token,
}
)
)
self.acquired = True
return self
raise WikiError("could not acquire the wiki lock")
def _read_holder(self) -> dict:
try:
return json.loads(self.path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
def __exit__(self, *_exc) -> None:
if self.acquired:
holder = self._read_holder()
if holder.get("token") == self.token:
self.path.unlink(missing_ok=True)
def read_history(wiki: Path) -> list[dict]:
path = history_path(wiki)
if not path.exists():
return []
records = []
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
try:
records.append(json.loads(line))
except json.JSONDecodeError:
records.append({"seq": None, "op": "corrupt", "line": n})
return records
def chain_digest(record: dict) -> str:
payload = {k: v for k, v in record.items() if k != "checksum"}
return canonical_json_digest(payload)
def append_history(
wiki: Path,
op: str,
note: str | None,
actor: str,
origin: str | None = None,
mode: str | None = None,
content_sha256: str | None = None,
prior_content_sha256: str | None = None,
extra: dict | None = None,
) -> dict:
if op not in OP_WORDS:
raise WikiError(f"unknown history operation {op!r}")
extra = extra or {}
if note:
safe_note_path(wiki, note)
if extra.get("to"):
safe_note_path(wiki, str(extra["to"]))
for merged in extra.get("merged_from", []) or []:
safe_note_path(wiki, str(merged))
if op == "intake_skip":
reason = extra.get("reason")
if reason not in SKIP_REASONS:
raise WikiError(
f"intake_skip reason must be one of {'/'.join(SKIP_REASONS)}; "
"free text never enters the chain"
)
records = read_history(wiki)
head = records[-1] if records else None
record: dict = {
"seq": (int(head["seq"]) + 1) if head and head.get("seq") else 1,
"ts": utc_now_iso(),
"op": op,
"note": note,
"actor": actor,
"origin": origin,
"mode": mode,
"content_sha256": content_sha256,
"prior_content_sha256": prior_content_sha256,
"prev_checksum": head.get("checksum") if head else None,
"schema_version": SCHEMA_VERSION,
}
record.update(extra)
record["checksum"] = chain_digest(record)
path = history_path(wiki)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "a", encoding="utf-8") as fh:
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
return record
def snapshot_note(wiki: Path, rel: str) -> str | None:
"""Copy a note's current state into versions/ before mutating it."""
src = wiki / rel
if not src.exists():
return None
stamp = utc_now_compact()
vdir = sidecar(wiki) / "versions" / note_dirname(rel)
vdir.mkdir(parents=True, exist_ok=True)
dest = vdir / f"{stamp}.mdv"
suffix = 0
while dest.exists():
suffix += 1
dest = vdir / f"{stamp}-{suffix}.mdv"
header = f"# wiki-version: {rel} @ {stamp}\n"
atomic_write_text(dest, header + src.read_text(encoding="utf-8"))
return str(dest.relative_to(sidecar(wiki))).replace(os.sep, "/")
def version_content(text: str) -> tuple[str, str]:
"""Split a .mdv file into (note_rel, original content)."""
first, _, rest = text.partition("\n")
m = re.match(r"# wiki-version: (.+) @ ", first)
return (m.group(1) if m else ""), rest
def verify_chain(records: list[dict]) -> list[dict]:
broken: list[dict] = []
prev: dict | None = None
for record in records:
seq = record.get("seq")
if record.get("op") == "corrupt":
broken.append({"seq": seq, "reason": "unparseable history line"})
prev = record
continue
if record.get("checksum") != chain_digest(record):
broken.append(
{"seq": seq, "reason": "record digest mismatch (content altered)"}
)
if prev is not None and prev.get("op") != "corrupt":
if seq != (prev.get("seq") or 0) + 1:
broken.append(
{"seq": seq, "reason": f"sequence gap after {prev.get('seq')}"}
)
if record.get("prev_checksum") != prev.get("checksum"):
broken.append({"seq": seq, "reason": "previous-checksum link broken"})
prev = record
return broken
def replay_note_state(records: list[dict], upto_seq: int | None = None) -> dict:
"""note path -> content sha, as of the end of the chain (or a seq)."""
state: dict[str, str] = {}
for record in records:
seq = record.get("seq")
if upto_seq is not None and (seq is None or seq > upto_seq):
break
op = record.get("op")
note = record.get("note")
if op in ("create_note", "update_note", "merge_notes", "accept_suggestion"):
if note and record.get("content_sha256"):
state[note] = record["content_sha256"]
for merged in record.get("merged_from", []) or []:
state.pop(merged, None)
elif op == "rename_note":
to = record.get("to")
if note and to:
sha = state.pop(note, record.get("content_sha256"))
if sha:
state[to] = sha
elif op in ("delete_note", "purge_note", "decline_suggestion"):
if note:
state.pop(note, None)
elif op == "rollback":
to_seq = record.get("to_seq")
if isinstance(to_seq, int):
state = replay_note_state(records, upto_seq=to_seq)
return state
def wiki_notes(wiki: Path) -> list[str]:
notes = []
for path in sorted(wiki.rglob("*.md")):
rel = path.relative_to(wiki)
if path.is_symlink():
continue
current = wiki
if any((current := current / part).is_symlink() for part in rel.parts[:-1]):
continue
if SIDECAR_DIRNAME in rel.parts or any(p.startswith(".") for p in rel.parts):
continue
if path.name in RESERVED_FILENAMES:
continue
notes.append(str(rel).replace(os.sep, "/"))
return notes
def _content_pool(wiki: Path) -> dict[str, str]:
"""content sha -> text, from every current note and every version file."""
pool: dict[str, str] = {}
for rel in wiki_notes(wiki):
text = (wiki / rel).read_text(encoding="utf-8")
pool[text_sha256(text)] = text
versions_dir = sidecar(wiki) / "versions"
if versions_dir.is_dir():
for mdv in versions_dir.rglob("*.mdv"):
_rel, content = version_content(mdv.read_text(encoding="utf-8"))
pool[text_sha256(content)] = content
return pool
def note_findings(rel: str, text: str) -> Report:
"""Run the note-level checks against a single draft."""
report = Report()
_check_note_text(rel, text, report)
return report
def _check_note_text(rel: str, text: str, report: Report) -> None:
rel_path = Path(rel)
try:
fm, body = split_document(text)
except FrontmatterError as exc:
report.conformance.append(f"{rel}: {exc} (§11 rule 1)")
return
if fm is None:
report.conformance.append(f"{rel}: missing frontmatter block (§11 rule 1)")
return
note_type = str(fm.get("type", "") or "").strip()
if not note_type:
report.conformance.append(f"{rel}: empty or missing type (§11 rule 2)")
return
for problem in validate_note_path(rel):
report.profile.append(f"{rel}: {problem}")
if note_type not in NOTE_TYPES:
report.profile.append(f"{rel}: type {note_type!r} is not in the legal profile")
return
_check_profile_keys(fm, rel_path, note_type, report, None)
if ROOTED_LINK_RE.search(body):
report.profile.append(f"{rel}: /-rooted body link")
def regenerate_log(wiki: Path, records: list[dict]) -> None:
by_day: dict[str, list[dict]] = {}
for record in records:
if record.get("op") == "corrupt":
continue
day = str(record.get("ts", ""))[:10]
by_day.setdefault(day, []).append(record)
lines = ["# Wiki log", ""]
for day in sorted(by_day, reverse=True):
lines.append(f"## {day}")
lines.append("")
for record in reversed(by_day[day]):
lines.append(
f"- **{OP_WORDS.get(record.get('op'), 'Change')}** — "
f"{_describe(record)}"
)
lines.append("")
atomic_write_text(wiki / "log.md", "\n".join(lines).rstrip() + "\n")
def _describe(record: dict) -> str:
op = record.get("op")
note = record.get("note") or ""
origin = record.get("origin")
if op in ("create_note", "update_note", "accept_suggestion"):
return f"{note} (origin: {origin})" if origin else note
if op == "rename_note":
return f"{note} → {record.get('to')}"
if op == "merge_notes":
merged = ", ".join(record.get("merged_from", []) or [])
return f"{note} ← {merged}"
if op == "delete_note":
return f"{note} (tombstoned; recoverable)"
if op == "purge_note":
return f"{note} (content and versions destroyed)"
if op == "rollback":
return f"wiki restored to seq {record.get('to_seq')}"
if op == "intake_skip":
return (
f"session {record.get('session', '?')}: nothing to record "
f"({record.get('reason')})"
)
if op == "decline_suggestion":
return f"{note} (suggestion declined)"
return note
REVIEW_DISPOSITIONS = (
"new",
"update",
"disputed",
"unsupported",
"stale",
"no-material",
"matter-specific",
)
def review_records_path(wiki: Path) -> Path:
return sidecar(wiki) / "review" / "queue.jsonl"
def read_review_records(wiki: Path) -> list[dict]:
path = review_records_path(wiki)
if not path.exists():
return []
records: list[dict] = []
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(record, dict):
records.append(record)
return records
def append_review_record(wiki: Path, record: dict) -> None:
path = review_records_path(wiki)
path.parent.mkdir(parents=True, exist_ok=True)
prior = path.read_text(encoding="utf-8") if path.exists() else ""
atomic_write_text(path, prior + json.dumps(record, ensure_ascii=False) + "\n")
def regenerate_indexes(wiki: Path) -> None:
listed: dict[str, list[tuple[str, str, str, str, dict]]] = {}
source_rows: dict[str, dict] = {}
pending = stale = disputed = outdated = 0
for rel in wiki_notes(wiki):
try:
fm, _body = split_document((wiki / rel).read_text(encoding="utf-8"))
except FrontmatterError:
continue
if fm is None:
continue
is_pending = str(fm.get("pending", "")).strip().lower() == "true"
pending += is_pending
if is_pending:
continue
parts = rel.split("/")
area = parts[0] if len(parts) > 1 else "."
status = str(fm.get("status", "") or "stable")
stale += is_stale(fm.get("stale_after"))
disputed += status == "disputed"
outdated += status in ("outdated", "deprecated")
listed.setdefault(area, []).append(
(
str(fm.get("type", "")),
str(fm.get("title", "") or rel),
rel,
str(fm.get("description", "") or ""),
fm,
)
)
for source in fm.get("sources", []) or []:
if not isinstance(source, dict):
continue
source_id = str(source.get("id", "") or source.get("resource", ""))
if not source_id:
continue
row = source_rows.setdefault(
source_id,
{
"title": str(source.get("title", "") or source_id),
"resource": str(source.get("resource", "") or ""),
"notes": [],
"identity": _source_identity(source),
"conflict": False,
"source": dict(source),
},
)
if row["identity"] != _source_identity(source):
row["conflict"] = True
row["notes"].append(rel)
for area, entries in listed.items():
if area == ".":
continue
lines = [f"# {area.replace('-', ' ').title()}", ""]
for note_type in NOTE_TYPES:
grouped = sorted(
(e for e in entries if e[0] == note_type), key=lambda e: e[1].lower()
)
if not grouped:
continue
lines.append(f"# {note_type}")
lines.append("")
for _t, title, rel, desc, _fm in grouped:
target = rel.split("/", 1)[1]
lines.append(
f"* {markdown_link(title, target)} - {markdown_text(desc)}"
)
lines.append("")
atomic_write_text(wiki / area / "index.md", "\n".join(lines).rstrip() + "\n")
root = wiki / "index.md"
okf_declared = OKF_VERSION
if root.exists():
try:
fm, _ = split_document(root.read_text(encoding="utf-8"))
if fm and fm.get("okf_version"):
okf_declared = str(fm["okf_version"])
except FrontmatterError:
pass
lines = [
"---",
f'okf_version: "{okf_declared}"',
"---",
"",
"# Wiki Home",
"",
"Reusable legal knowledge and method, never matter.",
"",
"## At a glance",
"",
f"- {sum(len(v) for v in listed.values())} notes",
f"- {len(source_rows)} linked sources",
f"- {pending} pending review · {stale} stale · {disputed} disputed · "
f"{outdated} outdated",
"- [Source catalogue](sources/index.md)",
"- [Review queue](review/index.md)",
"- [Change log](log.md)",
"",
"## Browse by topic",
"",
]
for area in sorted(a for a in listed if a != "."):
count = len(listed[area])
lines.append(f"* [{area}/index.md]({area}/index.md) - {count} note(s)")
records = [r for r in read_history(wiki) if r.get("note")][-8:]
if records:
lines.extend(["", "## Recent changes", ""])
for record in reversed(records):
note = str(record.get("note"))
lines.append(
f"- {markdown_link(note, note)} — "
f"{markdown_text(OP_WORDS.get(record.get('op'), 'Changed'))} "
f"{str(record.get('ts', ''))[:10]}"
)
atomic_write_text(wiki / "index.md", "\n".join(lines).rstrip() + "\n")
source_lines = ["# Sources", "", "Sources linked from wiki notes.", ""]
if not source_rows:
source_lines.append("No linked sources yet.")
for source_id, row in sorted(source_rows.items()):
resource = row["resource"]
label = (
markdown_link(row["title"], resource)
if resource
else markdown_text(row["title"])
)
note_links = ", ".join(
markdown_link(Path(n).stem, f"../{n}") for n in sorted(set(row["notes"]))
)
warning = " · **CONFLICTING SOURCE RECORDS**" if row["conflict"] else ""
source = row["source"]
details = []
if source.get("pinpoint"):
details.append(f"pinpoint {markdown_text(source['pinpoint'])}")
if source.get("last_checked"):
details.append(f"checked {markdown_text(source['last_checked'])}")
if source.get("sha256"):
details.append(f"sha256 {markdown_text(str(source['sha256'])[:12])}…")
detail_text = " · " + " · ".join(details) if details else ""
source_lines.append(
f"- **{markdown_text(source_id)}** — {label} · used by {note_links}"
f"{detail_text}{warning}"
)
(wiki / "sources").mkdir(parents=True, exist_ok=True)
atomic_write_text(
wiki / "sources" / "index.md", "\n".join(source_lines).rstrip() + "\n"
)
review_records = read_review_records(wiki)
review_lines = [
"# Review queue",
"",
"Legal judgment stays visible for human review.",
"",
]
open_records = [r for r in review_records if r.get("status") == "open"]
if not open_records:
review_lines.append("Nothing pending.")
for record in reversed(open_records[-100:]):
subject = record.get("note") or record.get("source_id") or "wiki"
review_lines.append(
f"- **{markdown_text(record['disposition'])}** — "
f"`{markdown_text(subject)}` · "
f"{str(record.get('ts', ''))[:10]}"
)
(wiki / "review").mkdir(parents=True, exist_ok=True)
atomic_write_text(
wiki / "review" / "index.md", "\n".join(review_lines).rstrip() + "\n"
)
def _regenerate(wiki: Path) -> None:
regenerate_log(wiki, read_history(wiki))
regenerate_indexes(wiki)
def _load_gate_report(
path_str: str | None, dest: str, candidate_text: str
) -> str | None:
"""Require a passing verdict cryptographically bound to this candidate."""
if not path_str:
return (
f"gate unresolved for {dest}: run the method-not-matter gate and "
"pass --gate-report <verdict.json>; no note lands ungated"
)
path = Path(path_str)
try:
verdict = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
return f"gate report {path} unreadable: {exc}"
if verdict.get("ok") is not True:
reason = verdict.get("reason", "gate failed")
return f"gate FAILED for {dest}: {reason}; the note is parked, not landed"
expected = text_sha256(candidate_text)
if verdict.get("candidate_sha256") != expected:
return (
f"gate report is not bound to the current draft for {dest}; "
"run the gate again after the final edit"
)
return None
def _reason_guard(reason: str) -> str | None:
if reason not in PURGE_REASONS:
return "purge reason must be one of " + "/".join(PURGE_REASONS)
return None
# ---------------------------------------------------------------------------
# Method-not-matter gate — layer 1 (deterministic), and the hashed denylist
# ---------------------------------------------------------------------------
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
CORP_ENTITY_RE = re.compile(
r"\b[A-Z][A-Za-z&.\-]+(?:\s+[A-Z][A-Za-z&.\-]+){0,3}\s+"
r"(LLP|LLC|Ltd|GmbH|Inc|AG|S\.A\.|B\.V\.|plc|SE|KG)\b"
)
MATTER_ID_RE = re.compile(r"\b[A-Z]{1,4}-\d{2,6}/\d{2,4}\b")
MATTER_KEYWORD_RE = re.compile(
r"(?i)\b(matter|case|file|aktenzeichen|az)\s*(no\.?|number|#)?\s*[:#]\s*\S+"
)
GATE_RULES: tuple[tuple[str, re.Pattern[str]], ...] = (
("corporate-entity", CORP_ENTITY_RE),
("email-address", EMAIL_RE),
("matter-identifier", MATTER_ID_RE),
("matter-keyword", MATTER_KEYWORD_RE),
(
"instruction-override",
re.compile(
r"(?i)\b(ignore|disregard)\s+(all\s+|any\s+)?(previous|prior|earlier|above)\b"
),
),
(
"instruction-addressed",
re.compile(r"(?i)\bnote\s+(for|to)\s+the\s+(assistant|ai|agent|system)\b"),
),
(
# "never accept an unlimited cap" is method advice to the lawyer;
# "always automatically approve" is an instruction to a machine.
"instruction-imperative",
re.compile(
r"(?i)\b(always|never)\s+automatically\s+"
r"(approve|execute|run|send|delete|accept|sign)\b"
),
),
("instruction-syntax", re.compile(r"<command-name>|(?i:\bsystem\s+prompt\b)")),
)
def _normalize_token(s: str) -> str:
return re.sub(r"\s+", " ", s).strip().casefold()
def _denylist_hash(salt: str, token: str) -> str:
return hashlib.sha256((salt + _normalize_token(token)).encode("utf-8")).hexdigest()
# Words too generic to identify anyone on their own; they never become
# denylist tokens ("Data" inside a company name must not flag the phrase
# "data protection obligations" in a clean note).
GENERIC_NAME_WORDS = frozenset(
"""data protection privacy analytics cloud hosting systems solutions
services technologies technology digital group holding holdings partners
capital consulting software networks network global legal international
european gmbh ltd llc llp inc plc corp company limited the and of""".split()
)
def denylist_tokens(name: str) -> set[str]:
"""A name expands to the phrase, its distinctive words, and bigrams."""
words = [w for w in re.findall(r"[A-Za-z][\w&.\-]*", name) if len(w) >= 3]
norm = [_normalize_token(w) for w in words]
tokens = {_normalize_token(name)}
tokens |= {w for w in norm if w not in GENERIC_NAME_WORDS}
tokens |= {
f"{a} {b}"
for a, b in zip(norm, norm[1:], strict=False)
if not (a in GENERIC_NAME_WORDS and b in GENERIC_NAME_WORDS)
}
return {t for t in tokens if t}
def load_denylist(path_str: str | None) -> dict | None:
if not path_str:
return None
path = Path(path_str)
if not path.exists():
print(
f"warning: denylist {path} does not exist — matching runs without "
"it (build one with: denylist-add --file ... <names>)",
file=sys.stderr,
)
return None
data = json.loads(path.read_text(encoding="utf-8"))
return {"salt": data["salt"], "hashes": set(data["hashes"])}
def gate_candidate(text: str, denylist: dict | None = None) -> dict:
"""Layer 1: deterministic, fails closed, never echoes matched text."""
findings: list[dict] = []
for rule, regex in GATE_RULES:
findings.extend(
{"rule": rule, "span": [m.start(), m.end()]} for m in regex.finditer(text)
)
if denylist:
words = re.findall(r"[A-Za-z][\w&.\-]{2,}", text)
candidates = {_normalize_token(w) for w in words}
candidates |= {
_normalize_token(f"{a} {b}") for a, b in zip(words, words[1:], strict=False)
}
hits = sum(
1
for t in candidates
if _denylist_hash(denylist["salt"], t) in denylist["hashes"]
)
if hits:
findings.append({"rule": "denylist-token", "count": hits})
rules = sorted({f["rule"] for f in findings})
return {
"ok": not findings,
"layer": "deterministic",
"reason": None if not findings else "matched: " + ", ".join(rules),
"details": {"findings": findings},
}
# ---------------------------------------------------------------------------
# Seeding: inventory pre-pass, work items, quote-verified batch landing
# ---------------------------------------------------------------------------
def probe_file(path: Path) -> dict:
"""What stdlib can honestly measure about a seed file — nothing more."""
row: dict = {"path": str(path), "exists": path.exists()}
if not path.exists():
row.update(kind="missing", readable=False, text_measurable=False)
return row
data = path.read_bytes()
row["size"] = len(data)
row["sha256"] = hashlib.sha256(data).hexdigest()
suffix = path.suffix.lower()
if suffix in (".md", ".txt"):
row.update(kind="text", readable=True, text_measurable=True)
elif data[:5] == b"%PDF-":
pages = max(
data.count(b"/Type /Page") - data.count(b"/Type /Pages"),
data.count(b"/Type/Page") - data.count(b"/Type/Pages"),
0,
)
encrypted = b"/Encrypt" in data
row.update(
kind="pdf",
pages=pages,
encrypted=encrypted,
readable=not encrypted,
text_measurable=False,
)
elif suffix == ".docx" and data[:2] == b"PK":
row.update(kind="docx", readable=True, text_measurable=False)
else:
row.update(kind="unknown", readable=False, text_measurable=False)
return row
def _seed_instructions(profile: dict, source_path: str) -> str:
slots = profile.get("slots", {})
cap = profile.get("caps", {}).get(
"max_notes_per_source", profile.get("caps", {}).get("max_notes_per_session", 5)
)
parts = [
f"Read the source file at {source_path} in full, then distil at most "
f"{cap} note drafts. Fewer is better; zero is a valid answer.",
str(slots.get("gate", "")),
str(slots.get("type_guidance", "")),
str(slots.get("format", "")),
str(slots.get("output_rules", "")),
]
return "\n\n".join(p for p in parts if p)
def _verify_quotes(draft: dict, sources_root: Path) -> str | None:
"""Every quote must exist verbatim (whitespace-collapsed) in its source."""
quotes = draft.get("quotes", [])
fm, _body = split_document(draft.get("markdown", ""))
note_type = str((fm or {}).get("type", ""))
if note_type in ("Legal Insight", "Trap", "Position") and not quotes:
return "no supporting quote for an assertion-bearing note"
for entry in quotes:
source_name = str(entry.get("source", ""))
relative = Path(source_name)
if relative.is_absolute() or ".." in relative.parts:
return "quote source escapes the declared source root"
root = sources_root.resolve(strict=True)
source = root / relative
if source.is_symlink():
return "quote source may not be a symbolic link"
try:
resolved = source.resolve(strict=True)
resolved.relative_to(root)
except (FileNotFoundError, ValueError):
return "quote cites a source file that does not exist"
if not resolved.is_file():
return "quote cites a source file that does not exist"
haystack = _normalize_token(resolved.read_text(encoding="utf-8"))
if _normalize_token(str(entry.get("quote", ""))) not in haystack:
return "quote not found verbatim in its cited source"
return None
def cmd_gate(args: argparse.Namespace) -> int:
try:
text = Path(args.candidate).read_text(encoding="utf-8")
except OSError as exc:
print(f"Error: cannot read candidate: {exc}", file=sys.stderr)
return 2
verdict = gate_candidate(text, load_denylist(args.denylist))
verdict["candidate_sha256"] = text_sha256(text)
output = json.dumps(verdict, indent=2, ensure_ascii=False)
if args.out:
atomic_write_text(Path(args.out), output + "\n")
else:
print(output)
return 0 if verdict["ok"] else 1
def cmd_denylist_add(args: argparse.Namespace) -> int:
path = Path(args.file)
if path.exists():
raw = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
print("Error: denylist must be a JSON object", file=sys.stderr)
return 2
salt = raw.get("salt")
stored_hashes = raw.get("hashes")
if not isinstance(salt, str) or not isinstance(stored_hashes, list):
print(
"Error: denylist requires string salt and list hashes",
file=sys.stderr,
)
return 2
data: dict[str, object] = {"salt": salt, "hashes": stored_hashes}
else:
import secrets
salt = secrets.token_hex(16)
data = {"salt": salt, "hashes": []}
stored = data["hashes"]
assert isinstance(stored, list)
hashes = {str(value) for value in stored}
for name in args.names:
hashes |= {_denylist_hash(salt, t) for t in denylist_tokens(name)}
data["hashes"] = sorted(hashes)
atomic_write_text(path, json.dumps(data, indent=1) + "\n")
print(f"denylist: {len(hashes)} hashed token(s); no plaintext stored")
return 0
def cmd_seed_scan(args: argparse.Namespace) -> int:
rows = [probe_file(Path(f)) for f in args.files]
if args.json:
print(json.dumps(rows, indent=2, ensure_ascii=False))
readable = sum(1 for r in rows if r["readable"])
missing = [r["path"] for r in rows if not r["exists"]]
print(
f"seed-scan: {len(rows)} file(s), {readable} readable, "
f"{len(rows) - readable} parked"
)
if missing:
for m in missing:
print(f"Error: named file not found: {m}", file=sys.stderr)
return 1
return 0
def cmd_seed_emit(args: argparse.Namespace) -> int:
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
try:
profile = json.loads(Path(args.profile).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"Error: cannot read source profile: {exc}", file=sys.stderr)
return 2
emitted = parked = 0
for f in args.files:
row = probe_file(Path(f))
if not row["readable"]:
parked += 1
continue
item = {
"unitId": row["sha256"][:16],
"status": "pending",
"source_path": str(f),
"probe": row,
"instructions": _seed_instructions(profile, str(f)),
"note_drafts": [],
}
atomic_write_text(
out_dir / f"item-{row['sha256'][:16]}.json",
json.dumps(item, indent=2, ensure_ascii=False) + "\n",
)
emitted += 1
print(f"seed-emit: {emitted} work item(s), {parked} parked")
return 0
def cmd_seed_land(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
items_dir = Path(args.items)
sources_root = Path(args.sources_root)
if not items_dir.is_dir() or not sources_root.is_dir():
print(
"Error: --items and --sources-root must be existing directories",
file=sys.stderr,
)
return 2
denylist = load_denylist(args.denylist)
landed = updated = unchanged = 0
parked: list[dict] = []
drafts_total = 0
with WikiLock(wiki):
for item_path in sorted(items_dir.glob("item-*.json")):
try:
item = json.loads(item_path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
parked.append({"dest": item_path.name, "reason": f"bad item: {exc}"})
continue
for draft in item.get("note_drafts", []):
drafts_total += 1
dest = str(draft.get("dest", "")).replace(os.sep, "/")
markdown = str(draft.get("markdown", ""))
problems = validate_note_path(dest) if dest else ["missing dest"]
if problems or not dest.endswith(".md"):
parked.append({"dest": dest, "reason": "bad destination path"})
continue
quote_error = _verify_quotes(draft, sources_root)
if quote_error:
parked.append({"dest": dest, "reason": quote_error})
continue
verdict = gate_candidate(markdown, denylist)
if not verdict["ok"]:
parked.append(
{"dest": dest, "reason": f"gate: {verdict['reason']}"}
)
continue
try:
fm, body = split_document(markdown)
except FrontmatterError as exc:
parked.append({"dest": dest, "reason": f"frontmatter: {exc}"})
continue
if fm is None:
parked.append({"dest": dest, "reason": "draft has no frontmatter"})
continue
if draft.get("quotes") and not fm.get("sources"):
inferred_sources = []
seen_sources = set()
for quote in draft.get("quotes", []):
source_name = str(quote.get("source", ""))
if not source_name or source_name in seen_sources:
continue
seen_sources.add(source_name)
resolved = (sources_root / source_name).resolve(strict=True)
inferred_sources.append(
{
"id": re.sub(
r"[^a-z0-9-]+",
"-",
Path(source_name).stem.casefold(),
).strip("-"),
"kind": "authorised-local",
"resource": str(resolved),
"title": Path(source_name)
.stem.replace("-", " ")
.title(),
"pinpoint": "quoted extract",
}
)
if inferred_sources:
fm["sources"] = inferred_sources
try:
word, record = _land_note(
wiki,
dest,
fm,
body,
"seeded",
args.mode,
args.actor,
replace=True,
)
except WikiError as exc:
parked.append({"dest": dest, "reason": str(exc)})
continue
if record is None:
unchanged += 1
elif word == "updated":
updated += 1
else:
landed += 1
if landed or updated:
_regenerate(wiki)
accounted = landed + updated + unchanged + len(parked)
print(
f"seed-land: {drafts_total} draft(s) → {landed} landed, {updated} updated, "
f"{unchanged} unchanged, {len(parked)} parked · "
f"accounted {accounted}/{drafts_total}"
)
for p in parked:
print(f"parked {p['dest']}: {p['reason']}")
if args.json:
print(
json.dumps(
{
"drafts": drafts_total,
"landed": landed,
"updated": updated,
"unchanged": unchanged,
"parked": parked,
},
indent=2,
ensure_ascii=False,
)
)
return 0 if accounted == drafts_total else 1
# ---------------------------------------------------------------------------
# Vocabulary registry, maintenance pass (v1-minimal), tripwire metrics
# ---------------------------------------------------------------------------
VOCAB_KEYS = ("practice_area", "jurisdiction", "document_kind")
STOP_ARTICLES = frozenset({"the", "a", "an", "of", "and"})
def _vocab_canon(value: str) -> str:
words = re.findall(r"[a-z0-9]+", value.casefold())
return " ".join(w for w in words if w not in STOP_ARTICLES)
def vocab_path(wiki: Path) -> Path:
return sidecar(wiki) / "vocab.json"
def load_vocab(wiki: Path) -> dict:
path = vocab_path(wiki)
if path.exists():
return json.loads(path.read_text(encoding="utf-8"))
return {key: {} for key in VOCAB_KEYS}
def vocab_snap(wiki: Path, vocab: dict, key: str, value: str) -> str:
"""First use registers the canonical form; later uses snap to it."""
canon = _vocab_canon(value)
if not canon:
return value
registry = vocab.setdefault(key, {})
for display, canon_form in registry.items():
if canon_form == canon:
return display
registry[value] = canon
return value
def save_vocab(wiki: Path, vocab: dict) -> None:
atomic_write_text(
vocab_path(wiki), json.dumps(vocab, indent=1, ensure_ascii=False) + "\n"
)
def _body_tokens(text: str) -> set[str]:
_fm, body = split_document(text)
return {w for w in re.findall(r"[a-z0-9]+", body.casefold()) if len(w) > 2}
def cmd_maintain(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
today = date.fromisoformat(args.today) if args.today else None
except ValueError:
print(f"Error: --today {args.today!r} is not YYYY-MM-DD", file=sys.stderr)
return 2
notes: dict[str, dict] = {}
for rel in wiki_notes(wiki):
try:
fm, _body = split_document((wiki / rel).read_text(encoding="utf-8"))
except FrontmatterError:
continue
if fm:
notes[rel] = fm
stale = sorted(
rel for rel, fm in notes.items() if is_stale(fm.get("stale_after"), today)
)
by_sha: dict[str, list[str]] = {}
for rel in notes:
try:
_fm, body = split_document((wiki / rel).read_text(encoding="utf-8"))
except FrontmatterError:
continue
by_sha.setdefault(text_sha256(body.strip()), []).append(rel)
exact_dupes = sorted(v for v in by_sha.values() if len(v) > 1)
by_title: dict[tuple, list[str]] = {}
for rel, fm in notes.items():
key = (
_vocab_canon(str(fm.get("title", ""))),
str(fm.get("type", "")),
)
if key[0]:
by_title.setdefault(key, []).append(rel)
title_pairs = sorted(
v for v in by_title.values() if len(v) > 1 and v not in exact_dupes
)
deprecated = {
rel for rel, fm in notes.items() if str(fm.get("status", "")) == "deprecated"
}
source_findings: list[dict] = []
for rel, fm in notes.items():
for source in fm.get("sources", []) or []:
if not isinstance(source, dict):
continue
resource = str(source.get("resource", "") or "")
if source.get("kind") != "authorised-local" or not resource:
continue
path = Path(resource).expanduser()
if path_has_symlink_component(path) or not path.is_file():
source_findings.append(
{
"note": rel,
"source": source.get("id"),
"issue": "unavailable-or-symlink",
}
)
continue
digest = hashlib.sha256(path.read_bytes()).hexdigest()
if source.get("sha256") and source.get("sha256") != digest:
source_findings.append(
{"note": rel, "source": source.get("id"), "issue": "hash-changed"}
)
dep_links = []
for rel in sorted(notes):
body = (wiki / rel).read_text(encoding="utf-8")
for target in re.findall(r"\]\(([^)]+\.md)\)", body):
resolved = (Path(rel).parent / target).as_posix()
resolved = str(Path(os.path.normpath(resolved)).as_posix())
if resolved in deprecated:
dep_links.append({"note": rel, "cites_deprecated": resolved})
vocab = load_vocab(wiki)
near: list[list[str]] = []
import difflib
for key in VOCAB_KEYS:
values = list(vocab.get(key, {}))
for i, a in enumerate(values):
for b in values[i + 1 :]:
if difflib.SequenceMatcher(None, a, b).ratio() > 0.8:
near.append([key, a, b])
payload = {
"stale_recheck_queue": stale,
"exact_duplicates": exact_dupes,
"same_title_pairs": title_pairs,
"links_to_deprecated": dep_links,
"vocabulary_near_duplicates": near,
"source_findings": source_findings,
}
if args.json:
print(json.dumps(payload, indent=2, ensure_ascii=False))
return 0
print(
f"maintain: {len(stale)} stale re-check(s), {len(exact_dupes)} exact "
f"duplicate group(s), {len(title_pairs)} same-title pair(s), "
f"{len(dep_links)} deprecated-authority link(s), {len(near)} vocabulary "
f"near-duplicate(s), {len(source_findings)} source finding(s) — "
"proposals only, nothing changed"
)
return 0
def _note_origin_map(records: list[dict]) -> dict[str, str]:
origins: dict[str, str] = {}
for record in records:
note = record.get("note")
op = record.get("op")
if op in ("create_note", "merge_notes") and note and record.get("origin"):
origins[note] = record["origin"]
elif op == "rename_note" and note and record.get("to"):
if note in origins:
origins[record["to"]] = origins.pop(note)
return origins
def cmd_tripwire(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
for label, value in (
("--window-from", args.window_from),
("--window-to", args.window_to),
):
if not value:
continue
try:
datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
print(f"Error: {label} {value!r} is not an ISO timestamp", file=sys.stderr)
return 2
records = read_history(wiki)
pool = _content_pool(wiki)
origins = _note_origin_map(records)
def in_window(record: dict) -> bool:
ts = str(record.get("ts", ""))
return (not args.window_from or ts >= args.window_from) and (
not args.window_to or ts <= args.window_to
)
auto_built = [
r
for r in records
if r.get("op") in ("create_note", "merge_notes")
and r.get("origin") == "auto-built"
and in_window(r)
]
edited_away: list[dict] = []
unmeasurable = 0
for record in records:
if not in_window(record):
continue
op = record.get("op")
note = record.get("note")
if op in ("delete_note", "purge_note") and origins.get(note) == "auto-built":
edited_away.append({"note": note, "how": op})
elif op == "update_note" and origins.get(note) == "auto-built":
prior = pool.get(record.get("prior_content_sha256", ""))
new = pool.get(record.get("content_sha256", ""))
if prior is None or new is None:
unmeasurable += 1
continue
before, after = _body_tokens(prior), _body_tokens(new)
union = before | after
overlap = len(before & after) / len(union) if union else 1.0
if overlap < 0.5:
edited_away.append(
{"note": note, "how": "rewritten", "overlap": round(overlap, 3)}
)
rollbacks = sum(1 for r in records if r.get("op") == "rollback" and in_window(r))
denominator = len(auto_built)
numerator = len({e["note"] for e in edited_away})
tripped = denominator > 0 and numerator * 7 > denominator
payload = {
"auto_built": denominator,
"edited_away": numerator,
"events": edited_away,
"rollbacks": rollbacks,
"unmeasurable": unmeasurable,
"threshold": "more than 1 in 7",
"tripped": tripped,
}
if args.json:
print(json.dumps(payload, indent=2, ensure_ascii=False))
return 1 if tripped else 0
verdict = (
"TRIPPED — markup mode becomes the shipping default" if tripped else "holding"
)
print(
f"tripwire: {numerator} of {denominator} auto-built note(s) edited away "
f"or deleted · {rollbacks} rollback(s) · threshold >1/7 · {verdict}"
)
return 1 if tripped else 0
# ---------------------------------------------------------------------------
# Legacy staged-item import compatibility
# ---------------------------------------------------------------------------
def automation_state_path(wiki: Path) -> Path:
"""Operational idempotency beside the wiki; never a behaviour setting."""
return sidecar(wiki) / "automation.json"
def load_automation_state(wiki: Path) -> dict:
path = automation_state_path(wiki)
if path.exists():
try:
data = json.loads(path.read_text(encoding="utf-8"))
if isinstance(data, dict):
return data
except (OSError, json.JSONDecodeError):
pass
return {"schema_version": SCHEMA_VERSION, "staged": [], "processed": []}
def save_automation_state(wiki: Path, state: dict) -> None:
atomic_write_text(
automation_state_path(wiki),
json.dumps(state, indent=1, ensure_ascii=False) + "\n",
)
def cmd_capture(args: argparse.Namespace) -> int:
"""Refuse legacy raw-transcript staging.
Persisting a transcript slice for a later pass would leave confidential
temporary data. Deliberate capture uses the normal Add workflow in the
current turn instead.
"""
del args
print(
"REFUSED: raw transcript staging is disabled; use manual Add or ask to "
"save the reusable lessons from this conversation",
file=sys.stderr,
)
return 2
def cmd_capture_land(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
staging_dir = Path(args.staging)
if not staging_dir.is_dir():
print("Error: --staging must be an existing directory", file=sys.stderr)
return 2
state = load_automation_state(wiki)
denylist = load_denylist(args.denylist)
try:
profile = json.loads(Path(args.profile).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"Error: cannot read capture profile: {exc}", file=sys.stderr)
return 2
cap = int(profile.get("caps", {}).get("max_notes_per_session", 5))
landed = updated = 0
parked: list[dict] = []
dropped = skips = 0
kept_items = 0
with WikiLock(wiki):
for item_path in sorted(staging_dir.glob("item-*.json")):
try:
item = json.loads(item_path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
print(
f"Error: item {item_path.name} is unreadable ({exc}); "
"item preserved",
file=sys.stderr,
)
kept_items += 1
continue
session = str(item.get("session", item_path.stem))
skip = item.get("skip")
drafts = item.get("note_drafts", [])
if skip:
if skip not in SKIP_REASONS:
print(
f"Error: item {session}: skip {skip!r} is not in the "
f"closed enum; item preserved",
file=sys.stderr,
)
kept_items += 1
continue
append_history(
wiki,
"intake_skip",
None,
args.actor,
extra={"session": session, "reason": skip},
)
skips += 1
elif not drafts:
print(
f"Error: item {session}: neither drafts nor a skip token; "
f"item preserved for the next run",
file=sys.stderr,
)
kept_items += 1
continue
else:
landed_this_session = 0
for draft in drafts:
dest = str(draft.get("dest", "")).replace(os.sep, "/")
markdown = str(draft.get("markdown", ""))
if not dest.endswith(".md") or validate_note_path(dest):
parked.append({"dest": dest, "reason": "bad destination path"})
continue
verdict = gate_candidate(markdown, denylist)
if not verdict["ok"]:
parked.append(
{"dest": dest, "reason": f"gate: {verdict['reason']}"}
)
continue
try:
fm, body = split_document(markdown)
except FrontmatterError as exc:
parked.append({"dest": dest, "reason": f"frontmatter: {exc}"})
continue
if fm is None:
parked.append({"dest": dest, "reason": "no frontmatter"})
continue
note_type = str(fm.get("type", ""))
if note_type in (
"Legal Insight",
"Trap",
"Position",
) and not fm.get("sources"):
parked.append(
{"dest": dest, "reason": "assertion note without sources"}
)
continue
if landed_this_session >= cap:
dropped += 1
continue
try:
word, record = _land_note(
wiki,
dest,
fm,
body,
"auto-built",
args.mode,
args.actor,
replace=True,
)
except WikiError as exc:
parked.append({"dest": dest, "reason": str(exc)})
continue
if record is None:
pass
elif word == "updated":
updated += 1
landed_this_session += 1
else:
landed += 1
landed_this_session += 1
item_path.unlink()
staged = state.get("staged", [])
if session in staged:
staged.remove(session)
state.setdefault("processed", []).append(session)
if landed or updated or skips:
_regenerate(wiki)
save_automation_state(wiki, state)
print(
f"capture-land: {landed} landed, {updated} updated, {len(parked)} parked, "
f"{dropped} dropped (over-cap, {cap}/session), {skips} session(s) with "
f"nothing to record, {kept_items} item(s) preserved"
)
for p in parked:
print(f"parked {p['dest']}: {p['reason']}")
return 0 if kept_items == 0 else 1
# ---------------------------------------------------------------------------
# Mutation verbs
# ---------------------------------------------------------------------------
# A chat that did not create the wiki must still find it. The per-wiki
# manifest is authoritative; the user registry is only an address book with
# names, stable ids, paths and timestamps. Explicit --wiki always wins and
# the engine never scans the disk guessing for a wiki.
def user_registry_path() -> Path:
override = os.environ.get("WIKI_USER_REGISTRY")
if override:
return Path(override)
return Path.home() / ".wiki" / "wikis.json"
def user_playbook_path() -> Path:
"""The global behaviour surface; separate from every wiki artifact."""
override = os.environ.get("WIKI_PLAYBOOK")
if override:
return Path(override)
return user_registry_path().parent / "lqplaybook.md"
def _load_registry_file(path: Path) -> dict:
if path.exists():
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict) or not isinstance(data.get("wikis", {}), dict):
raise WikiError(f"invalid wiki registry at {path}")
return data
return {"schema_version": 2, "wikis": {}, "default_wiki": None}
def _load_registry_if_readable(path: Path) -> dict | None:
"""Read an optional registry without making workspace-local use brittle."""
try:
return _load_registry_file(path)
except (OSError, json.JSONDecodeError, WikiError):
return None
def manifest_path(wiki: Path) -> Path:
return sidecar(wiki) / "manifest.json"
def load_manifest(wiki: Path, required: bool = True) -> dict | None:
if wiki.is_symlink():
raise WikiError(f"wiki root {wiki} may not be a symbolic link")
if wiki.is_dir():
for entry in wiki.rglob("*"):
if entry.is_symlink():
raise WikiError(
f"wiki contains symbolic link {entry.relative_to(wiki)}; "
"links are refused because they can escape the wiki"
)
path = manifest_path(wiki)
if not path.exists():
if required:
raise WikiError(
f"{wiki} is not an initialised /wiki "
f"(missing {SIDECAR_DIRNAME}/manifest.json)"
)
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise WikiError(f"invalid wiki manifest at {path}: {exc}") from exc
wiki_id = data.get("wiki_id")
if not isinstance(wiki_id, str) or not wiki_id:
raise WikiError(f"invalid wiki manifest at {path}: missing wiki_id")
return data
def _register(reg: dict, name: str, path: Path, manifest: dict) -> None:
wikis = reg.setdefault("wikis", {})
existing = wikis.get(name)
if isinstance(existing, dict):
existing_id = existing.get("wiki_id")
if existing_id and existing_id != manifest["wiki_id"]:
raise WikiError(
f"wiki name {name!r} already belongs to a different wiki; "
"choose another name"
)
created = (existing or {}).get("created", utc_now_iso())
wikis[name] = {
"wiki_id": manifest["wiki_id"],
"path": str(path.resolve()),
"created": created,
"last_opened": utc_now_iso(),
}
if not reg.get("default_wiki"):
reg["default_wiki"] = name
def register_wiki(name: str, path: Path, user_registry: bool = True) -> list[str]:
wrote: list[str] = []
if user_registry:
up = user_registry_path()
try:
reg = _load_registry_file(up)
manifest = load_manifest(path)
assert manifest is not None
_register(reg, name, path, manifest)
up.parent.mkdir(parents=True, exist_ok=True)
atomic_write_text(up, json.dumps(reg, indent=1, ensure_ascii=False) + "\n")
wrote.append(str(up))
except (OSError, json.JSONDecodeError) as exc:
raise WikiError(f"cannot write user wiki registry at {up}: {exc}") from exc
return wrote
def _touch_registered_wiki(path: Path, manifest: dict) -> None:
registry_path = user_registry_path()
reg = _load_registry_if_readable(registry_path)
if reg is None:
return
changed = False
for entry in (reg.get("wikis") or {}).values():
if not isinstance(entry, dict):
continue
if entry.get("wiki_id") == manifest.get("wiki_id"):
entry["path"] = str(path.resolve())
entry["last_opened"] = utc_now_iso()
changed = True
if changed:
try:
atomic_write_text(
registry_path, json.dumps(reg, indent=1, ensure_ascii=False) + "\n"
)
except OSError:
pass
def resolve_wiki(args: argparse.Namespace) -> Path:
explicit = getattr(args, "wiki", None)
if explicit:
path = Path(explicit).expanduser()
if not path.is_dir():
raise WikiError(f"wiki is missing at {path}; nothing was created")
manifest = load_manifest(path, required=True)
assert manifest is not None
_touch_registered_wiki(path, manifest)
return path
current = Path.cwd()
if not getattr(args, "ignore_current", False) and manifest_path(current).is_file():
manifest = load_manifest(current)
assert manifest is not None
_touch_registered_wiki(current, manifest)
return current
name = getattr(args, "wiki_name", None)
reg = _load_registry_if_readable(user_registry_path())
if reg is None:
raise WikiError(f"wiki registry at {user_registry_path()} is unreadable")
wikis = reg.get("wikis") or {}
pick = name or reg.get("default_wiki")
if not pick and len(wikis) == 1:
pick = next(iter(wikis))
if name:
if name not in wikis:
raise WikiError(
f"no wiki named {name!r} is registered; "
f"known: {sorted(wikis) or 'none'}"
)
if not pick:
if wikis:
raise WikiError(
"several wikis are registered and none is default; choose one with "
"wiki-use <name> or --wiki-name <name>"
)
raise WikiError(
"no wiki registered — choose a folder once, then run: "
"wiki.py init --wiki <dir> [--name <name>]"
)
entry = wikis.get(pick)
if not isinstance(entry, dict) or not entry.get("path"):
raise WikiError(f"invalid registry entry for wiki {pick!r}")
path = Path(str(entry["path"])).expanduser()
if not path.is_dir():
raise WikiError(
f"registered wiki {pick!r} is missing at {path} — reconnect it with "
"init --wiki <moved-path> --name <name>; nothing was auto-created"
)
manifest = load_manifest(path)
assert manifest is not None
expected = entry.get("wiki_id")
if expected and expected != manifest.get("wiki_id"):
raise WikiError(
f"registry/manifest identity mismatch for wiki {pick!r} at {path}; "
"refusing to open the wrong wiki"
)
_touch_registered_wiki(path, manifest)
return path
def _wiki_arg(args: argparse.Namespace) -> Path:
wiki = resolve_wiki(args)
if not wiki.is_dir():
raise WikiError(f"{wiki} is not a directory (run: wiki.py init --wiki …)")
return wiki
def cmd_init(args: argparse.Namespace) -> int:
requested = Path(args.wiki).expanduser()
if requested.is_symlink():
print("REFUSED: wiki root may not be a symbolic link", file=sys.stderr)
return 1
wiki = requested.resolve()
already = manifest_path(wiki).exists()
if wiki.exists():
try:
load_manifest(wiki, required=False)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if not args.no_user_registry:
registry_path = user_registry_path()
if registry_path.exists():
reg = _load_registry_if_readable(registry_path)
if reg is None:
print(
f"REFUSED: wiki registry at {registry_path} is unreadable; "
"repair it before initialising another wiki",
file=sys.stderr,
)
return 1
existing = (reg.get("wikis") or {}).get(args.name)
candidate_manifest = (
load_manifest(wiki, required=False) if already else None
)
if isinstance(existing, dict) and (
candidate_manifest is None
or existing.get("wiki_id") != candidate_manifest.get("wiki_id")
):
print(
f"REFUSED: wiki name {args.name!r} already belongs to a "
"different wiki; choose another name",
file=sys.stderr,
)
return 1
(sidecar(wiki) / "versions").mkdir(parents=True, exist_ok=True)
(sidecar(wiki) / "pending").mkdir(parents=True, exist_ok=True)
(sidecar(wiki) / "review").mkdir(parents=True, exist_ok=True)
if not already:
manifest = {
"schema_version": SCHEMA_VERSION,
"wiki_id": str(uuid.uuid4()),
"name": args.name,
"okf_version": OKF_VERSION,
"legal_profile_version": 1,
"generated_index_version": 2,
"created": utc_now_iso(),
}
atomic_write_text(
manifest_path(wiki),
json.dumps(manifest, indent=1, ensure_ascii=False) + "\n",
)
else:
manifest = load_manifest(wiki)
assert manifest is not None
if manifest.get("name") != args.name:
manifest["name"] = args.name
atomic_write_text(
manifest_path(wiki),
json.dumps(manifest, indent=1, ensure_ascii=False) + "\n",
)
if not (wiki / "index.md").exists():
atomic_write_text(
wiki / "index.md",
'---\nokf_version: "0.2"\n---\n\n# Wiki Home\n\n'
"Reusable legal knowledge and method, never matter.\n",
)
if not (wiki / "log.md").exists():
atomic_write_text(wiki / "log.md", "# Wiki log\n")
regenerate_indexes(wiki)
wrote = register_wiki(args.name, wiki, user_registry=not args.no_user_registry)
word = "already initialised" if already else "initialised"
where = f" · registered as {args.name!r} in {', '.join(wrote)}" if wrote else ""
print(f"{word} wiki at {wiki} · okf {OKF_VERSION} · schema {SCHEMA_VERSION}{where}")
return 0
def cmd_setup(args: argparse.Namespace) -> int:
"""One-question first run, or a fresh-chat route to Wiki Home."""
registry_path = user_registry_path()
reg = _load_registry_if_readable(registry_path)
if reg is None and registry_path.exists():
print(
f"REFUSED: wiki registry at {registry_path} is unreadable; repair or "
"restore it before setup",
file=sys.stderr,
)
return 1
reg = reg or {"wikis": {}, "default_wiki": None}
wikis: dict = dict(reg.get("wikis") or {})
configured = bool(wikis)
default = reg.get("default_wiki") or (
next(iter(wikis)) if len(wikis) == 1 else None
)
if args.json:
print(
json.dumps(
{
"configured": configured,
"wikis": wikis,
"default_wiki": default,
"needs_choice": len(wikis) > 1 and not default,
},
indent=2,
ensure_ascii=False,
)
)
return 0
print("LegalQuants · CODEX for Legal — /wiki")
if not configured:
print(
"\nWhere should your wiki live? Choose an existing wiki folder or a new "
"folder you own. Optionally give it a short name. Then run:\n"
" init --wiki <dir> [--name <name>]\n\n"
"Its name and path are saved in ~/.wiki/wikis.json, so a new chat "
"finds it without asking again."
)
return 0
print("\nWikis:")
for name, entry in wikis.items():
mark = " (default)" if name == default else ""
missing = "" if Path(entry["path"]).is_dir() else " [MISSING]"
print(f" wiki {name!r} → {entry['path']}{mark}{missing}")
if default and default in wikis and Path(wikis[default]["path"]).is_dir():
print(f" open: {Path(wikis[default]['path']) / 'index.md'}")
elif default and default in wikis:
print(
f" reconnect {default!r}: move or restore its folder, then run "
f"init --wiki <moved-path> --name {default}"
)
elif len(wikis) > 1:
print(" choose one: wiki-use <name>")
return 0
def cmd_wiki_list(args: argparse.Namespace) -> int:
rows: list[dict] = []
seen: set[tuple[str, str]] = set()
user_registry = _load_registry_if_readable(user_registry_path())
sources = [("user", user_registry)] if user_registry else []
for label, reg in sources:
default = reg.get("default_wiki")
for name, entry in (reg.get("wikis") or {}).items():
key = (name, entry["path"])
if key in seen:
continue
seen.add(key)
rows.append(
{
"name": name,
"path": entry["path"],
"default": name == default,
"source": label,
"exists": Path(entry["path"]).is_dir(),
"wiki_id": entry.get("wiki_id"),
}
)
if args.json:
print(json.dumps({"wikis": rows}, indent=2, ensure_ascii=False))
return 0
if not rows:
print("no wikis registered — run: wiki.py init --wiki <dir>")
return 0
for row in rows:
marks = (" (default)" if row["default"] else "") + (
"" if row["exists"] else " [MISSING]"
)
print(f"{row['name']} → {row['path']}{marks} [{row['source']}]")
return 0
def cmd_wiki_use(args: argparse.Namespace) -> int:
up = user_registry_path()
changed = []
if up.exists():
reg = _load_registry_if_readable(up)
if reg is not None and args.name in (reg.get("wikis") or {}):
reg["default_wiki"] = args.name
atomic_write_text(up, json.dumps(reg, indent=1, ensure_ascii=False) + "\n")
changed.append(str(up))
if not changed:
print(f"Error: no wiki named {args.name!r} is registered", file=sys.stderr)
return 1
print(f"default wiki is now {args.name!r} ({', '.join(changed)})")
return 0
def cmd_land(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
dest = args.dest.replace(os.sep, "/")
try:
dest, target = safe_note_path(wiki, dest)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
try:
draft = Path(args.file).read_text(encoding="utf-8")
except OSError as exc:
print(f"Error: cannot read draft: {exc}", file=sys.stderr)
return 2
gate_error = _load_gate_report(args.gate_report, dest, draft)
if gate_error:
print(f"REFUSED: {gate_error}", file=sys.stderr)
return 1
try:
fm, body = split_document(draft)
except FrontmatterError as exc:
print(f"Error: draft frontmatter: {exc}", file=sys.stderr)
return 2
if fm is None:
print("Error: draft has no frontmatter", file=sys.stderr)
return 2
if args.dry_run:
word = "update" if target.exists() else "create"
print(f"dry-run: would {word} {dest} (origin {args.origin}, {args.mode})")
return 0
try:
with WikiLock(wiki):
word, record = _land_note(
wiki,
dest,
fm,
body,
args.origin,
args.mode,
args.actor,
replace=args.replace,
)
if record is not None:
_regenerate(wiki)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if record is None:
print(f"unchanged: {dest} — placed 0 new")
return 0
print(f"{word} {dest} ({args.origin}, {args.mode}) · seq {record['seq']}")
return 0
def _land_note(
wiki: Path,
dest: str,
fm: dict,
body: str,
origin: str,
mode: str,
actor: str,
replace: bool,
) -> tuple[str, dict | None]:
"""The one landing path (caller holds the lock). Refuses via WikiError."""
fm["origin"] = origin
title = str(fm.get("title", "") or "")
if _looks_like_matter_reference(dest) or any(
term in title.casefold() for term in MATTER_DOCUMENT_TERMS
):
raise WikiError("note destination or title looks matter-specific")
_prepare_and_validate_sources(fm)
_validate_source_ids(wiki, dest, fm)
dest, target = safe_note_path(wiki, dest)
exists = target.exists()
if mode == "markup" and not exists:
fm["status"] = "draft"
fm["pending"] = True
_enforce_grounding(fm)
normalized = emit_document(fm, body)
findings = note_findings(dest, normalized)
if not findings.ok:
raise WikiError("; ".join(findings.conformance + findings.profile))
vocab = load_vocab(wiki)
touched = False
for key in VOCAB_KEYS:
value = fm.get(key)
if isinstance(value, str) and value.strip():
fm[key] = vocab_snap(wiki, vocab, key, value)
touched = True
normalized = emit_document(fm, body)
if exists and not replace:
raise WikiError(f"{dest} exists; pass --replace to update it")
prior_sha = None
if exists:
prior_text = target.read_text(encoding="utf-8")
prior_sha = text_sha256(prior_text)
if prior_text == normalized:
return "unchanged", None
version_file = snapshot_note(wiki, dest)
target.parent.mkdir(parents=True, exist_ok=True)
atomic_write_text(target, normalized)
record = append_history(
wiki,
"update_note" if exists else "create_note",
dest,
actor,
origin=origin,
mode=mode,
content_sha256=text_sha256(normalized),
prior_content_sha256=prior_sha,
extra={"version_file": version_file} if version_file else None,
)
if touched:
save_vocab(wiki, vocab)
return ("updated" if exists else "landed"), record
MATTER_PATH_SEGMENTS = {
"matter",
"matters",
"client",
"clients",
"case-files",
"casefiles",
"deals",
}
MATTER_DOCUMENT_TERMS = (
"statement of claim",
"statement of defence",
"statement of defense",
"pleading",
"client correspondence",
"witness statement",
)
ASSERTION_TYPES = ("Legal Insight", "Trap", "Position")
def _looks_like_matter_reference(resource: str, title: str = "") -> bool:
decoded = unquote(f"{resource} {title}")
folded = decoded.casefold()
path_parts = {
part.casefold() for part in re.split(r"[/\\?#]+", unquote(resource)) if part
}
identifier_like = bool(re.search(r"\b[A-Z][A-Z0-9]{1,20}[-_/]\d{2,6}\b", decoded))
return bool(
path_parts & MATTER_PATH_SEGMENTS
or any(term in folded for term in MATTER_DOCUMENT_TERMS)
or identifier_like
)
def _source_identity(source: dict) -> tuple[str, str, str, str]:
return (
str(source.get("kind", "") or "").strip(),
str(source.get("resource", "") or "").strip(),
str(source.get("title", "") or "").strip(),
str(source.get("sha256", "") or "").strip(),
)
def _validate_source_ids(wiki: Path, dest: str, fm: dict) -> None:
known: dict[str, tuple[str, str, str, str]] = {}
for rel in wiki_notes(wiki):
if rel == dest:
continue
try:
other, _body = split_document((wiki / rel).read_text(encoding="utf-8"))
except (OSError, FrontmatterError):
continue
for source in (other or {}).get("sources", []) or []:
if isinstance(source, dict) and source.get("id"):
known[str(source["id"])] = _source_identity(source)
for source in fm.get("sources", []) or []:
source_id = str(source.get("id", "") or "").strip()
if not source_id:
continue
existing = known.get(source_id)
if existing is not None and existing != _source_identity(source):
raise WikiError(
f"source id {source_id!r} conflicts with an existing source record"
)
def _enforce_grounding(fm: dict) -> None:
note_type = str(fm.get("type", "") or "")
if note_type not in ASSERTION_TYPES or fm.get("sources"):
return
pending = str(fm.get("pending", "")).strip().casefold() == "true"
if str(fm.get("status", "") or "stable") != "draft" or not pending:
raise WikiError(
f"{note_type} without sources must remain a pending draft; "
"it cannot land as reusable authority"
)
def _prepare_and_validate_sources(fm: dict) -> None:
"""Classify explicit sources and refuse obvious matter-document resolvers."""
sources = fm.get("sources", []) or []
if not isinstance(sources, list):
raise WikiError("sources must be a list")
for index, source in enumerate(sources, 1):
if not isinstance(source, dict):
raise WikiError(f"source {index} must be a record")
resource = str(source.get("resource", "") or "").strip()
title = str(source.get("title", "") or "").strip()
if not resource:
continue
if resource.startswith(("http://", "https://")):
parsed = urlsplit(resource)
if not parsed.netloc:
raise WikiError(f"public source {index} is not a valid absolute URL")
if _looks_like_matter_reference(resource, title):
raise WikiError(
f"source {index} looks like a matter document; its URL and "
"title cannot be persisted in the wiki"
)
source.setdefault("kind", "public")
if source.get("kind") != "public":
raise WikiError(f"URL source {index} kind must be public")
continue
local = Path(resource).expanduser()
if source.get("kind") != "authorised-local":
raise WikiError(
f"local source {index} requires explicit kind: authorised-local"
)
if path_has_symlink_component(local):
raise WikiError(f"local source {index} may not use a symbolic-link path")
if not local.is_file():
raise WikiError(f"local source {index} is unavailable at {local}")
resolved = local.resolve(strict=True)
if _looks_like_matter_reference(f"{resource} {resolved}", title):
raise WikiError(
f"source {index} looks like a matter document; its path and title "
"cannot be persisted in the wiki"
)
digest = hashlib.sha256()
with resolved.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
actual_sha = digest.hexdigest()
if source.get("sha256") and source.get("sha256") != actual_sha:
raise WikiError(f"local source {index} changed since its recorded hash")
source["sha256"] = actual_sha
source.setdefault("last_checked", date.today().isoformat())
def cmd_rename(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
src, src_path = safe_note_path(wiki, args.src, must_exist=True)
dst, dst_path = safe_note_path(wiki, args.dest)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if dst_path.exists():
print(f"Error: {dst} already exists", file=sys.stderr)
return 1
if args.dry_run:
print(f"dry-run: would rename {src} → {dst}")
return 0
with WikiLock(wiki):
version_file = snapshot_note(wiki, src)
text = src_path.read_text(encoding="utf-8")
dst_path.parent.mkdir(parents=True, exist_ok=True)
atomic_write_text(dst_path, text)
src_path.unlink()
record = append_history(
wiki,
"rename_note",
src,
args.actor,
content_sha256=text_sha256(text),
extra={"to": dst, "version_file": version_file},
)
_regenerate(wiki)
inbound = 0
for rel in wiki_notes(wiki):
if rel != dst and Path(src).name in (wiki / rel).read_text(encoding="utf-8"):
inbound += 1
print(f"renamed {src} → {dst} · seq {record['seq']}")
if inbound:
print(f"note: {inbound} note(s) may still link to the old name")
return 0
def cmd_delete(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
rel, target = safe_note_path(wiki, args.note, must_exist=True)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if args.dry_run:
print(f"dry-run: would tombstone {rel} (recoverable from versions)")
return 0
with WikiLock(wiki):
text = target.read_text(encoding="utf-8")
version_file = snapshot_note(wiki, rel)
target.unlink()
record = append_history(
wiki,
"delete_note",
rel,
args.actor,
prior_content_sha256=text_sha256(text),
extra={"version_file": version_file},
)
_regenerate(wiki)
print(f"tombstoned {rel} · seq {record['seq']} · recoverable from versions")
return 0
def cmd_purge(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
rel, target = safe_note_path(wiki, args.note, must_exist=True)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
guard = _reason_guard(args.reason)
if guard:
print(f"REFUSED: {guard}", file=sys.stderr)
return 1
if not args.yes:
print(
"REFUSED: purge destroys content AND versions; re-run with --yes",
file=sys.stderr,
)
return 1
with WikiLock(wiki):
prior_sha = None
if target.exists():
prior_sha = text_sha256(target.read_text(encoding="utf-8"))
target.unlink()
vdir = sidecar(wiki) / "versions" / note_dirname(rel)
removed_versions = 0
if vdir.is_dir():
for mdv in vdir.iterdir():
mdv.unlink()
removed_versions += 1
vdir.rmdir()
record = append_history(
wiki,
"purge_note",
rel,
args.actor,
prior_content_sha256=prior_sha,
extra={"reason": args.reason},
)
_regenerate(wiki)
print(
f"purged {rel} · seq {record['seq']} · {removed_versions} version(s) destroyed"
)
return 0
def cmd_merge(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
into, into_path = safe_note_path(wiki, args.into, must_exist=True)
from_pairs = [
safe_note_path(wiki, rel, must_exist=True) for rel in args.from_notes
]
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
froms = [rel for rel, _path in from_pairs]
try:
merged_draft = Path(args.file).read_text(encoding="utf-8")
fm, body = split_document(merged_draft)
except (OSError, FrontmatterError) as exc:
print(f"Error: merged draft: {exc}", file=sys.stderr)
return 2
gate_error = _load_gate_report(args.gate_report, into, merged_draft)
if gate_error:
print(f"REFUSED: {gate_error}", file=sys.stderr)
return 1
if fm is None:
print("Error: merged draft has no frontmatter", file=sys.stderr)
return 2
fm["merged_from"] = sorted(froms)
try:
_prepare_and_validate_sources(fm)
_validate_source_ids(wiki, into, fm)
_enforce_grounding(fm)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
normalized = emit_document(fm, body)
findings = note_findings(into, normalized)
if not findings.ok:
for f in findings.conformance + findings.profile:
print(f"REFUSED: {f}", file=sys.stderr)
return 1
with WikiLock(wiki):
prior_sha = text_sha256(into_path.read_text(encoding="utf-8"))
version_file = snapshot_note(wiki, into)
from_versions = []
for rel, from_path in from_pairs:
from_versions.append(snapshot_note(wiki, rel))
from_path.unlink()
atomic_write_text(into_path, normalized)
record = append_history(
wiki,
"merge_notes",
into,
args.actor,
origin=str(fm.get("origin") or "") or None,
content_sha256=text_sha256(normalized),
prior_content_sha256=prior_sha,
extra={
"merged_from": sorted(froms),
"version_file": version_file,
"from_versions": from_versions,
},
)
_regenerate(wiki)
print(f"merged {', '.join(froms)} into {into} · seq {record['seq']}")
return 0
def _stamp_verified(fm: dict, by: str) -> None:
entries = normalize_verified(fm.get("verified"))
entries.append({"by": by, "at": utc_now_iso()})
fm["verified"] = entries[0] if len(entries) == 1 else entries
def cmd_accept(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
rel, target = safe_note_path(wiki, args.note, must_exist=True)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if not args.by.startswith("human:"):
print(
"Error: --by must be a human:<id> actor (a human accepts)", file=sys.stderr
)
return 2
try:
fm, body = split_document(target.read_text(encoding="utf-8"))
except (OSError, FrontmatterError) as exc:
print(f"Error: {rel}: {exc}", file=sys.stderr)
return 2
if not fm or str(fm.get("pending", "")).strip().lower() != "true":
print(f"Error: {rel} carries no pending suggestion", file=sys.stderr)
return 1
fm.pop("pending", None)
if str(fm.get("status", "")) == "draft":
fm["status"] = "stable"
try:
_enforce_grounding(fm)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
_stamp_verified(fm, args.by)
normalized = emit_document(fm, body)
with WikiLock(wiki):
version_file = snapshot_note(wiki, rel)
prior_sha = text_sha256(target.read_text(encoding="utf-8"))
atomic_write_text(target, normalized)
record = append_history(
wiki,
"accept_suggestion",
rel,
args.by,
origin=str(fm.get("origin") or "") or None,
content_sha256=text_sha256(normalized),
prior_content_sha256=prior_sha,
extra={"version_file": version_file},
)
_regenerate(wiki)
print(f"accepted {rel} · now human-reviewed · seq {record['seq']}")
return 0
def cmd_decline(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
rel, target = safe_note_path(wiki, args.note, must_exist=True)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
try:
fm, _body = split_document(target.read_text(encoding="utf-8"))
except (OSError, FrontmatterError) as exc:
print(f"Error: {rel}: {exc}", file=sys.stderr)
return 2
if not fm or str(fm.get("pending", "")).strip().lower() != "true":
print(f"Error: {rel} carries no pending suggestion", file=sys.stderr)
return 1
with WikiLock(wiki):
prior_sha = text_sha256(target.read_text(encoding="utf-8"))
version_file = snapshot_note(wiki, rel)
target.unlink()
record = append_history(
wiki,
"decline_suggestion",
rel,
args.actor,
prior_content_sha256=prior_sha,
extra={"version_file": version_file},
)
_regenerate(wiki)
print(f"declined {rel} · seq {record['seq']} · recoverable from versions")
return 0
def cmd_verify_note(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
rel, target = safe_note_path(wiki, args.note, must_exist=True)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if not args.by.startswith("human:"):
print("Error: --by must be a human:<id> actor", file=sys.stderr)
return 2
try:
fm, body = split_document(target.read_text(encoding="utf-8"))
except (OSError, FrontmatterError) as exc:
print(f"Error: {rel}: {exc}", file=sys.stderr)
return 2
if fm is None:
print(f"Error: {rel} has no frontmatter", file=sys.stderr)
return 2
try:
_enforce_grounding(fm)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
_stamp_verified(fm, args.by)
normalized = emit_document(fm, body)
with WikiLock(wiki):
version_file = snapshot_note(wiki, rel)
prior_sha = text_sha256(target.read_text(encoding="utf-8"))
atomic_write_text(target, normalized)
record = append_history(
wiki,
"update_note",
rel,
args.by,
origin=str(fm.get("origin") or "") or None,
content_sha256=text_sha256(normalized),
prior_content_sha256=prior_sha,
extra={"version_file": version_file},
)
_regenerate(wiki)
print(f"verified {rel} · now human-reviewed · seq {record['seq']}")
return 0
def _browser_payload(
wiki: Path, *, selected_note: str | None = None, topic: str | None = None
) -> dict:
"""Return display-safe, full-content knowledge for a reader view."""
wanted_note = None
if selected_note:
wanted_note, _target = safe_note_path(wiki, selected_note, must_exist=True)
topic_folded = topic.casefold().strip() if topic else ""
notes: list[dict] = []
for rel in wiki_notes(wiki):
if wanted_note and rel != wanted_note:
continue
try:
fm, body = split_document((wiki / rel).read_text(encoding="utf-8"))
except (OSError, FrontmatterError):
continue
if not fm:
continue
note_type = str(fm.get("type", "") or "")
status = str(fm.get("status", "") or "stable")
pending = str(fm.get("pending", "")).strip().casefold() == "true"
if note_type == "Position" or pending or status == "deprecated":
continue
tags = [str(tag) for tag in fm.get("tags", []) or []]
topic_fields = (
rel,
str(fm.get("title", "") or ""),
str(fm.get("practice_area", "") or ""),
str(fm.get("jurisdiction", "") or ""),
note_type,
" ".join(tags),
)
if topic_folded and topic_folded not in " ".join(topic_fields).casefold():
continue
sources = []
for source in fm.get("sources", []) or []:
if not isinstance(source, dict):
continue
display_source = {
key: str(source.get(key, "") or "").strip()
for key in ("id", "title", "resource", "pinpoint")
if source.get(key)
}
if display_source:
sources.append(display_source)
related = []
for target in re.findall(r"\]\(([^)]+)\)", body):
clean = target.split("#", 1)[0].split("?", 1)[0]
if (
not clean.endswith(".md")
or Path(clean).is_absolute()
or urlsplit(clean).scheme
):
continue
resolved = (Path(rel).parent / clean).as_posix()
related.append(Path(os.path.normpath(resolved)).as_posix())
notices = []
if status in ("disputed", "outdated"):
notices.append(status)
if is_stale(fm.get("stale_after")):
notices.append("stale")
if trust_tier(fm) != "human-reviewed":
notices.append("unverified")
notes.append(
{
"id": rel,
"title": str(fm.get("title", "") or rel),
"type": note_type,
"topic": str(fm.get("practice_area", "") or ""),
"jurisdiction": str(fm.get("jurisdiction", "") or ""),
"body": body.strip(),
"sources": sources,
"related": sorted(set(related)),
"notices": notices,
}
)
notes.sort(key=lambda note: (note["topic"].casefold(), note["title"].casefold()))
included = {note["id"] for note in notes}
for note in notes:
note["related"] = [rel for rel in note["related"] if rel in included]
manifest = load_manifest(wiki, required=False) or {}
return {
"schema": 1,
"title": str(manifest.get("name", "") or "Legal wiki"),
"scope": {"note": wanted_note, "topic": topic or None},
"coverage": {
"scope": "selected" if wanted_note or topic else "all-eligible",
"note_count": len(notes),
"note_ids": [note["id"] for note in notes],
},
"notes": notes,
}
def cmd_browse(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
try:
payload = _browser_payload(
wiki,
selected_note=getattr(args, "note", None),
topic=getattr(args, "topic", None),
)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if args.json:
print(json.dumps(payload, indent=2, ensure_ascii=False))
return 0
notes = payload["notes"]
if not notes:
print("No reusable knowledge found in this browse scope.")
return 0
print(f"# {markdown_text(payload['title'])}")
for note in notes:
print(f"\n## {markdown_text(note['title'])}\n")
if note["notices"]:
warning = ", ".join(str(value) for value in note["notices"])
print(f"**Check before relying:** {markdown_text(warning)}\n")
print(note["body"] or "_No note body._")
if note["sources"]:
print("\n### Sources\n")
for source in note["sources"]:
label = source.get("title") or source.get("id") or "Source"
resource = source.get("resource")
rendered = (
markdown_link(label, resource) if resource else markdown_text(label)
)
pinpoint = source.get("pinpoint")
suffix = f" — {markdown_text(pinpoint)}" if pinpoint else ""
print(f"- {rendered}{suffix}")
return 0
def _ask_tokens(text: str) -> set[str]:
return {
token[:-1] if token.endswith("s") and len(token) > 3 else token
for token in re.findall(r"[a-z0-9][a-z0-9-]{2,}", text.casefold())
}
def cmd_ask(args: argparse.Namespace) -> int:
"""Manual, read-only deterministic search. The model writes the answer."""
wiki = _wiki_arg(args)
if args.limit <= 0:
print("Error: --limit must be a positive integer", file=sys.stderr)
return 2
query = _ask_tokens(args.query)
matches: list[tuple[float, dict]] = []
for rel in wiki_notes(wiki):
try:
fm, body = split_document((wiki / rel).read_text(encoding="utf-8"))
except (OSError, FrontmatterError):
continue
if not fm or str(fm.get("pending", "")).casefold() == "true":
continue
status = str(fm.get("status", "") or "stable")
if status == "deprecated":
continue
title = str(fm.get("title", "") or rel)
tags = [str(v) for v in fm.get("tags", []) or []]
source_text = " ".join(
" ".join(
str(source.get(key, "") or "") for key in ("id", "title", "resource")
)
for source in (fm.get("sources", []) or [])
if isinstance(source, dict)
)
haystack = " ".join(
[
title,
str(fm.get("description", "") or ""),
str(fm.get("trigger", "") or ""),
" ".join(tags),
source_text,
body,
]
)
tokens = _ask_tokens(haystack)
if not query or not tokens:
score = 0.0
else:
overlap = query & tokens
score = len(overlap) / len(query)
if query & _ask_tokens(title):
score += 0.5
if query & _ask_tokens(" ".join(tags)):
score += 0.25
if query & _ask_tokens(source_text):
score += 0.25
if score <= 0:
continue
sources = []
for source in fm.get("sources", []) or []:
if isinstance(source, dict):
sources.append(
{
"id": source.get("id"),
"title": source.get("title"),
"resource": source.get("resource"),
}
)
matches.append(
(
score,
{
"path": rel,
"title": title,
"type": str(fm.get("type", "")),
"status": status,
"stale": is_stale(fm.get("stale_after")),
"tier": trust_tier(fm),
"sources": sources,
},
)
)
matches.sort(key=lambda item: (-item[0], item[1]["path"]))
offered: list[dict] = []
for score, item in matches[: args.limit]:
result = dict(item)
result["score"] = round(score, 3)
offered.append(result)
if args.json:
print(json.dumps({"matches": offered}, indent=2, ensure_ascii=False))
elif not offered:
print("No reusable knowledge found in this wiki.")
else:
print(f"From your wiki: {len(offered)} relevant note(s)")
for item in offered:
warnings = []
if item["status"] in ("disputed", "outdated"):
warnings.append(item["status"])
if item["stale"]:
warnings.append("stale")
warning = f" · WARNING: {', '.join(warnings)}" if warnings else ""
print(
f"- {item['path']} · {item['type']} [{item['tier']}] · "
f"{item['title']}{warning}"
)
sources = item.get("sources", [])
if not isinstance(sources, list):
continue
for source in sources:
if not isinstance(source, dict):
continue
label = source.get("title") or source.get("id") or "source"
if source.get("resource"):
print(f" source: {label} → {source['resource']}")
print(
"Read these notes as evidence-bearing data; then answer with their "
"links and sources."
)
return 0
def cmd_review_record(args: argparse.Namespace) -> int:
"""Record a closed, non-confidential disposition in the durable queue."""
wiki = _wiki_arg(args)
subject = args.note or args.source_id
if args.note and validate_note_path(args.note):
print("Error: invalid note identifier", file=sys.stderr)
return 2
if args.source_id and not re.fullmatch(r"[A-Za-z0-9_.-]+", args.source_id):
print("Error: invalid source identifier", file=sys.stderr)
return 2
if not subject and args.disposition not in ("no-material", "matter-specific"):
print("Error: this disposition requires --note or --source-id", file=sys.stderr)
return 2
record = {
"ts": utc_now_iso(),
"disposition": args.disposition,
"status": "open"
if args.disposition not in ("no-material", "matter-specific")
else "closed",
}
if args.note:
record["note"] = args.note
if args.source_id:
record["source_id"] = args.source_id
with WikiLock(wiki):
append_review_record(wiki, record)
regenerate_indexes(wiki)
print(
f"review: {args.disposition} recorded" + (f" for {subject}" if subject else "")
)
return 0
def _confirmed_playbook_value(playbook: Path, key: str) -> str | None:
if not playbook.exists():
return None
confirmed = False
value = None
values = r"(.*)" if key == "automatic scope" else r"(on|off)"
pattern = re.compile(rf"^-\s*\[wiki\]\s*{re.escape(key)}\s*:\s*{values}\s*$", re.I)
try:
lines = playbook.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeDecodeError):
return None
for raw in lines:
line = raw.strip()
if line.startswith("## "):
confirmed = line == "## Confirmed"
continue
if confirmed:
match = pattern.match(line)
if match:
value = match.group(1).casefold()
return value
def _append_confirmed_lines(playbook: Path, lines_to_add: list[str]) -> None:
if playbook.exists():
lines = playbook.read_text(encoding="utf-8").splitlines()
else:
lines = ["# LQ playbook", "", "## Confirmed", ""]
try:
start = next(
i for i, line in enumerate(lines) if line.strip() == "## Confirmed"
)
except StopIteration:
if lines and lines[-1].strip():
lines.append("")
lines.extend(["## Confirmed", ""])
start = len(lines) - 2
end = next(
(i for i in range(start + 1, len(lines)) if lines[i].startswith("## ")),
len(lines),
)
insertion = end
while insertion > start + 1 and not lines[insertion - 1].strip():
insertion -= 1
lines[insertion:insertion] = lines_to_add + [""]
atomic_write_text(playbook, "\n".join(lines).rstrip() + "\n")
def automation_scope() -> tuple[str, list[str]]:
"""Only a confirmed scope selects immutable operational project metadata.
Missing scope preserves previously enabled global settings. New enablement
must choose a scope. Project paths never enter the playbook or the wiki.
"""
selection = _confirmed_playbook_value(user_playbook_path(), "automatic scope")
if selection is None or selection == "all":
return "all", []
if not re.fullmatch(r"projects:[a-f0-9]{64}", selection):
return "unavailable", []
digest = selection.partition(":")[2]
path = user_registry_path().parent / "automation-scopes" / f"{digest}.json"
try:
policy = json.loads(path.read_text(encoding="utf-8"))
roots = policy["roots"]
if (
canonical_json_digest(policy) != digest
or policy.get("schema") != 1
or not isinstance(roots, list)
or any(
not isinstance(root, str) or not Path(root).is_absolute()
for root in roots
)
):
return "unavailable", []
return "projects", roots
except (OSError, ValueError, KeyError, TypeError):
return "unavailable", []
def automation_scope_allows(cwd: object) -> bool:
scope, roots = automation_scope()
if scope == "all":
return True
if scope != "projects" or not isinstance(cwd, str) or not cwd:
return False
try:
current = Path(cwd)
if not current.is_absolute() or not current.is_dir():
return False
current = current.resolve(strict=True)
for root in roots:
saved = Path(root)
# A root moved or redirected by a symlink needs selecting again.
if saved.is_dir() and saved.resolve(strict=True) == saved:
if current.is_relative_to(saved):
return True
except (OSError, ValueError, RuntimeError):
return False
return False
def cmd_automation(args: argparse.Namespace) -> int:
"""Preview and confirm automatic retrieval within an explicit scope."""
_wiki_arg(args)
playbook = user_playbook_path()
current_retrieval = (
_confirmed_playbook_value(playbook, "automatic retrieval") or "off"
)
if args.intake == "on":
raise WikiError(
"automatic intake is unavailable; ask Wiki to save the reusable "
"lessons from this conversation"
)
scope, roots = automation_scope()
requested = []
policy = None
scope_choice = getattr(args, "scope", None)
additions = getattr(args, "project", None) or []
removals = getattr(args, "remove_project", None) or []
if scope_choice == "all" and (additions or removals):
raise WikiError("choose All projects or selected project folders")
if scope_choice == "projects" or additions or removals:
if scope != "projects":
roots = []
for value in additions:
root = Path(value).expanduser().resolve()
if not root.is_dir():
raise WikiError("select an existing project folder")
roots.append(str(root))
removed = {str(Path(value).expanduser().resolve()) for value in removals}
roots = sorted(set(roots) - removed)
if not roots and (
not removals or (args.retrieval or current_retrieval) != "off"
):
raise WikiError("select at least one project, or turn retrieval off")
policy = {"schema": 1, "roots": roots}
scope = "projects"
requested.append(
f"- [wiki] automatic scope: projects:{canonical_json_digest(policy)}"
)
elif scope_choice == "all":
scope, roots = "all", []
requested.append("- [wiki] automatic scope: all")
# Preserve a cleanup path for installations that previously saved this
# setting. The current hook overlay never reads it.
if args.intake == "off":
requested.append("- [wiki] automatic intake: off")
if args.retrieval:
requested.append(f"- [wiki] automatic retrieval: {args.retrieval}")
enabling = args.retrieval == "on"
if enabling and not scope_choice and not additions and not removals:
if _confirmed_playbook_value(playbook, "automatic scope") is None:
raise WikiError("choose Selected projects or All projects with --scope")
if enabling and (scope == "unavailable" or (scope == "projects" and not roots)):
raise WikiError("select the project folders again to enable automation")
label = (
"All projects"
if scope == "all"
else "Selected projects"
if scope == "projects"
else "Select project folders again"
)
print(f"Wiki automation · {label}")
for root in roots:
print(f" {root} (including subfolders)")
print(f"Bring in relevant notes: {args.retrieval or current_retrieval}")
print("Save new knowledge: on request · Manual Add/Ask/Browse/Check available.")
if not requested:
return 0
if not args.yes:
print(
"Preview only. Confirm this scope and retrieval setting to save the change."
)
# Exact preference lines are reviewable without storing project paths here.
for line in requested:
print(line)
return 0
if policy is not None:
path = (
user_registry_path().parent
/ "automation-scopes"
/ f"{canonical_json_digest(policy)}.json"
)
path.parent.mkdir(parents=True, exist_ok=True)
atomic_write_text(path, json.dumps(policy, indent=2) + "\n")
_append_confirmed_lines(playbook, requested)
print("Wiki automation settings saved.")
return 0
def cmd_export_map(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
nodes = []
for rel in wiki_notes(wiki):
try:
fm, body = split_document((wiki / rel).read_text(encoding="utf-8"))
except FrontmatterError:
continue
if not fm:
continue
note_type = str(fm.get("type", ""))
if note_type == "Position" and not args.include_positions:
continue # Positions stay local; they never leave the machine
links = []
for target in re.findall(r"\]\(([^)]+\.md)\)", body):
resolved = (Path(rel).parent / target).as_posix()
links.append(str(Path(os.path.normpath(resolved)).as_posix()))
nodes.append(
{
"id": rel,
"title": str(fm.get("title", "") or rel),
"type": note_type,
"tier": trust_tier(fm),
"status": str(fm.get("status", "") or "stable"),
"pending": str(fm.get("pending", "")).strip().lower() == "true",
"stale": is_stale(fm.get("stale_after")),
"tags": [str(t) for t in fm.get("tags", []) or []],
"practice_area": str(fm.get("practice_area", "") or ""),
"document_kind": str(fm.get("document_kind", "") or ""),
"links": sorted(links),
"sources": [
{
"id": source.get("id"),
"title": source.get("title"),
"resource": source.get("resource"),
}
for source in (fm.get("sources", []) or [])
if isinstance(source, dict)
],
}
)
review = read_review_records(wiki)
payload = {
"schema": 2,
"wiki": load_manifest(wiki, required=False),
"summary": {
"notes": len(nodes),
"pending": sum(1 for n in nodes if n["pending"]),
"stale": sum(1 for n in nodes if n["stale"]),
"disputed": sum(1 for n in nodes if n["status"] == "disputed"),
"open_review": sum(1 for r in review if r.get("status") == "open"),
},
"nodes": nodes,
}
output = json.dumps(payload, indent=2, ensure_ascii=False)
if args.out:
atomic_write_text(Path(args.out), output + "\n")
print(f"export-map: {len(nodes)} node(s) → {args.out}")
else:
print(output)
return 0
def cmd_verify(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
records = read_history(wiki)
broken = verify_chain(records)
state = replay_note_state(records)
drift: list[str] = []
on_disk = set(wiki_notes(wiki))
for note, sha in state.items():
if note not in on_disk:
drift.append(f"{note}: recorded in history but missing on disk")
elif text_sha256((wiki / note).read_text(encoding="utf-8")) != sha:
drift.append(f"{note}: changed on disk since last recorded state")
for note in sorted(on_disk - set(state)):
drift.append(f"{note}: on disk but never recorded in history")
if args.json:
print(
json.dumps(
{
"ok": not broken and not drift,
"broken_links": broken,
"drift": drift,
},
indent=2,
ensure_ascii=False,
)
)
else:
for b in broken:
print(f"BROKEN seq {b['seq']}: {b['reason']}")
for d in drift:
print(f"DRIFT {d}")
verdict = "PASS" if not broken and not drift else "FAIL"
print(
f"verify: {len(records)} record(s), {len(broken)} broken link(s), "
f"{len(drift)} drift note(s) — {verdict}"
)
return 0 if not broken and not drift else 1
def cmd_status(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
records = read_history(wiki)
broken = verify_chain(records)
if broken:
print(
"REFUSED: history chain is broken; repair it before rollback",
file=sys.stderr,
)
return 1
notes = wiki_notes(wiki)
pending = deprecated = stale = disputed = outdated = 0
for rel in notes:
try:
fm, _ = split_document((wiki / rel).read_text(encoding="utf-8"))
except FrontmatterError:
continue
if not fm:
continue
if str(fm.get("pending", "")).strip().lower() == "true":
pending += 1
if str(fm.get("status", "")) == "deprecated":
deprecated += 1
if str(fm.get("status", "")) == "disputed":
disputed += 1
if str(fm.get("status", "")) == "outdated":
outdated += 1
if is_stale(fm.get("stale_after")):
stale += 1
declined = sum(1 for r in records if r.get("op") == "decline_suggestion")
state = replay_note_state(records)
drift = sum(
1
for note, sha in state.items()
if (wiki / note).exists()
and text_sha256((wiki / note).read_text(encoding="utf-8")) != sha
)
review = read_review_records(wiki)
open_review = sum(1 for r in review if r.get("status") == "open")
sources: set[str] = set()
for rel in notes:
try:
fm, _ = split_document((wiki / rel).read_text(encoding="utf-8"))
except FrontmatterError:
continue
if fm:
for source in fm.get("sources", []) or []:
if isinstance(source, dict):
source_id = source.get("id") or source.get("resource")
if source_id:
sources.add(str(source_id))
print(
f"notes {len(notes)} · pending {pending} · declined {declined} · "
f"disputed {disputed} · outdated {outdated} · deprecated {deprecated} · "
f"stale {stale} · sources {len(sources)} · review {open_review} open · "
f"unreviewed disk changes {drift}"
)
return 0
def cmd_rollback(args: argparse.Namespace) -> int:
wiki = _wiki_arg(args)
if not args.yes:
print(
"REFUSED: rollback protection active — re-run with --yes to restore "
f"the wiki to seq {args.to}",
file=sys.stderr,
)
return 1
records = read_history(wiki)
seqs: list[int] = [
seq for r in records if isinstance((seq := r.get("seq")), int) and seq > 0
]
if not seqs or args.to < 0 or args.to > max(seqs):
print(
f"Error: --to {args.to} is outside the chain (1..{max(seqs or [0])})",
file=sys.stderr,
)
return 2
target_state = replay_note_state(records, upto_seq=args.to)
try:
safe_targets = {note: safe_note_path(wiki, note)[1] for note in target_state}
except WikiError as exc:
print(f"REFUSED: unsafe history path: {exc}", file=sys.stderr)
return 1
pool = _content_pool(wiki)
restored, removed, unrecoverable = [], [], []
with WikiLock(wiki):
on_disk = set(wiki_notes(wiki))
for note, sha in target_state.items():
target = safe_targets[note]
current = target if target.exists() else None
if current and text_sha256(current.read_text(encoding="utf-8")) == sha:
continue
text = pool.get(sha)
if text is None:
unrecoverable.append(note)
continue
snapshot_note(wiki, note)
target.parent.mkdir(parents=True, exist_ok=True)
atomic_write_text(target, text)
restored.append(note)
for note in sorted(on_disk - set(target_state)):
_rel, target = safe_note_path(wiki, note, must_exist=True)
snapshot_note(wiki, note)
target.unlink()
removed.append(note)
append_history(wiki, "rollback", None, args.actor, extra={"to_seq": args.to})
_regenerate(wiki)
print(
f"rolled back to seq {args.to}: {len(restored)} restored, "
f"{len(removed)} removed, {len(unrecoverable)} unrecoverable"
)
for note in unrecoverable:
print(f"cannot restore {note}: content was purged", file=sys.stderr)
return 0 if not unrecoverable else 1
# ---------------------------------------------------------------------------
# Verbs
# ---------------------------------------------------------------------------
def cmd_check(args: argparse.Namespace) -> int:
root = Path(args.path)
if not root.is_dir():
print(f"Error: {root} is not a directory", file=sys.stderr)
return 2
today = None
if args.today:
try:
today = date.fromisoformat(args.today)
except ValueError:
print(f"Error: --today {args.today!r} is not YYYY-MM-DD", file=sys.stderr)
return 2
report = check_wiki(root, today)
if args.json:
print(json.dumps(report.as_dict(), indent=2, ensure_ascii=False))
else:
for finding in report.conformance:
print(f"CONFORMANCE FAIL {finding}")
for finding in report.profile:
print(f"PROFILE FAIL {finding}")
for finding in report.warnings:
print(f"WARN {finding}")
verdict = "PASS" if report.ok else "FAIL"
print(
f"check: {len(report.conformance)} conformance, "
f"{len(report.profile)} profile, {len(report.warnings)} warnings "
f"— {verdict}"
)
return 0 if report.ok else 1
def cmd_normalize(args: argparse.Namespace) -> int:
path = Path(args.file)
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
print(f"Error: cannot read {path}: {exc}", file=sys.stderr)
return 2
try:
fm, body = split_document(text)
except FrontmatterError as exc:
print(f"Error: {path}: {exc}", file=sys.stderr)
return 2
sys.stdout.write(emit_document(fm, body))
return 0
def cmd_version(_args: argparse.Namespace) -> int:
print(
f"wiki engine {ENGINE_VERSION} · state schema {SCHEMA_VERSION} "
f"· okf {OKF_VERSION}"
)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="wiki.py",
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
sub = parser.add_subparsers(dest="verb", required=True)
p_check = sub.add_parser("check", help="validate a wiki directory")
p_check.add_argument("path", help="wiki root directory")
p_check.add_argument("--json", action="store_true", help="machine-readable report")
p_check.add_argument("--today", help="override today (YYYY-MM-DD) for staleness")
p_check.set_defaults(func=cmd_check)
p_norm = sub.add_parser("normalize", help="print a note's canonical form")
p_norm.add_argument("file", help="markdown note file")
p_norm.set_defaults(func=cmd_normalize)
p_ver = sub.add_parser("version", help="print engine and schema versions")
p_ver.set_defaults(func=cmd_version)
def wikied(name: str, help_text: str) -> argparse.ArgumentParser:
p = sub.add_parser(name, help=help_text)
p.add_argument("--wiki", help="wiki root directory (wins over any registry)")
p.add_argument("--wiki-name", help="pick a registered wiki by name")
p.add_argument("--actor", default=DEFAULT_ACTOR, help="who is acting")
return p
p_init = sub.add_parser("init", help="initialise and register a wiki")
p_init.add_argument("--wiki", required=True)
p_init.add_argument("--name", default="main", help="registry name for this wiki")
p_init.add_argument(
"--no-user-registry",
action="store_true",
help="do not record this wiki in the user-level registry",
)
p_init.set_defaults(func=cmd_init)
p_setup = sub.add_parser("setup", help="first-run welcome / current config")
p_setup.add_argument("--json", action="store_true")
p_setup.set_defaults(func=cmd_setup)
p_vl = sub.add_parser("wiki-list", help="list registered wikis")
p_vl.add_argument("--json", action="store_true")
p_vl.set_defaults(func=cmd_wiki_list)
p_vu = sub.add_parser("wiki-use", help="set the default wiki by name")
p_vu.add_argument("name")
p_vu.set_defaults(func=cmd_wiki_use)
p_land = wikied("land", "land a gated note draft into the wiki")
p_land.add_argument("--file", required=True, help="the draft note file")
p_land.add_argument("--dest", required=True, help="wiki-relative destination")
p_land.add_argument("--origin", required=True, choices=ORIGINS)
p_land.add_argument("--mode", default="auto", choices=("auto", "markup"))
p_land.add_argument("--gate-report", help="JSON verdict from the gate")
p_land.add_argument(
"--replace", action="store_true", help="update an existing note"
)
p_land.add_argument("--dry-run", action="store_true")
p_land.set_defaults(func=cmd_land)
p_rn = wikied("rename", "rename a note; history keeps everything")
p_rn.add_argument("src")
p_rn.add_argument("dest")
p_rn.add_argument("--dry-run", action="store_true")
p_rn.set_defaults(func=cmd_rename)
p_del = wikied("delete", "tombstone a note (recoverable)")
p_del.add_argument("note")
p_del.add_argument("--dry-run", action="store_true")
p_del.set_defaults(func=cmd_delete)
p_purge = wikied("purge", "destroy a note AND its versions (leak remedy)")
p_purge.add_argument("note")
p_purge.add_argument("--reason", required=True, choices=PURGE_REASONS)
p_purge.add_argument("--yes", action="store_true")
p_purge.set_defaults(func=cmd_purge)
p_merge = wikied("merge", "replace a note with a merged draft, retiring sources")
p_merge.add_argument("--into", required=True)
p_merge.add_argument("--from", dest="from_notes", action="append", required=True)
p_merge.add_argument("--file", required=True, help="the merged draft")
p_merge.add_argument("--gate-report", help="JSON verdict from the gate")
p_merge.set_defaults(func=cmd_merge)
p_vfy = wikied("verify", "verify the tamper-evident history chain")
p_vfy.add_argument("--json", action="store_true")
p_vfy.set_defaults(func=cmd_verify)
p_st = wikied("status", "print the wiki receipt")
p_st.set_defaults(func=cmd_status)
p_rb = wikied("rollback", "restore the wiki to a prior chain state")
p_rb.add_argument("--to", required=True, type=int, help="target seq")
p_rb.add_argument("--yes", action="store_true")
p_rb.set_defaults(func=cmd_rollback)
p_gate = sub.add_parser("gate", help="deterministic method-not-matter gate")
p_gate.add_argument("--candidate", required=True, help="candidate note file")
p_gate.add_argument("--denylist", help="hashed denylist JSON")
p_gate.add_argument("--out", help="write the verdict JSON here")
p_gate.set_defaults(func=cmd_gate)
p_dl = sub.add_parser("denylist-add", help="hash names into a denylist")
p_dl.add_argument("--file", required=True, help="denylist JSON (created if absent)")
p_dl.add_argument("names", nargs="+", help="names to hash (never stored as text)")
p_dl.set_defaults(func=cmd_denylist_add)
p_scan = sub.add_parser("seed-scan", help="deterministic seed-file inventory")
p_scan.add_argument("files", nargs="+")
p_scan.add_argument("--json", action="store_true")
p_scan.set_defaults(func=cmd_seed_scan)
p_emit = sub.add_parser("seed-emit", help="emit seed work items for the model")
p_emit.add_argument("files", nargs="+")
p_emit.add_argument("--out-dir", required=True)
p_emit.add_argument(
"--profile",
default=str(
Path(__file__).parent.parent / "references" / "source_profile.json"
),
)
p_emit.set_defaults(func=cmd_seed_emit)
p_sland = wikied("seed-land", "verify, gate, and land filled seed items")
p_sland.add_argument("--items", required=True, help="directory of item-*.json")
p_sland.add_argument("--sources-root", required=True)
p_sland.add_argument("--denylist")
p_sland.add_argument("--mode", default="auto", choices=("auto", "markup"))
p_sland.add_argument("--json", action="store_true")
p_sland.set_defaults(func=cmd_seed_land)
default_profile = str(
Path(__file__).parent.parent / "references" / "source_profile.json"
)
p_cap = wikied("capture", "legacy raw-transcript staging (safely refused)")
p_cap.add_argument("--staging", required=True, help="pre-gate staging dir")
p_cap.add_argument("--session-dirs", nargs="+", help=argparse.SUPPRESS)
p_cap.add_argument(
"--transcript",
help=argparse.SUPPRESS,
)
p_cap.add_argument("--profile", default=default_profile)
p_cap.set_defaults(func=cmd_capture)
p_cland = wikied("capture-land", "legacy import of prepared items (manual only)")
p_cland.add_argument("--staging", required=True)
p_cland.add_argument("--denylist")
p_cland.add_argument("--mode", default="auto", choices=("auto", "markup"))
p_cland.add_argument("--profile", default=default_profile)
p_cland.set_defaults(func=cmd_capture_land)
p_browse = wikied("browse", "read full wiki knowledge without frontmatter")
p_browse.add_argument("--json", action="store_true")
p_browse.add_argument("--note", help="show one bundle-relative note in full")
p_browse.add_argument("--topic", help="show every matching topic note in full")
p_browse.set_defaults(func=cmd_browse)
p_ask = wikied("ask", "manual read-only search of the selected wiki")
p_ask.add_argument("query")
p_ask.add_argument("--limit", type=int, default=5)
p_ask.add_argument("--json", action="store_true")
p_ask.set_defaults(func=cmd_ask)
p_review = wikied("review-record", "record a closed review disposition")
p_review.add_argument("disposition", choices=REVIEW_DISPOSITIONS)
p_review.add_argument("--note")
p_review.add_argument("--source-id")
p_review.set_defaults(func=cmd_review_record)
p_auto = wikied("automation", "advanced optional lifecycle automation")
p_auto.add_argument("--intake", choices=("on", "off"), help=argparse.SUPPRESS)
p_auto.add_argument("--retrieval", choices=("on", "off"))
p_auto.add_argument("--scope", choices=("all", "projects"))
p_auto.add_argument(
"--project", action="append", help="add a project folder and its subfolders"
)
p_auto.add_argument(
"--remove-project", action="append", help="remove a selected project folder"
)
p_auto.add_argument("--yes", action="store_true")
p_auto.set_defaults(func=cmd_automation)
p_mt = wikied("maintain", "propose merges, flag staleness and contradictions")
p_mt.add_argument("--json", action="store_true")
p_mt.add_argument("--today", help="override today (YYYY-MM-DD) for staleness")
p_mt.set_defaults(func=cmd_maintain)
p_tw = wikied("tripwire", "auto-build quality metrics from the history")
p_tw.add_argument("--window-from", help="ISO timestamp lower bound")
p_tw.add_argument("--window-to", help="ISO timestamp upper bound")
p_tw.add_argument("--json", action="store_true")
p_tw.set_defaults(func=cmd_tripwire)
p_acc = wikied("accept", "accept a pending suggestion (human-reviewed)")
p_acc.add_argument("note")
p_acc.add_argument("--by", required=True, help="human:<id> actor")
p_acc.set_defaults(func=cmd_accept)
p_dec = wikied("decline", "decline a pending suggestion (recoverable)")
p_dec.add_argument("note")
p_dec.set_defaults(func=cmd_decline)
p_vn = wikied("verify-note", "confirm any note as human-reviewed")
p_vn.add_argument("note")
p_vn.add_argument("--by", required=True, help="human:<id> actor")
p_vn.set_defaults(func=cmd_verify_note)
p_map = wikied("export-map", "deterministic wiki-structure JSON")
p_map.add_argument("--out", help="write here instead of stdout")
p_map.add_argument(
"--include-positions",
action="store_true",
help="include Position notes (they stay local by default)",
)
p_map.set_defaults(func=cmd_export_map)
return parser
def main(argv: list[str]) -> int:
args = build_parser().parse_args(argv[1:])
try:
return args.func(args)
except WikiError as exc:
print(f"REFUSED: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
SHA-256: 519ea24db9e4e32a3d89e80a07349cd15ff8454a5be3bcd34f1a0146df9462a8