diff --git a/README.md b/README.md
index f3a91ce..5724631 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@ Turn local HTML — or any LLM's extraction output — into schema-valid JSON wh
`local-only` · `deterministic replay` · `no browser` · `no external service`
+
+
Most extractors stop at JSON. That is where review starts.
@@ -62,10 +64,10 @@ const receipt = ground({
});
receipt.coverage; // { grounded: 2, total: 3, invalid: 1, ratio: 0.67 }
-receipt.issues; // [{ code: 'quote-not-found', pointer: '/color', ... }]
+receipt.issues; // [{ code: 'quote-not-found', pointer: '/color', ... }]
```
-Ask your model to return a quote per field, then let the gate decide. With `strict: true`, the hallucinated value never reaches your database.
+Ask your model to return a quote per field, then let the gate decide. A runnable version with a recorded LLM response lives in [`examples/llm`](./examples/llm) (`node examples/llm/run.mjs`). With `strict: true`, the hallucinated value never reaches your database.
## Install
diff --git a/docs/demo.svg b/docs/demo.svg
new file mode 100644
index 0000000..4948b75
--- /dev/null
+++ b/docs/demo.svg
@@ -0,0 +1,25 @@
+
diff --git a/examples/llm/llm-response.json b/examples/llm/llm-response.json
new file mode 100644
index 0000000..282ef7d
--- /dev/null
+++ b/examples/llm/llm-response.json
@@ -0,0 +1,15 @@
+{
+ "_note": "Recorded structured output from an LLM asked to return {value, quote} per field. 'remote' is hallucinated.",
+ "title": {
+ "value": "Senior Backend Engineer",
+ "quote": "Senior Backend Engineer"
+ },
+ "city": { "value": "Karlsruhe", "quote": "Karlsruhe" },
+ "salaryMin": {
+ "value": 68000,
+ "quote": "68,000",
+ "transforms": ["parse-number"]
+ },
+ "remote": { "value": "fully remote", "quote": "fully remote" },
+ "germanLevel": { "value": "B2", "quote": "B2" }
+}
diff --git a/examples/llm/run.mjs b/examples/llm/run.mjs
new file mode 100644
index 0000000..3047cdd
--- /dev/null
+++ b/examples/llm/run.mjs
@@ -0,0 +1,39 @@
+// Ground a recorded LLM extraction against its source.
+// Run from the repo root after `npm run build:package`:
+// node examples/llm/run.mjs
+import { readFileSync } from 'node:fs';
+import { ground } from 'grounded-json';
+import { z } from 'zod';
+
+const dir = new URL('./', import.meta.url);
+const source = readFileSync(new URL('source.txt', dir), 'utf8');
+const { _note, ...fields } = JSON.parse(
+ readFileSync(new URL('llm-response.json', dir), 'utf8'),
+);
+
+// Prompt pattern: "For every field return { value, quote } where quote is copied verbatim from the source."
+const data = Object.fromEntries(
+ Object.entries(fields).map(([k, f]) => [k, f.value]),
+);
+const evidence = Object.entries(fields).map(([k, f]) => ({
+ pointer: `/${k}`,
+ quote: f.quote,
+ ...(f.transforms && { transforms: f.transforms }),
+}));
+
+const receipt = ground({
+ data,
+ sources: [{ mediaType: 'text/plain', content: source }],
+ evidence,
+ schema: z.object({
+ title: z.string(),
+ city: z.string(),
+ salaryMin: z.number(),
+ remote: z.string(),
+ germanLevel: z.string(),
+ }),
+});
+
+console.log('coverage:', receipt.coverage);
+for (const issue of receipt.issues)
+ console.log(`REJECT ${issue.pointer}: ${issue.code}`);
diff --git a/examples/llm/source.txt b/examples/llm/source.txt
new file mode 100644
index 0000000..86bd6fe
--- /dev/null
+++ b/examples/llm/source.txt
@@ -0,0 +1,3 @@
+Senior Backend Engineer (m/w/d) – Karlsruhe
+Full-time, hybrid (3 days on site). Salary range: 68,000–82,000 EUR per year.
+Stack: TypeScript, Node.js, PostgreSQL. German B2 required.