How to iterate the bridge: fast builds, digest pin, session model, discovery, launch serialization, cancellation, error routing, safety model, rebuild swaps, conformance gates. No diary entries, no harness names, no build-specific data.
Discovery skips pid-verified dead sessions and deletes stale descriptors; probes are bounded so dead sessions delay discovery by at most one timeout. The bridge exits after the in-flight request once it detects a rebuild, and error results omit structuredContent so strict clients surface real errors. Concurrent launches are serialized with automation/starting.lock: the first caller spawns the GUI, later callers wait for the same descriptor instead of opening another window. Session states and hello/capabilities responses now carry the bridge build identity (version, build_rev, tool_schema), and the agent instructions point the build announcement at it.
With objects already selected, OFFSET opens at the side step with the
preview following the cursor instead of asking for a distance first
(#422). At the side step a typed distance places the offset at once on
the cursor's side, and a negative one on the other side; Enter at the
start repeats the last distance (#418). Erase and Layer, which lived on
the skipped distance step, are offered there too. With nothing selected
the distance-first flow is unchanged.
The opposite side is found by mirroring the cursor through its nearest
point on the outline, capped at half the offset distance so it cannot
cross a narrow gap back to the cursor's own side. Two new prompts are
added to the catalog and every locale.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The exponential camera step is the right call. `0.9 ^ delta` is strictly
positive for every real delta, in and out cancel exactly where the
subtraction lost a percent of the view on every round trip, and the file's
own header has promised an "exponential feel" since it was written. Three
surfaces were left behind it, and the wider range exposed a fourth thing.
The range of the system variable. 3 to 100 is the range ZOOMFACTOR has, and
the User Preferences page was built against it; `SETVAR ZOOMFACTOR` accepts
that and nothing else again. A faster wheel is the Options window's business
instead: the slider covers the variable's own range, the field beside it
reaches 500, and a read-out says what a notch actually does ("10.0% a
notch") so the number being set is readable rather than implied. A value
above 100 therefore lives only in this application's own configuration file,
where it is nobody else's to read, and a bare `SETVAR ZOOMFACTOR` still
reports it. The bounds and the clamp live in one place now
(`settings::clamp_zoom_factor`): four literal copies of `3, 100` are what
let a surface be missed in the first place.
A floating viewport in MSPACE. It zooms its own view through
`zoom_active_viewport`, which kept subtracting: the factor reached zero at
6.67 steps, so from ZOOMFACTOR 360 up every notch inside a viewport bottomed
out on the clamp — a tenfold jump a notch, and the top third of the slider
doing nothing at all. Exponential there too, from a base of 0.85 so the
default notch still moves a viewport the 15% it always has. Each surface
keeps its own long-standing feel rather than inheriting the other's.
The trackpad pinch. `pinch_zoom_steps` was calibrated against the
subtraction (`1 - step / 10 = 1 / (1 + m)`) so that a pinch of m leaves the
view exactly 1 + m times closer — the property docs/trackpad.md documents
and the reason the gesture tracks the fingers at all. Inverting the zoom law
by hand is what let the two drift apart, so the step is asked of the camera
now (`Camera::zoom_steps_for_ratio`), which cannot drift by construction.
`pinch_is_one_to_one` recomputed the old formula rather than the camera's,
so it kept passing through all of this; it zooms a real camera and measures
the distance it lands at instead.
Also: `zoom` had a floor and no ceiling, and zooming out multiplies. At
ZOOMFACTOR 500 a notch out is x2.4, so about ninety notches — a couple of
seconds of scrolling — ran the distance past what an f32 far plane
(`distance * 1000`) can hold, and the viewport went black on an infinite
projection. A degenerate pinch magnification of -1 or beyond reached the
same place through the other side. Both are bounded now.
Also: two doc comments quoted the linear law (`zoom_view_at_cursor` and the
one on `pinch_zoom_steps`), and docs/trackpad.md stated the pinch identity
and claimed the tests covered it against the camera. All updated, with what
a notch does and where each surface's range comes from written down.
Tests, each confirmed against the behaviour it describes: the zoom law at
every factor the application can hold (positive, monotonic in the setting,
in and out cancelling), the ceiling under a long scroll out in both
projections, the published inverse and what it does with a nonsense ratio,
the viewport factor and a real viewport zoomed three notches at the top of
the range, the SETVAR range including a value the Options field set, the
Options field (above 100, above the ceiling, cleared, not a number), its
agreement with the slider, Close putting both the value and the field back,
and the read-out's percentages. 1882 lib tests pass.
The two new Options strings are not in the locale catalog yet, so they
render in English until it is regenerated.
Co-Authored-By: Claude Opus 5 <[email protected]>
--http (headless and GUI-hosted) and --serve --port answered anything
that reached the loopback port, and the REST responses allowed every
web origin. Loopback is shared by every process and user on the
machine, so the port proved nothing.
- Every REST request now needs Authorization: Bearer <token>, and every
--serve --port request line a "token" field; otherwise 401 /
unauthorized (the socket is closed).
- The token is OCS_API_TOKEN when the launcher sets it, else 32 random
bytes printed on stderr at startup; compared in constant time.
- No CORS headers are sent, so a web page cannot read an answer.
- stdio --serve and --mcp are unchanged (private to their parent / the
GUI's own token).
GHSA-c27p-xfx2-9r5x
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Define Ty, Param, Rule, and OpDef in src/mcp_ops.rs for all 39 mutations
- Generate Draft 2020-12 schema using per-op anyOf variants with op enum discriminators
- Disambiguate colliding properties (replace, center, name) across mutation variants
- Add pre-dispatch validator enforcing required fields, rules, and parameter types
- Emit unknown parameter warnings into response warnings
- Introduce target_document_id for entities_copy_to with deprecated document_id fallback
- Update unit tests and automation smoke assertions
Add cross-platform SystemSpeller abstraction with three backends:
- Windows: direct COM vtable calls to ISpellChecker (Win32 Spell API),
no external crate — uses the same hand-rolled FFI pattern as
file_association.rs. Handles ALL-CAPS words by also testing lowercase.
- macOS: NSSpellChecker via objc2/objc2-app-kit (already in deps).
- Linux: Hunspell .dic file reader + hunspell CLI pipe fallback.
text_audit gains three new parameters:
system_spellcheck: true — activate OS spell-checker
language: en-US — BCP-47 language tag (default: OS default)
suggest: true — include spelling suggestions
The agent dictionary and system speller work as a layered pipeline:
words matching the agent dictionary are accepted immediately; only
remaining words are forwarded to the OS checker.
Response includes a system_speller object (enabled, available, backend,
language) and an unrecognized_words array with handle, position, and
optional suggestions.
Integration test: text_audit_system_spellcheck_flags_misspelled_words
(20/20 p1_tests pass). Release build verified.
- Adds ext_audit operation under ocs_read, control bridge, and headless server
- Implements pairs / dry_run_pairs simulation returning diffs, coordinates, and unmatched queries
- Implements check_terms suspect matching and dictionary vocabulary verification
- Adds dry_run: true to ext_replace to preview modifications without mutating entities or pushing undo
- Supports bounds, layers, handles, and scope filtering
- Updates MCP tool definitions, schemas, and automation test suites
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.
A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- An inverted clip's roundtrip record goes in the filter's extension
dictionary through the codec's own API (ensure_extension_dictionary /
xrecord / extension_dictionary_handle) instead of a scan of every
object, which the renderer ran for each clipped reference.
- Pin cadcodec 1c3be0c (the DXF writer now links a spatial filter's
extension dictionary, so that record is not left unreferenced) and
cadkernel deb529b; plugin crates and the Python plugin lock follow.
- The XCLIP boundary preview stays f64.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Pin cadcodec upstream at d290dbe (the merged underlay change) and
cadkernel at 4134ae3, which follows it; drop the fork patch, move the
plugin-API and web-worker crates and the Python plugin lock to the same
codec so one copy is built.
- "Import as block" with raster images left IMAGE entities without an
image definition: block definition now creates it like add_entity does,
through one shared helper.
- The three preview outlines (attach frame, clip boundary, import area)
stay f64 instead of casting world points to f32.
- Page size / total / PDF scale entries use the catalog's positional
placeholder; the bare {name} form failed to format in 20 locales.
- The Underlay Layers bulb is the SVG layer icon, not a text glyph.
- PDF dialogs use the shared dialog buttons and radios.
- Options opened from a PDF dialog clears its saved state on return.
- Paper-space underlay background reuses the model block handle already
resolved; dead `let _ = i` lines and a misplaced doc comment removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- `--export ... --target-version`, and explicit target_format /
target_version on automation and protocol saves; a conversion that
would drop unsupported records is refused unless allow_lossy is set.
- `audit` read op: dangling layers/blocks, duplicate handles, non-finite
bounds, serialization, lossy records and the source DXF handle graph.
- `save_verified`: writes the requested version from the same snapshot a
normal save writes, reopens it, checks the version and the entity
manifest, audits the ASCII DXF handle graph and returns a SHA-256.
- ZOOM EXTENTS includes SDF text glyphs.
- Live MCP acceptance script and an optional reference-console audit
runner under docs/automation.
Squashed from the PR with review fixes: merged with the DWT save path,
the save snapshot, the tool lists added since, and vendor-neutral
harness names; the dated verification record is left out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The operations docs/automation/API-SPEC.md (PR #1383) describes, brought
over from huaninstratech/OpenCADStudio so the specification no longer runs
ahead of the code:
- entity CRUD: entities_create, entities_delete, entities_transform,
entities_copy_to, and `where` filters on query
- blocks and export: block_define, block_delete, a normalised wblock
- publishing: layout_create, page_setup_set, plot (per_page included)
- metadata: sysvar, xdata_set / xdata_get, file_identity
- collections: group_create, selection_set_save / load
- control plane: batch, events, view_focus, close
- the REST transport: `--http <port>`, the /api/v1 routes and their
OpenAPI 3 description
- person-in-the-loop picks: user_select and getpoint, hosted by the GUI
over the same REST routes, with on-screen pending signals
Taken from the fork's commits b7a7ed80, 69396b42, 64f1cf6a, c3aafc8e,
bb5a7bfa, 52317a5c, 50d293ba, b90e194f, 517c6397, 53a5c4c9, a49ac045,
37de5a46, a78b1822, 6d9aed2a, 33f522da, 5b5cafe2, 02a7e78d, 94df40dc,
fd5bc14d, 485fac61 and 2c5fc994.
Left out on purpose: 37de5a46 also removed the donation surface (the
Patreon fetch, the donation prompt, the Start-page supporters rail, the
DONATE command and its locale strings) and replaced the About dialog's call
to action; that is the fork's own product decision and stays out. The
fork's version bump and its unrelated IFC/BIM work are not part of this.
Adapted while porting: the REST server and the GUI bridge are desktop-only
and a template start is refused in the web build, so the wasm target still
compiles; the two plot tests read the PDF through the shared stream helper,
because exported streams are Flate-compressed; the benches set the new
crosshair field; comments and docs describe each operation in its own
terms rather than by another product's API names.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in the DXF round-trip fixes merged as cadcodec #48 and #51 and the
block-definition fixes for cadcodec issues #49 and #52. cadkernel moved to
the same codec revision first, so the graph keeps a single acadrust. The
Python plugin, which is outside the workspace, has its own lock moved too:
its staging script requires the plugin and the host to share one acadrust
source.
The canaries that asserted the old wrong results were flipped against the
real-IPC audits, all 60 of which pass with the plugin built: Helix, Light,
Underlay, ViewBorder and SectionSymbol now round-trip through DXF, as do the
text style flags, the dimension style's text style name, the block
description, the table's merged ranges and the leader's annotation link and
vectors. Those five kinds move to Complete in the coverage ledger.
Two narrower gaps stay pinned. The ATTDEF writer emits no group 280, so
`lock_position` is still lost on a DXF save; every other field now
survives. A true-colour leader `override_color` cannot be held by DXF group
77, which takes an ACI index, and the DWG writer does not store it.
Behaviour change for users: a text style's oblique angle is now written to
DXF in degrees, as the format defines it. DXF files written by earlier
versions carry radians there and reopen with a much smaller slant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bundled `opencad.python` plugin drives OCS through plugin API v7: all
43 canvas entity kinds, the drawing tables, and OCS's own commands run
step by step. RustPython stays in the plugin process — the plugin crate is
outside the workspace with its own lock file, so the application's own
dependencies are unchanged.
Reconciled with the plugin command work that landed as #1425, which the
branch was written before:
- Both added `impl Drop for PluginProcessInteractiveAdapter`, which git
merges silently into two impls that do not compile. One body now
cancels the command and then drops it from the runner, in that order: a
plugin waiting on a step has to be released before the runner forgets
the command.
- `execute_command` (#1425) and `run_command` (#1423) are both routes from
a script to the command line, and only the second was guarded. The
SCRIPTCOMMANDS switch and the denylist are shared now, so neither route
is the unguarded one.
- `run_command` dispatched with plugin dispatch still enabled, so a script
naming a plugin command re-entered the plugin that was blocked waiting
for it. It sets the suppression flag for the length of the dispatch, as
`execute_command` does.
The denylist read the command as typed, while the dispatcher resolves
aliases: a user-defined `Q` for `QUIT` (ALIASEDIT writes those) walked
past it. It resolves the alias first.
Also: the active-command comparison ran on every message, a mouse move
included, allocating two strings for names that are already `&'static
str`; SCRIPTCOMMANDS' own messages go through the translation macros; and
`docs/.DS_Store` (now ignored) and an unrelated RTF note about constraint
commands are out of the tree.
Co-Authored-By: Claude Opus 5 <[email protected]>
The pinch is read from AppKit rather than through iced, which is the kind of
decision that reads as arbitrary without the reason next to it. The file also
records what the pixel/notch split costs: a smooth-scrolling mouse pans with a
trackpad.
Remove personal names, local paths and fork-only process notes from code comments,
docs and the plugin manifest (repository now points at the upstream project), and
drop the handoff section from the plan.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
What it is, how to check it on Linux or Windows without a Mac, the reading order,
the small edits to existing code, what each area proves, the decisions that are the
maintainer's, and what is deliberately not done.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
A saved user preference (on by default) toggled by the SCRIPTCOMMANDS command
gates HostApi::run_command; Cancel stays allowed so a waiting command is never
stranded. The runner refuses the SCRIPTCOMMANDS command itself, so a script
cannot change it. Documents why no helper-thread or stack-growing dependency was
added: native builds link with at least 8 MiB, the nesting depth is fixed, and the
deepest step found needs 2 to 4 MiB in a debug build.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Rewrite the host-model design note for the current architecture (entities, tables,
commands, nested validation, known limits), refresh the plan's current-state header,
the ledger and gaps-doc status lines (cadcodec PR #48), the README status and the
superseded ocs.command notes, and drop dangling references to a sibling project that
is not in this repository.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
polyline_close/open/width/reverse/join (a line or arc is turned into a polyline
first), array_path and array_3d, written against observed prompts and checked
for exact geometry in the real-IPC audit. Polylines default their pick point to
the first vertex. The audit runs on a large-stack thread because PEDIT's
line-to-polyline step needs more than a test thread's 2 MiB in a debug build.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The generated dict-to-struct conversions for nested records defaulted every
absent field and ignored unknown keys, so an LwPolyline built from
{'x': .., 'y': ..} vertices became zero-coordinate vertices. They now refuse an
unknown key (naming the keys the record takes) and a manifest list,
required_struct_fields, enforces the fields that define a nested record's
geometry (vertex locations, hatch edge ends and centres, section symbol points).
Regression test over real IPC; every existing lifecycle audit still passes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Each is written against the prompt sequence observed in the exploration harness
and checked for exact geometry in the real-IPC audit. HATCH is left to
create_entity because its command needs an interactive boundary pick.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
HostApi::run_command drives the real OCS command through the same primitives
the automation channel uses, synchronously on the host thread: run a line,
start a command and answer point/text/token/entity/selection/enter prompts, or
cancel (closing anything the command opened). Refused while another command is
active, for a denylist (QUIT, NEW, OPEN, SAVE*, PY_* ...) and off the active tab.
Python: doc.command, doc.start_command and doc.modify (offset, trim, extend,
fillet, move, copy, rotate, scale, mirror, erase). Real-IPC audit checks exact
geometry for every wrapper plus guards and undo; IPC and Python unit tests; docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
TableOperation gains simple-linetype create/modify/rename/delete (standard names
taken, rename follows layers and entities, guarded delete) and paper-space
layout create/rename/delete/set-current/set-page (default page setup and sheet
viewport, the application's own layout switch, Model protected). CTAB now reports
the current layout. Real-IPC audit with DWG/DXF persistence and undo, IPC and
Python unit tests, docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
BlockEntityAdd adds a validated entity to a block definition (owner set by the
host, same validation as a model-space entity, self-nesting inserts refused),
reusing the scene's block-editor routing. Deleting a block member now also
drops its handle from the block record. Real-IPC audit with an attribute
definition inside a block, DWG/DXF persistence and undo; IPC and Python unit
tests; docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
TableOperation gains BlockCreate (copy entities into a definition shifted by the
base point, optionally erasing originals), BlockModify, BlockRename (inserts
follow) and BlockDelete (refused while referenced; removes members, markers and
orphaned draw-order tables). Real-IPC audit with DWG/DXF persistence and undo,
IPC and Python unit tests, docs; the DXF loss of the block description is pinned
by a canary.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Extends TableOperation with text style create/modify, dimension style
create/modify (DimStyle fields as a validated JSON object), rename, delete and
set-current. Reuses OCS's style in-use scan and reference-following rename
(four style_ops methods widened to pub(super)); a renamed text style also
updates dimension styles that name it. Real-IPC audit with DWG/DXF persistence
and undo, IPC and Python unit tests, docs. Four cadcodec findings are pinned by
canaries.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds the additive v7 TableOperation request and HostApi::table_operation for
layer create/modify/rename/delete/set-current. Refusals are decided before the
undo step is recorded. Python wrapper, real-IPC audit with DWG/DXF persistence,
IPC round-trip test, Python unit test and docs.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The page merged from #1401 described --serve and --mcp as one surface.
Rewritten against src/app/automation.rs, src/app/control/mod.rs and
src/mcp.rs: the --serve readiness banner and --port mode, the two request
styles on that channel and how each treats a waiting command
(command_busy versus replacement), measure being available headlessly
while capture is not, --export writing DWG for every extension but .dxf,
startup dialogs belonging to the editor behind --mcp only, MCP sessions
being shared rather than one editor per connection, the MTEXT editor's
own actions, and the legacy run reply fields (blocked_by, unconsumed)
that only that path returns.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A scripted RasterImage now reuses one ImageDefinition per file and registers
it in ACAD_IMAGE_DICT, which owns it, as for an AutoCAD-attached image. The
reactor is deliberately not written because its owner convention cannot be
verified. An ignored spike records that OCS's own IMAGE command creates bare
images that lose their file path on save.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
The shared driver now activates the layout that owns a viewport and requires
a drawn frame. The host assigns a unique id of at least 2 and derives
custom_scale as height / view_height on every scripted create or edit, as
MVIEW does, so custom_scale is read-only. A control showed the default id 0
is not invisible, so the id matters for uniqueness.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
EmbedPicture builds the frame with OCS's own EmbeddedImage and
build_embedded_ole, so the opaque storage is never scripted. PNG, JPEG and
BMP are stored as read and other formats are re-encoded as PNG. The lifecycle
test now creates through Python; a new test covers formats, aspect, origin,
layer and refusals.
Co-Authored-By: Claude Sonnet 5 <[email protected]>