Import runs¶
Activation gate¶
Same gate as notebook-corpus.md — satisfied; this contract is runtime-normative. Single-file ingest via IngestService + .ingest-journal.json remains a supported import path. This contract defines the bulk orchestration layer used by bulk-import UI/CLI.
Lifecycle¶
scan → plan → validate → commit
Only commit mutates the corpus (notebook entities, managed bytes, corpus index, ImportRun outcomes). Scan/plan/validate are read-only w.r.t. corpus authority (they may write disposable plan drafts under the ImportRun store).
Adapters¶
Folder-per-notebook, naming conventions, scanner batches, PDF trees, etc. are import adapters. They emit one canonical ImportPlan. Adapter heuristics must not leak into notebook identity (notebook_id is always preallocated/generated—never “folder name”).
Notebook cover (folder-per-notebook / file-name heuristic)¶
When creating or filling a notebook from imported image sources:
If a source basename is
cover.jpg,cover.jpeg, orcover.png(case-insensitive), and the notebook has nocover_page_idyet, setcover_page_idto that source’s page (within-sourcepage_index0).Otherwise leave
cover_page_idunset; display/Open fall back to the first page in notebook order (project.pages[0]), not earliest dated page.Do not overwrite an existing user-set
cover_page_id. Cover PDFs are out of scope for this heuristic.
Ordering ambiguity¶
Natural sort may be the proposed order in a plan. Ambiguous ordering (duplicate numbers, mixed PDF/image without an explicit rule, missing indices, conflicting cues) is a validate error. Commit is refused until the plan is resolved. Silent guessing is non-conformant.
Plan operations¶
An ImportPlan is a list of items. Each item is exactly one of:
|
Meaning |
|---|---|
|
Create a new managed notebook directory + |
|
Append sources/pages into an existing |
These must not be collapsed into one ambiguous “import” op. Recovery paths differ (see Crash boundaries).
Stable IDs and preallocation¶
Before commit begins, the validated plan must contain:
ID |
Rule |
|---|---|
|
Stable UUID for this plan document; immutable for the plan’s life |
|
Stable UUID per plan item; immutable |
Resulting |
Preallocated for |
Resulting |
Preallocated for every page/source the item would create |
Retry/idempotency must not regenerate these IDs. A resumed commit uses the same plan bytes / same IDs. If an item already committed those IDs, recommit is a no-op for that item.
Plan fingerprint and policy¶
Field |
Rule |
|---|---|
|
Integer; |
|
Stable string naming duplicate/ordering policy package (e.g. policies that include |
|
Lowercase hex SHA-256 of the canonical plan body defined below |
Canonical plan body (fingerprint input)¶
Compact UTF-8 JSON with sorted keys containing exactly:
schema_versionplan_idimport_policy_iditems— array in plan order; each item includesitem_id,op, target/notebook_id, ordered intended page specs with preallocated IDs, source fingerprints (sha256), media type, within-sourcepage_indexlist, and policy-relevant flags (e.g. explicit corpus-wide dedupe request). Exclude mutable external paths from the fingerprint, or include them only inside a nestedprovenanceobject that is omitted from the fingerprint input.
Idempotent equality (replaces vague “same plan + policy”)¶
A commit attempt is an idempotent retry of a prior attempt iff all hold:
plan_idequalplan_fingerprintequalimport_policy_idequalItem
item_idset equal (same multiset of items)
Otherwise it is a different plan and must not reuse another run’s committed item outcomes as no-ops unless an explicit “continue with new plan” product flow creates a new ImportRun.
ImportRun storage¶
Field |
Value |
|---|---|
Format |
|
|
|
Location |
|
Registry / index (optional companion) |
|
Writes |
Atomic replace under corpus lock (short critical section) |
Run document shape (normative fields)¶
import_run_id,format,schema_versionplan_id,plan_fingerprint,import_policy_id,import_manifest.schema_versionImmutable history: frozen plan snapshot reference or inlined canonical plan body used for fingerprint; discovery inputs summary; timestamps
created_atMutable execution state:
status, per-item outcomes,updated_at, error summariesPer-item outcome:
item_id,state(pending|committed|skipped|failed|cancelled_pending), resulting IDs, skip classification, failure code/message
Immutable history vs mutable execution¶
Plan identity fields and canonical plan body are immutable after run creation.
Item outcome records append/advance forward only (pending → committed/skipped/failed/cancelled_pending). Committed outcomes must not be rewritten to a different ID set.
Final
statusis written once to a terminal value (see Cancellation).
Retention¶
ImportRun files are retained by default (audit/resume). Deletion is an explicit operator action, not automatic GC on success.
Optional future compaction may archive terminal runs older than a configured age; compaction must not delete the only record of committed page provenance linkage (
import_run_idon sources) without a documented export.
Intra-notebook ingest constraint¶
Existing .ingest-journal.json permits at most one active ingest transaction per notebook. Bulk orchestration must enforce this: never start a second source commit against a notebook that has a live journal; never assume intra-notebook parallelism. Parallelism across notebooks is allowed only under notebook-corpus.md lock rules.
Cancellation¶
Cancellation stops pending items; it never rolls back already committed pages/sources/notebooks.
Terminal statuses:
Status |
Meaning |
|---|---|
|
All items committed or intentionally skipped per policy; none failed |
|
At least one committed and at least one failed (and/or cancelled pending), not a clean full success |
|
No items committed; one or more failed (or planning/commit aborted before any commit) |
|
Operator cancel with no items committed (clean no-op cancellation) |
|
Operator cancel after one or more items already committed |
UI/CLI must not collapse cancelled and cancelled_with_commits.
Crash / recovery boundaries¶
At each boundary, recovery picks an authoritative winner and is resume-safe.
Boundary |
After success looks like |
Authoritative winner on restart |
|---|---|---|
1. Corpus registration (create) |
Corpus index entry present for |
If |
2. Notebook creation |
|
Winner is on-disk |
3. Source promotion |
Managed source file present + matches planned |
File + hash win; journal continues toward manifest commit. |
4. Render promotion |
Render PNG present + matches planned render hash |
File + hash win. |
5. |
Pages/sources/renders include preallocated IDs |
Manifest wins; clear per-notebook ingest journal only after manifest matches journal intent. |
6. ImportRun item commit |
Item outcome |
ImportRun item record wins for idempotency; do not recreate. |
7. Final run-state commit |
Terminal |
Terminal status wins; do not reopen terminal runs except via explicit new plan. |
Create vs register race: If project.json was created with the preallocated notebook_id but corpus registration did not commit, recovery must complete registration (append index entry) rather than creating a second notebook or deleting the valid project. If registration exists without a loadable project, report integrity error—do not delete the index entry silently.
Journals: corrupt / malformed¶
Per-notebook
.ingest-journal.jsonand ImportRun execution journals/scratch must not be silently discarded when unreadable or schema-invalid.Required behaviour: report (doctor/CLI error) and quarantine (e.g. rename to
.ingest-journal.corrupt.<timestamp>/ move undercorpus/quarantine/) leaving an audit trail.Automatic delete of unreadable journals is non-conformant for bulk-import generation safety. Recovery may roll back only when the journal is well-formed and indicates a non-
manifest_pendingincomplete state per project-on-disk.md.
Relation to per-notebook ingest¶
ImportRun orchestrates many notebook-local commits. Each page/source commit still uses the existing stage → journal → promote → atomic project.json replace mechanism. ImportRun does not replace that journal; it records outcomes around it.
Non-goals¶
One filesystem transaction for an entire multi-thousand-page run
Silent path-based resume when planned IDs are absent
Offering
replace_sourcein v1 policy IDs