forked from 1b-agent/opencadstudio-upstream
- ARCHITECTURE.md: add 'Flow: from the single source of truth to a committed change' mermaid diagram (spec/entities.toml + layouts.json -> codegen -> facade -> wire -> transport -> executor -> host backend/Scene); fix stale claims (hand-maintained enums not 'GENERATED'; accuracy-first mass properties + Scene cold-tess budget; with_body borrow; tab auth); add Review round 3 change-log; update Known deviations. - README.md: Core guarantees now include Loft/query caps, accurate mass properties (Scene budget persists + graceful fallback), and tab authorization. - IMPLEMENTATION.md: accuracy paragraph (Scene budget + fallback) + current test counts (623 host lib + 8 crate + 3 consistency + 34 plugin_api). - Regenerate api_reference.md (26 sections) + binding_schema.json (28 ops / 12 queries / 14 handles / 19 families). - 623 host lib + 8 crate + 3 consistency + 34 plugin_api green.
481 lines
25 KiB
Markdown
481 lines
25 KiB
Markdown
# ocs_doc_api — Architecture
|
||
|
||
This document explains how `ocs_doc_api` is put together and, most importantly,
|
||
the **workflow for adding a new entity, family, or method**. It assumes you've
|
||
read `README.md` (the user-facing surface).
|
||
|
||
## Layered design
|
||
|
||
```
|
||
┌─ Facade (facade.rs) DocApi → Document → collections → typed handles
|
||
│ Object-centric, hand-written. Methods build Operation/Query and call
|
||
│ Transport::apply. OpGroup (client compensation), query_batch, assert_revision.
|
||
│
|
||
├─ Wire (envelope/ops/query) DocApiEnvelope = Op(single) | Queries(batch)
|
||
│ bincode + serde. Operation/Query enums are HAND-MAINTAINED (src/gen,
|
||
│ append-only) → stable discriminants; the spec's op/query names must map to
|
||
│ them (asserted by spec_wire_consistency tests).
|
||
│
|
||
├─ Transport (transport/) trait Transport: Send+Sync { apply, alive }
|
||
│ InProcess (host) | OcsPluginApiIpc (plugin). The ONLY plugin↔host boundary.
|
||
│ Portability contract: &self + internal serialization + correlation + timeout.
|
||
│
|
||
├─ Executor (executor.rs) apply_op / apply_queries ← crate-owned logic
|
||
│ Per-op atomicity: validate+compute BEFORE push_undo; mutate; finalize_op once.
|
||
│
|
||
├─ Backend (backend.rs) trait DocApiBackend ← thin host hook
|
||
│ resolve_body / store_solid / update_solid / add_curve / transform_entity /
|
||
│ bounds / volume / centroid / revision / push_undo / finalize_op /
|
||
│ ensure_transformable / can_remove / ...
|
||
│
|
||
└─ Host (src/app/doc_api.rs) impl DocApiBackend for HostSession
|
||
Maps to scene/app primitives: register_solid_model, restore_solid_models
|
||
(lift-on-miss), solid_to_sat, edge_wires, erase_entities, meshes.metrics.
|
||
```
|
||
|
||
The same `executor` runs in-process and over IPC — one implementation, versioned
|
||
in the crate. The host only supplies scene primitives through `DocApiBackend`.
|
||
|
||
## Flow: from the single source of truth to a committed change
|
||
|
||
The **single source of truth** for the API surface is `spec/entities.toml` —
|
||
every entity family, its construction ops, geometric actions, and queries, and
|
||
their kernel/acadrust mapping. Everything downstream derives from it. The full
|
||
flow from spec to a committed document change:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph SRC["Single source of truth"]
|
||
SPEC["spec/entities.toml<br/>families · constructors · methods · kernel map"]
|
||
LAYOUTS["src/gen/layouts.json<br/>Operation/Query wire layouts"]
|
||
end
|
||
|
||
subgraph GEN["build.rs codegen (cargo build, CI drift-checked)"]
|
||
OPS["src/gen/ops_gen.rs<br/>Operation enum (append-only)"]
|
||
QRY["src/gen/query_gen.rs<br/>Query enum (append-only)"]
|
||
APIREF["src/gen/api_reference.md"]
|
||
SCHEMA["src/gen/binding_schema.json<br/>(object model + layouts inlined)"]
|
||
end
|
||
|
||
subgraph FACADE["Facade (facade.rs) — plugin-facing, object-centric"]
|
||
API["DocApi (root)"]
|
||
DOC["Document (per tab)"]
|
||
COLL["collections: solids() / curves() / entities()"]
|
||
HND["typed handles: Solid · Line · ArcCurve · Text · ...<br/>methods build ops (intersect, transform, create_*)"]
|
||
end
|
||
|
||
subgraph WIRE["Wire (envelope/ops/query)"]
|
||
ENV["DocApiEnvelope<br/>Op(Operation) | Queries(Vec)"]
|
||
REC["Receipt: outcome + query_results + new_revision"]
|
||
end
|
||
|
||
subgraph TRANS["Transport (the only plugin↔host boundary)"]
|
||
INP["InProcess (host / tests)"]
|
||
IPC["OcsPluginApiIpc (plugin)<br/>PluginRequest::DocApiRequest{tab_id, bytes}"]
|
||
end
|
||
|
||
subgraph EXEC["Executor (executor.rs) — crate-owned, one impl"]
|
||
AO["apply_op: validate+compute → push_undo → mutate → finalize_op"]
|
||
AQ["apply_queries: read-only batch (capped)"]
|
||
end
|
||
|
||
subgraph HOST["Host backend (src/app/doc_api.rs — HostSession)"]
|
||
BE["DocApiBackend impl<br/>resolve_body · store_solid · transform · add_* · bounds · volume · centroid"]
|
||
TAB["tab auth: reject tab_id ≠ bound tab"]
|
||
SCN["Scene (per tab)<br/>solid_models · meshes · erase_entities<br/>geometry_epoch · doc_api_mass_cache · doc_api_cold_tess_used"]
|
||
CAD["CadDocument (acadrust)<br/>entities · block_records · handles"]
|
||
end
|
||
|
||
SPEC -->|spec → enum names + mapping| OPS
|
||
SPEC -->|spec → enum names + mapping| QRY
|
||
SPEC -->|spec → reference| APIREF
|
||
SPEC -->|spec → schema| SCHEMA
|
||
LAYOUTS -->|layouts inlined| SCHEMA
|
||
|
||
API --> DOC --> COLL --> HND
|
||
HND -->|method → Operation / Query| ENV
|
||
ENV --> INP
|
||
ENV --> IPC
|
||
INP --> AO
|
||
IPC -->|bincode → host routes DocApiRequest| AO
|
||
AO --> TAB
|
||
AQ --> TAB
|
||
TAB --> BE
|
||
BE --> SCN
|
||
BE --> CAD
|
||
SCN --> REC
|
||
CAD --> REC
|
||
REC -->|deserialized| HND
|
||
|
||
classDef truth fill:#1f6feb,stroke:#0b3d91,color:#fff
|
||
classDef gen fill:#2da44e,stroke:#14532d,color:#fff
|
||
classDef hot fill:#bf8700,stroke:#7a5200,color:#fff
|
||
class SPEC,LAYOUTS truth
|
||
class OPS,QRY,APIREF,SCHEMA gen
|
||
class AO,AQ,TAB,SCN hot
|
||
```
|
||
|
||
**How to read it:**
|
||
|
||
- **Blue (source of truth)** — `spec/entities.toml` + `layouts.json`. Change
|
||
*these* to change the API surface; everything else regenerates or maps onto them.
|
||
- **Green (codegen)** — `build.rs` derives the wire enums, the human reference,
|
||
and the binding handover schema. The `Operation`/`Query` enums are the canonical
|
||
append-only wire vocabulary; the spec's `op`/`query` names must map to them
|
||
(enforced by `spec_wire_consistency` tests).
|
||
- **Facade → wire → transport** — a plugin's `solid.intersect(&other)` becomes an
|
||
`Operation`, wrapped in a `DocApiEnvelope`, and shipped via the one `Transport`
|
||
(in-process or IPC). Identical request either way.
|
||
- **Amber (hot paths)** — the executor's per-op invariant and the host's tab
|
||
authorization + per-tab `Scene` state (solid cache, geometry epoch, the
|
||
persistent cold-tessellation budget). This is where atomicity and the DoS
|
||
boundaries live.
|
||
- **Response** — the host produces a `Receipt` (outcome + query results + new
|
||
geometry revision) that the facade turns back into a typed result (`Solid`,
|
||
`Aabb`, `f64`, …).
|
||
|
||
## Per-op execution model (the load-bearing invariant)
|
||
|
||
Every write op follows this exact sequence (see `executor::apply_op`):
|
||
|
||
1. **Validate + compute** — resolve inputs, run kernel geometry (`make_solid`,
|
||
`brep::combine`, `brep::transform`), pre-flight fallible host work
|
||
(`prepare_solid_model_display`), check `ensure_transformable` / `can_remove`.
|
||
*No mutation yet.*
|
||
2. **`push_undo(label)`** — begin the host's undo delta capture.
|
||
3. **Mutate** — `store_solid` / `update_solid` / `add_curve` / `transform_entity` /
|
||
`remove_entity`.
|
||
4. **`finalize_op()`** — close the undo delta + republish the document view.
|
||
|
||
On any error in steps 1–3, the op returns before `finalize_op`: **nothing is
|
||
applied, no undo step, no revision bump.** This is the per-op atomicity
|
||
guarantee. `brep::combine` is pure; `erase_sources` only runs on success.
|
||
|
||
The geometry revision advances via the host's internal `bump_geometry` inside
|
||
`add_entity`/`update_entity`/`register_prepared_solid_model`. The epoch moves
|
||
monotonically per op (one undo step per op); the exact bump count is a host
|
||
internal — do not assert on it.
|
||
|
||
## Object identity and the solid cache
|
||
|
||
- **`ObjectId`** = `acadrust::Handle` (u64). Kernel keys never cross the API.
|
||
- **Solids** live in the host's `scene.solid_models: HashMap<Handle, Body>`
|
||
cache. `resolve_body` does **lift-on-miss** (`restore_solid_models`): if a
|
||
solid isn't cached, its body is re-lifted from the entity's ACIS SAT. This is
|
||
what makes DocApi-created solids visible to host-native booleans and vice
|
||
versa, and what makes a reloaded DWG's solids usable.
|
||
- `store_solid` / `update_solid` pre-flight the mesh (`prepare_solid_model_display`)
|
||
**before** committing, so the post-commit `register_prepared_solid_model`
|
||
cannot fail — there is no rollback path that would bump the revision on failure.
|
||
|
||
## Bulk ops and read batching
|
||
|
||
- `CreateMany` / `TransformMany` / `DeleteMany` are **single ops**, one undo
|
||
step, one publish — but **all-or-nothing via upfront validation**: every
|
||
fallible check (make_solid, `ensure_transformable`, `can_remove`) runs before
|
||
`push_undo`, so the apply loop cannot fail part-way. Item-capped
|
||
(`BULK_ITEM_CAP`) to bound host execution.
|
||
- Read-only query batching (`query_batch`) shares one round-trip and never bumps
|
||
the revision. Also capped.
|
||
- Why: per-op `publish_document_view` is a full-document serialization, so N
|
||
sequential ops cost O(N²) — bulk ops collapse that to O(N) once. See
|
||
"Performance" below.
|
||
|
||
## Codegen (build-time, spec-driven)
|
||
|
||
`build.rs` reads `spec/entities.toml` and regenerates the committed snapshots:
|
||
|
||
- `src/gen/ops_gen.rs`, `src/gen/query_gen.rs` — the `Operation`/`Query` enums
|
||
(the canonical wire vocabulary; **append new variants at the END only**).
|
||
- `src/gen/api_reference.md` — human API reference.
|
||
- `src/gen/binding_schema.json` — the merged binding-handover schema (object
|
||
model + method signatures + op/query mapping + wire layouts), exposed as
|
||
`binding_schema_json()` for REPL/scripting bridges.
|
||
|
||
`cargo build` regenerates them deterministically; CI fails on drift.
|
||
|
||
---
|
||
|
||
## Workflow: adding a new entity, family, or method
|
||
|
||
This is the common task. Follow these steps in order.
|
||
|
||
### A. Add a new `Operation` or `Query` variant (the wire vocabulary)
|
||
|
||
1. **Append** the variant to `src/gen/ops_gen.rs` / `src/gen/query_gen.rs`
|
||
**at the END** of the enum (never reorder — bincode discriminants are
|
||
order-based). Use existing payload DTOs or add new plain-data mirrors in
|
||
`src/ops.rs` / `src/query.rs`.
|
||
2. Add its name to `Operation::op_name` / `Query::query_name` in `src/ops.rs` /
|
||
`src/query.rs`.
|
||
3. Regenerate: `cargo build -p ocs_doc_api` — the spec/schema pick up the wire
|
||
change. Update `src/gen/layouts.json` so `binding_schema.json` stays
|
||
self-contained.
|
||
|
||
### B. Add a family (e.g. a new entity kind)
|
||
|
||
1. **Spec** — add a `[[family]]` block in `spec/entities.toml` (name,
|
||
`acadrust_variant`, `handle`, `collection`, and its `[[family.method]]`s).
|
||
2. **Handle type** — add a `handle!(NewType)` line in `facade.rs` (gives it
|
||
`bounds`/`delete`/`transform` + `id()`), plus `HasId`. Add family-specific
|
||
methods as an `impl NewType` block if it has geometric actions.
|
||
3. **Construction** — add a `create_*` method on the right collection
|
||
(`SolidCollection`/`CurveCollection`/`EntityCollection`) building the op.
|
||
4. **Backend conversion** — extend `src/app/doc_api_convert.rs`
|
||
(`curve_spec_to_entity`, `entity_kind_name`, `entity_bounds`) to map the new
|
||
acadrust `EntityType` variant.
|
||
5. **Executor** — if the family has a new op variant, add its arm to
|
||
`executor::apply_op`.
|
||
|
||
### C. Add a method to an existing family
|
||
|
||
1. **Wire** — if it's a new operation/query, do step A first. If it reuses an
|
||
existing op (e.g. another boolean), skip to step 3.
|
||
2. **Spec** — add a `[[family.method]]` entry under the family (name, kind,
|
||
op/query, args, returns, `fixed` fields).
|
||
3. **Facade** — add the method to the handle's `impl` block in `facade.rs`;
|
||
it should build the `Operation`/`Query` and call `session.apply_op` /
|
||
`session.one_query`, returning the typed result.
|
||
4. **Executor + backend** — if the op is new, implement its arm in
|
||
`executor::apply_op` (driving `DocApiBackend`), and add/override the backend
|
||
method in the host (`src/app/doc_api.rs`) mapping to the scene primitive.
|
||
5. **Test** — add a mock-backend unit test (`tests/mock_backend.rs`) and, for
|
||
host-affecting ops, a `HostSession` integration test in `src/app/doc_api.rs`
|
||
(model it on the existing roundtrip tests).
|
||
|
||
### D. Keep the invariants
|
||
|
||
- Validate + compute **before** `push_undo`; never mutate in the validate phase.
|
||
- One undo step per op; `finalize_op` only on success.
|
||
- New op/query variants **append at the end** of the generated enums.
|
||
- Host geometry stays in the host (kernel keys never cross the wire); the wire
|
||
carries `ObjectId`s and plain-data DTOs.
|
||
|
||
---
|
||
|
||
## Error model
|
||
|
||
`ApiError::{Validation, Geometry, UnknownId, Unsupported, Transport}` —
|
||
serializable, so IPC returns the same structured error the in-process executor
|
||
produced. Bulk-op validation names the failing index. A stale handle's methods
|
||
return `UnknownId` (structured), never a panic.
|
||
|
||
## Performance notes
|
||
|
||
- **Per-op publish** (`finalize_op` → `publish_document_view`) is a
|
||
full-document serialization; N sequential ops cost O(N²). This is the
|
||
documented trade of per-op autocommit and the reason bulk ops exist.
|
||
Measured estimate: ~10⁵ sequential creates+transforms ≈ minutes; the same as
|
||
one `CreateMany`+`TransformMany` ≈ ~1 s. Use bulk ops for N ≳ a few hundred.
|
||
- **`volume`/`centroid`** are **accuracy-first**: a fine tessellation
|
||
(`mesh_body(0.1, 1e-4)`) for near-analytic results, memoized per
|
||
(handle, geometry_epoch) on the per-tab `Scene`. The cold-tessellation budget
|
||
(256/tab) bounds the DoS surface and **persists across DocApi dispatches**;
|
||
when exhausted the path degrades to the approximate `meshes.metrics` value
|
||
rather than failing.
|
||
- **`resolve_body` lift-on-miss** is cheap when the cache is warm (no-op). The
|
||
read-only `with_body` accessor borrows the cache in place (O(1)) instead of
|
||
deep-cloning the B-rep — `bounds()` uses it.
|
||
- **IPC** serializes the envelope once; the `bytes` fields use `serde_bytes` so
|
||
the outer frame is a single length-prefixed copy.
|
||
- **Tab authorization**: `execute_doc_api` rejects a request whose `tab_id`
|
||
doesn't match the bound tab, so a plugin can't reach tabs it isn't bound to.
|
||
|
||
---
|
||
|
||
## Changes from the original design (review-driven)
|
||
|
||
The initial implementation was reviewed (`/review`) and refined in two rounds.
|
||
This section records the concrete changes and *why* they were made, so the
|
||
reasoning isn't lost. Tests are green after every change (592 host lib + 8 crate
|
||
unit + 7 `HostSession` integration tests, incl. the regression tests added for
|
||
the CRITICAL fixes).
|
||
|
||
### Files introduced / modified in `src/app/`
|
||
|
||
The host-side integration lives in `src/app/`. Concretely:
|
||
|
||
**New files**
|
||
- **`src/app/doc_api.rs`** — the `DocApiBackend` implementation for `HostSession`
|
||
(the thin host hook). Contains:
|
||
- `execute_doc_api(host, tab_id, bytes)` — the dispatch entry: deserialize the
|
||
`DocApiEnvelope`, run the crate executor, serialize `ApiResult<Receipt>`.
|
||
- `impl DocApiBackend for HostSession` — `resolve_body` (lift-on-miss via
|
||
`restore_solid_models`), `store_solid`/`update_solid` (pre-flight
|
||
`prepare_solid_model_display` before commit → `register_prepared_solid_model`),
|
||
`add_curve`, `remove_entity` (direct `erase_entities` + locked-layer error),
|
||
`can_remove`, `ensure_transformable`, `transform_entity`, `bounds`
|
||
(lift-on-miss for solids), `volume`/`centroid` (`meshes.metrics` cache +
|
||
divergence fallback), `revision`, `push_undo`/`finalize_op`.
|
||
- helpers `obj_to_handle`/`handle_to_obj`, `kernel_placement`,
|
||
`mesh_volume_centroid` (single divergence-theorem helper).
|
||
- 7 `#[cfg(test)]` integration tests (create/boolean/query roundtrip, unknown-id
|
||
error, transform-many all-or-nothing, query-batch cap, 2D-entity roundtrip,
|
||
geometric-methods roundtrip, **DWG write→reload roundtrip**).
|
||
- **`src/app/doc_api_convert.rs`** — acadrust `EntityType` ↔ DTO conversions:
|
||
`curve_spec_to_entity` (Line/Circle/LwPolyline/Point from `Curve2Spec`),
|
||
`entity_kind_name`, `entity_bounds` (per-family coarse bounds for non-solids).
|
||
|
||
**Modified files**
|
||
- **`src/app/mod.rs`** — registers the two new modules (`doc_api`,
|
||
`doc_api_convert`), both gated `#[cfg(not(target_arch = "wasm32"))]`.
|
||
- **`src/app/plugin_host.rs`** — extends `HostSession`:
|
||
- a `doc_api_pending: Option<PendingDelta>` field (the in-flight per-op undo delta);
|
||
- accessors used by the backend: `scene()`/`scene_mut()`,
|
||
`commit_entity_handle(entity)` passthrough, `begin_doc_api_undo(label)`
|
||
(→ `app.begin_undo`), `commit_doc_api_undo()` (→ `app.commit_undo_delta` +
|
||
`publish_document_view`; **no** extra `bump_geometry` — the entity op already
|
||
bumped);
|
||
- `impl HostApi for HostSession::doc_api_dispatch` → routes to
|
||
`doc_api::execute_doc_api` (gated `not(wasm32)`).
|
||
|
||
**`ocs_plugin_api` (sibling crate, also touched)**
|
||
- `src/ipc/protocol.rs` — appended `PluginRequest::DocApiRequest{tab_id, bytes}`
|
||
and `PluginResponse::DocApiResponse{bytes}` (append-only; `serde_bytes` on the
|
||
payload).
|
||
- `src/host.rs` — appended defaulted `HostApi::doc_api_dispatch` (returns
|
||
"not supported"; `HostSession` overrides it). Append-only, object-safe.
|
||
- `src/ipc/server.rs` — routes `DocApiRequest` → `host.doc_api_dispatch(...)`.
|
||
|
||
### Review round 1 — correctness & safety fixes
|
||
|
||
Two CRITICAL and six WARNING findings were fixed:
|
||
|
||
1. **Bulk ops are now truly all-or-nothing** (was CRITICAL). The original
|
||
`TransformMany`/`DeleteMany`/`CreateMany` only existence-checked ids; a
|
||
mid-apply failure (e.g. `store_solid` register failure, or `transform_entity`
|
||
hitting a non-solid) left partial state with no undo step. Now the executor
|
||
pre-validates *everything fallible* before `push_undo`:
|
||
- `TransformMany` → new backend method `ensure_transformable(id)` (v1:
|
||
solids-only) validated for **every** id first.
|
||
- `DeleteMany` → `can_remove(id)` (existence + not-on-locked-layer) for every id.
|
||
- `CreateMany` → its prepare loop was already pure; the residual `store_solid`
|
||
post-commit failure is eliminated by the pre-flight in (8).
|
||
- Regression test: `doc_api_transform_many_with_non_solid_fails_all_or_nothing`.
|
||
|
||
2. **Query batch is capped** (was CRITICAL / DoS). `apply_queries` had no bound —
|
||
a ≤64 MiB envelope could carry millions of `GetVolume`/`GetCentroid` queries,
|
||
each tessellating a solid on the synchronous UI thread. Now capped at
|
||
`BULK_ITEM_CAP` like the write bulk ops. Regression test:
|
||
`doc_api_query_batch_over_cap_is_rejected`.
|
||
|
||
3. **Locked-layer erase no longer silently succeeds** (was WARNING).
|
||
`HostSession::remove_entity` returns `false` for locked-layer entities; the
|
||
executor treated `Ok(false)` as success (`Delete` reported `Deleted`, boolean
|
||
`erase_sources` left the second input live). Now:
|
||
- backend `remove_entity` returns `Err(Unsupported)` when the entity is on a
|
||
locked layer;
|
||
- the boolean-erase path validates `can_remove(b)` **before** `update_solid(a)`;
|
||
- `Delete`/`DeleteMany` pre-validate `can_remove` before `push_undo`.
|
||
|
||
4. **One publish per op for deletes** (was WARNING). The backend's `remove_entity`
|
||
went through `HostSession::remove_entity`, which calls `publish_document_view`
|
||
per invocation → `DeleteMany` triggered N+1 full-document publishes. The backend
|
||
now calls `scene.erase_entities` directly (clears `solid_models`/meshes/hatches
|
||
+ records the undo delta); the single publish happens at `finalize_op`.
|
||
|
||
5. **`bounds` lift-on-miss for solids** (was WARNING). `bounds` checked
|
||
`solid_models.contains_key` and fell through to a non-solid `Unsupported` for a
|
||
solid with a cold cache (e.g. dropped by a host-side `update_entity`). Now it
|
||
resolves solids through `resolve_body` (lift-on-miss), consistent with
|
||
`volume`/`centroid`.
|
||
|
||
6. **`volume`/`centroid` accuracy + budget** (was WARNING / perf). They
|
||
re-tessellated the full body (`mesh_body`) + cloned the B-rep per query. Now
|
||
they compute via a **fine tessellation** (`mesh_body(0.1, 1e-4)`) for
|
||
near-analytic accuracy, memoized per (handle, geometry_epoch); the cold-tess
|
||
budget lives on the per-tab `Scene` (persists across dispatches) and degrades
|
||
to the `meshes.metrics` value when exhausted. The duplicated divergence loops
|
||
were merged into one `mesh_volume_centroid` (single source of truth, shared
|
||
by the host + both mock backends).
|
||
|
||
7. **Double revision bump documented** (was WARNING). Solid write ops bump
|
||
`geometry_epoch` twice (the entity add/update path + `register_prepared_solid_model`).
|
||
This is host-internal and shared with native commands; the meaningful guarantee
|
||
(one undo step per op, monotonic epoch) holds and is tested. Documented rather
|
||
than changed, to avoid touching shared host scene bump behavior.
|
||
|
||
8. **`store_solid` failure no longer moves the revision** (was WARNING). The
|
||
original rollback path (`rollback_new_entities` on `register_solid_model`
|
||
failure) bumped the epoch *again* on a failed op, violating "failed op = no
|
||
bump." Now `store_solid`/`update_solid` **pre-flight** `prepare_solid_model_display`
|
||
**before** `commit_entity_handle`, so a mesh-prep failure errors before any
|
||
mutation — the rollback path is eliminated (and the double `prepare` work is
|
||
removed). This also feeds the `CreateMany` all-or-nothing guarantee in (1).
|
||
|
||
### Review round 2 — cleanups (SUGGESTION)
|
||
|
||
- **`serde_bytes` on the IPC payload** — `PluginRequest::DocApiRequest.bytes` /
|
||
`PluginResponse::DocApiResponse.bytes` now use `#[serde(with = "serde_bytes")]`
|
||
so the outer frame is a single length-prefixed copy (was a per-byte seq walk +
|
||
a second full copy on 10–20 MB bulk payloads).
|
||
- **Dead code removed** — `src/gen/dispatch_gen.rs` (never generated/included),
|
||
`Operation::references` (no call sites, false doc comment), the discarded
|
||
serde-reflection tracer stub + `serde-reflection` build-dep in `build.rs`
|
||
(it reads `layouts.json` directly now), and the dead `Session.tab` field
|
||
(transport carries `tab_id`; `DocApi::document(tab)` keeps the arg for
|
||
forward-compat but no longer stores it).
|
||
|
||
### Subsequent additions
|
||
|
||
- **Rename** `ocs_doc_api_2` → `ocs_doc_api` (directory, package, lib, all refs).
|
||
The plan document under `.kilo/` keeps the historical name.
|
||
- **DWG roundtrip robustness** — the stable-DWG test writes to
|
||
`target/doc_api_roundtrip_intersected.dwg` (kept for inspection). It releases a
|
||
stale lock first and falls back to a process-unique path if still locked, so it
|
||
never flakes on Windows file locking (error 33).
|
||
- **Docs** — `README.md`, this file, `IMPLEMENTATION.md`, enriched
|
||
`spec/entities.toml` + the generated `api_reference.md` (constructors, generic
|
||
methods, cross-cutting, bulk ops, `unsupported` markers).
|
||
- **Examples** — five runnable examples under `examples/` (e1 primitives+queries,
|
||
e2 booleans, e3 transform-chain, e4 2D+bulk, e5 failure+guards) over a shared
|
||
in-process mock backend (`examples/common/mod.rs`).
|
||
- **Tests** — 7 `HostSession` integration tests in `src/app/doc_api.rs`, incl.
|
||
roundtrips for 2D entities, geometric methods, and a **DWG write→reload**
|
||
roundtrip proving an intersected solid survives with valid ACIS and re-lifts to
|
||
the expected volume.
|
||
|
||
### Review round 3 — branch review fixes (tab auth, geometry, budget, caps)
|
||
|
||
A full branch review (all six tracks) found 3 CRITICAL + 5 WARNING + suggestions,
|
||
all fixed with regression tests:
|
||
|
||
1. **Tab authorization (confused deputy)** — `execute_doc_api` ignored `tab_id`,
|
||
so any plugin could reach any open tab. Now rejects `tab_id != host.tab_id()`
|
||
before dispatch (`doc_api_tab_mismatch_is_rejected`).
|
||
2. **Negative-bulge arc center mirrored** — `bulge_arc_segment` used signed
|
||
`tan(half)` AND a sign flip, double-counting so CW arcs got the center on the
|
||
wrong side. Now apothem magnitude + sign-picks-side (CW below / CCW above).
|
||
3. **Cold-tess budget ineffective** — the 256-tessellation budget lived on
|
||
`HostSession` (rebuilt per dispatch → reset every request). Moved `mass_cache`/
|
||
`cold_tess_used` to the per-tab `Scene` so budget + memoization persist; when
|
||
exhausted it degrades to the approximate `meshes.metrics` value (no hard-fail).
|
||
4. **Loft uncapped** — added `BULK_ITEM_CAP` (was the only uncapped multi-item op
|
||
→ unbounded kernel call from one request). Regression test.
|
||
5. **CreateMany not all-or-nothing** — Curve specs are pre-validated (ellipse ratio)
|
||
in the prepare phase before `push_undo`. Regression test.
|
||
6. **Partial-ellipse rotation** — `start`/`end_parameter` now offset by `rot_z`
|
||
(the Arc arm already did). Regression test.
|
||
7. **`resolve_body` deep-clone** — added `with_body` borrow accessor; the host
|
||
borrows the cache in place (O(1)) and `bounds()` uses it (no per-query B-rep copy).
|
||
8. **`add_raster_image` unvalidated path** — now validates non-empty + image
|
||
extension and rejects UNC/network + parent-traversal paths. Regression test.
|
||
9. **Deploy capability gate** — `API_VERSION` 5→6 so DocApi plugins are rejected
|
||
at handshake by old hosts (clean refusal, not a runtime decode error); append-only
|
||
wire + defaulted `doc_api_dispatch` keep older V2–V5 plugins loading unchanged.
|
||
10. **Dead code** — `query_name` wired into `apply_queries` error labels; the host
|
||
uses the crate's `ObjectId::from_handle`/`to_handle` (dropped local duplicates);
|
||
deleted `is_null`/`backend_mut`; test mock now stores curve specs + computes
|
||
curve bounds (aligned with the example mock); Spline/Dimension bounds delegate
|
||
to `points_aabb`.
|
||
|
||
### Known deviations (documented, not bugs)
|
||
|
||
- **Two `geometry_epoch` bumps per solid write op** (see #7 above) — host-internal,
|
||
shared with native commands; monotonic and one-undo-step-per-op are guaranteed.
|
||
- **`volume`/`centroid`** are fine-mesh-approximate (`mesh_body(0.1, 1e-4)`), not
|
||
exact analytic values; the render-mesh `meshes.metrics` is the coarse display LOD.
|
||
- **`Angular2Ln` constructor** exists; `Radius`/`Diameter`/`Angular3Pt`/`Linear`
|
||
are the supported dimension sub-types.
|
||
|