Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ cwd = instance root; the tag lives in `.dex-engine-pin`, bumped by sync):
| `dex-enrich mark` | heal one ledger entry: the sanctioned correction verb |
| `dex-enrich pass` | record a stage completion (harvest/digest/wiki) in `state/passes.jsonl` |
| `dex-enrich item new` | create a corpus item from a capture file (id rules and provenance; code writes frontmatter) |
| `dex-enrich item digest` | write an item's digest from a JSON payload — signal, topics and facts are the judgment, the file's shape is the engine's |
| `dex-enrich item digest` | write an item's digest from a JSON payload — signal, topics and facts are the judgment, the file's shape is the engine's; the digest pass is recorded in the same call |
| `dex-normalize` | raw chat exports to corpus items (DiscordChatExporter JSON) |
| `dex-lint` | mechanical health check: wikilinks, citations (shortid flags included), orphans, index drift, stale pages, count drift, restated-fact warnings, ledger schema, ledger↔corpus integrity, cap fires, thread-completeness markers, digest shape, pass records (`--write` reconciles derived wiki frontmatter) |
| `dex-exclude <json>` | permanently purge out-of-scope items — corpus file, enrichment, ledger entries — surviving re-normalization |
Expand Down
62 changes: 56 additions & 6 deletions design/ingestion-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ Files:
| file | holds |
|---|---|
| `state/enrichment-ledger.jsonl` | work units (§5) |
| `state/passes.jsonl` | per-item stage records — `{stage: harvest, item, rules, date}`; "ran and promoted nothing" must be distinguishable from "never ran" |
| `state/passes.jsonl` | per-item stage records — `{stage: harvest, item, rules, date}`; "ran and promoted nothing" must be distinguishable from "never ran", and the health check reads both sides of that distinction (§10). The digest pass is recorded by `enrich item digest` itself, in the same call as the file (§10); `enrich pass` records the harvest and wiki stages, and remains the manual re-record for any stage |
| `state/migrations.jsonl` | applied-migrations log (§12) |
| `state/issue-reports.jsonl` | filed/commented issue fingerprints (§13) |
| `state/digests/<id>.md` | per-item fact indexes; the one markdown corner of `state/`. Claude's judgment, the engine's shape: `enrich item digest --file <payload>` serializes it (§14). Removed with its item by `dex exclude` — the one thing that ever deletes one |
Expand Down Expand Up @@ -1245,6 +1245,44 @@ Harvest-rule changes bump a version constant in the engine; passes are
recorded in `state/passes.jsonl`; re-assessment of old items is
migration-seeded (§12), not scan-inferred.

**The pass record has a reader on both sides of its distinction.** The
health check flags harvest passes recorded under an older rules version
(re-judge), and it flags harvest that **never ran**: a live item that owes
no further work, has at least one fetched page on record — a `done` unit
that is not a media download or an extracted asset, the URL cap's own
reading of "page" — and has no harvest pass under any rules version.
Without that reader, a session that skipped harvest left an item digested
and cited on the strength of the shared link alone, on no surface. The
predicate follows the ingest procedure's own bounds: an item still owing a
unit derives `raw` and has not reached the harvest step, so it is never
listed; a no-source capture, and an item whose every unit died unfetched,
has no page to read the subject rule over and owes no pass — its owed work
is description and digest, which the run report already names. A recorded
pass covers its item by trailing shortid, the same match exclusions use
across renames, so a renamed item's standing record never reads as a
skipped judgment; an item re-seeded by migration keeps its original pass
and answers to the rules-version check, not this one. The finding fires
beside the enrichment-newer-than-digest backstop rather than deferring to
it — the backstop's repair is digest → place → wiki, which never runs
harvest, so deferring would carry the skipped judgment straight through
the repair. Like the stale-pass row it is a finding, never exit 1: no
later mechanical stage breaks on a missing harvest pass the way the wiki
layer breaks on a malformed digest, and the repair — run the judgment now,
then `enrich pass --stage harvest` — is judgment, the report's business.

**The digest pass is recorded by the digest verb, not by a second
command.** `enrich item digest` records the pass itself, through the same
`record_pass` path `enrich pass` uses — a separate recording command was a
step a session could forget, and a forgotten one silently cost the
staleness backstop its comparand: a digest with no pass record is dated by
nothing, so no later enrichment could ever read as newer than it. The
verb records after validation and before the file write, so both failure
shapes stay honest: a refused payload records nothing, and a crash
between the record and the write leaves a pass with no digest file —
which the backstop's no-digest branch lists loudly — never the unreadable
opposite. `enrich pass --stage digest` remains the manual re-record, and
the harvest and wiki stages still record through `enrich pass`.

## 11. Rendering: judgment decides, code renders

