Release v0.1.0-rc7 - #915
Open
WaylandYang wants to merge 102 commits into
Open
WaylandYang wants to merge 102 commits into
WaylandYang wants to merge 102 commits into
Conversation
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>
…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>
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
chat_reasoning_effortin the workspace settings rides on every chat call, extraction and errata run atminimal, alignment and rule proposals atlow, all measured on the typed-graph bench (Errata retracts on two votes and a flag, alignment shortlists its candidates, and each task thinks as hard as it needs #906, migration 0075).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,
deletedis 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
search_docstool now finds the MCP guide, with a test that keeps the docs corpus and the Docs page in step (fix(server): index mcp.md in the chat-searchable docs corpus #905).Export and provenance
valuebeforesummary, a typing conclusion exports its class, and prose is alwaysxsd:string(fix(rdf): resolve object_value text robustly; emit no rdf:object when literal cannot be parsed (#831) #865); exported rules say which family they belong to (Export says which family a rule belongs to, and an axiom rule its kind and declaring predicate #910, Document the scope of exported rule identities #904).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.