"""AtoCore Wiki β navigable HTML pages from structured data. A lightweight wiki served directly from the AtoCore API. Every page is generated on-demand from the database so it's always current. Source of truth is the database β the wiki is a derived view. Routes: /wiki Homepage with project list + search /wiki/projects/{name} Full project overview /wiki/entities/{id} Entity detail with relationships /wiki/search?q=... Search entities, memories, state """ from __future__ import annotations import markdown as md from atocore.context.project_state import get_state from atocore.engineering.service import ( get_entities, get_entity, get_entity_with_context, get_relationships, ) from atocore.memory.service import get_memories from atocore.projects.registry import ( GLOBAL_PROJECT, INBOX_PROJECT, load_project_registry, ) _TOP_NAV_LINKS = [ ("π Home", "/wiki"), ("π‘ Activity", "/wiki/activity"), ("π Triage", "/admin/triage"), ("π Dashboard", "/admin/dashboard"), ] def _render_topnav(active_path: str = "") -> str: items = [] for label, href in _TOP_NAV_LINKS: cls = "topnav-item active" if href == active_path else "topnav-item" items.append(f'{label}') return f'' def render_html(title: str, body_html: str, breadcrumbs: list[tuple[str, str]] | None = None, active_path: str = "") -> str: topnav = _render_topnav(active_path) crumbs = "" if breadcrumbs: parts = [] for label, href in breadcrumbs: if href: parts.append(f'{label}') else: parts.append(f"{label}") crumbs = f'' nav = topnav + crumbs return _TEMPLATE.replace("{{title}}", title).replace("{{nav}}", nav).replace("{{body}}", body_html) def render_homepage() -> str: projects = [] try: registered = load_project_registry() for p in registered: entity_count = len(get_entities(project=p.project_id, limit=200)) memory_count = len(get_memories(project=p.project_id, active_only=True, limit=200)) state_entries = get_state(p.project_id) # Pull stage/type/client from state entries stage = "" proj_type = "" client = "" for e in state_entries: if e.category == "status": if e.key == "stage": stage = e.value elif e.key == "type": proj_type = e.value elif e.key == "client": client = e.value projects.append({ "id": p.project_id, "description": p.description, "entities": entity_count, "memories": memory_count, "state": len(state_entries), "stage": stage, "type": proj_type, "client": client, }) except Exception: pass # Group by high-level bucket buckets: dict[str, list] = { "Active Contracts": [], "Leads & Prospects": [], "Internal Tools & Infra": [], "Other": [], } for p in projects: t = p["type"].lower() s = p["stage"].lower() if "lead" in t or "lead" in s or "prospect" in s: buckets["Leads & Prospects"].append(p) elif "contract" in t or ("active" in s and "contract" in s): buckets["Active Contracts"].append(p) elif "infra" in t or "tool" in t or "internal" in t: buckets["Internal Tools & Infra"].append(p) else: buckets["Other"].append(p) lines = ['
Autonomous actors: ' +
" Β· ".join(f'{k} ({v})' for k, v in auto_actors.items()) +
'
Entities that don\'t belong to a specific ' 'project yet. Inbox holds pre-project leads and quotes. ' 'Global holds cross-project facts (material properties, ' 'vendor capabilities) that apply everywhere.
' ) lines.append('Pre-project leads, quotes, early conversations.
' f'Cross-project facts: materials, vendors, shared knowledge.
' f'{p["description"][:140]}
') lines.append(f'Projects that appear in memories but aren\'t yet registered. ' 'One click to promote them to first-class projects.
') lines.append('{len(all_entities)} entities Β· {len(all_memories)} active memories Β· {len(projects)} projects
') # Triage queue prompt β surfaced prominently if non-empty if pending: tone = "triage-warning" if len(pending) > 50 else "triage-notice" lines.append( f'ποΈ {len(pending)} candidates awaiting triage β ' f'review now β
' ) lines.append(f'Triage Queue Β· API Dashboard (JSON) Β· Health Check
') return render_html("AtoCore Wiki", "\n".join(lines), active_path="/wiki") def render_project(project: str) -> str: from atocore.engineering.mirror import generate_project_overview markdown_content = generate_project_overview(project) # Resolve [[Wikilinks]] before markdown so redlinks / cross-project # indicators appear in the rendered HTML. (Issue B) markdown_content = _wikilink_transform(markdown_content, current_project=project) # Convert entity names to links entities = get_entities(project=project, limit=200) html_body = md.markdown(markdown_content, extensions=["tables", "fenced_code"]) for ent in sorted(entities, key=lambda e: len(e.name), reverse=True): linked = f'{ent.name}' html_body = html_body.replace(f"{ent.name}", f"{linked}", 1) return render_html( f"{project}", html_body, breadcrumbs=[("Wiki", "/wiki"), (project, "")], ) import re as _re _WIKILINK_PATTERN = _re.compile(r"\[\[([^\[\]|]+?)(?:\|([^\[\]]+?))?\]\]") def _resolve_wikilink(target: str, current_project: str | None) -> tuple[str, str, str]: """Resolve a ``[[Name]]`` target to ``(href, css_class, extra_suffix)``. Resolution order (Issue B): 1. Same-project exact name match β live link (class ``wikilink``). 2. Other-project exact name match β live link with ``(in project X)`` suffix (class ``wikilink wikilink-cross``). 3. No match β redlink pointing at ``/wiki/new?name=...`` so clicking opens a pre-filled "create this entity" form (class ``redlink``). """ needle = target.strip() if not needle: return ("/wiki/new", "redlink", "") same_project = None cross_project = None try: candidates = get_entities(name_contains=needle, limit=200) except Exception: candidates = [] lowered = needle.lower() for ent in candidates: if ent.name.lower() != lowered: continue if current_project and ent.project == current_project: same_project = ent break if cross_project is None: cross_project = ent if same_project is not None: return (f"/wiki/entities/{same_project.id}", "wikilink", "") if cross_project is not None: suffix = ( f' (in {cross_project.project})' if cross_project.project else ' (global)' ) return (f"/wiki/entities/{cross_project.id}", "wikilink wikilink-cross", suffix) from urllib.parse import quote href = f"/wiki/new?name={quote(needle)}" if current_project: href += f"&project={quote(current_project)}" return (href, "redlink", "") def _wikilink_transform(text: str, current_project: str | None) -> str: """Replace ``[[Name]]`` / ``[[Name|Display]]`` tokens with HTML anchors. Runs before markdown rendering. Emits raw HTML which python-markdown preserves unchanged. """ if not text or "[[" not in text: return text def _sub(match: _re.Match) -> str: target = match.group(1) display = (match.group(2) or target).strip() href, cls, suffix = _resolve_wikilink(target, current_project) title = "create this entity" if cls == "redlink" else target.strip() return ( f'' f'{_escape_html(display)}{suffix}' ) return _WIKILINK_PATTERN.sub(_sub, text) def render_new_entity_form(name: str = "", project: str = "") -> str: """Issue B β "create this entity" form targeted by redlinks.""" from atocore.engineering.service import ENTITY_TYPES safe_name = _escape_attr(name or "") safe_project = _escape_attr(project or "") opts = "".join( f'' for t in ENTITY_TYPES ) lines = [ 'This entity was referenced via a wikilink but does not exist yet. ' 'Fill in the details to create it β the wiki link will resolve on reload.
'), '', """""", ] return render_html( f"Create {name}" if name else "Create entity", "\n".join(lines), breadcrumbs=[("Wiki", "/wiki"), ("Create entity", "")], ) def _render_visual_evidence(entity_id: str, ctx: dict) -> str: """Render EVIDENCED_BY β artifact links as an inline thumbnail strip.""" from atocore.assets import get_asset artifacts = [] for rel in ctx["relationships"]: if rel.source_entity_id != entity_id or rel.relationship_type != "evidenced_by": continue target = ctx["related_entities"].get(rel.target_entity_id) if target is None or target.entity_type != "artifact": continue artifacts.append(target) if not artifacts: return "" tiles = [] for art in artifacts: props = art.properties or {} kind = props.get("kind", "other") caption = props.get("caption", art.name) asset_id = props.get("asset_id") asset = get_asset(asset_id) if asset_id else None detail_href = f"/wiki/entities/{art.id}" if kind == "image" and asset and asset.mime_type.startswith("image/"): full_href = f"/assets/{asset.id}" thumb = f"/assets/{asset.id}/thumbnail?size=240" tiles.append( f'π ' f'Open PDF ({asset.size_bytes // 1024} KB)
' ) elif asset_id: out.append(f'') if capture_context: out.append('{_escape_html(capture_context)}') return out def _escape_html(s: str) -> str: if s is None: return "" return (str(s) .replace("&", "&") .replace("<", "<") .replace(">", ">")) def _escape_attr(s: str) -> str: return _escape_html(s).replace('"', """) def render_entity(entity_id: str) -> str | None: ctx = get_entity_with_context(entity_id) if ctx is None: return None ent = ctx["entity"] lines = [f'
Project: {ent.project}
') if ent.description: desc_html = _wikilink_transform(ent.description, current_project=ent.project) lines.append(f'{desc_html}
') if ent.properties: lines.append('No results found.
') lines.append('') return render_html( f"Search: {query}", "\n".join(lines), breadcrumbs=[("Wiki", "/wiki"), ("Search", "")], ) # --------------------------------------------------------------------- # /wiki/capture β DEPRECATED emergency paste-in form. # Kept as an endpoint because POST /interactions is public anyway, but # REMOVED from the topnav so it's not promoted as the capture path. # The sanctioned surfaces are Claude Code (Stop + UserPromptSubmit # hooks) and OpenClaw (capture plugin with 7I context injection). # This form is explicitly a last-resort for when someone has to feed # in an external log and can't get the normal hooks to reach it. # --------------------------------------------------------------------- def render_capture() -> str: lines = ['If you\'re reaching for this page because you had a chat somewhere AtoCore didn\'t see, ' 'fix the capture surface instead β don\'t paste. The deliberate scope is Claude Code + OpenClaw.
' ) lines.append('') lines.append(""" """) lines.append( 'The extractor is aggressive about capturing signal β don\'t hand-filter. ' 'If the conversation had nothing durable, triage will auto-reject.
' ) return render_html( "Capture β AtoCore", "\n".join(lines), breadcrumbs=[("Wiki", "/wiki"), ("Capture", "")], active_path="/wiki/capture", ) # --------------------------------------------------------------------- # Phase 7E β /wiki/memories/{id}: memory detail page # --------------------------------------------------------------------- def render_memory_detail(memory_id: str) -> str | None: """Full view of a single memory: content, audit trail, source refs, neighbors, graduation status. Fills the drill-down gap the list views can't.""" from atocore.memory.service import get_memory_audit from atocore.models.database import get_connection with get_connection() as conn: row = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone() if row is None: return None import json as _json mem = dict(row) try: tags = _json.loads(mem.get("domain_tags") or "[]") or [] except Exception: tags = [] lines = [f'') # Metadata row meta_items = [ f'{mem["status"]}', f'{mem["memory_type"]}', ] if mem.get("project"): meta_items.append(f'{mem["project"]}') meta_items.append(f'confidence: {float(mem.get("confidence") or 0):.2f}') meta_items.append(f'refs: {int(mem.get("reference_count") or 0)}') if mem.get("valid_until"): meta_items.append(f'valid until {str(mem["valid_until"])[:10]}') lines.append(f'{mem["content"]}
{" Β· ".join(meta_items)}
') if tags: tag_links = " ".join(f'{t}' for t in tags) lines.append(f'') lines.append(f'') # Graduation if mem.get("graduated_to_entity_id"): eid = mem["graduated_to_entity_id"] lines.append( f'
This memory was promoted to a typed entity: ' f'{eid[:8]}
' ) # Source chunk if mem.get("source_chunk_id"): lines.append(f'{mem["source_chunk_id"]}
{a["timestamp"]} '
f'{a["action"]} '
f'{a["actor"]}{note}No tag specified.
", breadcrumbs=[("Wiki", "/wiki"), ("Domains", "")]) all_mems = get_memories(active_only=True, limit=500) matching = [m for m in all_mems if any((t or "").lower() == tag for t in (m.domain_tags or []))] # Group by project by_project: dict[str, list] = {} for m in matching: by_project.setdefault(m.project or "(global)", []).append(m) lines = [f'{tag}No memories currently carry the tag {tag}.
Domain tags are assigned by the extractor when it identifies ' 'the topical scope of a memory. They update over time.
' ) return render_html( f"Domain: {tag}", "\n".join(lines), breadcrumbs=[("Wiki", "/wiki"), ("Domains", ""), (tag, "")], ) # Sort projects by count descending, (global) last def sort_key(item: tuple[str, list]) -> tuple[int, int]: proj, mems = item return (1 if proj == "(global)" else 0, -len(mems)) for proj, mems in sorted(by_project.items(), key=sort_key): proj_link = proj if proj == "(global)" else f'{proj}' lines.append(f'By action: ' + " Β· ".join(f'{k}: {v}' for k, v in sorted(by_action.items(), key=lambda x: -x[1])) + '
') lines.append('By actor: ' +
" Β· ".join(f'{k}: {v}' for k, v in sorted(by_actor.items(), key=lambda x: -x[1])) +
'
{ts_short} '
f'{a["action"]} '
f'{a["actor"]} '
f'{mid_short}'
f'{note}'
+ (f'