Skip to content

feat(schematic): move and hide a symbol's field text - #489

Merged
mixelpixx merged 4 commits into
mixelpixx:mainfrom
triglav-modular:feat/schematic-field-placement
Sep 11, 2026
Merged

mixelpixx merged 4 commits into
mixelpixx:mainfrom
triglav-modular:feat/schematic-field-placement

Conversation

@triglav-modular

@triglav-modular triglav-modular commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Nothing in the server can position or hide a placed symbol's field text. reset_schematic_field_positions only restores the library default, so a field that lands badly — the classic case is a rotated gate whose Value sits across its own pin numbers — has no fix inside the tooling. The way round is to empty the Value and draw the text some other way, which is a worse schematic.

edit_schematic_component gains field_placements, keyed by field name, each entry setting any of x, y, rotation and hide.

Issue: #490. It satisfies every acceptance criterion there; there is no follow-up half.

Closes #490

Approach

A separate argument rather than an overload of fields: a caller moving the Value text is not changing what it says, and widening one argument's value type would make the schema lie about what it accepts. An omitted part of an entry is left as the file has it, so moving a field cannot silently unhide it.

No new tool. sch_components is at MAX_TOOLS_PER_TOOLSET, and that constant asks for a conversation before it moves — so this goes on the existing per-symbol field editor, which is its natural home anyway. No registry, tool-directory.md or doc-count changes.

Coordinates are absolute schematic millimetres, as KiCad stores them — not offsets from the body. That makes them per-placement, so unit names the one meant, and a position given for a component with several placed units and no unit named is refused. Writing one coordinate to each would stack a quad gate's four Values on a point and report success.

hide writes (hide yes) as a direct child of the property. That is not the form KiCad's own writer uses on a placement — it nests the flag in (effects …) there, and uses the direct child in lib_symbols. The two are equivalent to KiCad 10.0.6: a file in each form exports byte-identical SVG, and its writer round-trips this one unchanged. The readback accepts either, so a field KiCad itself hid reads as hidden rather than visible.

The readback reports each field's observed position and visibility, and every requested placement is compared against the committed file before the call reports success. That check earns its place — see Validation.

Branch and dependencies

Base branch: main, rebased onto f58b222. Depends on: nothing. Independent of #485 (schematic mirror), which touches the same file but not the same code; they merge cleanly in either order. Unique commits: four — the feature, the fix for what adversarial review found in it, and the two corrections from maintainer review: refusing a malformed existing placement, then aborting the whole call when the file's placement is malformed.

Compatibility and safety

The public contract, in full

Everything added is additive; nothing is renamed, removed, or changed in meaning.

Request — two new optional properties on edit_schematic_component:

field type meaning
field_placements object, keyed by field name Each value is an object taking any of x (number), y (number), rotation (number), hide (boolean). An omitted member leaves that aspect as the file has it.
unit integer ≥ 1 Which placed unit field_placements applies to. Required when x or y is given and the component has more than one placed unit.

Response — one new field on each unit of the component mutation readback:

field type meaning
units[].field_placements object, keyed by field name { "x": number, "y": number, "rotation": number, "hide": boolean } as observed in the committed file, not as requested. Present for every property carrying an (at …).

Omitting both request fields reproduces the previous behaviour exactly, and the new response field is added alongside the existing ones rather than replacing any.

  • File mutations go through this handler's existing text-edit path and commit_command; the only new writes are a property's own (at …) and its (hide yes) token.
  • Rollback is reverting the two commits. No format migration: what it writes is what eeschema writes.

One limitation worth stating plainly, and it is pre-existing — now filed as #499. This handler commits and then reads back, so a refusal still leaves the file changed. Both bugs found below returned stale_target over an already-written file. The value-edit path and two other handlers share that shape, so this change does not introduce it — but "the tool errored" cannot be read as "the file is untouched". #499 names the shared path (commit_command and its three sch_components.rs callers) and states the atomic and accurately-reported outcomes; it is not implemented here, and this change adds no new unsafe path of its own.

Validation

cargo fmt --all -- --check                                        clean
cargo test --workspace --locked --lib --tests                     pass
cargo test --workspace --locked --doc                             pass
cargo clippy --workspace --locked --all-targets -- -D warnings    clean

Run on the rebased head. Tests were also run with HOME and KICAD10_SYMBOL_DIR pointed at an empty directory, so nothing here depends on an installed KiCad.

Every guard was neutered and watched to fail — twelve tests, each pinning one named guard:

