The Schema Doesn't Know It's Code

mache projects CVE feeds, Notion exports, and agent audit logs into the same queryable filesystem it builds for source code — because the schema mechanism was never about code, it was about shape.

· 4 min read

mache developer-tools data-modeling agents

mache · part 3 of 3


The first post in this series argued that code isn’t text, and the second showed what falls out once it’s rows in a database instead: a lint rule stops being a plugin and becomes a WHERE clause. Both posts talked about mache entirely in terms of source code. That was never the actual boundary.

One mechanism, two ways of pointing at things

Every mache schema — the ones that parse Go, the ones that don’t touch code at all — is the same three-part shape: nodes describe the directories a data source unpacks into, files/file_sets describe the leaves rendered under them, and a selector on each node picks what lands where. What changes is the selector language. Source code gets a tree-sitter query, matching against the grammar. Structured data gets a JSONPath expression, matching against the shape of a document. Point either one at the right input and mache build / mache mount / mache serve don’t know or care which kind they’re looking at — same SQLite projection, same navigable tree, same query surface underneath.

That’s a narrower claim than “mache handles JSON too.” It’s that code was never a special case in the schema mechanism — it was one selector family among two, and the projection, storage, and query layers sit entirely above that choice.

What the same mechanism looks like on a CVE feed

Here’s a JSON-data schema in full — kev-schema.json, which projects the CISA Known Exploited Vulnerabilities catalog:

{
  "version": "v1",
  "nodes": [{
    "name": "vulns",
    "selector": "$",
    "children": [{
      "name": "{{.item.cveID}}",
      "selector": "$[*]",
      "files": [
        { "name": "vendor", "content_template": "{{.item.vendorProject}}" },
        { "name": "product", "content_template": "{{.item.product}}" },
        { "name": "description", "content_template": "{{.item.shortDescription}}" },
        { "name": "raw.json", "content_template": "{{. | json}}" }
      ]
    }]
  }]
}

$[*] and .item. are JSONPath, not tree-sitter — the .item. prefix exists because the underlying store wraps every record as {"schema": ..., "identifier": ..., "item": {...}}. Mount this against a KEV export and you get /vulns/CVE-2021-1234/{vendor,product,description,raw.json} — a real directory per vulnerability, grep-able, diffable, the same way a mache-mounted Go package gives you /pkgname/functions/. Swap the schema for nvd-schema.json, notion-schema.json, or llm-conversations-schema.json, and the same four commands project the NVD feed, a Notion export, or an LLM conversation archive instead. Nothing about mache build changes; only the JSON file describing where things go does.

The case with no API at all

The interesting failure of that pitch is the source that isn’t structured yet. Agent-coordination files — STATUS.md, MASTER-TRIAGE.md, a pile of per-dimension audit reports — are markdown written for humans, with no schema to point a selector at.

audit-indexer.py is the fix, and it’s honest about what it is: a small, disposable script that parses those specific markdown conventions (### C1: SQL Injection in auth module, a **Status:** line under it) into one flat .index.json, deduplicating findings by ID across files. Once that file exists, audit-schema.json projects it exactly like the KEV catalog — /findings/:id with title, severity, status, dimension leaves — and the indexer has a --watch mode meant to run as a PostToolUse hook, re-indexing after every edit so the mounted view never goes stale.

The division of labor is the point. The indexer’s whole job is turning one team’s markdown conventions into JSON shaped enough for a selector to find things in. Once that’s done, none of mache’s machinery needs to know the data started life as prose written for humans — mounting, querying, and watching all come from the same layer that never changed.

Why this was free

None of this required new work on mache’s part. The projection, the SQLite backing store, find-smells’ SQL-over-the-database layer — all of it operates on rows and node kinds, not on “this came from a .go file.” A schema declares shape; it doesn’t declare the shape’s origin. That’s the same property constraint-driven development keeps naming in different clothes: a predicate that composes doesn’t need to know what it’s being checked against, only what true looks like. A tree-sitter query knows what a Go function looks like. A JSONPath expression knows what a KEV record looks like. Neither one knows, or needs to know, that the other exists — and that’s exactly why plugging in a third one, for a format nobody had in mind when the mechanism was built, is a config file, not a feature request.

mache is open source: github.com/agentic-research/mache. The schemas referenced here, and a dozen more — Terraform, SQL DDL, MCP server manifests — live in examples/.