Repository navigation
Release: relationships on the fact side, composite keys, Mermaid/DBML export, merge-conflict and YAML fixes - #159
Merged
Merged
Conversation
…tionship mode safe - A model save no longer rewrites the whole `relationships:` list. It is synced entry by entry: an unchanged list is not touched, a changed entry keeps its YAML node (comments, unknown keys such as `role:`), entries the reader skips stay where they are, and a change that cannot be made in place is refused with the model file and line. - A canvas edit that would rewrite or delete a model file open with unsaved edits is refused, naming the file, instead of replacing them. - A domain or model file that does not parse is counted by what its text shows, so a merge conflict cannot switch the project between per-domain and library relationships. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, kept line endings (S1-S5) - S1: the unsaved-tab guard compared resolved paths letter for letter, so a tab VS Code spelled in another case (a `c:` drive letter, a macOS folder typed differently) was missed. One helper, `dirtyFiles` / `pathKey` in src/providers/dirtyDocuments.ts, compares case-insensitively on win32 and darwin; the canvas guard and Move Relationships use it. - S2: a save wrote `dataType: unknown` (the reader's default) into every column the file left untyped. A column node keeps a key it leaves out or empty when the desired value is only that read default. - S3: model yml and domain JSON written straight to disk keep their own line endings: LogicalModelService.renderModel / renameModel, minimalEdits' flow-mapping branch (Move Relationships) and the v4 -> v5 migration's domain rewrite, through src/services/lineEndings.ts. - S4: the positional take-over in syncRelationships reuses an old entry's node only when it shares an end with the new one, so an unrelated link no longer inherits another's comment or `role:`. - S5: Draw from dbt refuses, naming the file, when a model file it would write is open with unsaved edits, before anything is written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tten-back cardinality default - projectDiscovery.samePath compares through the shared, vscode-free `pathKey` (moved to src/services/pathKey.ts), so project ownership and the project picker treat a folder spelled in another case as the same folder on Windows and macOS. - Stripping legacy domain tags keeps a schema yml's CRLF line endings. - A relationship entry taken over by a rename leaves every field that reads the same as written, so a missing cardinality is not written back as `many-to-one`. - renameModel keeps the old file's line endings when it has to regenerate it. - Draw from dbt names what failed when it cannot read the model library to plan the diagram, instead of failing the command generically. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… with an optional role (#133) A library relationship was saved in the file of whichever model it was drawn *from*. Drawing from a dimension to a fact, or pressing the edge's ⇄ swap, put it in the dimension's file, so adding a new fact meant editing every dimension it joins. The many (FK) end now always owns it. core's canonicalRelationship() turns a one-to-many into the swapped many-to-one (one-to-one and many-to-many keep the drawn direction), and every path that chooses a library file applies it: writeLibraryRelationship (add, ⇄ swap, edit), routeToLibrary (Draw from dbt), planMoveToLibrary / resolveConflict, and the CLI's add-relationship fix. A drag that starts on a key is turned round before the dialog opens, because the dialog's many-to-one default would otherwise record the dimension as the many side. The same two columns joined either way round are one link: the reverse is refused as a duplicate (host and dialog), mergeLibraryRelationships draws it once, and DiscrepancyService matches it whichever end each stage reads it from — dbt may test a link from its one side, which the many-side logical entry must still match. "Move Relationships to Model Library" also re-homes library entries stored on their one side (planRehome), on the same disk-only, all-or-nothing writer, keeping roles and never overwriting an entry the fact already holds. Optional `role` (a label, not identity; normalised, max 60 chars) is parsed, written by every writer, drawn on the edge, editable in the dialog and kept through ⇄ swaps; addRelationship / editRelationship carry it with payload validation. Schemas, SCHEMA_CONTENT (harness v26), the setup skill, the reference doc, CLAUDE.md and the changelog describe it; the changelog warns that 1.6.7 drops roles when it rewrites a model's list. Merges main's 1.6.7 release stamp so the new notes sit under Unreleased. Rebased onto the relationship-list safety fix: LogicalModelService now syncs `role` entry by entry with the other relationship keys, so a role-only edit is saved and clearing a role removes the key. Closes #133 (cherry picked from commit ec56fe3) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
isReferencedKey() counted any primary-key column as the end being pointed at, so a drag from a hub to a satellite, whose hub hash key is part of its composite key, was left as drawn and stored on the hub. Only a model's whole PK or NK counts as the referenced end now, which also covers a fact keyed by its dimension keys. Adds Data Vault orientation tests and a host-level lifecycle test (add with role, ⇄ swap there and back across two diagrams, edit, model rename, column removal). Taken from da3d1b4 without its tidy-up pop-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
removeLibraryRelationships matched the exact ends, so deleting a line left a teammate's reverse copy on the other model, which then drew again, and an edit left a twin that disagreed. Removal (delete, edit, ⇄) now drops every copy of the link in either model file and in the domain file, by core's new linkKey / sameLink: two `model.column` ends, unordered, without case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…der (#133) mergeLibraryRelationships kept the first copy in model order, so a 1.6.7 copy saved backwards on the dimension could hide the fact's correct one. The winner is now fixed: a copy in its home (not one-to-many) beats one that is not, then one not leaving its model's whole primary key, then the lowest model name, then the first entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tion (#133) relationshipsForAddedModels forced many-to-one in the direction dbt's relationships test was declared, so a test on the dimension was stored backwards on the dimension and markDraftKeys flagged its key as a foreign key. The derived cardinality (from `unique` evidence) is now put in its stored form first, links are deduped either way round, and a key column is never flagged a foreign key by the draft. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t rewrite safely (#133) planRehome dropped the dimension's copy of a link whenever the fact already held one, even when the two disagreed (cardinality, direction or role). Both copies are now left in place and listed in the preview as needing attention. setYamlRelationships re-renders a whole `relationships:` list, so a file whose list holds a comment or an entry the reader cannot understand (an unknown key, an unrecognised cardinality, a skipped entry) is no longer rewritten: relationshipsRewriteLoses() marks it, the plan leaves what was bound for it where it is, and the preview names the file. The check runs again before writing, in case the file changed while the dialog was open. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
core's linkKey / sameLink (two `model.column` ends, unordered, without case) and the new reverseRelationship (ends swapped, many-to-one ↔ one-to-many) replace the copies that had grown up: discrepancyService's linkKey and readFromOtherEnd, libraryRelationships' sameColumnPair and hasLibraryRelationship, the CLI diff's exact-end library lookup, and the New Relationship dialog's inline reverse check and edit-time flip. canonicalRelationship is now reverseRelationship applied to a one-to-many. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-column v4 file (#133) Input fixtures from 0872b88: relationships stored one-to-many on the one side, a 1.6.7 backwards copy alone and next to the fact's, the stray FK flag Draw from dbt left on a key, a diagram-file copy that differs, and a hand-edited v4 domain whose columns have no text name. Both golden suites pin how they load; no existing golden changed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…delete by pair and the move's left-alone cases Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ards on a dimension (#133) 1.6.7's dialog defaulted to many-to-one from the drag's start, so a line drawn from a dimension's key was stored on the dimension as a many-to-one pointing at the fact: probably the most common old shape, and one Move left alone. planRehome now also turns round a many-to-one whose fromColumn is its own model's whole primary (or natural) key and whose toColumn is not the target's whole key, into the target's file as many-to-one, keeping its role. It follows the existing rules: an agreeing copy on the fact means the dimension's is just dropped, a disagreeing one leaves both listed, locked files stay untouched, and a second run finds nothing. Anything without that key evidence is left alone; the stray FK flag on the dimension key is not cleared. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…diagrams as one (#133) planMoveToLibrary grouped domain-file relationships by the directional relationshipKey, so `dim → fct one-to-many` in one diagram and `fct → dim one-to-one` in another never met as a conflict: both went to toLibrary with the same stored ends, one replaced the other, the one-to-one was lost and the preview listed the link twice. They are now grouped by linkKey, with each copy's cardinality read in the direction of the first one seen: copies that disagree are an ordinary conflict for the existing QuickPick, and copies that agree once turned round merge into one many-to-one on the fact. Domain entries are taken out by linkKey too, so resolveConflict and the domain-file rewrite remove both directions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rawn once (#133) keyEvidence(model, column) -> whole-key | not-key | unknown is the one place relationship direction reads key flags; contradictsKeys and keyedRelationship ("keys win") build on it, so dbt evidence can be added there later (L1). M: a many-to-one is read the other way round only when the target declares a whole key that is a different column, never when it flags no key at all. D1: mergeLibraryRelationships dedupes a domain file's own copies of one link too (it skipped them when the library was empty), and the read winner breaks ties by content rather than entry order, preferring a labelled copy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s overwritten (#133) planRelationshipWrite is the one pure planner for the canvas's add / ⇄ / edit / delete, run inside the provider's commitRelationshipWrite (one applyDomainEdit, one undo step). An edit folds every copy of the link into one entry in its home with the drawn line's role, else any copy's (F2, F4); ⇄ refuses while copies carry different roles, and refuses to make a whole key the many side (F3b), which the New Relationship dialog warns about with "Create anyway". A delete in a library project names other diagrams that still draw their own copy (D3), writing nothing. routeToLibrary never overwrites a stored link, and leaves a link another diagram draws from its own copy in the new domain's file (F1). Move stores diagram copies as keyedRelationship reads them, so one run is final (F3); conflicts are whole definitions — cardinality, role, and the FK end of a one-to-one (F5, F6) — and a many-to-many drawn both ways is one link stored from its lower end. A diagram role lands on the entry the library keeps, also when that entry is being turned round (F7); diagram copies the library's version replaces are listed (D2), as is every entry turned round (D4). applyMoveToModel removes a turned entry by itself, never a sibling copy, and is shared by the command and the checker. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…precated in model files (#133) D5: erd-studio diff's set-cardinality fix names the entry exactly as it belongs in the yml — on its many side, never one-to-many — the file it goes in, and `movesFrom` when the change moves it to the other model's file; remove-relationship names the stored entry and its file. One-to-one and many-to-many keep the direction they are stored in. The setup skill's fix table follows. D6: the model-file schema marks one-to-many deprecated (still read), and the reference doc notes a toModel with no model file is not drawn, keys win, and the Move's definitions. SCHEMA_CONTENT states the keys-win rule; harness v27, fixture copies regenerated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eal planner (#133) Every small state of one link's stored copies (two models, two diagrams, six key profiles) through every canvas edit, Add from dbt, rename and Move, against the storage invariants. The edits run planRelationshipWrite and the Move runs planMoveToLibrary / applyMoveToModel — the provider's and the command's own functions, not copies. CI runs the small scope (~11 s) and fails on any signature outside ALLOWED (L4's case-only renames, and renames of another diagram's own copy, out of scope); STATESPACE_SCOPE=full and STATESPACE_BFS=1 widen it. A sampled cross-check drives the real provider and Move command over files (~20 s). The findings tests pin F1–F7 and D1–D3 on the real code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#133 L4) Core's respellRelationship spells each end as the domain's models and columns spell it (exact first, then without case); mergeLibraryRelationships applies it to everything it draws, including the domain-only path, and the v4 reader to inline relationships. `toModel: Dim_Customer` now draws, and graphTransformer stays exact. Writes normalise only the entry they touch: the yml sync matches ends without case, so a respelled entry keeps its node (comments, unknown keys). Column and model renames and removals cascade without case through library entries and the domain file (renameColumnInDomainRelationships / renameModelInDomainRelationships, shared with the checker). Move and routeToLibrary resolve a model to one object and one file by its real name. The checker's four case-only rename rows are gone from its allowlist. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry key" (#133 L1) Core's keyEvidence moves to keyEvidence.ts with column sets and dbt's tests: the model's flags (the full PK or NK set is the key, anything else in a keyed model is not), else a unique test, a unique_combination_of_columns or a relationships test (buildDbtKeyIndex, unique beats not-unique in any order). orientLink decides a link's direction from its two ends' evidence, symmetrically; with nothing to go on it is not decided. The index reaches every consumer: the read winner (getDomain's dbtKeyIndex, from the canvas and computeDomainDiff, so CLI and canvas agree), the write planner's ⇄ refusal, Move's turn-round (loadDbtKeyIndex) and the editable payload's DisplayColumn.dbtKey (withDbtKeyHints; never the viewer, the CLI or the goldens). DisplayColumn.isForeignKeyDeclared carries the stored flag; the logical goldens gain it. Drag orientation is orientLink (isReferencedKey is gone). The dialog states which side holds the foreign key and why, asks with two model-named choices when nothing says (Create waits), offers "Turn round" against the keys, and offers "Mark … as primary key" when the one side has no key (ticked when dbt says unique). markKey travels on addRelationship / editRelationship, validated at the boundary (validateMarkKey) and applied by the planner in the same WorkspaceEdit; refused when a key appeared meanwhile. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A drag onto another column of the same model opens the New Relationship dialog for a self-reference (employee.manager_id → employee.employee_id), oriented by orientLink like any other; only a column dropped on itself is refused, with a toast that says how to draw one. The dialog drops its "model cannot relate to itself" error for "a column can't point at itself", and the host refuses that pair at the boundary too (validateRelationshipEnds). Several loops on one node nest: graphTransformer gives each its loopIndex among the node's self-loops by edge id, and FkEdge widens the radius by it. The details panel lists a self-reference once, under outgoing, marked ↻. Storage, the physical stage, the diff, Draw from dbt and Move already handled one model at both ends; tests now pin each: stored in the model's own file (a one-to-many swapped there), turned round by Move inside one file written once, derived many-to-one from dbt, matched by the diff, and named on employee.yml by erd-studio diff. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#133 L2) A composite foreign key is one entry per column pair, all sharing a `compositeKey`, stored together on the FK side — so ERD Studio 1.6.7, which reads each entry as a single link and drops unknown keys on save, never loses a pair (mixed167.compositeKey runs 1.6.7's writer to prove it; the rejected array form would be deleted outright). Core: Relationship / DisplayRelationship.compositeKey, normaliseCompositeKey, ColumnPair, groupKey, compositeGroupProblem (2–8 pairs, one model to one model, one cardinality, many-to-one or one-to-one, no column twice) and contradictsKeysOf on column sets. The readers keep the key; the read path assembles valid groups after the per-link winner (named and labelled by the first member, placed at the first one's spot) and draws invalid ones as singles with one warning. Writes: planRelationshipWrite treats an edge of a group as the group — add/edit take extraPairs (named fk_<toModel>, _2, …; single links already stored are taken in, "Grouped N existing relationships into …"; a pair another group owns is refused), ⇄ and cardinality change every member (never many-to-many; keys win on the sets), an edit down to one pair writes a single link, delete expands to the group, and removing a member column removes the whole group (library and domain file). extraPairs is validated at the boundary (validateExtraPairs; markKey and the self-reference check read every pair). The model-file writer and the Move's minimal edits keep compositeKey. Move: diagram groups move as units (planGroupMoves) — conflicts over whole group definitions, singles of a moving group taken out and listed, a group left whole (and said why) when its file is missing or locked or the library holds a pair otherwise; planRehome turns a backwards group round together. Schemas, the reference doc, SCHEMA_CONTENT (composites, self-references, real spelling, marking the referenced key; harness v28, fixture copies regenerated) and a composite-and-self golden. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he CLI (#133 L2) The schema yml and the manifest yield compositeForeignKeys from dbt ≥ 1.9 model-level `foreign_key` constraints and `dbt_constraints.foreign_key` tests (two or more columns; `to` by ref(), or a rendered relation by its last identifier against node name then alias). buildPhysicalDomain merges both sources by groupKey, and derivePhysicalRelationships emits each declaration's members with a compositeKey (the constraint's name, else fk_<toModel>) — many-to-one, one-to-one when the from set is a declared unique combination — deduping a pair a relationships test also declares. The physical stage stays derived; its goldens are unchanged. Compare to Physical matches pair by pair and reports each composite once: matched, one cardinality difference, or one informational extra (`composite`, `undeclaredPairs`) — target links a composite consumes are never missing. The sync plan carries pairs under a joined selection key and asks dbt to declare an undeclared composite rather than delete it from the logical model. `erd-studio diff` gains the advisory declare-composite-foreign-key fix (blocking under --strict); add-relationship and set-cardinality carry pairs on the canonical file. The discrepancy panel shows the informational row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#133 L2) graphTransformer folds a composite's members into one edge carrying its pairs (foldComposites / relationshipDisplayKey / relationshipEdgeId, ids and discrepancy keys joined `a+b`), a missing composite into one ghost edge. FkEdge shows a ⧉ N chip and lists the pairs in its hover text; ⇄, delete and the details panel act on the first pair, which the host expands to the group. Selecting the edge highlights every member column, and the details panel lists a composite as one row. The New Relationship dialog gains "+ Add another column pair" rows (each with ×), titles itself "New Composite Relationship", disables many-to-many with two or more pairs, checks each row (both columns, a column used twice, a column pointing at itself), says when a pair is already a relationship it will take in, and reads direction, the key choice and "Mark … as primary key" on the column sets. Edit opens with every pair; messages carry extraPairs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… L5) Renaming a column on the canvas rewrote the open diagram and the model library, but another diagram's own copy of a relationship (a per-diagram project, or a copy left as it is) kept the old name and stopped drawing. applyDomainEdit gains `otherDomains`: a rewrite run on every other v5 domain file, each one it changes added to the same WorkspaceEdit (one undo step, saved and flushed on undo like the model files it writes) and refused by name when that file is open with unsaved edits. A column rename rewrites the other diagrams' copies (renameColumnInDomainRelationships, without case); a model rename rewrites their copies and, so they keep drawing the model, their logical.models entry and stored position. The exhaustive checker's rename mirrors the provider; its last four allowlist rows go, and the allowlist is empty. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r nothing (#133 L2) Found by the extended exhaustive check: - A write found a composite from the stored entries (the first copy of each link), so a stray copy of a member in the other model's file could make a delete or ⇄ act on one member only. The planner, the delete expansion and column removal now take the group from what the read path draws (drawnGroupOf), whichever copies won; column removal reads it from the whole library, so no copy of a member outlives the key in the open diagram. - groupOf takes the entry itself when it is in the list (a file holding two groups of one name) and counts a link saved twice as one pair, as the read path does. - A valid library group whose members are stored in different files was drawn with its members in opposite directions (two edges); every member is now read the way the first is. - Move judged another copy of a member on its single column, which can contradict the group's column-set reading, then dropped only that member's re-home and moved half the group. Every copy is now read as Move stores it (a member as part of its group), and a group's re-homes are kept or dropped together. A single entry Move re-homes no longer carries a stale compositeKey, and a diagram keeping a member's link as another entry that says something else leaves the group whole, named in the preview. - Move compared a library group with a diagram's by groupKey, whose canonical from-model differs for a group stored the other way round, so a backwards library group left the diagrams' copies behind; it now compares the pairs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-references and composite keys (#133 X) The main universe gains four dbt-evidence key profiles (dbtStar, dbtOnlyUnique, flagsBeatDbt, dbtCombo — the index reaches the read winner, the write planner and the Move, as on the canvas) and a case-variant star (the other end's model spelled `Dim` / `Fct`, the column `K`), with the invariants C1 (an end resolving without case is drawn, in the real spelling), C2 (a write respells only what it touches), E1 (flags dominate dbt), E2 (orientLink is symmetric) and E3 (no silent default from drag order). Its Move mirror looks models up as the command does, by real name. relationshipStateSpace.universes.ts adds two universes on the same planner, read path, Move and renderer: SELF (emp.mgr ↔ emp.id; S1 self-home, I2/I3, I5, M3) and GROUP (a composite of two pairs, its partial and its 1.6.7-split singles; G1 one edge, G2 atomic, G3 home, G4 no pair lost to 1.6.7, G5 regroup, G6 column removal, I5). The real-provider cross-check writes each profile's dbt tests as a schema yml and gives the real Move its loadDbtKeyIndex. CI scope ~16 s alone (9,768 + 1,317 states); full scope (40,784 states) and BFS depth 3 are clean. The allowlist stays empty. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When two people move models on different branches, git can leave <<<<<<< / ======= / >>>>>>> markers in a domain file's viewConfig.positions. parseDomainJson handed that text to JSON.parse, so the failure was a plain 'invalid-json' DomainFileError: the canvas spent ~1.2s re-reading it (the reason is transient, meant for a file caught mid-write) and then showed the full-screen "Invalid JSON" page, which reads as corruption. A model yml with markers showed as a YAML error on whatever line the parser stopped. Core now checks text that has already failed to parse for a complete conflict (git's is_conflict_marker shapes: an opening line, a separator and a closing line in that order, a diff3 base allowed; BOM and CRLF tolerated), so a valid file can never be flagged and a lone Markdown "=======" is not a conflict. A domain file gets DomainFileError.mergeConflict (always present, false by default) and the first marker's line, keeps reason 'invalid-json' and is not transient; a model file gets ModelLoadError.mergeConflict with kind 'yamlOther'. Both are flags rather than new union members, because widening DomainFileErrorReason or ModelLoadErrorKind would break exhaustive switches and Records in consumers (the Confluence app, telemetry) and need a MINOR package release; a host that predates the flag still says "invalid JSON" / "YAML error", which stays true. Additive, PATCH. The canvas error screen explains the conflict, drops Retry and offers "Open as Text to Resolve"; it reloads by itself on the save that resolves it (every document change already re-reads the domain). The model node, notification and CLI say "unresolved git merge conflict"; diff / export report the code merge-conflict, and doctor fills conflictedDomainFiles. Telemetry is unchanged (still a 'json' load failure). Closes #145 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The orphaned-canvas safety net armed a 5 s timer whenever no domain was loaded. Its effect depended only on `domain`, so it kept running behind the full-screen load-error page and set `showReconnectOverlay`, which nothing ever reset. Pressing Retry cleared the error and rendered the loading screen with the overlay already up, offering a pointless save-all + window reload (for ~1.2 s on invalid JSON while the host re-read the file). The timing rule now lives in `useReconnectWatchdog(waiting)`, with `waiting = !domain && !error`. An `error` is an answer from the host, so it stops the timer and hides the overlay; Retry clears the error and starts a fresh 5 s grace period, so a host that then stays silent still gets the overlay. The hook returns `timedOut && waiting`, so a stale value can never show for a render. Closes #149 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… to the user Order: doctor's resolve-merge-conflicts next step now comes straight after confirm-venv and before install-dbt, create-profile, run-deps, run-parse/refresh-parse and run-catalog. A conflicted diagram cannot load whatever state dbt is in, so an assistant working down the list must raise it before installing dbt or running anything against the warehouse. confirm-venv stays first: it is a safety question about running a program the project ships. fix-model-yaml keeps its existing place. Wording: the step's why no longer tells the reader to keep a side and run git add. It asks an assistant to show the user each file and line, say either side is safe when only positions differ, otherwise ask which side to keep, and not to pick a side or run git itself; then re-run doctor. It reads "This file still holds" for one file. The model-file conflict hint (unreadableHint, used by doctor's fix-model-yaml and diff's fix explain) says the same, and fix-model-yaml adds that a merge conflict is the user's to resolve. The terminal lines are short and neutral: "unresolved git merge conflict — resolve it (keep one side), save, then re-run doctor|diff" (describeUnreadable now takes the command to name). Tests: the doctor test now asserts the order (conflict before install-dbt, run-parse and run-catalog; confirm-venv before the conflict) and the new wording; the diff test checks the fix explain and the terminal line. Docs: @erd-studio/core's README documents DomainFileError's mergeConflict and line, that a merge-conflict error is not transient, and a model's loadError.mergeConflict; parseLogicalModelText's docstring names the ModelMergeConflictError it throws for conflicted text. CHANGELOG's #145 entry mentions the doctor ordering and wording. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… it ignores Two review findings on #144. Doctor listed every column holding a `data_type:` key under "read as dataType and saved back as dataType" — including columns that also hold a `dataType`. Those are never renamed (the reader ignores their `data_type` and the writer keeps it as written), so no save ever cleared the warning. `erd.dataTypeAliasFiles[]` now carries `columns` (read from `data_type`; renamed the next time the file is written) and a new `ignored` (beside a `dataType` that wins; only deleting it clears the line). The writer and doctor share one rule, `dataTypeAliasFate`, matching core's reader — so an empty `dataType:` beside a `data_type` (where the reader does use `data_type`) is now renamed too, the empty key giving way and any comment on it moving to the renamed key. Human output prints one line per case, singular for one column ("payment_id uses …"), counted for more, and a long list names the first five then "and N more". Still a warning, never a next step. "The next save" overpromised: the key is renamed only when ERD Studio rewrites that model's file — a canvas edit to the model or to a relationship stored in it (applyModelEdit, commitRelationshipWrite's changed models, a rename, Draw from dbt / Add Existing Model routing), not moving nodes or Move Relationships to Model Library (surgical edits, no column sync). CHANGELOG, the reference doc, doc comments and the doctor line now say so, and the CHANGELOG notes the Confluence app shows the type once it updates to this version. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…st's timeout The first test in each file that parsed Mermaid paid the whole cold load of both releases (mermaid 10.0.0 and mermaid-current) inside vitest's 5 s test timeout: the parsers were dynamic imports in the shared helper, and 10.0.0 also lazily loads its ER grammar on the first parse. Measured (Node 24, 18 cores): - Idle, alone: import 10.0.0 ~0.36 s, its first parse 0.33-0.58 s, import mermaid-current 0.31-0.61 s; every later parse ~0-4 ms. - Full `npm test`, before: the first Mermaid test in all three files timed out in both runs - exportMermaid "opens with erDiagram" 11.3 s / 9.0 s, diagramExport "as mermaid" 10.7 s / 13.1 s, exportDiagram.golden "showcase mermaid golden" 8.7 s / 18.1 s; a third run 6.2 s / 6.0 s (both failed) / 5.7 s. - Full `npm test`, after (two runs): 3588 passed, 1 skipped, 0 failed; no test in those files over 184 ms. exportParsers.ts now has a memoised loadMermaidParsers() that imports both releases once and warms each with one parse; parseMermaid and parseMermaidOldest go through it, and a failed load is forgotten so the next call retries. exportMermaid, exportDiagram.golden and test/unit/diagramExport call it in a beforeAll with its own 60 s timeout (MERMAID_LOAD_TIMEOUT_MS), so the global testTimeout is unchanged. The DBML parsers were already outside any test's timeout: they are static imports, loaded while vitest collects the file, and the first readDbmlEverywhere costs ~15 ms, so exportDbml.test.ts is unchanged. Closes #152 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… to the user describeDomainError passed core's neutral "Resolve them in the file" message straight through, which an assistant running the setup skill read as an instruction to resolve the conflict itself. For a DomainFileError with mergeConflict it now says what doctor's resolve-merge-conflicts step and the model-file hint say: show the user the file and line, ask which side to keep, never pick a side or run git. Core's message is unchanged (the Confluence app shows it); the code stays merge-conflict and the path stays project-relative. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, and read aliases as the reader does Dropping an empty `dataType:` beside a `data_type` the reader uses now merges every comment on both keys (the dropped key's first) instead of keeping one and discarding the rest, and gives the next key a blank line only when the source really had one — the yaml library marks one after any empty value that soaked up the comment lines below it. The rename rule also resolves a `dataType` alias before deciding it is empty, so an alias of `~` reads as empty here as it does in core's reader, and doctor no longer says to delete the `data_type` the type actually comes from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ad of resolving them Depends on #148 (#145): it teaches the /erd-studio-setup skill the signals that PR adds to the CLI, and must merge after it. - SKILL.md (Stage 1): a `resolve-merge-conflicts` step, a `merge-conflict` error or a `fix-model-yaml` with `mergeConflict: true` means git left two versions in a file; stop, follow troubleshooting.md, never pick a side or run git. Stage 4 step 7 and Stage 5 carve the conflict out of "fix every fix-model-yaml". Still 498 lines (four paragraphs rewrapped to make room). - references/troubleshooting.md: new "Merge conflicts" section - every signal (`erd.conflictedDomainFiles`, `erd.unreadableModelFiles` / `mergeConflict: true`, diff code `merge-conflict`), show file and line in plain words, positions-only means either side is safe (edit only on the user's say-so), otherwise ask; never `git add` / `checkout --ours/--theirs` / `merge` / `commit` or any git command; re-run doctor once saved. - references/verify-and-fix.md, building-the-model.md: a merge-conflict fix-model-yaml is not a YAML fix; "a key twice" now reads "written twice by hand". - SCHEMA_CONTENT: the YAML quoting rule's "fix that before anything else" gets the same exception. - HARNESS_VERSION 28 -> 29; fixture schema skill regenerated (marker + the one SCHEMA_CONTENT line). allowed-tools unchanged (no git pre-approved). - Tests pin the guidance against doctor.ts / diff.ts's own spellings. Closes #150 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ot SyntaxError
A canvas open before git wrote conflict markers into its domain file still
draws the old document; an edit made before the refresh parsed the document
with a bare JSON.parse, so the refusal toast read as a raw parser error.
Every user-facing read of the open domain document (applyDomainEdit, the
edit handlers, the relationship write, Add from dbt, the sync plan) now goes
through one helper, parseDomainDocument, over core's parseDomainJson — the
load path's parser. An empty, invalid or conflicted document throws
EditRefused with core's plain message ("…has unresolved git merge conflicts
(first at line N)…"), the file named project-relative, before any
WorkspaceEdit is built, so nothing is written and the handler posts one
error toast. Telemetry sniffs (recordCanvasOpen, recordRelationshipState),
the other-panels refresh and the other-domains rewrite keep their tolerant
JSON.parse. Valid documents edit exactly as before.
Depends on #148 (core's merge-conflict DomainFileError, #145): merge after it.
Closes #151
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the empty dataType beside a data_type (its key or value), or the data_type key itself, carries a YAML anchor, a write no longer renames or drops either key: dropping it left aliases such as *x dangling and the file unparseable, and writing into it would retype every alias. The type stays in data_type, where the reader takes it from, and an edit is written there. dataTypeAliasFate returns 'anchored' for this case, so doctor lists those columns apart (erd.dataTypeAliasFiles[].anchored). readDefaultsLeftOut now reads aliases through, so `dataType: *x` of an empty anchor stays an alias instead of becoming `unknown`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ours/theirs and re-run wording The CLI sets `mergeConflict: true` only on `unreadableModelFiles[]` entries (doctor's `erd.unreadableModelFiles`, diff's `unreadableModelFiles`); the `fix-model-yaml` fix and doctor step carry no such field. SKILL.md, the verify-and-fix, building-the-model and troubleshooting references and the SCHEMA_CONTENT quoting rule now say "a fix-model-yaml whose unreadableModelFiles entry has mergeConflict: true" (fixture schema skill regenerated). A test checks the CLI types and fails on any skill text that puts the flag on the fix. Troubleshooting: conflict labels are described by content, since ours and theirs swap during a rebase; after the assistant edits the file on the user's say-so it re-runs doctor itself. SKILL.md Stage 5 again says the file does not parse. Still 498 lines; no HARNESS_VERSION bump (29 is unreleased). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…agram; a refusal isn't addFromDbtFailed The #151 tests now drive Add models from dbt (refused before the picker opens, and refused by the queued re-read when git conflicts the diagram while the picker is open) and Generate Sync Plan on the physical stage (refused, no plan file written), each with one plain merge-conflict toast and nothing written. Both Add-from-dbt catch sites now skip EditRefused / RelationshipsRewriteRefused when counting addFromDbtFailed, through one isExplainedRefusal helper that the relationship handlers' recordRelationshipFailure also uses: a refusal the toast explains is not a breakage. This also stops the existing "model file has unsaved changes" refusal in those two handlers from counting as a failure. Telemetry only; nothing user-visible changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
syncMap replaced a managed key whose value was an alias (`dataType: *x`) with a plain copy of the resolved value on every save, even when nothing changed, dropping any comment on the line. That was already true on develop for `dataType: *x # c` (and aliased flags, scdType, meta, rationale); the data_type rename (#144) extended it to renamed `data_type: *x` keys, and to the comment foldEmptyDataType moves onto the alias. An alias that already reads as the desired value — compared as core's reader reads the key: str() for text, bool() for flags, Number() for scdType, core's own parser for meta / rationale / columns — is now left alone. One that changed is replaced by the new value, carrying the alias's comments (a block map or list takes them as its first lines). syncMeta replaces a changed alias the same way. A dropped empty dataType's comment stays on the renamed key's line only when it was written on the dropped key's own line; a comment line below it stays a line above the renamed key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
11e1e26 kept an alias (`description: *g`) that already read as the model wants. Keys are synced in MODEL_KEYS / COLUMN_KEYS order, not file order, so a key synced later could still rewrite that alias's anchor in place (`grain: &g foo` -> `bar`), and the kept alias then read the new value: a key the canvas never changed was retyped. Every alias a save judges to read as wanted — kept in syncMap or syncMeta, under a key left alone, the data_type kept for an anchored column, or inside a relationship entry that already matches — is now noted with a detached copy of what it read at that moment. After every write, one that no longer reads that (its anchor was rewritten, or dropped) becomes that plain value, keeping the line's comments; one whose anchor did not change stays an alias. So after a save every key the canvas did not change reads as before, every changed key reads its new value, and no comment is lost. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ases The yaml library keeps the text a scalar was parsed from in `source`, and the reader (core's scalarValue, and this service's) reads a number by that text. A scalar changed in place kept its old `source`, so after `scdType: &s 2` was edited to 1 the save still read the anchor as 2: an alias kept beside it (`scdType: *s` in another column, `description: *s`, a `meta` value) was not settled and followed the anchor to 1. Every in-place scalar write in the model sync — syncMap's update and its coerced-text pin, syncMeta, and the data_type key rename — now goes through setScalarValue, which clears `source` with the value. Nothing prints from `source`, so the file text is unchanged. A scalar whose new value is a map or list is replaced by a node rather than written into the scalar. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
doctor's fix-model-yaml step says "fix them before anything else", and a model file with git conflict markers now arrives there, yet it was listed after install-dbt, run-deps, run-parse and run-catalog. A model file that does not parse renders as an empty model whatever state dbt is in — the same reasoning that puts a conflicted diagram first — so the step now sits straight after resolve-merge-conflicts (and still after confirm-venv). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d-diagram # Conflicts: # CHANGELOG.md
fix: say "merge conflict" when a diagram file holds git conflict markers (#145)
fix: read dbt's data_type key in model files instead of dropping the type (#144)
…rlay fix(webview): Retry no longer flashes a "Canvas disconnected" overlay (#149)
test: load the Mermaid parsers once per file, outside any test's timeout (#152)
fix(skill): the setup guide asks the user about merge conflicts instead of resolving them (#150)
…diagram fix(canvas): an edit on a conflicted diagram says "merge conflict", not SyntaxError (#151)
…157) - An alias inside a relationships: entry (role: *r) no longer blocks every save: the entry is read with its aliases resolved against the file. - Every alias in the file, not only those under managed keys, is kept as it reads before the save; one whose anchor the save rewrites, removes or moves after it is written out as the value it read, keeping its comment, so an unmanaged key never follows an edit and the save never fails with "Unresolved alias". - A number prints as the text it was written with (007, 1e3, .5, +1), and a scalar the reader already reads as the wanted value is left as written, so description: true is not quoted and a reused number settles in one save. - A plain value that becomes a map or list keeps the line's comment. - Move Relationships to Model Library leaves (and names) a model file whose relationships: list holds a YAML anchor or alias, rather than re-render it and drop the anchor. - A seeded randomised test covers anchors and aliases across managed, unmanaged and relationship keys (1500 files, edits, idempotence). - CHANGELOG: one Added section, stray blank line removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e reader did not take as text (#157) Review findings on the #157 fix: - Keeping and settling every alias called Alias.resolve, which walks the whole document, once per alias: a save of a file with thousands of aliases took seconds to minutes. Both passes now resolve every alias in one walk. - A relationship entry's keys are read only as text, so `role: true` holds no role; the "already reads the same" check let a canvas role "true" leave the boolean in place and the edit was lost. Those keys must now match as text exactly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix: YAML anchors, aliases and numbers in model files survive a save (#157)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Releases everything on
developsince 1.6.7. The deploy workflow bumps the patch (1.6.7 → 1.6.8). The full notes are under## UnreleasedinCHANGELOG.md.Highlights
erd-studio export(feat: export a diagram as Mermaid or DBML #146).data_type:model files show their types (Model files that use dbt's data_type: lose their column types silently #144). Git merge conflicts are reported as merge conflicts on the canvas, in the CLI and in the setup guide (A git merge conflict in a diagram file shows as a broken file instead of a merge conflict #145, Setup skill: guide AI assistants through merge-conflict findings without picking a side #150, Canvas edits on a conflicted diagram show a raw SyntaxError instead of "merge conflict" #151). Retry no longer flashes "Canvas disconnected" (Retry on the canvas error screen flashes a misleading "Canvas disconnected" overlay #149).deploy.yml(feat: relationship health telemetry and self-checks for the #133 rework #142, ci: deploy the telemetry Worker from CI/CD, before the extension it serves #143).Also released
@erd-studio/coreand@erd-studio/renderer0.1.3 go out viapublish-packages.yml. Every API change is additive and optional.Verified on
develop(1c8fc2e)doctor0,diff --all1), mcp-server build and smoke test, VSIX 24 files,npm auditclean.Closes #133
Closes #138
Closes #144
Closes #145
Closes #149
Closes #150
Closes #151
Closes #152
Closes #157
🤖 Generated with Claude Code