Skip to content

Admin read-only Model explorer (catalog idiom) - #52

Merged
ashwin-agami merged 9 commits into
mainfrom
ACE-014-model-explorer
Jun 28, 2026
Merged

ashwin-agami merged 9 commits into
mainfrom
ACE-014-model-explorer

Conversation

@ashwin-agami

Copy link
Copy Markdown
Contributor

Spec: ACE-014

Summary

Adds the admin Model tab — a read-only Model explorer for the deployed semantic model. It's a pure projection of model_store.load_organization (the same tree the MCP tools serve, so it can't drift) + load_memory for the domain doc. Replaces the cognitively-heavy "dump every column of every table with full per-column trust metadata" approach with a DataHub/Sundial-style catalog: a browse rail (datasource → subject area → table) and one page at a time.

Changes

  • GET /admin/model (session-gated, GET-only — read-only by construction; no write path) with ?datasource=, ?area=, ?table=, ?view=context.
  • Three+1 screens (builders split for previewing): a datasource overview (glossary, storage-connection names/types, subject areas, cross-area metrics/entities), a subject-area landing (tables + metric/entity cards), a table (dataset) page, and a domain-context page.
  • Table page — calm schema table; trust as exceptions (one table-level confidence badge; per-column flags only where they carry signal: PK / FK→target / sensitive / unit / enum / caveat); caveats elevated to an amber callout + inline note; ai_unvalidated marked; relationships + metrics that use the table. Columns grouped when the table authors column_groups (collapsible, labelled), else a flat "show all N" list (wide tables stay short).
  • ui.md — a tiny escape-first markdown subset (headings, bold/italic, inline + fenced code, lists, http(s)-only links) for ORGANIZATION.md; raw HTML in the doc is inert.
  • model_store.list_datasources; _TABS gains a Model tab (its own /admin/model href); the catalog design folded into ui._CSS (light rail, full-width, mobile); render_previews.py renders the model pages from the real builders over neutral seeded data; README documents the view.

Safety

  • Read-only: a single GET route, no POST/PATCH/DELETE under /admin/model. Editing stays conversational in Claude (the in-app editor is Hosted).
  • No secret leak: connection names + types onlystorage_config (hosts/credentials) is never rendered. Verified by a security pass.
  • Escaping: every model field goes through ui.esc; the markdown renderer is escape-first with an http(s) link allowlist that fails closed. No new egress.

Test plan

tests/test_admin_model.py (27 tests): gating + GET-only (405 on POST); empty state; datasource picker (present >1, absent =1) + stale-param fallback; overview (glossary, storage names, never storage_config); area landing; the table page's every flag (PK/FK/enum/unit/sensitive/caveat/ai_unvalidated/low-confidence); wide-table "show all 14"; grouped-vs-flat columns; SQL-backed table; relationships + metrics; ui.md subset + escaping (a <script> in the doc is inert) + http(s)-only links; cross-area objects; markdown empty/unterminated-fence guards.

Checklist

  • uv run dev.py check green — 937 tests, ruff lint, gitleaks
  • Patch coverage 98% (uv run dev.py cover)
  • /code-review + security review — 0 must-fix; XSS/secret surface verified clean
  • Read-only + admin-gated; no storage_config/secrets; no new egress
  • Neutral placeholders only (acme / SALES_DATA / you@example.com) — no customer data
  • Decisions recorded in the spec; previews regenerate via render_previews.py

Read-only /admin/model GET — a projection of load_organization: a datasource
picker (>1 served), org overview (glossary + storage names/types, never
storage_config), and a subject-area landing (tables + metric/entity cards).
Adds model_store.list_datasources and the Model tab. Slice A of ACE-014.
Columns rendered grouped (authored column_groups, collapsible + labelled) or a
flat 'show all N' list otherwise; per-column flags only where they carry signal
(PK / FK→target / enum / unit / sensitive / caveat); table caveats elevated to
an amber callout; ai_unvalidated marked; relationships + metrics that use the
table. Browse tree expands the active area to its tables. Slice B of ACE-014.
ui.md is a tiny escape-first markdown subset (headings, bold/italic, inline +
fenced code, lists, http(s)-only links) for the deployed ORGANIZATION.md — raw
HTML in the doc is inert. The overview now surfaces org-level cross-area
metrics/entities so they aren't dropped. Slice C of ACE-014.
Fold the catalog design into ui._CSS (browse tree, calm schema table + type
pills, trust/sensitive/caveat badges, collapsible column groups, the caveat
callout) — light rail, full-width, mobile. render_previews.py renders the model
pages (overview / area / table flat + grouped / domain context) from the real
builders over a neutral seeded model. README documents the read-only Model view.
Slice D of ACE-014.
Copilot AI review requested due to automatic review settings June 27, 2026 09:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an admin-only, read-only “Model” explorer at /admin/model that renders the deployed semantic model and domain context directly from the served DB-backed model tree, with an escape-first markdown subset for ORGANIZATION.md.