Composition that is fully determined by data is computed in code and emitted
Expand Down Expand Up @@ -1354,7 +1392,12 @@ retry without being asked. So `manual` (the engine has given up) becomes
**Needs you**, while `blocked`, `waiting` and `error` (the engine retries by
itself) become **Waiting on the engine**, with the retry state visible on
the entry. The same rule renames every other engine-internal label to what
the reader must do about it.
the reader must do about it. One exception rides the row, not the status:
a parked media unit under `media_fetch: none` is deferred by the drain,
never retried, so it waits on the owner flipping the config — the payload
builder, the one place the config is in hand (payloads stay
self-contained), marks the row `resting`, and it renders under **Needs
you** with a reason naming what unblocks it and no retry framing.

**Open question — where `error` entries belong.** They sit under **Waiting
on the engine** today, on the rule above: `error` retries by itself, once
Expand Down Expand Up @@ -1384,8 +1427,8 @@ silently short list is a lie about scale.
|---|---|
| **Needs writing up** | items with new enrichment for the session to digest |
| **Read these yourself** | jobs resolving to the cognitive floor (OCR, extraction Claude must do with eyes) |
| **Needs you** | entries the engine has given up on: ledger status `manual` |
| **Waiting on the engine** | entries the engine retries unasked: `blocked`, `waiting`, `error` |
| **Needs you** | entries the engine has given up on (ledger status `manual`), plus media units resting under `media_fetch: none` — the owner's config change is what moves them |
| **Waiting on the engine** | entries the engine retries unasked: `blocked`, `waiting`, `error` — minus media units resting under `media_fetch: none`, which it will not retry |
| **Not finished** | items still `raw` because a unit they own has not landed |
| **Digest these** | items whose enrichment is newer than their digest |
| **Waiting on a capability** | the `waiting` cohort, counted by the capability it needs |
Expand Down Expand Up @@ -1433,7 +1476,12 @@ believes it. (`dex-ingest`, split into `dex-capture` + `dex-run`, is the
case this exists for.) The same holds INSIDE a live skill: a synced `dex-*`
directory mirrors the template exactly, so a reference file the template
dropped is removed too, reported the same way — copy-only sync left it
loading its stale procedure in every instance forever. A symlinked skill
loading its stale procedure in every instance forever. The mirror binds
shape as well as content: where a release replaces a synced file with a
same-named directory, or folds a directory down to a file, the conflicting
entry is removed — reported as a machinery change — and the template's
shape is written; copy-only sync crashed on the standing entry instead and
took the whole sync with it. A symlinked skill
directory is unlinked rather than recursed into, so whatever it pointed at
is left alone. Nothing outside the `dex-` prefix is ever touched — an
owner's own skills are instance-owned.
Expand Down Expand Up @@ -1838,7 +1886,9 @@ src/dex_engine/
total validation — a missing, unknown or mistyped key, a
`signal` outside the vocabulary, empty `topics` or
`facts`, an id naming no corpus item — and nothing is
written on a refusal. Rewriting is allowed and carries
written on a refusal, no pass record included. Records
the digest pass itself, before the file write (§10).
Rewriting is allowed and carries
nothing over: every field is the payload's judgment or
the corpus item's fact)
normalize.py imports shared detect/types (private kind_of copy deleted)
Expand Down
5 changes: 3 additions & 2 deletions instance/dex-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ never state, never synced.
is append-only and verb-written: ledger heals via `bin/dex enrich mark`,
stage records via `bin/dex enrich pass`. Digests too: the judgment goes
in as JSON and `bin/dex enrich item digest --file <payload>` writes the
file. A hand-appended line is how state and reality diverge; the verbs
are what make a malformed record impossible.
file, recording the digest pass in the same call. A hand-appended line
is how state and reality diverge; the verbs are what make a malformed
record impossible.
- `wiki/` is a build artifact: regenerable, never the only home of a fact. Pages cite
corpus item ids in backticks — full ids, ALWAYS, and NEVER other wiki
pages — so citations stay mechanically checkable.
Expand Down
12 changes: 12 additions & 0 deletions instance/skills/dex-lint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ step 4.
digest: cite it on
the best existing page, or ledger it into `uncategorized-shares` in
`state/taxonomy.json` if genuinely low-signal.
- **Items a page cites but no taxonomy topic records** — the other face
of the coverage invariant: a citation is not a placement. Read the
digest and append the id to each matching topic's `items` in
`state/taxonomy.json`, or ledger it into `uncategorized-shares` if
genuinely low-signal.
- **Index drift** — regenerate the affected `wiki/index.md` entries.
- **Stale pages** (members newer than the page) — fold the newer items
in via rewrite-not-append; if the new material supersedes old claims,
Expand All @@ -95,6 +100,13 @@ step 4.
- **Possible restated facts** — read each flagged pair: same fact →
merge into one sentence carrying both citations; genuinely distinct →
leave them (the flag is a question, not a verdict).
- **Harvest these (no pass on record)** — the item's pages were fetched
but no harvest pass was ever recorded: "never ran" is the standing
state, not "ran and promoted nothing". Run the harvest judgment now
under the current subject rule (dex-run's `references/ingest-item.md`),
then `bin/dex enrich pass <item> --stage harvest` — the pass is
recorded even when nothing was promoted, and recording it is what
keeps this row empty.
- **Harvest passes under old rules** — re-run the harvest judgment for
those items under the current subject rule (dex-run's
`references/ingest-item.md`), then `bin/dex enrich pass ... --stage
Expand Down
5 changes: 4 additions & 1 deletion instance/skills/dex-query/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ full fetched source — and summarize from that.
page), file it as `wiki/syntheses/<slug>.md` — frontmatter `type: synthesis`,
`question:`, `generated:` date; body cites item ids and wikilinks related
topics. Add to index, log it, commit and push. Syntheses are question-shaped, dated
snapshots; the health check refreshes them as new material lands.
snapshots; no mechanical check refreshes one — a check cannot tell a
stale synthesis from a deliberately time-scoped one. Refreshing is
judgment, done here: when a query reads a synthesis whose `generated:`
date predates material that changes its answer, rewrite it.
6. Never cite wiki pages as sources — item ids only.
13 changes: 9 additions & 4 deletions instance/skills/dex-run/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,12 @@ Then do a run now, whatever the host.
act on nothing.

8. **Backstop.** `bin/dex enrich status` — any item listed under **Digest
these** is an interrupted previous session: complete its digest →
place → wiki steps now. Every item listed is digestible; one still owing
these** is an interrupted previous session: complete its remaining
per-item steps now, harvest → digest → place → wiki
(`references/ingest-item.md`). The listing says a digest is owed, not
that harvest ran — the interruption can predate either step, and no
surface here would catch a skipped harvest before the next health
check. Every item listed is digestible; one still owing
a unit is `raw` and never appears there, however long it stays parked.
**Needs you** and **Waiting on the engine** here are the standing view of
everything parked in the instance, not just this session's: judge the
Expand Down Expand Up @@ -145,8 +149,9 @@ Then do a run now, whatever the host.

Where a state file has a verb, the verb writes it and you never do:
corpus items come from `enrich item new`, digests from `enrich item digest
--file cache/digest.json`, ledger lines from `enrich mark` and the run
itself, stage records from `enrich pass`. You supply the judgment as JSON
--file cache/digest.json` (which records the digest pass itself), ledger
lines from `enrich mark` and the run itself, harvest and wiki stage
records from `enrich pass`. You supply the judgment as JSON
or arguments; the engine decides the shape, so a malformed file cannot be
written. Same motion as rendering, below. `state/taxonomy.json` is the one
you still write directly.
Expand Down
3 changes: 2 additions & 1 deletion instance/skills/dex-run/references/ingest-item.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ tweet earns two, and padding to a number invents facts. Interpretive
context lives here, not in item bodies. Topics: canonical names from
`state/taxonomy.json` when it exists; otherwise 2–5 kebab-case candidates.
Revising a digest later is the same call with a new payload — the file is
rewritten whole. Then `bin/dex enrich pass <item-id> --stage digest`.
rewritten whole. The verb records the digest pass itself — the
confirmation says so — so there is no separate pass command to run here.

A parked item (waiting/blocked/manual) still exists — provenance and note
were captured at ingest — but gets no digest or wiki work until its
Expand Down
4 changes: 3 additions & 1 deletion instance/skills/dex-run/references/state-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ a malformed record impossible:
- `state/passes.jsonl` — per-item stage records `{stage, item, date,
rules?}` ("ran and promoted nothing" is distinguishable from "never
ran"; `rules` versions the harvest rules). Written by
`bin/dex enrich pass`.
`bin/dex enrich pass`; the digest pass is recorded by `enrich item
digest` itself, in the same call as the file (`enrich pass --stage
digest` remains the manual re-record).
- `state/migrations.jsonl` — applied-migrations log `{number, engine,
date}`. Written by sync's migration runner.
- `state/issue-reports.jsonl` — what this instance filed/commented
Expand Down
6 changes: 4 additions & 2 deletions src/dex_engine/enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ def build_parser() -> argparse.ArgumentParser:
help="provenance display name (default: owner)",
)
digest_parser = item_commands.add_parser(
"digest", help="write an item's digest from a JSON payload (judgment in, shape decided)"
"digest",
help="write an item's digest from a JSON payload (judgment in, shape decided) "
"and record the digest pass",
)
digest_parser.add_argument(
"--file",
Expand Down Expand Up @@ -168,7 +170,7 @@ def _dispatch(args: argparse.Namespace, ctx: RunContext) -> str: # noqa: PLR091
case "pass":
return record_pass(ctx, args.item, args.stage)
case "item" if args.item_command == "digest":
return item_digest(args.file, instance=ctx.instance)
return item_digest(args.file, ctx=ctx)
case "item":
return item_new(
args.capture,
Expand Down
35 changes: 32 additions & 3 deletions src/dex_engine/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

wiki — broken wikilinks (vs reserved/unbuilt), citations of ids not in the
corpus, shortid-shaped citations (backticked 6-hex is a probable malformed
citation everywhere, index included), coverage orphans, index consistency,
citation everywhere, index included), coverage orphans and their cited
complement (items pages cite that no taxonomy topic records), index
consistency,
stale pages, page item-count drift (frontmatter ``items:`` vs the page's
MEMBER count — its taxonomy topic's items, or its entity-members list;
never its citation count), and difflib sentence similarity ("possible restated
Expand All @@ -18,7 +20,8 @@
enrichment directory, where the item that owns it cannot list it —
both asked of the item the corpus says owns the unit),
waiting cohorts and cognitive-job
summary, harvest passes recorded under old rules, and the
summary, items whose fetched pages landed but whose harvest pass was
never recorded, harvest passes recorded under old rules, and the
enrichment-newer-than-digest orphan listing (the interrupted-session
backstop, shared with ``enrich status``).

Expand Down Expand Up @@ -63,7 +66,7 @@
from .pipeline import ledger
from .pipeline.ownership import corpus_owners
from .pipeline.registry import DRIVERS
from .pipeline.run import CAP_BOUNDS, HARVEST_RULES_VERSION, digest_orphans
from .pipeline.run import CAP_BOUNDS, HARVEST_RULES_VERSION, digest_orphans, never_harvested
from .pipeline.transcribe import read_enrichment_fields
from .pipeline.types import Config, Format, Instance, LedgerEntry, Need, Status
from .render import surfaces
Expand Down Expand Up @@ -154,6 +157,12 @@ def run_lint(

ledgered = _uncategorized_items(taxonomy)
orphans = sorted(corpus_ids - scan.cited - ledgered)
# The coverage invariant's other face: a citation is not a placement,
# so a cited item can still be in no topic's items and off the
# uncategorized ledger — invisible to the orphan check, which only
# asks about the uncited. The cited set is the wiki scan's; the
# placed set is the taxonomy's own listing.
unplaced = sorted(scan.cited - _placed_items(taxonomy))
unindexed, ghost_index = _index_consistency(instance, pages, taxonomy)
# Shortid-shaped citations flag everywhere — index included: latent
# shortids in an index never tripped the old citation check because no
Expand All @@ -176,6 +185,7 @@ def run_lint(
"bad_citations": scan.bad_citations,
"shortid_citations": scan.shortid_citations,
"orphans": orphans,
"unplaced": unplaced,
"unindexed": unindexed,
"ghost_index": ghost_index,
"stale_pages": scan.stale_pages,
Expand Down Expand Up @@ -229,6 +239,24 @@ def _uncategorized_items(taxonomy: dict[str, object]) -> set[str]:
return {item for item in items if isinstance(item, str)}


def _placed_items(taxonomy: dict[str, object]) -> set[str]:
"""Every item id some topic's ``items`` records — uncategorized-shares included.

The coverage invariant's whole placed set, read with the same
tolerance for hand-mangled shapes :func:`_uncategorized_items` shows:
a malformed topic contributes nothing rather than a crash.
"""
topics = taxonomy.get("topics", {})
if not isinstance(topics, dict):
return set()
placed: set[str] = set()
for topic in topics.values():
items = topic.get("items", []) if isinstance(topic, dict) else []
if isinstance(items, list):
placed |= {item for item in items if isinstance(item, str)}
return placed


def _pre_taxonomy_outcome(instance: Instance) -> LintOutcome | None:
"""The two pre-taxonomy states: fresh instance, or broken mid-ingest.

Expand Down Expand Up @@ -551,6 +579,7 @@ def _state_checks(
payload["missing_outputs"] = integrity.missing
payload["misfiled_outputs"] = integrity.misfiled
payload["capped"] = _cap_fires(entries)
payload["never_harvested"] = never_harvested(instance)
payload["stale_passes"] = _stale_passes(instance)
threads = _incomplete_threads(instance)
payload["incomplete_threads"] = threads.rows
Expand Down
Loading