> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Ledger understanding

> client.workspaces.start_ledger_understanding — measure the ingested journal package

`client.workspaces.start_ledger_understanding` measures the workspace's
ingested journal package — table roles, integrity, open questions, and
optional GL parsing — and returns a `Job` whose result is a
`LedgerUnderstandingResult`. `client.workspaces.get_ledger_understanding`
reads the latest report without starting a run.

A sibling of journal-entry testing, not a search. JET *tests* a criterion
over the package; this *reads* the package so later tests start from a
settled census. Without `force`, an unchanged package republishes the
stored report instead of scanning again.

```python theme={"dark"}
from ndi_sdk import NdiClient
from ndi_sdk.models.workspaces import LedgerUnderstandingConfig

with NdiClient() as client:
    client.workspaces.update(
        workspace_id,
        ledger_understanding=LedgerUnderstandingConfig(trigger="auto", gl_parsing=True),
    )
    job = client.jobs.wait(
        client.workspaces.start_ledger_understanding(workspace_id).job_id,
        timeout=3600,
    )
    report = job.result.report
    print(report.stale, report.blocking, report.parsing)
    stored = client.workspaces.get_ledger_understanding(workspace_id)
    print(stored.package_fingerprint, len(stored.tables))
```

With the `llm` engine, `package_confirmation="required"` (the default) stops discovery
at source review: a person confirms the package and its columns before GL parsing runs.
If the run later settles different columns, or the selection was confirmed by a model,
parsing is skipped and `report.parsing.confirmation_required` lists the changes;
confirm `report.settled_structure` as a structure review to parse.

With `package_confirmation="automatic"`, discovery can confirm a package and start its
measured run automatically. When the result has `follow_up_job_id`, wait for that job before
reading the final analysis. `report.proposals.auto_confirmation` explains the
confirmation or why review is needed. `report.auto_decisions` records model choices,
suggestions, and validation refusals; measured evidence still controls parsing.

***

## Method signature

```text theme={"dark"}
def start_ledger_understanding(
    workspace_id: UUID | str,
    *,
    force: bool = False,
    path_prefix: str | None = None,
    paths: Sequence[str] | None = None,
    selection_id: str | None = None,
    wait_seconds: int = 0,
    idempotency_key: str | None = None,
) -> Job
```

### Parameters

| Parameter         | Type            | Required | Description                                                                         |
| ----------------- | --------------- | -------- | ----------------------------------------------------------------------------------- |
| `workspace_id`    | `UUID` or `str` | Yes      | Workspace that already holds an ingested ledger package                             |
| `force`           | `bool`          | No       | Re-measure even when a report for this package already exists                       |
| `path_prefix`     | `str`           | No       | Package folder, relative to the workspace                                           |
| `paths`           | `Sequence[str]` | No       | Exact source paths, intersected with the folder                                     |
| `selection_id`    | `str`           | No       | Confirmed source selection from the console; cannot be combined with path selectors |
| `wait_seconds`    | `int`           | No       | Hold the submission request open (max 300)                                          |
| `idempotency_key` | `str`           | No       | Override the key the SDK normally mints                                             |

### Returns

A `Job` whose `result` is a `LedgerUnderstandingResult` (`result_type` is
`ledger_understanding`). The report also lives on `result.report`.

***

## Read the latest report

```text theme={"dark"}
def get_ledger_understanding(
    workspace_id: UUID | str,
    *,
    path_prefix: str | None = None,
    paths: Sequence[str] | None = None,
    selection_id: str | None = None,
) -> LedgerUnderstandingReport
```

`GET` returns the report itself, not a job. `stale` is true when the
package has moved since this reading. A workspace that has never produced
a report raises `404 ledger_understanding_not_found`.

For a confirmed source selection, pass the same `selection_id` to start and read.
Changing selected content or adding dataset parts requires a new confirmation;
`force=True` does not override that guard. A source selection is separate from the
workspace default and keeps the primary population fixed when passed to JET.

***

## Confirm sources

```text theme={"dark"}
def list_ledger_sources(
    workspace_id: UUID | str,
    *,
    path_prefix: str | None = None,
    paths: Sequence[str] | None = None,
    selection_id: str | None = None,
    review_reading_id: str | None = None,
) -> SourceCatalog
```

```text theme={"dark"}
def select_ledger_sources(
    workspace_id: UUID | str,
    *,
    primary_source_id: str,
    expected_catalog_revision: str,
    supporting_source_ids: Sequence[str] | None = None,
    review: SourceReviewReference | None = None,
    previous_selection_id: str | None = None,
    confirm_unrecognized_primary: bool = False,
) -> SelectionSummary
```

```text theme={"dark"}
def get_ledger_selection(
    workspace_id: UUID | str,
    selection_id: UUID | str,
) -> SelectionDetails
```

`list_ledger_sources` is the review catalog. Pass `review_reading_id` from a
reading that asked for source selection. `select_ledger_sources` freezes a
primary and supporting sources against that catalog revision. Use the returned
`selection_id` on start and get.

***

## Workspace policy

`create` and `update` accept a `ledger_understanding` block. Updating
replaces the block whole.

`package_scope=LedgerPackageScope(path_prefix="engagement")` sets the default
for automatic runs, reads and reviews. Import `LedgerPackageScope` from
`ndi_sdk.models.ledger_understanding`. Empty request selectors use this default.
Read a per-request package with the same selectors used to start it.
See [package scope and parse coverage](/api-reference/ledger-understanding).

| Field                  | Default      | Meaning                                                                                                                                                                                                                                  |
| ---------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trigger`              | `"auto"`     | `auto` chains a run after ingestion only when the workspace already holds a journal-line table. `always` chains after every ingestion that landed files. `off` stops chaining; it never refuses an explicit `start_ledger_understanding` |
| `gl_parsing`           | `True`       | Map each journal's account / debit-credit groups to balanced sub-journals                                                                                                                                                                |
| `package_confirmation` | `"required"` | With the `llm` engine, `required` parses only a mapping a person confirmed; `automatic` lets the model confirm the package                                                                                                               |

A document-only corpus with `trigger="auto"` never starts a run: the
predicate is over sidecars ingestion already wrote.

When ingestion does chain a run, `IngestionResult.ledger_understanding_job_id`
names it. Poll that id with `jobs.wait`.

***

## Reading the report

| Field            | What it is                                                          |
| ---------------- | ------------------------------------------------------------------- |
| `stale`          | The package has moved since this reading                            |
| `blocking`       | Integrity found something that makes the population unfit to test   |
| `tables`         | One card per table: role, row count, journal key, balance share     |
| `conventions`    | What the numbers mean — journal key, amount bases, date roles       |
| `integrity`      | Findings and skipped checks. Empty findings is an answer            |
| `join_graph`     | Expected joins, measured in both directions                         |
| `open_questions` | Things the data could not settle, asked once each                   |
| `parsing`        | GL-parse counts, never rows. `None` when parsing was off or skipped |

```python theme={"dark"}
report = client.workspaces.get_ledger_understanding(workspace_id)
if report.stale:
    client.workspaces.start_ledger_understanding(workspace_id, force=True)
if report.blocking:
    for finding in report.integrity.findings:
        print(finding.check, finding.severity, finding.count, finding.message)
for question in report.open_questions:
    print(question.id, question.question)
```

## Conflicts

A second `start_ledger_understanding` while a run is already in flight
returns `409 ledger_understanding_in_progress`. Wait for the in-flight
job, or read the current report with `get_ledger_understanding`.

See [Workspaces](/sdks/python/workspaces) and the
[ledger-understanding API reference](/api-reference/ledger-understanding).
