Analysis document¶
Canonical input to ported analysis modules. Built by Transcribe adapters from a managed notebook project. Cores consume this document only — never Page objects, Streamlit state, or filesystem paths.
On-disk project layout: project-on-disk.md. Effective page text: page-result.md. Result envelopes: analysis-result.md. Storage: analysis-run-storage.md. Eligibility: notebook-eligibility.md.
Identity¶
formatmust be"transcribe.analysis-document"schema_versionmust be1for this contractUnsupported
schema_version→ refuse (no silent upgrade)This schema is frozen for the core set: adapters and ports must not invent alternate document shapes. Schema changes require a new
schema_versionand an explicit contract revision.
Schema (v1)¶
Field |
Type |
Required |
Notes |
|---|---|---|---|
|
string |
yes |
Stable id for this analysis view; must equal the project’s |
|
string |
yes |
Document-level text (see concatenation rules) — canonical representation for document-level modules |
|
array |
yes |
Ordered analysis units (may be empty only transiently before refusal; see validation) |
|
string |
yes |
Version id of the unit-splitting rules used |
|
string |
yes |
Named split profile ( |
Each unit:
Field |
Type |
Required |
Notes |
|---|---|---|---|
|
string |
yes |
Stable id (never array index alone); unique within |
|
string |
yes |
Exact effective text for this unit; must be a non-empty string (adapters omit blank pages — never emit empty units) |
|
number |
yes |
Chronology key; finite and ≥ 0 |
|
string | null |
no |
When present: ISO-8601 date only ( |
|
object |
yes |
Validated Transcribe back-pointer (below); opaque to cores |
Uniqueness and ordering guarantees¶
document_idis unique per managed project analysis view (core: one document id perproject_id)unit_idvalues are unique within a document (duplicate_unit_idif not)unitsmust be strictly sorted by(order, unit_id)ascendingAnalysis cores consume units in array order;
orderis chronology, not identitySame inputs + same
granularity_version+ samesplit_profile⇒ sameunit_idset and same offsets
Unit id rules¶
Page unit:
unit_id == page_id(project-stable page identity)Derived paragraph/span unit:
"{page_id}/span:{start}-{end}"wherestart/endare character offsets into that page’s effective text after the versioned splitter identified bygranularity_version+split_profileSame page effective text + same splitter identity ⇒ same ids across reopen/rerun; edit / reorder / include-exclude changes ids or membership predictably
Document text concatenation (reproducible)¶
For schema_version 1, document text is the sole canonical document-level string:
Sort units by
(order, unit_id)Join unit
textvalues with a single newline (\n)Do not trim unit texts; do not insert extra blank lines beyond the single separator
The stored
textfield must equal this concatenation exactly (adapters must not carry a divergent document string)
Validation / refusal¶
Refuse with named errors (no silent repair of ids or order):
Error |
Condition |
|---|---|
|
|
|
any unit |
|
unit lacks |
|
duplicate |
|
|
|
|
|
|
|
|
|
fails |
|
unknown version |
|
|
Analysis consumes units in array order (already sorted by (order, unit_id)). order is chronology, not identity.
Excluded pages, blank text, failed OCR¶
Adapters build units from the managed project. Normative membership:
Page situation |
Adapter behaviour |
|---|---|
Page marked excluded from analysis |
Omit from |
Included page with empty or whitespace-only effective text (blank OCR, empty edit, failed attempt with no usable |
Omit from |
Included page with non-empty effective text |
Emit unit(s) per |
Active OCR attempt |
Use effective text per page-result.md; failure status alone does not exclude the page |
If omission yields zero units → document validation fails with empty_document_text; callers map that to analysis-result insufficient_data (or skipped_not_applicable when an eligibility policy produced an empty eligible set — see notebook-eligibility.md).
Frozen core split profiles¶
The core set admits only these split_profile values. New profiles require a contract bump of granularity_version and explicit documentation — they are not free implementation choices once results and citations persist.
split_profile: "page" (granularity_version: "page_v1")¶
page profile canonicalisation (exact):
Walk manifest
pagesin order; skipanalysis_excludedpages and blank/whitespace-only / failed-empty effective textOne unit per remaining page;
unit_id == page_idsource_ref:{"kind":"page","page_id":...}order: 0-based index among emitted units (not raw manifest index of omitted pages)text: exact effective text code points as stored — no NFC/NFKC rewritedate:YYYY-MM-DDonly when the page has a day-precision user date; otherwisenullDocument
text= join of unit texts with a single\nContent fingerprint includes only contract fingerprint fields — not project title/tags/cover/OCR settings
Reject non-string texts and strings containing unpaired surrogates at validation
split_profile: "paragraph_v1" (granularity_version: "paragraph_v1")¶
Deterministic, identity-preserving paragraph derivation (required before Moments / highlights / QA evidence ports that use span units):
For each included page with non-empty effective text
T, find split points at every run of two or more consecutive\ncharacters (blank-line separated blocks)Each block is the half-open substring
T[start:end]with leading/trailing\nfrom the separator run excluded from the block; do not otherwise trim interior whitespaceIf no blank-line separator exists, the whole
Tis a single block[0, len(T))Skip blocks whose text is empty or whitespace-only
unit_id = "{page_id}/span:{start}-{end}"withstart/endthose offsets intoT
source_ref:{"kind":"page_span","page_id":...,"char_start":start,"char_end":end}order:(page_order * 1_000_000) + start(stable, chronology-preserving across pages)text: exact substringT[start:end]
Derived units must remain resolvable after reopen: stable ids + source_ref offsets into the fingerprinted page effective text.
source_ref (adapter/storage boundary; opaque to cores)¶
Analysis cores must treat source_ref as opaque and must not parse it.
Transcribe adapters must emit a validated durable form before persistence or UI navigation. Unconstrained strings are non-conformant.
Allowed forms (v1)¶
Page
{"kind": "page", "page_id": "<page_id>"}
Span on page (offsets into that page’s effective text)
{"kind": "page_span", "page_id": "<page_id>", "char_start": 0, "char_end": 10}
Validation at adapter/storage write¶
kind∈{page, page_span}; unknownkind→ refusepage_idmust exist in the project manifestFor
page_span:char_start/char_endintegers;0 ≤ char_start ≤ char_end ≤ len(page_effective_text); half-open intervalRequired keys only; reject extra required-unknown shapes
UI navigation depends on this validated form.
Span indexing convention¶
Character offsets are zero-based, half-open [char_start, char_end) indices into the exact Unicode string stored as:
the unit’s
units[].textwhen citing within a unit, orthe page effective text when
source_ref.kind == "page_span"
Indexing is Python str code-point indices (not UTF-8 byte offsets). Offsets must not be interpreted against a different normalization, a concatenated document string (unless the unit text is that string), or post-edit page text without rebuilding the AnalysisDocument and content fingerprint.
Effective text source (canonical representation)¶
Page units use project effective text per page-result.md:
edited_textif not null, else active attemptraw_textSpan units use the exact substring of that page effective text selected by the versioned splitter
Included-unit set is exactly
unitsmembership (excluded and blank/failed-empty pages omitted)Canonical text for analysis is always this effective text (and derived substrings / document concatenation). Adapters must not silently substitute OCR confidence strings, raw-only text when an edit exists, or display-normalized variants
After any edit, reorder, include/exclude, or OCR change that alters effective text, adapters must rebuild the
AnalysisDocumentand content fingerprint before citing or caching results against it
Canonical content fingerprint (content_fingerprint_version: 1)¶
Content fingerprints are normative — not implementation-defined. Analysis-run-storage composes cache identity using this value and must not invent a second content-hash meaning.
Algorithm¶
Build a canonical object containing only fingerprint-relevant fields, with sorted keys at every object level:
{
"content_fingerprint_version": 1,
"document_id": "...",
"granularity_version": "...",
"split_profile": "...",
"text": "...",
"units": [
{
"date": null | "YYYY-MM-DD",
"order": <number>,
"source_ref": { ... canonical object with sorted keys ... },
"text": "...",
"unit_id": "..."
},
...
]
}
unitsmust already be sorted by(order, unit_id)(validation ensures this).date: JSONnullif absent; otherwise normalizedYYYY-MM-DD.Serialize as UTF-8 JSON with sorted object keys and no insignificant whitespace (compact separators).
content_fingerprint= lowercase hex SHA-256 of that UTF-8 byte string.
Same logical document ⇒ same fingerprint across processes and languages that implement this serialization.