Changes:

  • Introduces GET /admin/model (session-gated, GET-only) with overview/area/table/context views and safe, encoded navigation URLs.
  • Adds a minimal, escape-first markdown renderer (ui.md) with an http(s)-only link allowlist for rendering domain context safely.
  • Adds datasource listing support in model_store plus previews and a comprehensive test suite covering gating, safety (no storage_config leak), rendering behavior, and markdown escaping.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_admin_model.py New test suite covering auth gating, read-only contract, rendering behavior, safety/escaping, and markdown subset behavior.
render_previews.py Generates static preview HTML pages for the new model explorer views using seeded neutral model data.
packages/agami-core/src/ui.py Adds explorer CSS, safe markdown subset renderer, and updates admin tab routing to support /admin/model.
packages/agami-core/src/model_store.py Adds list_datasources() to support selecting among served datasources.
packages/agami-core/src/admin.py Implements the read-only model explorer handlers/builders and mounts the GET-only route.
packages/agami-core/README.md Documents the new Model tab and its read-only/safety properties.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Pin status==200 on the fallback paths (distinguish a 500-with-substring),
assert flat columns actually render (not just 'grp' absent), and add negatives:
a table with no relationships omits the section, orders shows no stray caveat
callout, description-less columns render the em-dash, relationship cardinality
is pinned. Drop a vacuous 'Model'-tab assertion.
A 'Relationships' rail node (shown only when the model has cross-area joins) →
a page listing org.cross_subject_area_relationships grouped by area-pair
(from→to · join columns · cardinality · confidence). Makes the cross-area
topology readable in one place; within-area joins stay on each table page.
Motivated by real models in local testing (meridian 20, servicenow 66 across
16 areas). Adds 3 tests + a preview.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread packages/agami-core/src/ui.py Outdated
Comment on lines +275 to +290
def _md_inline(s: str) -> str:
"""Inline markdown on an ALREADY-escaped string: inline code, bold, italic, and scheme-checked
links. Code is substituted first so `**` inside backticks is left literal."""
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
s = re.sub(r"(?<![*\w])\*([^*]+)\*(?![*\w])", r"<em>\1</em>", s)

def _link(m: "re.Match[str]") -> str:
text, url = m.group(1), m.group(2)
# Only http(s) links — the text is already escaped, so a `javascript:`/`data:` URL renders as
# plain text (never a live href). This is the one place a URL becomes an attribute.
if url.startswith(("http://", "https://")):
return f'<a href="{url}" rel="noopener noreferrer" target="_blank">{text}</a>'
return text

return re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", _link, s)
Comment on lines +861 to +865
picker = (
'<form class="ds" method="get" action="/admin/model">'
'<span class="muted">Datasource</span>'
f'<select name="datasource" onchange="this.form.submit()">{opts}</select></form>'
)
…S fallback

1. ui._md_inline stashes inline-code spans before running bold/italic/link and
   restores them last, so a link/bold inside backticks stays literal (no live
   <a>/<strong> inside <code>).
2. The datasource picker gains a real submit button (the no-JS fallback), so it
   works with JavaScript disabled like the rest of the admin UI.
Both with regression tests.
@ashwin-agami

Copy link
Copy Markdown
Contributor Author

Thanks @copilot — both addressed in 91a437b:

  1. ui.md inline-code re-parsing_md_inline now stashes code spans (NUL-delimited placeholders) before the bold/italic/link passes and restores them last, so a link or bold inside backticks stays literal (no live <a>/<strong> inside <code>). Regression test: test_md_inline_code_is_literal_not_reparsed.
  2. Datasource picker no-JS fallback — added a real <button type=submit> Go control alongside the onchange, so switching datasources works with JavaScript disabled (matching the rest of the JS-free admin UI). Asserted in test_picker_only_when_multiple_datasources.

@ashwin-agami
ashwin-agami merged commit 647394d into main Jun 28, 2026
6 checks passed
@ashwin-agami
ashwin-agami deleted the ACE-014-model-explorer branch June 28, 2026 04:24
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants