Agentic-Service-Data-Eyond-Catalog / docs /knowledge /KNOWLEDGE_OUTPUT_CONTRACT.md
Rifqi Hafizuddin
[NOTICKET] fix(knowledge_extraction): rule.txt few-shots stop contaminating the gold set (X23)
7e8614f
|
Raw History Blame
26.2 kB
# Knowledge Pipeline β€” Output Contract **v2**
> **Implemented 2026-08-26.** `src/knowledge_extraction/` emits this shape.
> The schema review of 2026-08-26 landed with it: ids on every entity, formulas referenced rather
> than restated, rules linked to formulas and terms, and 16 fields cut.
>
> **Not yet re-measured.** The prompt changes this required have not been scored β€” no source
> document, parsed artifact or MinerU install exists on the dev box (all three `data/` directories
> are gitignored and absent), so the free stages have nothing to read. The last measured figures in
> Β§12 predate these edits. See Β§13.
**Supersedes:** v1 of this document (2026-08-24)
**Reference run:** `v2_full_document_2026-08-24_093051` Β· BUMA `STD_2026_006_MNO` Β· 9 pages Β· `gpt-5.4-nano`
**Producer:** `python -m src.knowledge_extraction.cli <artifact.json> --extract`
**Status:** offline artifacts only β€” no endpoint, no table.
---
## 0 Β· What changed from v1
Harry's review of v1 asked for entity ids, a formula reference instead of a restated formula, and a
link between rules and formulas. All are adopted. The schema was also cut back β€” v1 carried fields
nothing consumes.
| Change | Detail |
|---|---|
| **Ids on every entity** | `term_id`, `formula_id`, `brief_id`; `rule_id` re-specified. All deterministic and content-derived |
| **Glossary no longer restates formulas** | `formula_latex` removed β†’ `defining_formula_id` reference |
| **Rules link to formulas and terms** | `formula_ids[]`, `term_ids[]` |
| **`rule_type` added** | `interpretation` \| `calculation` β€” see Β§2 for why |
| **16 fields cut, 8 added** | 50 β†’ 42 fields total |
| **`chunk_id` separator fixed** | `DOC#0007` β†’ **`DOC::0007`**, adopting the parser's format |
| **Reference numbers corrected** | v1 quoted a superseded run (13 chunks / 66 clusters). Current is **31 chunks / 58 clusters** |
### Why the ids had to be re-specified
Neither id in v1 survived a re-run. `cluster_id` was a **frequency-rank ordinal**
(`f"{doc_id}#c{i:03d}"` over a list sorted by mention count), and `rule_id` was **written by the
model** (`rule.txt` asks for SCREAMING_SNAKE_CASE). Either way, re-running with a tuned prompt
reshuffles the ids β€” and if an expert has approved 40 entries, we have lost which 40.
**Approval continuity, not linking, is the real reason these exist.**
---
## 1 Β· Id scheme
All ids are deterministic, content-derived, and stable across re-runs of the same document.
None is model-generated; none is positional.
```
term_id = "t_" + sha256(doc_id + "|" + canonical)[:10]
formula_id = "f_" + sha256(doc_id + "|" + name_or_latex_or_chunk_id)[:10]
rule_id = "r_" + sha256(doc_id + "|" + chunk_id + "|" + char_start)[:10]
brief_id = "b_" + sha256(doc_id)[:10]
```
Implemented in `src/knowledge_extraction/ids.py`. `term_id` is keyed on the **cluster canonical**,
not on the `term` the model returned β€” the cluster is the stable thing across runs, and the model is
free to answer "PA" or "Physical Availability" for the same cluster. `formula_id` falls back through
`name` β†’ `formula_latex` β†’ `chunk_id`, because both of the first two are Optional and without the
fallback every unnamed formula in a document would collide.
**Scope is per document.** The same term appearing in two documents receives two different
`term_id`s. Deciding that BUMA's "PA" and a textbook's "PA" are the same concept is an **expert
judgement at review time**, not something the pipeline asserts β€” the same reasoning that makes the
pipeline record "Physical *of* Availability" instead of normalising it, and the reason MTTR at BUMA
(breakdown duration Γ· breakdown frequency) must not silently merge with MTTR in IT.
**Known limit.** Ids derive from content, so if content changes the id changes β€” retune clustering
such that a term canonicalises differently and its `term_id` moves. The alternative is a persisted
registry, which belongs with persistence (DEV_PLAN Β§0.8 D2).
---
## 2 Β· The four artifacts
| File | Entity | Holds |
|---|---|---|
| `glossary.json` | `GlossaryEntry[]` | Terms and their definitions |
| `interpretation_pack.json` | `RuleEntry[]` | Rules of thumb β€” how to interpret data or behaviour |
| `formulas.json` | `FormulaEntry[]` | Formulas, transcribed from the document |
| `brief_context.json` | `BriefContext` | One orientation brief per document |
Plus three derived/audit payloads: `review_queue.json`, `rejected.json`, `usage.json`.
### Why `RuleEntry` carries a `rule_type`
The Interpretation Pack exists to **improve analytics insight from expert interpretation rules** β€”
interpretation logic, action benchmarks, and rules for when to conclude or not conclude from data.
Measured against the reference document, **0 of the 15 gold rules are interpretation rules.** They
are calculation conventions (`PA_COMPOSITE_WEIGHTED`), data-sourcing rules
(`PTY_PRODUCTION_SOURCE`), and unit conventions (`GAINLOSS_UNITS`). This is a property of the
document type: a *Standard Parameter* document defines how to **calculate** parameters, not how to
**read** them. Interpretation rules live in other document types, or in the expert's head.
`rule_type` keeps both in one artifact rather than discarding the conventions we do extract:
- `interpretation` β€” how to read a value or behaviour. **The target.**
- `calculation` β€” how a value is computed or sourced. What this corpus actually yields.
A consumer wanting only interpretation logic filters on `rule_type`.
---
## 3 Β· Transport reality
Every payload is written by the CLI as a file into `--out-dir` (default `out/knowledge/`), as a
**bare JSON array** with no envelope. There is no endpoint and no table.
Three conventions hold across every payload:
- **`page` is 0-based**, exactly as the parser reports. Converting to 1-based is the UI's job, done
once at display time.
- **All content fields are nullable by design.** `null` is a valid, correct answer β€” the model
abstaining, not failing. On the reference run **48 of 58** entries carry no definition.
- **`provenance` is mandatory and `provenance.span` is verbatim-checked.** A field whose span cannot
be located in the source is set to `null` and logged to `rejected.json` β€” **never repaired.**
---
## 4 Β· Common types
### `Provenance` β€” required on every entity
```json
{
"doc_id": "STD_2026_006_MNO",
"span": "Adalah jumlah equipment/unit yang digunakan untuk menghasilkan produksi dalam suatu periode.",
"page": 3,
"section_no": "2.1.2",
"chunk_id": "STD_2026_006_MNO::0007"
}
```
| Field | Type | Req | Notes |
|---|---|---|---|
| `doc_id` | string | βœ… | |
| `span` | string | βœ… | **Verbatim** from the source. The anti-hallucination control β€” the reviewer checks a quote against a page, not a claim against memory |
| `page` | int \| null | β€” | **0-based** |
| `section_no` | string \| null | β€” | e.g. `"2.1.2"`; null when the document is unnumbered |
| `chunk_id` | string \| null | β€” | Back-reference into the parsed artifact. **Format `<doc_id>::<seq>`**, matching `KNOWLEDGE_PARSING_OUTPUT_CONTRACT.md`. Treat as opaque β€” do not split it |
### Enums
| Enum | Values |
|---|---|
| `subdomain_tags[]` | `production` Β· `maintenance` Β· `hauling` Β· `loading` Β· `drilling_blasting` Β· `equipment` Β· `safety` Β· `quality` Β· `planning` Β· `cost` Β· `geology` Β· `other` |
| `extraction_status` | `ok` Β· `no_definition_found` Β· `escalated` |
| `diff_status` | `new` Β· `duplicate` Β· `conflicting` |
| `rule_type` | `interpretation` Β· `calculation` |
| `latex_verification` | `verified` Β· `unverified_no_markup` Β· `unverified_operator` |
`subdomain_tags` is a **closed enum β€” classification, not generation.** Adding a member changes what
the model is allowed to answer, which makes it a prompt change, not a data change.
---
## 5 Β· `glossary.json` β†’ `GlossaryEntry[]` β€” 13 fields
```json
[
{
"term_id": "t_9f2a41c0b7",
"term": "Qty",
"full_name": "Quantity",
"source_wording": "2.1.2. Quantity (Qty)",
"definition": "Adalah jumlah equipment/unit yang digunakan untuk menghasilkan produksi dalam suatu periode.",
"defining_formula_id": "f_31de08aa95",
"subdomain_tags": ["production", "equipment"],
"mention_count": 20,
"provenance": {
"doc_id": "STD_2026_006_MNO",
"span": "Adalah jumlah equipment/unit yang digunakan untuk menghasilkan produksi dalam suatu periode.",
"page": 3,
"section_no": "2.1.2",
"chunk_id": "STD_2026_006_MNO::0007"
},
"extraction_status": "ok",
"diff_status": "new",
"definition_conflict": false,
"conflict_variants": []
},
{
"term_id": "t_5b71ce0d34",
"term": "Gain/Loss",
"full_name": null,
"source_wording": null,
"definition": null,
"defining_formula_id": null,
"subdomain_tags": ["production"],
"mention_count": 4,
"provenance": {
"doc_id": "STD_2026_006_MNO",
"span": "Gain/Loss",
"page": 6,
"section_no": null,
"chunk_id": "STD_2026_006_MNO::0011"
},
"extraction_status": "no_definition_found",
"diff_status": "new",
"definition_conflict": false,
"conflict_variants": []
}
]
```
| Field | Type | Req | Notes |
|---|---|---|---|
| `term_id` | string | βœ… | **New in v2.** Stable across re-runs β€” the key an approval hangs on |
| `term` | string | βœ… | The only required content field |
| `full_name` | string \| null | β€” | Expanded name. Span-guarded as a literal transcription |
| `source_wording` | string \| null | β€” | **The document's literal wording**, un-normalised. Read deterministically from the section heading, *not* asked of the model β€” asked directly, it returned the tidied form |
| `definition` | string \| null | β€” | `null` = correct abstention (**48 of 58** on the reference run) |
| `defining_formula_id` | string \| null | β€” | **New in v2**, replaces `formula_latex`. The formula that *defines* this term. Null when the formula branch did not extract one |
| `subdomain_tags` | enum[] | βœ… | Defaults `[]` |
| `mention_count` | int | βœ… | Default `0`. **Drives review-queue ordering** |
| `provenance` | Provenance | βœ… | |
| `extraction_status` | enum | βœ… | Default `ok` |
| `diff_status` | enum \| null | β€” | Set by the diff stage against the active glossary version |
| `definition_conflict` | bool | βœ… | Default `false` |
| `conflict_variants` | string[] | βœ… | Default `[]`. The competing definitions β€” the pipeline **never picks a winner** |
**Removed in v2:** `formula_latex` (β†’ `defining_formula_id`) Β· `interpretation` (belongs in the
Interpretation Pack, reached via `term_id`) Β· `domain` Β· `company` Β· `language`.
> `domain` and `company` were never in the extraction prompt's field guide β€” the model filled them
> by copying worked examples β€” and neither was span-guarded, so a wrong value was undetectable.
> `language` is a genuine three-value field (`id`/`en`/`mixed`) but nothing consumes it and it is
> recoverable from the document; it is cut for simplicity and can return if a consumer needs it.
> **Writer split.** The LLM fills only `term`, `full_name`, `definition` and `subdomain_tags`.
> `term_id`, `defining_formula_id`, `source_wording`, `mention_count`, `extraction_status`,
> `diff_status`, `definition_conflict` and `conflict_variants` are all set by deterministic code β€”
> **the model never writes its own ids, links or audit fields**, so none of them can be hallucinated.
---
## 6 Β· `interpretation_pack.json` β†’ `RuleEntry[]` β€” 8 fields
```json
[
{
"rule_id": "r_c07be41f22",
"rule_type": "calculation",
"statement": "Production yang digunakan dalam perhitungan adalah produksi hasil joint survey.",
"condition": "Apabila data joint survey belum tersedia",
"consequence": "maka digunakan data produksi berdasarkan truck count sebagai dasar perhitungan",
"formula_ids": ["f_77c1ab9e40"],
"term_ids": ["t_a4e2f81b06"],
"provenance": {
"doc_id": "STD_2026_006_MNO",
"span": "Apabila data joint survey belum tersedia maka digunakan data produksi berdasarkan truck count sebagai dasar perhitungan",
"page": 4,
"section_no": "2.1.5",
"chunk_id": "STD_2026_006_MNO::0009"
}
}
]
```
| Field | Type | Req | Notes |
|---|---|---|---|
| `rule_id` | string | βœ… | **Re-specified in v2.** Was written by the model, therefore unstable; now deterministic |
| `rule_type` | enum | βœ… | `interpretation` \| `calculation` β€” see Β§2 |
| `statement` | string \| null | β€” | The rule as prose. **Prose, not an equation** β€” evidence carrying both yields the prose, and the equation goes to the formula branch |
| `condition` | string \| null | β€” | The triggering condition, split out β€” not left buried in prose |
| `consequence` | string \| null | β€” | What follows when the condition holds |
| `formula_ids` | string[] | βœ… | **New in v2.** The formulas this rule constrains. Default `[]` |
| `term_ids` | string[] | βœ… | **New in v2.** The terms this rule governs. Default `[]` |
| `provenance` | Provenance | βœ… | |
**Removed in v2:** `applies_to` (free text β†’ `term_ids[]`) Β· `subdomain_tags` Β· `language` Β·
`extraction_status`.
`condition` + `consequence` is the point of this artifact, and the seed of the interpretation logic
tree: a rule stored as one paragraph cannot be attached to a skill later; a trigger and its
consequence can.
> **Deliberately not modelled yet:** the interpretation logic tree, action benchmarks, and explicit
> conclude / do-not-conclude verdicts. Those are the direction of travel, but designing their schema
> against zero real examples would be guessing. They land when a document containing them does.
---
## 7 Β· `formulas.json` β†’ `FormulaEntry[]` β€” 7 fields
```json
[
{
"formula_id": "f_77c1ab9e40",
"name": "Production",
"formula_latex": "Production = MOHH \\times Qty \\times PA \\times UA \\times Pty",
"variables": [
{ "symbol": "MOHH", "term_id": "t_1c9d5e7a83", "meaning": "Machine on Hand Hours" },
{ "symbol": "Qty", "term_id": "t_9f2a41c0b7", "meaning": "Quantity" },
{ "symbol": "PA", "term_id": "t_a4e2f81b06", "meaning": "Physical Availability" },
{ "symbol": "UA", "term_id": "t_6d40b2fc17", "meaning": "Utilization of Availability" },
{ "symbol": "Pty", "term_id": "t_e8b3092d55", "meaning": "Productivity" }
],
"unit": "BCM",
"latex_verification": "unverified_operator",
"provenance": {
"doc_id": "STD_2026_006_MNO",
"span": "Production = MOHH x Qty x PA x UA x Pty",
"page": 1,
"section_no": "2.1",
"chunk_id": "STD_2026_006_MNO::0003"
}
}
]
```
| Field | Type | Req | Notes |
|---|---|---|---|
| `formula_id` | string | βœ… | **New in v2** |
| `name` | string \| null | β€” | What the formula computes, as named in the source |
| `formula_latex` | string \| null | β€” | **Transcribed, never derived.** The pipeline copies what the document states |
| `variables[]` | object[] | βœ… | `{ symbol (required), term_id \| null, meaning \| null }`. `term_id` is **new in v2** |
| `unit` | string \| null | β€” | Result unit if stated |
| `latex_verification` | enum \| null | β€” | **Added 2026-08-26.** Which guarantee `formula_latex` actually carries. Set by the span check, never by the model. `null` when there is no `formula_latex` to characterise |
| `provenance` | Provenance | βœ… | |
**Removed in v2:** `extraction_status`.
> **Read `latex_verification` before trusting `formula_latex`.** Three paths leave the field populated and
> they are not equally strong. `verified` means the transcription was located in the source markup
> (`Chunk.latex`), compared in a notation-aware canonical form. The two `unverified_*` values mean the
> claim could not be checked either way and the entry is guarded by its provenance span alone β€” the same
> guarantee everything carried before 2026-08-26.
>
> `unverified_operator` is not rare. MinerU's **`pipeline`** backend transcribes multiplication as a bare
> letter `x`, so every product formula from such an artifact lands here β€” including the sample above.
> An artifact parsed with `hybrid/high` carries `\times` and verifies normally.
> `variables[].symbol` is not always a legend abbreviation β€” the extraction prompt's own worked
> example emits `"Total Hours"` and `"Breakdown"` as symbols. So `term_id` resolution is
> **best-effort and nulls are expected**; the link stage reports its dangle rate rather than hiding it.
---
## 8 Β· `brief_context.json` β†’ `BriefContext` β€” 5 fields *(single object)*
```json
{
"brief_id": "b_2ef60a8c19",
"title": "Standard Parameter Produksi & ECA",
"purpose": "Standard Parameter ini merupakan pedoman dalam perhitungan parameter produksi yang meliputi Qty, PA, UA, PTY, serta Equipment Capacity Analysis (ECA)...",
"key_parameters": ["Qty", "PA", "UA", "PTY", "ECA", "Grouping (Composite)"],
"provenance": {
"doc_id": "STD_2026_006_MNO",
"span": "Standard Parameter ini merupakan pedoman dalam perhitungan parameter produksi",
"page": 0,
"section_no": "1",
"chunk_id": "STD_2026_006_MNO::0000"
}
}
```
| Field | Type | Req |
|---|---|---|
| `brief_id` | string | βœ… **New in v2** |
| `title` | string \| null | β€” |
| `purpose` | string \| null | β€” |
| `key_parameters` | string[] | βœ… Default `[]` |
| `provenance` | Provenance | βœ… |
**Removed in v2:** `scope` (overlaps `purpose`, frequently null) Β· `summary_md`.
> ⚠️ **The only branch that cannot be span-checked.** A summary is not verbatim by construction, so
> the primary anti-hallucination control does not apply β€” a plausible summary is indistinguishable
> from a correct one. That is why `summary_md` is cut (longest unverifiable prose on the least
> verifiable branch), why this branch belongs on a larger model tier when one is provisioned
> (DEV_PLAN Β§0.8 D3), and why a reviewer should treat this artifact differently from the other three.
---
## 9 Β· `review_queue.json` β†’ `ReviewQueueRow[]` β€” 10 fields
**This is the consumption surface** β€” the payload a review UI binds to. A flattened projection of
`glossary.json`, deliberately denormalised so a row renders with no joins.
```json
[
{
"rank": 1,
"term_id": "t_a4e2f81b06",
"term": "PA",
"definition": "Adalah ketersediaan fisik suatu equipment/unit yang menunjukkan proporsi waktu equipment/unit tersebut berada pada kondisi available (siap pakai) selama suatu periode tertentu.",
"source_wording": "2.1.3. Physical of Availability (PA)",
"mention_count": 43,
"page": 3,
"section_no": "2.1.3",
"span": "Adalah ketersediaan fisik suatu equipment/unit",
"review_reason": "source wording differs from the expanded name β€” confirm which is correct"
}
]
```
| Field | Type | Req | Notes |
|---|---|---|---|
| `rank` | int | βœ… | 1-based display order β€” see sorting below |
| `term_id` | string | βœ… | **New in v2.** What an approve / edit / reject decision attaches to, so it survives a re-run |
| `term` Β· `definition` Β· `source_wording` Β· `mention_count` | | | Copied from the entry |
| `page` Β· `section_no` Β· `span` | | | **Lifted out of `provenance`** so a row is self-contained |
| `review_reason` | string | βœ… | Human-readable, one of five |
**Removed in v2:** `extraction_status` Β· `diff_status` Β· `definition_conflict` Β·
`conflict_variants` β€” all four are already expressed by `review_reason`, and remain available on the
entry itself.
**Sort order** β€” `(conflicting first, then mention_count descending)`. Conflicts outrank frequency
unconditionally, because a contradiction is a decision only the expert can make.
**`review_reason` values:**
| Value | Meaning |
|---|---|
| `conflicting definitions β€” expert decision required` | Contradiction found; the pipeline picked no winner |
| `source wording differs from the expanded name β€” confirm which is correct` | e.g. "Physical **of** Availability" vs "Physical Availability" |
| `term found but no definition in document` | Correct abstention β€” **48 of 58** on the reference run |
| `definition rejected by span check or absent` | Control fired; see `rejected.json` |
| `routine confirmation` | Nothing anomalous |
---
## 10 Β· Linking model
`GlossaryEntry` is the hub. Every link is computed **after** both branches run, by a deterministic
never-throw link stage. **No additional LLM calls, and nothing a model could fabricate.**
```
RuleEntry ──term_ids[]────────► GlossaryEntry ◄──variables[].term_id── FormulaEntry
└────formula_ids[]──────────────────────────────────────────────────────► β–²
GlossaryEntry ──defining_formula_idβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
Two properties that must hold:
1. **A dangling link is `null` or an empty array β€” never a fabrication.** Same rule as spans.
2. **The "appears in" edge is derived, not stored.** That PA appears inside
`Production = MOHH Γ— Qty Γ— PA Γ— UA Γ— Pty` is recoverable by scanning `formulas[].variables[]`.
Do not add a field for it.
---
## 11 Β· Audit payloads β€” unchanged from v1
**`rejected.json` β†’ `RejectedField[]`** β€” what the span check *caught*, kept so a reviewer sees the
control working, not only what it let through. The reference run rejected **2 fields**.
```json
[
{
"entry_term": "<term>",
"field": "full_name",
"offending_value": "<value the model returned>",
"reason": "full_name is not a verbatim transcription of the source",
"branch": "glossary"
}
]
```
| Field | Type | Req |
|---|---|---|
| `entry_term` | string | βœ… |
| `field` | string | βœ… |
| `offending_value` | string | βœ… |
| `reason` | string | βœ… |
| `branch` | `glossary` \| `rule` \| `formula` \| `summary` | βœ… |
**`usage.json` β†’ `CallUsage[]`** β€” per-call accounting. `cached_tokens` is read from the API, never
modelled: caching does not engage below 1024 prompt tokens, so assuming it would understate input
cost by roughly 10Γ—.
```json
[
{
"branch": "glossary",
"deployment": "gpt-5.4-nano",
"tier": "nano",
"prompt_tokens": 2676,
"cached_tokens": 2389,
"completion_tokens": 214,
"latency_s": 3.4,
"retries": 0,
"structured_output_mode": "json_schema",
"simulated": false
}
]
```
`simulated: true` marks a `--mock` run β€” **never a quality measurement.**
---
## 12 Β· Reference run β€” real figures
From `eval/knowledge/results/v2_full_document_2026-08-24_093051.json`. The parsing module built the
artifact; extraction ran all four branches.
| | |
|---|---|
| Artifact | `schema_version` 0.2.0 Β· **31 chunks** Β· 9 pages Β· `mineru/pipeline 3.4.4` |
| Funnel | 188 raw mentions β†’ 163 after noise β†’ **58 clusters** (compression 2.81Γ—) |
| Entries | **58 glossary Β· 8 rules Β· 6 formulas** |
| LLM | 77 calls Β· 139,283 prompt tokens Β· **79.6% cached** Β· 168.4 s |
| Quality | **48 abstained**, 10 with a definition Β· **2 fields rejected** by the span check |
| E1 term-filter recall | **0.8049** (kill line 0.70, PASS) |
| E3 schema-fill precision | **0.90** (kill line 0.80, **PASS** β€” the prototype failed this at 0.75) |
> v1 of this contract quoted 13 chunks / 66 clusters / 66 entries. That run predates the
> section-aware chunker. **Anything calibrated against those figures should be re-checked.**
>
> These figures also predate the v2 schema itself: they were measured on the v1 prompts, which asked
> for fields that no longer exist. The funnel counts (chunks, clusters, calls) should carry over
> unchanged β€” the filter, cluster and rank stages were not touched β€” but **E3 must be re-scored**,
> because trimming a prompt can move definition quality even when the scored fields are unchanged.
---
## 13 Β· Open items
| # | Item | Owner |
|---|---|---|
| 1 | **Unevaluated.** Cutting `interpretation` / `formula_latex` and adding `rule_type` are prompt changes, and no eval has run against them β€” there is no document or parsed artifact on the dev box to run one. Same position X20 was committed in, and it needs the same first paid run to clear | Rifqi |
| 2 | **Rule-branch quality is a separate track.** The branch measures 3/15, an unevaluated prompt fix is already in flight (X20), and a prompt/gold contradiction caps it at 14/15 (X23). `rule_type` and the X23 de-contamination make this the *third and fourth* uncommitted-to-measurement changes on that one prompt β€” the first paid run measures the prompt as a whole, and isolating any single edit would cost one full run each. **Expect the raw score to move in both directions:** X20 should add rules back, X23 removes a free hit (example 1 was gold `PTY_PRODUCTION_SOURCE` verbatim) and lifts the 14/15 ceiling | Rifqi |
| 2b | **`rule.txt` grew again.** X20 already pushed it past the 1024-token cache floor; example 4 adds more. Only the API's `cached_tokens` proves a hit β€” verify on the first paid run | Rifqi |
| 3 | **Persistence.** Stage output is JSON on disk; parsed artifacts, candidate entries, glossary versions and the approval audit trail still need one consolidated DDL handoff. Go owns the schema β€” Python never executes DDL | Rifqi β†’ Harry |
| 4 | **Page indexing at the API boundary** β€” stays 0-based to the UI, or converts once at the boundary? Currently 0-based everywhere | Harry + Rifqi |
| 5 | **Endpoint shape** β€” four endpoints, or one with an `?artifact=` parameter? Not started; the offline CLI is the honest first milestone | Rifqi |
---
## Caveat on the sample values
Field shapes, types and defaults are the **agreed v2 target**. Run-level figures in Β§12 are literal,
from the cited result file. The per-entity **sample values are illustrative** β€” the underlying
`out/*.json` from that run is not in version control, and all `*_id` values shown are placeholders
that demonstrate format, not real hashes. Replace them with literal output once v2 is implemented
and a run exists.