guard removed test that must fail
value-bounded (at …) search field_placement_never_rewrites_a_value_that_looks_like_a_placement
same-line hide deletion field_placement_hide_removal_keeps_a_neighbour_on_the_same_line
multi-unit position refusal field_placement_refuses_a_position_across_units_without_one_named
non-numeric coordinate refusal field_placement_refuses_a_non_numeric_coordinate_without_writing
writer's (at …) edit field_placement_moves_the_text_and_keeps_its_value
writer's hide insert field_placement_hides_then_shows_a_field
post-write comparison placement_landed_compares_only_what_was_requested
readback reading only a property's direct children field_placement_reads_the_hidden_flag_kicad_nests_in_effects
unit targeting field_placement_targets_one_named_unit
strict parse of an existing (at …) field_placement_refuses_a_malformed_existing_placement_without_writing
MalformedFile abort arm malformed_placement_aborts_a_combined_edit_without_writing
checked u32::try_from on unit an_oversized_unit_is_refused_rather_than_wrapping_to_a_real_unit

Three defects were found after the feature already worked and passed its first tests — two by adversarial review of the writer, one by checking the readback against what KiCad actually writes. All are fixed in the second commit, and each is worth naming because none was caught by a test:

  1. A field value containing (at was rewritten instead of the placement. A value of "mounted (at 45 deg)" matched the search first, and moving that field turned its text into "mounted (at 11 22 0)". Both searches now start after the value's closing quote.
  2. Removing hide deleted back to the preceding newline, taking whatever shared the line. On a file written as (at 1 2 0) (hide yes) — not KiCad's own layout, but one it is asked to read — unhiding a field destroyed its position.
  3. The readback looked for the hidden flag only among a property's direct children, which is the lib_symbols form. Every field KiCad itself had hidden was reported visible, and because the writer's check is textual and does see the nested token, asking to hide such a field became a no-op that then failed its own post-write comparison. Found by reading what KiCad writes, not by a test: every other test drives a field this fixture leaves visible.

The first two each produced a file that parses, loads in KiCad and exports a netlist, and is wrong; both were caught by the post-write comparison against the committed file, which is the argument for having it. The third made that same comparison refuse a correct file. Each now has a regression test.

Three tests of mine passed while neutered on the first attempt and were rewritten rather than kept: one accepted any error from a handler with several failure paths, one counted a substring after an unwrap_or(0) that had quietly made the whole file the search window, and one compared two placements against a distance the 1.27 mm grid snap had already changed.

Verified against real KiCad (10.0.6), not only in-process: a field moved to (at 70 75 90) and a (hide yes) written and removed, then kicad-cli sch export netlist loading the result and kicad-cli sch upgrade --force — KiCad's own writer — round-tripping the moved position and the property-level hide token unchanged. The two hide forms were also rendered against each other: kicad-cli sch export svg from a file carrying the property-level token and one carrying (effects … (hide yes)) produces byte-identical output, which is why the writer keeps one form and the readback accepts both.

Not run: Windows and Linux; no environment here. Nothing in the change is platform-dependent — it is byte-range edits in the schematic writer — and hosted CI covers the matrix.

Review checklist

  • The diff is focused and contains no generated output, personal data, or unrelated cleanup.
  • The branch includes current upstream/main and has no merge conflicts.
  • The branch was based on latest upstream/main, not a release tag.
  • The PR shows only its unique commits and diff; it has no dependencies.
  • New names follow docs/NAMING_CONVENTIONS.md; no public renames.
  • New behavior and failure paths have regression coverage.
  • File mutations are atomic and preserve unrelated content.
  • IPC mutations — none in this change.
  • No tools added or removed, so no counts or docs to update.

@neusse

neusse commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Ask first: repair the terminal issue reference and record the post-commit failure separately; do not rebase yet.

  1. Put Closes #490 on its own ordinary Markdown line. It is currently inside backticks, so GitHub does not recognize a closing reference.
  2. Open one focused follow-up issue for the pre-existing commit-then-readback behavior described under “One limitation.” Capture that a validation/refusal can be returned after the file has already changed, identify the shared mutation path, and state the desired atomic or accurately reported outcome. Link it here, but it does not need to be implemented in this PR unless this change introduces a new unsafe path.
  3. Ensure the PR body explicitly lists the additive request and response fields as the public compatibility contract.

This PR overlaps #485 in sch_components.rs and predates current main. Do not rebase yet; we will set the exact order and refresh point after #442 lands. The implementation remains queued for substantive review after these accounting items are visible.

@triglav-modular

Copy link
Copy Markdown
Contributor Author

Done, and no rebase — the branch is untouched, waiting on the order and refresh point you set after #442.

  1. Closes #490 is now its own plain Markdown line; GitHub registers the closing reference (it reports this PR closes No tool can position or hide a placed symbol's field text — reset_schematic_field_positions only restores the library default #490).
  2. Filed as Schematic mutation handlers can return a stale_target refusal after the file has already been changed #499 — "Schematic mutation handlers can return a stale_target refusal after the file has already been changed." It names the shared path (commit_command at konnect-sexp/src/command.rs:603, and its three sch_components.rs callers — handle_edit_schematic_component, handle_add_component_annotation, handle_group_components), gives a concrete reproduction, and states both acceptable outcomes: validate the prospective content before committing, or report accurately and restore. Linked from the body. Not implemented here, and this change adds no new unsafe path of its own — the two writer bugs that exposed the behaviour are fixed in this PR's second commit and have regression tests.
  3. The compatibility section now carries the additive contract as two explicit tables: the request fields (field_placements, unit) with their types and semantics, and the response field (units[].field_placements), stated as observed in the committed file rather than echoed from the request. Omitting both request fields reproduces the previous behaviour exactly.

@neusse neusse added status:waiting-on-dependency Next actor: the dependency owner — see linked blocking issue and removed status:waiting-on-author Next actor: the PR author — one checklist, 14-day target labels Sep 8, 2026
@mixelpixx

Copy link
Copy Markdown
Owner

#442 has landed as 1cd4978, so here is the refresh base and order that was waiting on it.

Refresh base: current main at 1cd4978.
Order in the sch_components.rs overlap set: this PR first (one file, terminal Closes #490, its two writer bugs already fixed and tested), then #485.

When convenient: reconstruct once onto 1cd4978 with only your two unique commits, push with --force-with-lease, let the ten checks run on the new head, and reply with the head SHA. I will review that exact head; no further accounting changes are needed.

@neusse — this is the base/order you said we would set after #442; override here if you had a different sequence in mind.

@triglav-modular
triglav-modular force-pushed the feat/schematic-field-placement branch from 52ed7ce to 27d5b39 Compare September 9, 2026 07:54
@triglav-modular

Copy link
Copy Markdown
Contributor Author

This is no longer only a rebase. The reconstruction is done and sits on current main, but re-checking the change against real KiCad output before pushing turned up a defect in the response field this PR introduces, and the fix is folded into the second commit. Still two unique commits, but the diff is larger than the one you said you would review — so this is here rather than left to be found.

Head: 27d5b39f46bc6ee04699ec4566fb9d9a9cbcad11

Base: 296641b. You named 1cd4978; by then it was three commits back, with eddf131 (#483) and 296641b (#482) merged on top. CONTRIBUTING.md:120 makes "the branch includes current upstream/main" a merge-readiness condition in its own right, and docs/BRANCH_AND_PULL_REQUEST_WORKFLOW.md:46 says to rebase onto current upstream/main, while :195 says not to rebase repeatedly. Onto 1cd4978 would have been two rebases where "reconstruct once" asks for one. Both SHAs are named here so the difference is yours to see.

The defect

The new units[].field_placements.<field>.hide read the flag only from the property's direct children. That is the lib_symbols form. On a placement KiCad writes it inside (effects …) — the reverse of what this PR's own comments claimed.

In this repository's own crates/konnect-core/tests/fixtures/ecc83_multiunit.kicad_sch, R1 carries:

(property "Datasheet" ""
	(at 157.48 85.09 0)
	(effects
		(font
			(size 1.524 1.524)
		)
		(hide yes)
	)
)

and the tool reported "Datasheet": {"hide": false, …} — Description likewise. Both are genuinely hidden.

The second consequence is worse than the wrong field. The writer's own check is textual and does see the nested token, so it correctly no-ops; the readback then reported false, and the post-write comparison refused with stale_target over a file that was already correct. That is a second instance of #499 — a refusal returned after the file has already been committed — so it belongs to that issue rather than diluting this one.

No existing test could have caught either. Every field_placement test drives Value, which that fixture leaves visible; the fields it hides are Datasheet and Description, which nothing touched. The gap was in what the fixture was asked about, not in the assertions.

What changed, and what did not

The readback now accepts either form. The writer is unchanged, and the feature itself was never broken: the token it inserts is honoured. Exporting SVG at KiCad 10.0.6 from a file carrying the property-level token and one carrying (effects … (hide yes)), under identical filenames so the title block matches, gives byte-identical output. Only the reasoning and the readback were wrong; the two source comments that stated it backwards are corrected.

tool-directory.md's row is updated to match the tool description this PR changed. The body is updated to match: the corrected paragraph, the third defect in the list, its guard-table row, and the SVG evidence.

$ git log --oneline main..HEAD
27d5b39 fix(schematic): bound field-placement edits to the placement, not the value
0a162e6 feat(schematic): move and hide a symbol's field text

$ git diff --stat main...HEAD
 crates/konnect-core/src/tools/sch_components.rs | 732 +++++++++++++++++++++++-
 tool-directory.md                               |   2 +-

Gate on this head:

cargo fmt --all -- --check                                        clean
cargo test --workspace --locked --lib --tests                     1674 passed, 0 failed, 23 ignored
cargo test --workspace --locked --doc                             8 passed, 0 failed, 2 ignored
cargo clippy --workspace --locked --all-targets -- -D warnings    clean

The new guard was neutered and watched to fail before being restored, as the others were.

If you would rather review the reconstruction clean, say so and I will split the readback fix into its own PR on top of it.

@neusse neusse added status:waiting-on-review Next actor: maintainer status:waiting-on-author Next actor: the PR author — one checklist, 14-day target and removed status:waiting-on-dependency Next actor: the dependency owner — see linked blocking issue status:waiting-on-review Next actor: maintainer labels Sep 9, 2026
@neusse

neusse commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Please refresh this branch onto current main and rerun the ten required checks; no code redesign is being requested.

The PR was correctly reconstructed at 296641b, but #509 has since moved main to ab337816. The change is independent, so one clean update should be sufficient. After that exact head is green, it returns to maintainer review; #485 remains behind it because both touch sch_components.rs.

@triglav-modular
triglav-modular force-pushed the feat/schematic-field-placement branch from 27d5b39 to 8e4bc01 Compare September 10, 2026 06:24
@triglav-modular

Copy link
Copy Markdown
Contributor Author

Refreshed onto current main.

Head: 8e4bc013c54ce64209bc0065ce3a95a70be22015
Base: 2fbc5f5 — not ab337816. #402 merged after you wrote and moved main one further, so both SHAs are named here rather than left to be discovered.

Clean rebase, no conflicts. Both commits kept their patch-ids, so nothing but the base changed. The tool-directory.md row this PR edits is not the one #402 rewrote.

Ten checks green on this exact head.

@neusse neusse added status:waiting-on-review Next actor: maintainer and removed status:waiting-on-author Next actor: the PR author — one checklist, 14-day target labels Sep 10, 2026
neusse
neusse previously requested changes Sep 10, 2026

@neusse neusse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the existing-file placement parser strict and add the durable public API migration entry before admission.

Spec review passes #490 on exact head 8e4bc013c54ce64209bc0065ce3a95a70be22015, including the multi-unit contract, arbitrary field-text safety, committed readback, KiCad evidence, negative controls, and separate #499 accounting. Standards review found one file-safety hole:

  1. set_property_placement parses an existing (at …) using filter_map, then defaults missing values to zero. An existing malformed (at bad 20 90) becomes [20, 90]; a rotation-only request can rewrite it as (at 20 90 NEW), silently moving the field. Require finite numeric x/y, and finite rotation when present, positionally; refuse before commit_command if the existing placement is malformed. Add a byte-identical-on-refusal test using malformed file content (the current non-numeric request test is a different guard).
  2. Add docs/API_MIGRATIONS.md coverage for the additive field_placements, unit, and units[].field_placements contract: absolute-mm coordinates, per-unit targeting, supported visibility forms, response-derived readback, and next-minor release impact. The PR body and tool-directory row are not the durable migration record used elsewhere.
  3. Keep FieldPlacement private; no other module consumes it, so pub(crate) is broader than needed.

The issue-first design gate was missed because the implementation preceded #490; record that as process learning, but no history rewrite is requested. The second commit subject is also slightly over the roughly-72-character guide; leave the published commits intact and keep future subjects shorter.

After these focused corrections, rerun all ten checks and return the new head. #489 remains the first sch_components PR; #485 stays behind it.

@neusse neusse added status:waiting-on-author Next actor: the PR author — one checklist, 14-day target and removed status:waiting-on-review Next actor: maintainer labels Sep 10, 2026
@triglav-modular

Copy link
Copy Markdown
Contributor Author

All three done. Head: 72db166592e9f170b50bbf7161737f55786f8f13, a third commit on top of the reviewed head — the two published commits are untouched, so the delta since your review is the only thing to read.

1. Malformed existing placement. (at …) is now parsed positionally and strictly: finite numeric x and y, and a finite rotation when a third value is present. The refusal happens before commit_command, so the file is untouched.

You were right that the existing non-numeric test guards a different door. field_placement_refuses_a_malformed_existing_placement_without_writing corrupts the fixture's own (at 157.48 85.09 90) to (at bad 85.09 90), issues a rotation-only edit, and asserts the file is byte-identical afterwards. Neutering the strict parse back to filter_map + unwrap_or(0.0) reproduces your scenario exactly: is_error: false, and the readback shows Value at x: 85.09, y: 90.0 instead of 157.48, 85.09 — committed.

One thing there goes beyond what you asked, and I would rather name it than have you find it. I also refuse an (at …) carrying more than three values. You asked only for finite x/y and finite rotation when present. My reasoning is that parsing index 2 and ignoring the rest would silently drop whatever the extras carried when the writer re-emits three tokens — the same silent-loss class this correction exists to close — and a refusal cannot corrupt anything. The tradeoff is forward compatibility: if a future KiCad format grows a fourth token this refuses loudly rather than eating it, which I think is the better failure, but it is one line to relax and I will drop it if you would rather it only enforced what you specified.

2. docs/API_MIGRATIONS.md gains ## Unreleased: schematic field text placement (minor release), covering the additive field_placements / unit / units[].field_placements contract: absolute-millimetre coordinates and the absence of the 1.27 mm snap, per-unit targeting and exactly when unit is required, both visibility forms read and which one is written, readback observed from the committed file with stale_target on mismatch and the post-write-verification caveat, the new malformed-placement refusal, and next-minor impact. It is in the same commit as the behaviour it records.

3. FieldPlacement is module-private.

A question your wording surfaced, which I have not acted on. "Refuse before commit_command" holds for a placement-only call, which is what the new test covers. But a malformed placement refuses that field and the handler continues: if the same call also carried a field edit that succeeded, changed is non-empty and that sibling edit still commits. So the byte-identical guarantee is narrower than the sentence reads. That is pre-existing behaviour on every per-field error path in edit_schematic_component rather than something this PR introduces, so I have deliberately left it alone — but if you want a malformed target to abort the whole call, say so and I will open it as its own issue rather than widen this PR.

Not rebased — the branch is still level with main at 2fbc5f5.

Ten checks green on this head.

Process notes taken: the issue-first design gate, and the commit-subject length — this commit's subject is 59 characters.

@neusse

neusse commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Ask first: please make these five focused corrections, then return the refreshed head SHA. Your latest commit completed the three items from the earlier review; these are the remaining findings from reviewing head 72db166592e9f170b50bbf7161737f55786f8f13.

  1. Make a malformed existing field_placements entry abort the entire edit_schematic_component call before commit_command. A combined request can currently retain another valid edit in changed and commit it even though the placement is malformed.
  2. Add a combined-edit regression test: include a malformed existing placement together with an otherwise valid sibling/value edit, and prove the file remains byte-identical on refusal.
  3. Replace the unchecked unit as u32 conversion with a checked conversion such as u32::try_from, returning a structured invalid-argument refusal on overflow. An input such as 4294967297 must not wrap to unit 1.
  4. Add a regression test proving an oversized unit cannot wrap around and modify a real unit.
  5. After those changes, reconstruct/rebase once onto current main, run all ten required checks on that exact head, and reply with the new head SHA.

The conservative refusal of an (at …) containing more than three values is acceptable; keep it. If the malformed-placement path becomes call-atomic as requested, the current API_MIGRATIONS.md statement remains accurate. The pre-existing post-commit readback limitation remains separately accounted for by #499 and does not need to expand this PR.

The existing status:waiting-on-author label is correct, so no label change is needed.

@neusse

neusse commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Queue refresh — the five-item correction request in the preceding maintainer comment remains current. Please make the four code/test corrections now, but avoid repeated rebases: after #502 and the maintainer-owned #481 land, reconstruct once onto the resulting main, preserve all migration entries, run all ten checks, and reply with the exact head SHA.

This remains immediately ahead of #485 because both modify sch_components.rs. The current status:waiting-on-author label is correct.

@triglav-modular
triglav-modular force-pushed the feat/schematic-field-placement branch from 72db166 to 2b3e008 Compare September 10, 2026 20:44
@triglav-modular

Copy link
Copy Markdown
Contributor Author

All four corrections done, reconstructed once. Head: 2b3e008747fd8845d003f54dab90657a584e9bc5 on fbebd99 — current main after #502 (52e8ef8) and #481 (fbebd99). Four unique commits; all four kept their patch-ids across the reconstruction, so it moved the base and nothing else. Every API_MIGRATIONS.md entry is preserved — the file now carries fifteen, including the ones #481 and #502 brought in.

1 & 2. A malformed file now aborts the whole call, before commit_command. set_property_placement returns a typed refusal instead of a string: MalformedFile aborts, TargetMissing keeps the existing per-field behaviour.

The split reads "malformed" as unparseable content — a bad, missing or extra (at …) value, a broken property or hide token — and leaves "symbol, field or unit not found" per-field, since that is the caller naming something absent rather than the file being broken, and you did not ask for that path to change. The one borderline case is a property carrying no (at …) at all, which I put on the malformed side: across the 14 tracked .kicad_sch fixtures there are 1120 properties and not one lacks it, so its absence is anomalous rather than a legitimate state. Say the word if you meant the abort broader.

malformed_placement_aborts_a_combined_edit_without_writing pairs a malformed (at bad 85.09 90) with a valid fields edit and asserts the file is byte-identical and the sibling text absent. You were right that the placement-only test could not see this: removing the abort arm gives is_error: false, changes: ["Description → sibling edit that must not land"], the placement failure demoted to an errors array, and the sibling committed.

One question on shape rather than behaviour: the abort reuses this handler's existing No fields were updated on '<ref>': … error, to match the file's idiom. That sentence reads as your request matched nothing, where this case means the file is malformed and I refused to touch it — happy to give it a structured error kind instead if you would rather it be distinguishable.

3 & 4. unit is converted with u32::try_from, returning a structured invalid_argument naming the out-of-range value. an_oversized_unit_is_refused_rather_than_wrapping_to_a_real_unit asserts the kind, the reason and a byte-identical file. Restoring as u32 reproduces your example precisely: 4294967297 wraps to 1 and unit 1's Value moves to (10.0, 10.0) under is_error: false.

One report, no action taken. Two other unit casts in this file — add_schematic_component (sch_components.rs:557) and replace_component (:3624) — narrow with an unchecked as. They are f64 → u32, which has saturated since Rust 1.45, so an oversized value clamps to 4294967295 rather than wrapping to 1; they cannot reproduce this hazard. They are still wrong in a smaller way — the caller gets a nonsense unit rather than a refusal, NaN maps to 0, and the first defaults a missing unit to 1. Different tools, untouched by this PR, so they are noted here rather than changed.

Unchanged as you directed: the (at …) over-three-value refusal stays, API_MIGRATIONS.md needs no edit now the path is call-atomic, and #499 is not expanded.

Ten checks green on this head.

@neusse

neusse commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

The four requested corrections are present on head 2b3e008747fd8845d003f54dab90657a584e9bc5: malformed placement now aborts the combined call before commit, the combined-edit regression proves byte-identical refusal, unit uses checked u32::try_from, and the oversized-unit regression covers the wraparound case. The conservative extra-token refusal is still acceptable.

One final queue refresh is now needed because main advanced after this reconstruction. Please reconstruct/rebase the same four unique commits once onto current main (5afdcbec76b3a8e55f87da778411ee09a98ae45b), run the ten required checks on that exact head, and reply with its SHA. No redesign or additional code change is requested. This remains first in the sch_components.rs overlap sequence; #485 stays behind it.

triglav-modular and others added 4 commits September 11, 2026 07:46
Nothing in the server could position or hide a placed symbol's Reference,
Value or any other field. reset_schematic_field_positions only restores
the library default, so a field that lands badly -- a rotated gate whose
Value sits on its pin numbers -- had no fix inside the tooling, and the
way round was to empty the Value and draw the text some other way.

edit_schematic_component gains `field_placements`, keyed by field name,
each entry setting any of x, y, rotation and hide. An omitted part is
left as the file has it, so moving a field cannot silently unhide it. It
is a separate argument from `fields` rather than an overload of it: a
caller moving the Value text is not changing what it says, and widening
one argument's value type would make the schema lie about what it takes.

Coordinates are absolute schematic millimetres, as KiCad stores them.
That makes them per-placement, so `unit` says which one is meant, and a
position given for a component with several placed units and no unit
named is refused -- writing one coordinate to each would stack a quad
gate's four Values on a point and report success.

`hide` writes `(hide yes)` as a direct child of the property, which is
what eeschema writes on a placement; the `(effects (hide yes))` form
belongs to lib_symbols definitions. Verified against KiCad 10.0.6, whose
own writer round-trips the property-level token unchanged.

The readback now reports each field's observed position and visibility,
and every requested placement is compared against the committed file
before the call reports success: this handler edits source text, and a
search that matched nothing would otherwise pass silently.

Every guard was neutered and watched to fail. Two of the tests passed
while neutered on the first attempt and were rewritten: one accepted any
error from a handler with several ways to fail, and one asserted a
substring after an unwrap_or(0) that had quietly made the whole file the
search window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… value

Adversarial review found two ways the writer could corrupt a field, and
checking the result against real KiCad output found a third defect in
the readback.

A field's value is arbitrary text and may contain "(at " or "(hide ".
The searches ran over the whole property block, so a value like
"mounted (at 45 deg)" matched first and the move rewrote the *value*:
"mounted (at 11 22 0)". The result is still valid S-expression, so
nothing downstream notices. Both searches now start after the value's
closing quote.

Removing the hide token deleted back to the preceding newline, which
takes whatever else is on that line. On a file that writes
`(at 1 2 0) (hide yes)` together -- not KiCad's own layout, but one it
is still asked to read -- unhiding a field destroyed its position. The
deletion now takes the indentation before the token, and the newline
only when the token had the line to itself.

The readback looked for the hidden flag only among a property's direct
children, which is the form lib_symbols uses. KiCad writes a
*placement's* flag inside `(effects ...)`, so every field KiCad itself
had hidden was reported visible. Worse, the writer's own check is
textual and does see the nested token, so asking to hide such a field
became a no-op that then failed its own post-write comparison with
`stale_target` over a file that was already correct. The readback now
accepts either form. The two are equivalent to KiCad 10.0.6: a file in
each exports byte-identical SVG.

The first two were caught by the post-write comparison rather than by
any test, which is the argument for that check: each left a committed
file that parses, exports a netlist, and is wrong. The third was found
by reading what KiCad actually writes -- every existing test drove a
field the fixture leaves visible, so none of them could have seen it.
All three now have regression tests, and each guard was neutered and
watched to fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of 8e4bc01 found a file-safety hole in the writer. An existing
(at ...) was parsed with filter_map and the gaps defaulted to zero, so
an unparseable token shifted the rest left: (at bad 85.09 90) read as
x=85.09, y=90, and a rotation-only edit then committed (at 85.09 90 0)
-- moving the field to a position the caller never asked for and the
file never held, and reporting success while doing it.

The placement is now parsed positionally and strictly: finite numeric x
and y, a finite rotation when a third value is present, and a refusal
when there are more values than that, since rewriting those as three
tokens would drop whatever the extras carried. It refuses before
commit_command, so the file is untouched.

field_placement_refuses_a_malformed_existing_placement_without_writing
corrupts the fixture's own (at ...) and asserts the file is byte
identical after the refusal. That is a different guard from the existing
non-numeric request test, which checks what the caller sent rather than
what the file already contained. Neutered back to filter_map with
unwrap_or(0.0) and watched to fail: it returns is_error false and the
readback shows Value at x=85.09, y=90 instead of 157.48, 85.09.

Also from the same review: docs/API_MIGRATIONS.md gains the additive
contract for field_placements, unit and units[].field_placements, which
the PR body and the tool-directory row do not durably record; and
FieldPlacement drops to module-private, since nothing outside this file
consumes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of 72db166 found that a malformed existing placement refused only
its own field. This handler edits text and commits once at the end, so a
valid sibling edit in the same request was still committed while the call
reported the refusal -- the file we had just found to be broken got
written anyway.

set_property_placement now returns a typed refusal instead of a String.
MalformedFile aborts the whole call before commit_command; TargetMissing
keeps the existing per-field behaviour. The split follows the request:
"malformed" is unparseable content -- a bad, missing or extra (at ...)
value, a broken property or hide token -- whereas a symbol, field or unit
that is simply absent is the caller naming something that is not there,
which is local to the field they named and was not asked to change.

unit is also converted with u32::try_from rather than `as`. The integer
cast truncates instead of refusing, so 4294967297 became 1 and edited a
real unit the caller never named; overflow now returns a structured
invalid_argument naming the value.

Both guards were neutered and watched to fail. Removing the abort arm
returns is_error false with the sibling committed and the placement
demoted to an errors array. Restoring `as u32` moves unit 1's Value to
(10, 10) and reports success.

The two other unit casts in this file are in different tools and are
f64 -> u32, which saturates rather than truncating, so they cannot
reproduce this; they are reported separately rather than changed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@triglav-modular
triglav-modular force-pushed the feat/schematic-field-placement branch from 2b3e008 to 35a18bc Compare September 11, 2026 05:49
@triglav-modular

Copy link
Copy Markdown
Contributor Author

Rebased. Head: 35a18bc7a5a7f21bc4d8e7f393a67d3d9b974392 on f58b222 — not 5afdcbe; #520 merged after you wrote, so both SHAs are named here rather than left to be discovered.

Same four unique commits, no content change: git range-diff reports all four as identical, and each kept its patch-id across the move.

Ten checks green on this head.

@mixelpixx mixelpixx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head review on 35a18bc7a5a7f21bc4d8e7f393a67d3d9b974392 (four unique commits on f58b222; ten checks green; zero review threads; git range-diff identical to the head George accepted).

Local gate on this head: fmt / clippy -D warnings / --lib --tests (1758 passed, 0 failed) / --doc all exit 0; cargo xtask fix-doc-counts --check unchanged (no tool added). Neuter: the MalformedFile abort arm disabled with a false match guard → malformed_placement_aborts_a_combined_edit_without_writing fails as it should (1 of 12 fails, the rest pass); restored tree green.

Read end to end against the five corrections. Every unit refusal (non-integer, 0, u32::try_from overflow) and the malformed-file abort return before the handler's single commit_command; the readback is rebuilt from the reparsed committed file, reads (hide yes) both as a direct child and nested in (effects …), and placement_landed refuses with stale_target on mismatch. The MalformedFile / TargetMissing split is the right cut and the borderline case (a property with no (at …) at all) is on the right side of it. Twelve new tests, each pinning a named guard.

Merging on this head. Two small things for after, neither blocking and neither worth a fifth push:

  • Body only: it still says "Unique commits: two" (the head has four), the neutering table lists eight of the twelve tests, and the "File mutations go through this handler's existing text-edit path" bullet appears twice. An edit to the description is enough.
  • Follow-up issue, filed by me with credit to you: (a) rotation in a placement is written verbatim, so 450 or -90 lands raw and placement_landed accepts it, while the symbol-rotate path in the same file normalises with rem_euclid(360.0) — KiCad re-saves it differently than Konnect reported, the #273 class; (b) the (unit search inside a property block is unbounded, unlike the (at / (hide searches you bounded past the closing quote after the first defect, so a value containing (unit would be misread as unit 1 via unwrap_or(1); (c) the abort error carries no structured kind, unlike its sibling refusals. Same tool, same author if you want it — it is a P2.

Closes #490 is the terminal reference. Label → status:ready-to-merge on this exact SHA.

@mixelpixx mixelpixx added status:ready-to-merge Next actor: automation or maintainer — exact head reviewed and removed status:waiting-on-author Next actor: the PR author — one checklist, 14-day target labels Sep 11, 2026
@mixelpixx
mixelpixx dismissed neusse’s stale review September 11, 2026 13:02

Stale: this review was on 8e4bc01. George confirmed on 2026-09-10T23:03Z that all four requested corrections are present on 2b3e008 and only a rebase remained; head 35a18bc is range-diff identical after that rebase. Dismissed by mixelpixx to unblock the merge on the exact head reviewed.

@mixelpixx
mixelpixx merged commit 4a5dd41 into mixelpixx:main Sep 11, 2026
10 checks passed
triglav-modular added a commit to triglav-modular/Konnect that referenced this pull request Sep 11, 2026
Konnect's tool schemas are public API and API_MIGRATIONS.md is where an
argument's contract is recorded; the PR body and the tool-directory row
are not that durable record. Asked for on mixelpixx#489 and on mixelpixx#514's design
acceptance in the same words, so this branch carries its own entry
rather than waiting to be asked a third time.

The entry covers what a caller needs: that `mirror` is optional on
add_schematic_component and on each batch_place_components entry, that
it speaks the file format's own "x"/"y"/"none", that omitting it keeps
the previous behaviour exactly, why there is no both-axes spelling, that
an unknown axis refuses the call (or only its own batch entry) rather
than placing the symbol upright, that the field anchors follow the
reflection, and that mirror_x/mirror_y are read from the reparsed
committed schematic beside x, y and rotation and bound as intent, so a
reflection that did not reach the file refuses with stale_target.

No code change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:ready-to-merge Next actor: automation or maintainer — exact head reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No tool can position or hide a placed symbol's field text — reset_schematic_field_positions only restores the library default

3 participants