Skip to content

Release v0.1.0-rc7 - #915

Open
WaylandYang wants to merge 102 commits into
mainfrom
dev
Open

WaylandYang wants to merge 102 commits into
mainfrom
dev

Conversation

@WaylandYang

@WaylandYang WaylandYang commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Six days in which the typed graph became a view that keeps itself honest: alignment learned to reconcile what it writes, to shortlist what it asks about and to read a model's answer in whatever shape it comes; an errata agent now needs two votes and a structural reason before it retracts anything; a name meets its entity through a vector as well as a string; and a source can push statements straight into the open contract.

Alignment writes a view, and the view reconciles itself

An errata agent, on two votes and a flag

An agent reviews each document's typed facts through the gate, structure first (0044 decision 7, #885). The first measured runs showed it retracting mostly what the document did say, so a retraction or revision now needs a structural flag and a second vote asked alone with the document; an unflagged fact is never retracted by the agent alone. Additions opened the other way: a name the document contains becomes an entity, and a document with no typed facts is reviewed once with an empty list (#906). On 100 documents: 34 wrong retractions to zero, recall 9.3% to 18.4%.

A name meets its entity

Recall proposes a pair by name vector, so a short form or a name in another script meets its entity through a question rather than a silent second entity (0041 cut 2, #877, migration 0080). A pair proposed by similarity rather than by the same string is never merged on the batch verdict alone: it takes the tool-using second look, or goes to a person when that look cannot run (#889). Review paging looks the two entities up before their same-name peers (#888).

A source may push statements

A Statements source accepts the open extraction contract over HTTP with its own push token: what a client sends is what the graph reads, no model in between, and it reaches the typed layer the same way a document does, through alignment. Same identity is an update, deleted is a tombstone, a key outside the contract or a subject not listed is refused at the door (0054, #884).

What a call to the endpoint survives

Chat and the interface

Export and provenance

Measured

A typed-graph bench on Re-DocRED measures what alignment recovers against the record's thresholds, waits on the right jobs, scores what errata adds and logs usage once per reply (#880, #893); the second measured runs and every cut above are tabled in scripts/bench/README.md. The #875 replay (premise-linked plan steps) runs as store tests (#898, #897, #896).

Housekeeping

Login takes the same time whether the email or the password is wrong (#867). Worker concurrency falls back to the migration's default (#871). CI runs lint, test and build as parallel jobs behind one aggregator, database-backed tests included, and the store integration tests compile as one binary (#879, #872, #873, #869, #868, #886).

Upgrading

Migrations 0070 to 0076, 0080 and 0090, all forward-only, applied on startup. An rc6 database with data was migrated and read back by this build without warnings.

WaylandYang and others added 30 commits September 19, 2026 16:37
Main back into dev after v0.1.0-rc6
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Keep XML references in imported Office text

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Keep unknown Office references from aborting the document import

Signed-off-by: dada-yan <BinjunYann@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
…#780)

* Keep overlapping document processing from duplicating chunks

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Discard document reads superseded before chunk replacement

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* The replacement lock does not block the document's own children

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
* An app is a declared agent, written down

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

* The sandbox tier follows a protocol with backends

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

* Code an app is written in gets its own record

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

* The app surface is MCP, and the app center is refused

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

---------

Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…793)

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
)

* Keep failed sync attempts from skipping incremental source updates

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Keep checkpoint tests independent of the filename regression module

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* The checkpoint tests pin the bound that was sent

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
* Retract aligned types when kind-word votes disagree

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Commit kind-word decisions and entity projections atomically

Signed-off-by: dada-yan <BinjunYann@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Cuts 1 and 2 of #477 landed as record 0030 + the runner in #485;
the backend's `proof()` now returns `ProofStep` with a recursive
`premises: Vec<ProofStep>` field (the proof is a tree, depth bounded
by the same cap as the fixed-point iteration). The frontend type
was still flat and the renderer treated the result as a single
``<ol>``, which works for proofs whose premise tree is one level
deep and silently underflows for anything deeper.

## What this changes

- `web/src/api.ts`: `ProofStep` gains `premises: ProofStep[]`,
  mirroring the Rust `utopia_core::models::ProofStep`. Comment now
  says 'tree, not chain' (0030's wording).
- `web/src/pages/Graph.tsx`: `ProofSteps` is unchanged at the API
  level (still takes `steps: ProofStep[]`). Internally it delegates
  each row to a new `ProofStepRow` component, which renders one
  step plus, recursively, its own `premises` as a nested `<ol>`.
  The recursion bottoms out at the server's depth cap; leaves have
  `premises.length === 0`.

Both call sites (`derivedProof` on a landed derivation,
`blockedProof` on a violation that blocked one) get the recursion
without further change.

## Why a left border on the nested block

The nested `<ol>` gets `ml-4 border-l border-edge pl-3` so a
multi-level proof reads as a tree visually, not as one tall column.
This is the existing convention for indented children in the same
file (`ExpandCard`, the recursion in `MapPane`).

## Verification

```
pnpm typecheck   # tsc --noEmit, clean
pnpm test        # 82 vitest tests, all pass
pnpm build       # vite build, clean (1.7 MB JS, pre-existing)
pnpm guard       # style-guard, 84 files compliant
```

Refs #477

Signed-off-by: rollroyces <rollroyces@users.noreply.github.com>
Co-authored-by: rollroyces <rollroyces@users.noreply.github.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
* Add 100k benchmark first cut against real tables

Rework of #713 per maintainer's review: the previous cut measured
Postgres filtering of a synthetic 100k-row table, which does not
tell us anything about Utopia. This cut populates the real
documents, chunks and facts tables for one base through the
production write functions and times `graph::entity_detail`.

## Fixture

- 1 organisation, 1 workspace, 1 knowledge base, 1 user, 1
  data source (`sources::create`), 1 hub entity, 1 spare entity.
- `UTOPIA_BENCH_DOCS` (default 1000) documents, each with
  `UTOPIA_BENCH_CHUNKS_PER_DOC` (default 3) chunks via the real
  `chunk_text` from utopia-ingest.
- `UTOPIA_BENCH_HUB_FACTS` (default 1000) facts via
  `graph::insert_fact`, accumulating on the hub entity.

## Scenario

`graph::entity_detail(kb_id, hub_id, at=now, as_of=now)`, the
read path a base opens when an entity is clicked. 30 iterations
with 1 warmup discarded.

## Gating

The test is gated behind `UTOPIA_BENCH=1` and skipped silently
otherwise. `cargo test -p utopia-store` does not run it on a
machine without that env var, and CI does not set it. Per
maintainer's Q5: 'informational only, like 0035'.

## Output

A markdown report lands at
`docs/benchmarks/<utc-date>-100k.md` (path resolved via
`CARGO_MANIFEST_DIR` so it lands at the workspace root regardless
of cargo's cwd). Includes the populate-elapsed time, p50/p95/p99
read latency, mean, throughput.

## Local run

```
UTOPIA_DATABASE_URL=postgres://utopia:utopia@127.0.0.1:1517/utopia \
UTOPIA_BENCH=1 cargo test -p utopia-store --test bench_100k -- \
    --nocapture
```

Result from a local run: 1000 documents in 6.4s populate, 100ms
p50 / 106ms p95 `entity_detail` against the hub.

## What this PR does NOT do

- Does not bump UTOPIA_BENCH_DOCS to 100 000 by default. The
  populate step is currently ~6.4s for 1000 documents, so
  100 000 would be ~10 minutes — fine for a monthly run, not
  fine for the local iteration loop. Operators can pass
  `UTOPIA_BENCH_DOCS=100000` to run the maintainer's target.
- Does not add additional scenarios (entity detail, hybrid
  retrieval, reconcile_new_fact, paging, deletion). Each is its
  own PR per the design in #782.

Refs #713

Signed-off-by: rollroyces <rollroyces@users.noreply.github.com>

* The bench takes its corpus back out

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

---------

Signed-off-by: rollroyces <rollroyces@users.noreply.github.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: rollroyces <rollroyces@users.noreply.github.com>
Co-authored-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
* Add utopia CLI with backup and restore subcommands

The operator-facing CLI splits into its own crate so the runtime
image does not need postgresql-client just to run the server
(#712). `utopia backup` snapshots Postgres into a tarball with a
self-describing manifest and checksums; `utopia restore` rebuilds
the database and data dir from one.

## What it does

`utopia backup` writes a single `*.tar.gz` containing a
`manifest.json` (schema version, utopia build, UTC timestamp,
component list, sha256 checksums) plus the `pg_dump -Fc` output
and, optionally, the on-disk data directory. Refuses to overwrite
an existing archive.

`utopia restore --from <archive> [--target-data-dir] [--force]
[--yes]` extracts the tarball, verifies every checksum against
the bytes on disk, refuses forward-incompatible manifests
(schema_version greater than this binary's CURRENT), warns on
older ones, then runs `pg_restore --clean --if-exists` against
the target database and copies the data dir back. Without
`--force`, refuses to overwrite a non-empty target data dir.

`schema_version` is the count of applied migrations in
`migrations/`. A test cross-checks the constant against the
directory at test time, so a future migration file without a
bump to the constant fails the build instead of silently skewing
forward-compatibility checks.

## Workspace

`crates/utopia-cli/` is a new crate and is added to the
workspace `[members]` so `cargo check -p utopia-cli`,
`cargo test -p utopia-cli`, and `cargo run --bin utopia`
all work. Without this entry, the crate compiles in isolation
but the workspace ignores it.

## Smoke test (run against the docker-compose stack)

```
$ createdb / dropdb / createdb cycle on the test database
$ utopia backup --output /tmp/utopia-smoke.tar.gz
$ utopia restore --from /tmp/utopia-smoke.tar.gz --yes
$ SELECT u.email, o.name FROM users u JOIN organizations o ON u.org_id = o.id;
 smoke@test.local | Smoke Org
(1 row)
```

## Negative paths verified

- Forward-incompatible manifest: refuse with explicit error
  ("newer than this binary's 69; refusing to restore").
- Checksum mismatch: refuse with both expected and actual
  hashes named ("manifest says a29f... file is 771b...").
- Archive without `manifest.json`: refuse with "not a utopia
  backup?".
- Older manifest: warn and continue ("older than this binary's
  69; continuing").

## Tests

`utopia-cli` 12 unit tests, all pass (was 8):

- Existing: parses, host redactor, hex encoder.
- New: manifest round-trip, schema-version policy predicate,
  archive-without-manifest, checksum mismatch, schema-version
  count cross-check.

`cargo clippy -p utopia-cli --all-targets -- -D warnings`
clean. `cargo fmt --check` clean.

Refs #712

Signed-off-by: rollroyces <rollroyces@users.noreply.github.com>

* The design note says restore is built

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>

---------

Signed-off-by: rollroyces <rollroyces@users.noreply.github.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: rollroyces <rollroyces@users.noreply.github.com>
Co-authored-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Count phrase signatures by statements rather than evidence rows

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Cover located and stable evidence selection in phrase signatures

Signed-off-by: dada-yan <BinjunYann@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
)

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
…809)

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Maya-Kid and others added 30 commits September 24, 2026 02:42
* Keep stale chat navigation callbacks out of the current view

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Document new-conversation ownership and browser regression invocation

Signed-off-by: dada-yan <BinjunYann@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
* Reread unanswered history once when chat reattachment is idle

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Verify idle refresh cannot overwrite a new send

Signed-off-by: dada-yan <BinjunYann@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
… logs usage once per reply (#893)

* The typed bench waits on documents and rule proposals, scores errata's additions, and can stand in for the reviewer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Record the first measured typed-graph runs in the bench README

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

---------

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…866) (#870)

CONTRIBUTING (both languages) tells contributors to run database-backed
tests against localhost:5432, but docker-compose.yml deliberately maps
the dev container to 1517 on the host (with a comment explaining why —
local Postgres installs commonly occupy 5432). .env.example agrees and
uses 1517. CONTRIBUTING is the only place still pointing at 5432.

Following CONTRIBUTING literally makes the whole utopia-store integration
suite silently skip via test_db::url()'s None-on-missing fallback, so the
failure mode is "green tests, no SQL exercised". CI doesn't catch this
because the migrations job uses a GitHub Actions service container that
exposes 5432 on the runner.

Fix:
- Change the example URL to localhost:1517 in CONTRIBUTING.md and
  CONTRIBUTING.zh-CN.md.
- Add a short comment explaining why (1517 is host-side; inside the
  compose network the app still talks to db:5432).

CI's ports: ["5432:5432"] in .github/workflows/ci.yml stays correct —
the runner is the host of its service container, so 5432 is the right
port on the runner. The mismatch is only on contributor machines.

Closes #866.

Signed-off-by: rollroyces <royce@rollroyces.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: rollroyces <royce@rollroyces.com>
The catalog-derived guard added for migration 0070 proved every
column-level reference edge on the ledger surface is protected in the
schema, but export preflight remained a third, hand-maintained
representation of the same reference surface: a future migration could
add a correctly protected same-KB edge and forget to scan it before
streaming, and a stale preflight branch could outlive its edge.

Factor the preflight scan into a single checked-in source,
export_provenance_integrity.sql, which the runtime executes via
include_str! and the guard parses for -- @edge / -- @filter markers.
Coverage is keyed by structural edge identity (table, column, target),
not by error label, so entity_type_disjoint's two legs still share one
label. Merged-entity checks stay a separate export-filter class.

Widen the scan to all 39 protected structural edges (26 declarative
composite-FK + 13 trigger-covered). The catalog guard now fails when a
protected edge has no preflight branch, when a branch names an edge
the schema no longer protects, or when markers are malformed or
duplicated; a third drift probe plants a protected edge absent from
preflight and expects the new failure bucket.

Clean ledgers emit byte-identical Turtle; malformed ledgers may now be
refused for a same-KB violation on an edge the serializer does not yet
emit, which is the intended fail-closed reading of ADR 0048. Measured
preflight cost is ~4 ms on a ~1000-document / ~3000-fact corpus; total
export time stays within run-to-run noise.



Generated with [Devin](https://devin.ai)

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
…password branches (#867)

The login handler at crates/utopia-server/src/api/auth_routes.rs:121
returned early when the supplied email was not found, before any
argon2 verification ran. The bad-password branch did run verify_password
(~50ms on default params m=19456,t=2,p=1). An attacker could therefore
distinguish registered emails from unregistered ones by timing the
response, and roll a credential-stuffing pass against the survivors.

Fix: keep the user lookup, but always run verify_password before
returning Unauthorized. When the user is missing, verify against a
fixed dummy argon2 hash (DUMMY_PASSWORD_HASH in auth.rs); the result is
discarded, but the call still pays the argon2 cost.

The dummy plaintext (`00-utopia-fixed-timing-attack-mitigation-only-x9f3k-2026-09-23`)
is a clearly-test-only string and the unit test asserts that no common
password or near-miss matches the dummy hash — i.e. the dummy hash's
plaintext is guaranteed never to collide with a real user's password.
The test also asserts the dummy *does* match its own plaintext, which
is the property the timing mitigation depends on (otherwise the
unknown-email branch would fall through PasswordHash::new's
`unwrap_or(false)` and the timing gap would re-appear).

Audit log reason stays accurate (unknown_email vs bad_password) so
admins can still see the attack shape.

Signed-off-by: rollroyces <royce@rollroyces.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: rollroyces <royce@rollroyces.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
…tes so a whole base no longer stays untyped in silence (#894)

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…in a bounded number of times (#895)

parse_phrase_response only read {"b": [[id, key, direction]]}. Any other
JSON object parsed to zero triples with zero malformed, and align_phrases
then treated every signature as "one vote missing", with no WARN and no
re-enqueue (only failed > 0 requeued). The kind-word side had exactly this
bug: DeepSeek-V3.2 at temperature 0 answers an id-keyed object, b as an
object, keys wrapped in arrays, and a whole knowledge base stayed unbound
for twelve documents.

Same treatment for phrases:

- phrase_align: read triples from an id-keyed object whose value is
  [key, dir], {"key","direction"}, null or [id, key, dir]; from b as an
  object; from {"id","key","direction"} entries; from [id, [key, dir]];
  and from a bare top-level array. A reply with no b and non-id keys is
  malformed, not "nothing answered". Direction is case-insensitive.
- align: parse_value accepts a bare array (same hunk as the kind-word fix).
- phrase_alignment: WARN with pass, counts, finish reason and a reply
  snippet when a pass parses to nothing, counted as failed; count
  unanswered signatures and log them; align_phrases_reasking takes the
  reask count from the job payload and re-enqueues itself with reask + 1
  and a growing delay, at most MAX_REASK times, when a round is unfinished
  and the inputs did not change.
- main: the align_phrases job reads reask from its payload.

Unit tests cover each shape; integration tests show an id-keyed first vote
and an object-valued second vote still bind the phrase, and an unreadable
reply writes no row, queues one reask=1 job and stops at the cap.

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
* Keep a stated end when an earlier mention that the fact held merges into its row

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Pin the earlier ended observation path alongside the holds regression

Signed-off-by: dada-yan <BinjunYann@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: Floating-Y <118035379+Floating-Y@users.noreply.github.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Floating-Y <118035379+Floating-Y@users.noreply.github.com>
#897)

* Settle derivations after a missing-document cleanup, as a single delete already does

Signed-off-by: dada-yan <BinjunYann@gmail.com>

* Verify source cleanup preserves unrelated conclusion identities

Signed-off-by: dada-yan <BinjunYann@gmail.com>

---------

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
The backend corpus at crates/utopia-server/src/docs_corpus.rs only
indexed ingest.md. The frontend Docs page (web/src/pages/Docs.tsx)
listed both ingest.md and mcp.md, so users reading the docs saw MCP
documentation that the chat's search_docs tool could never find.
A user asking the chat about MCP tokens, agent access, or write
permissions would get nothing back even though the answer is in the
docs.

Fix: add mcp to ARTICLES.

Drift guard: new test every_corpus_md_file_is_indexed walks
web/src/docs/ and asserts the .md files match the slugs in ARTICLES
one-to-one. The invariant is "on-disk → indexed" (not the other way
round) so a new file dropped in without being indexed fails the test.
This is the same shape as the_backstop_can_be_raised — a number defined
in two places, drift between them is the bug, the test locks the
invariant.

Signed-off-by: rollroyces <royce@rollroyces.com>
Co-authored-by: rollroyces <royce@rollroyces.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
… literal can't be parsed (#831) (#865)

Picking up #831 — the exporter's literal_value() had two real bugs:

1. v.get("value").unwrap_or(v) falls through to the whole object when
   the value field is absent. The match then hits other =>
   other.to_string(), and serde serializes the object — {"summary": ...}
   becomes the literal '{"summary":...}'. A consumer reading the dump
   sees a literal whose lexical form is JSON, not text.

2. When literal_value produced an empty Literal (""), the caller in
   emit_fact/emit_derived still wrote the rdf:object triple. That
   contradicts #821 (an_absent_object_is_not_an_empty_literal).

Fix: split literal_value into two — literal_text() resolves the value
to a string (summary first, value second, scalars as-is, arrays / unknown
object shapes return None), and literal_value() returns Option<Literal>.
The callers in emit_fact, emit_derived, and the qualifier branch all
adopt the Option result and skip the triple when it's None.

Updated a_rule_conclusion_reaches_the_export_as_a_literal: the fixture
uses {"class": "gas_well"}, which is the exact bug shape; the old
assertion tested the bug. The new assertion follows #821 and accepts
that no rdf:object is emitted.

New test an_object_value_with_summary_or_value_does_not_emit_a_json_literal
covers six cases across Turtle and JSON-LD: summary wins, value alone,
summary alone, empty summary falls through, null value + no summary,
unknown object keys.

Signed-off-by: rollroyces <royce@rollroyces.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: rollroyces <royce@rollroyces.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
Signed-off-by: Floating-Y <118035379+Floating-Y@users.noreply.github.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
…related steps (#898)

Signed-off-by: dada-yan <BinjunYann@gmail.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
… VISIBLE (#907)

* fix(server): index mcp.md in the chat-searchable docs corpus

The backend corpus at crates/utopia-server/src/docs_corpus.rs only
indexed ingest.md. The frontend Docs page (web/src/pages/Docs.tsx)
listed both ingest.md and mcp.md, so users reading the docs saw MCP
documentation that the chat's search_docs tool could never find.
A user asking the chat about MCP tokens, agent access, or write
permissions would get nothing back even though the answer is in the
docs.

Fix: add mcp to ARTICLES.

Drift guard: new test every_corpus_md_file_is_indexed walks
web/src/docs/ and asserts the .md files match the slugs in ARTICLES
one-to-one. The invariant is "on-disk → indexed" (not the other way
round) so a new file dropped in without being indexed fails the test.
This is the same shape as the_backstop_can_be_raised — a number defined
in two places, drift between them is the bug, the test locks the
invariant.

Signed-off-by: rollroyces <royce@rollroyces.com>

* test(alerts): lock the role rank order across rank(), PartialOrd, and VISIBLE

The comment at alerts.rs:340 names the invariant: `rank()`,
`Role`'s derived `PartialOrd`, and the CASE inside `VISIBLE`
all agree on Viewer<Editor<Admin<Owner (0,1,2,3).

The drift shape is the same as the_backstop_can_be_raised: a
single concept expressed in three places that can move out of
sync silently. Consequences here are sharper than the worker
concurrency case — a rank off by one lets viewers see editor-only
alerts (information leak) or owner-only alerts (escalation noise).

Add a static-analysis test that catches:
- `rank()` not in the same order as the enum's PartialOrd
- The CASE in VISIBLE missing WHEN 'viewer'/'editor'/'admin'
- The CASE in VISIBLE not having ELSE 3 (Owner falls through)
- `rank(Owner) != 3` (drift between Rust and SQL rank)

Adding a new role between Admin and Owner causes `rank()` to
fail to compile (non-exhaustive match) — that's the first guard.
This test catches the subtler drift where someone changes one of
the three places without touching the other two.

Whitespace-safe: VISIBLE is normalised via split_whitespace before
the substring checks, since the source uses double-space alignment
("'admin'  THEN") that varies.

Signed-off-by: rollroyces <royce@rollroyces.com>

---------

Signed-off-by: rollroyces <royce@rollroyces.com>
Co-authored-by: rollroyces <royce@rollroyces.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
…refetches it (#909)

GitHub's camo proxy cached an empty body for the weekly badge (200,
image/svg+xml, 0 bytes) while the daily one renders and trendshift
itself serves both. The cache key is the URL, so a harmless query
parameter makes the proxy fetch it again. Both READMEs.

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Let a source push statements in the open contract instead of prose a model must read back

A statements source takes the extraction contract itself (e/s/n) on
POST /sources/{id}/statements with the api push's identity, versions and
tombstones; the payload is one chunk and extraction parses it instead of
prompting, so a pushed statement is an open statement and reaches the typed
graph only through alignment. Record 0054.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Keep the embed settings on the pushed-statements path so name vectors are still computed

Only the chat client is absent when a chunk is the contract itself; the
embedding model that #877 uses for name vectors comes from the same settings
row and applies to pushed names as much as extracted ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Locate a pushed statement's time words in the payload itself and refuse an unlisted subject at the door

`locate_time` begins with `quote?`, and the door requires a pushed
statement's quote to be null, so every `when` / `ended` on a pushed
statement fell into the TIME_NOT_IN_QUOTE branch: the fact landed with
only doc_time and a drop signal, and no time mention was ever recorded.
On the pushed path the chunk is the payload, so the words are looked up
in the chunk directly. The DB test now asserts the mention lands; it
fails without this change.

The door now resolves each statement's subject and each name's entity
against `e`, folding whitespace and case as `name_key` does, and answers
422 instead of letting extraction drop the item silently as UNKNOWN_REF.
An object not in `e` still lands as a literal value, as it does for a
model's reply. The route carries a body limit of four payloads so the
2 MiB default is no longer buffered before the token check; the
documented 64 KiB refusal stays a 422 with its message.

Tests added: tombstone marks and a new push revives, a tombstone for an
unknown identity is a no-op, the missing Authorization header, the
64 KiB limit over HTTP, the 200-item limit, an `n` entity not in `e`,
a non-null `n` quote, and the folded subject that must pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Let a statements source's push token be viewed and rotated like an api source's

Found by running the server end to end: the token came back once at
creation, then GET /token and POST /rotate-token answered 404 because
both still gated on kind == "api". One predicate, has_push_token, now
names the two kinds that carry a push token, and creation, view and
rotation use it. A test signs in as the editor, reads the token back,
rotates it, and checks that the old token is refused and the new one
pushes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

---------

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Let business rules join entities and conclude relations

Signed-off-by: wangzifei <wangzifei@cit.group.hk>

* Let the review queue see a refused rule relation, restore the derived count, and bucket the join

Review fixes on #861, applied as maintainer edits.

run() and materialize() now share one resolve() step: asserted edges
plus the surviving rule-concluded relation edges form the pool, derive()
runs over it once per round, and contradictions() sees every relation
candidate. A candidate that loses leaves the pool but stays in the
candidate list, so the queue row exists for it and says which business
rule produced it (`rule: business_rule`, `attribute_rule_id`); the
conclusions that stood on it retire with it, and a refused key is not
retried, so the fixed point still ends. The queue's key and foreign key
fall back to the last asserted premise when the chain runs through a
provisional edge.

`Rule::Business` names such a candidate instead of borrowing
`Transitive`, so nothing positional keeps a rule-concluded edge out of
the axiom persistence loop. `derive_with_blocked` goes: the pool is
rebuilt each round, so a refused edge is simply not in it.

`DeriveReport.derived` is again what the engine produced (the last
round's axiom derivations and relation candidates, plus the rules'
distinct conclusions), which `a_contradiction_points_upstream` pins.

`joined_evaluate` buckets the join edges by subject: scanning all edges
per X was quadratic in pairs, 5.9 s for 100,000 pairs against 82 ms for
10,000; bucketed it is 105 ms. Decision 4's numbers are in the record
and the PR; the caps stay.

Migration 0071 keeps its number, CURRENT_SCHEMA_VERSION is the file
count (75), and the two tests dev gained since the branch use the new
condition side and join argument.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

---------

Signed-off-by: wangzifei <wangzifei@cit.group.hk>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: wangzifei <wangzifei@cit.group.hk>
Co-authored-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
…d and declaring predicate (#910)

A derivation's generating rule reached the export as a prov:Activity
with only an rdfs:label, so a reader parsed the label to learn whether
an axiom or a business rule produced a conclusion, and for inverse and
sub_property could not recover which predicate the axiom was declared
on without re-deriving the engine's convention from the exported
owl:inverseOf / rdfs:subPropertyOf.

Every rule resource is now typed utopia:AxiomRule or utopia:BusinessRule.
An axiom rule carries utopia:axiomKind (the closed enum) and
utopia:declaredOn, the IRI of the predicate the declaration sits on,
read from rules.predicate_id in the same derived_page query. The label
stays. Business-rule conditions and expressions are not exported: a
business rule is edited in place, so its IRI cannot vouch for the
definition an older conclusion was drawn under; that waits for rule
versioning. Recorded as a dated revision of 0020; the MCP guide says
the same.

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… it was drawn under (#913)

A business rule was one row edited in place, and a derivation pointed at
the row. Change a threshold and the invalidated conclusions pointed at a
rule that now said something else: the record axis kept "we concluded
this, then it stopped holding" and lost "under which definition".

Every edit that changes what a rule says now opens a version in
attribute_rule_versions, a full snapshot (subject class, conclusion, join
predicate, conditions) with a record time, and closes the previous one.
Name, description and the enabled switch open nothing; whether the
definition changed is decided by comparing the snapshot JSON, produced by
one SQL expression the migration's backfill and the store share.

A derivation names the version it was drawn under
(derived_facts.attribute_rule_version_id). A conclusion that still stands
after an edit keeps its row and moves to the new version, counted as
`redefined`; the rows the edit invalidates keep pointing at the version
they were drawn under. The proof carries the version number and its
definition, the rules panel shows the version next to the name and opens
the history, and GET /kbs/{id}/rules/{rule_id}/versions reads it for an
integration, with the labels the ids resolve to today. Existing rules
start at version 1 from the migration.

Recorded as 0060. CURRENT_SCHEMA_VERSION is 76.

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…e date it was pushed with (#911)

* Materialization reconciles the rows it writes, and a version keeps the date it was pushed with

Two gaps from the #875 replay (#899, #900).

Typed materialization wrote rows through the graph's gate but never ran
the uniqueness-timeline reconciliation the write path runs after every
state fact, so a functional attribute kept two open values and a later
observation could not close the earlier one without a manual reconcile.
materialize and try_materialize now collect the rows they wrote (new,
merged and implied) and reconcile them after the commit, the same
reconcile_facts the write path uses; Outcome reports corrected and
conflicts. A timeline rewrite (close, rehome) now carries
from_statement_id, implied, typed_fact_sources and implied_fact_sources
onto the corrected row, so the next round sees it as already computed
instead of adding a second one.

A same-identity update replaces the document's doc_time, so evidence
that stayed on the earlier version dated at the later time and the two
values looked simultaneous. document_versions gains doc_time (migration
0090, backfilled for the current version), every version row records
the document's date at that moment, and the timeline dates a fact by
its evidence's own version, falling back to the document's date for
older rows.

A pushed statements document now carries the observation's external_id
and doc_time ahead of the three arrays, so two observations that saw
the same thing are two documents under the one-document-per-content
index, and the same payload under a new identity is never a rename.
The parser reads only e, s and n. The record and the guide say so.

Tests: the two reproductions from #899 and #900 run un-ignored as
acceptance; the explicit-reconcile test now expects the materialization
to have done the work; a route test pushes one payload under two
identities and gets two dated documents. Store and server suites green
on a fresh database, clippy clean.

Closes #899
Closes #900

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Count migration 0076 into the schema version after #913

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

---------

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…didates, and each task thinks as hard as it needs (#906)

* A retraction needs a flag and two votes; an addition may name a new thing the document names

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* A workspace can tell its chat model how hard to think, and the bench runs extraction at minimal

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* The alignment prompt describes each candidate once per batch, and rules are asked only of bound signatures

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* The aligner shortlists a signature's candidates by relevance when the structure fits too many

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* The rule prompt shares its glossary, kind words get a shortlist too, and a hidden fitting key still counts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Alignment and rule proposals think at the endpoint's default effort; bulk tasks keep the workspace's

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* An ontology embedding job that lost a batch fails so it retries instead of leaving vectors missing

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* A label the model answers instead of a key still names the property when it is unique

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Alignment batches, rule proposals and the bench judge run four at a time instead of one after another

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Log the head of an alignment reply that carried malformed votes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Errata sees the properties nearest the document, and rule proposals think at low effort

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Drop a needless borrow the parallel batches left behind

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Alignment thinks at low effort, a measured middle between off and the endpoint's default

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Keep the malformed-vote log reachable and the extract test helper whole after the rebase

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Record the second measured runs: where the tokens went and what each cut changed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* Pick the reasoning effort with the Dropdown control, as the design rules require

The settings page rendered a native select for the four-way effort choice; the style guard refuses a raw select, a rounded-md corner and a py-1.5 gap, and the design says a small bounded enum is a Dropdown so a page never shows two kinds of popup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

---------

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…e; tell the adjudicator the names differ (#889)

* Never auto-merge a similarity-proposed pair on the batch verdict alone; tell the adjudicator the names differ

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* The second look records what it tried after its lookup budget ran out

On the identity bench, roughly one second look in three ended as "the agent
looked but did not conclude": six lookups, then two more turns that never
reached decide or defer. The trace did not say what those two turns were, so
it now records a refused lookup (with the tool and arguments the model asked
for) and a turn that only spoke (its first 200 characters). Turns that hit
the limit are budgeted separately from lookups, and the limit message says
to answer with decide or defer. Whether the budget itself is too small is
what the next bench run is for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* The second look may look eight times, the length of its own menu

Two bench runs with the trace recording showed what the inconclusive second
looks were doing after six lookups: asking for the two lookups the prompt
still lists, namesakes and consequences, and being refused both. A look that
was refused once still concluded on its last turn; one refused twice never
did. Six was two short of the menu the prompt offers; eight is that menu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

* A similarity-proposed same is escalated when the second look cannot run, under governance too

The batch verdict on a name-vector pair could still merge on two paths:
when look_again returned None (loop budget spent, model error) the
adjudicator fell through to put_verdict and apply_verdict, and the
cached verdict then skipped the second look on every later encounter;
and with governance on, wants_second_look had no similarity rule, so a
0.85 same on an unrelated-shaped pair merged on the batch alone.

Now batch_verdict_may_apply says when a batch verdict may land without
the second look (never for a similarity-proposed same), and both paths
escalate as second_look_unavailable instead, without caching. The
name_vector prefix lives once, in utopia_core::review_reasons; the
store writes it, the server reads it, and the extractor takes only the
cosine. The stale six-lookups comment follows MAX_STEPS. The design
note and 0041's status line say what the rule is now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>

---------

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…member it (#914)

Since #786 every streamed call sends max_tokens 65,536 so that a cut
reply is ours to detect. An endpoint whose completion limit is lower
refuses instead of clamping, and every chunk fails: gpt-4o-mini caps at
16,384 (#891).

When a 400 states the endpoint's own ceiling, the call is retried once
at that number, and the number is kept on the client, shared with its
clones, so a process pays the refusal once rather than once per chunk.
The ceiling only ever goes down. Only a number the message itself calls
tokens counts, with or without thousands separators and no lower than
1,024: the digits of a model name, a status code or our own echoed
request are not limits, and a message that parses to nothing leaves the
caller with exactly the 400 it gets today. A context-window refusal
lowers to the window and may still fail on the retry, as today.

Landed from #892 by basil-k-aji-dev with the two review points applied:
the ceiling on the client instead of a local, and the shape rule for the
number instead of "any integer below what we sent".

Closes #891
Closes #892

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: basil-k-aji-dev <70605804+basil-k-aji-dev@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…iding the same cluster ten times (#916)

Every document's extraction enqueues a govern job for its base, and
enqueue_unless_queued only skips a job that is still queued; with 64
workers each new job starts at once. On the typed-graph bench nine
govern runs overlapped on one base, all read the same queue heads, and
1346 review pairs received 8888 keep decisions (one pair ten decisions
in 38 seconds from ten runs); a third of the run's model tokens went
there, and the duplicate-key errors on agent_decisions_open were the
concurrent proposals colliding.

govern() now takes a per-base session-level advisory lock (try only, as
the vector index build does). A run that does not get it exits and
enqueues one govern for a minute later, deduplicated against queued
jobs only, so pairs that arrive after the running job read its last
queue head are still picked up. The queue and cluster reads skip rows
the running job has marked adjudicating. close_review_auto reports how
many rows it closed, and a keep or already-merged outcome records no
decision when the row was closed by someone else.

Store test: the second lock attempt on a base fails until the first is
released; another base is another lock.

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ith tokens by phase (#917)

Two groups on dev at 961c3c0 with gemini-3.5-flash, judged and with
errata: precision 91.0% / 88.5% before errata and 96.0% / 96.5% after;
same-sentence gold recall 11.3% / 13.2% before errata and 18.6% / 22.4%
after, with the errata retractions mostly wrong on this binary (72/98,
164/196), which is what #906's two-vote retraction addresses; 158k and
183k tokens per document. The token table by phase shows where they
went: the alignment window carried the rule proposals' per-item property
tables, the two-vote phrase alignment, and the governance agent deciding
the same pairs from up to ten concurrent runs (9481 decisions on 1421
pairs), fixed in #916.

The bench judge now fails fast when it would read a sealed key from
llm_settings instead of sending the ciphertext and collecting 401s.

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…tokens by phase (#918)

Same corpus, model and judge as the pre-#906 baseline, on dev with #906
and #916. Judged precision 81.0% / 79.0% before errata and 83.0% / 80.5%
after (the loss against the baseline is misworded facts from minimal
reasoning); same-sentence gold recall 14.2% / 14.3% before errata and
17.8% / 17.1% after, with errata now adding without retracting wrongly
(2 and 1 retractions, none judged stated); 31k and 34k tokens per
document, a fifth of the baseline and still five times 0044's ceiling,
seven tenths of it in alignment and rule proposals. 47 and 48 minutes a
group against three to five hours.

Signed-off-by: Wayland Yang <wayland0916@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants