From f30d59b989d93a9ce09da34e804577171902b613 Mon Sep 17 00:00:00 2001 From: Fernando Possebon Date: Thu, 6 Aug 2026 16:35:19 -0300 Subject: [PATCH] feat(docs): database documentation viewer and DBML export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(docs): add SchemaSnapshot model * feat(docs): infer relationship cardinality from foreign keys * fix(docs): resolve foreign keys within the source schema * feat(docs): convert group hue to sRGB hex for DBML * feat(docs): add DBML lexical primitives * feat(docs): render DBML table blocks * feat(docs): render DBML refs, enums and table groups * feat(docs): assemble complete DBML documents * feat(docs): collect schema snapshots with bounded fan-out * fix(docs): gate FK warning on engine capability and reference synthesized enums * fix(docs): qualify synthesized enum references in multi-schema output * test(docs): anchor the multi-schema enum reference assertion * feat(docs): add snapshot collection route * feat(docs): add collect_docs_snapshot to DbxBackend * feat(docs): add dbx dbml command * test(docs): add live snapshot and DBML verification Runs collect_snapshot + to_dbml against a real PostgreSQL database (organon, 47 tables) and asserts structural DBML validity: Project header, every table present, balanced braces, trailing newline. * refactor(docs): use sort_by_key for snapshot table ordering * docs(docs): add database documentation design and implementation plan Records the Part 1 design (SchemaSnapshot + DBML export) and the plan that produced it. The plan carries an appendix listing the thirteen assumptions that proved wrong during execution, so the corrected facts are not re-derived from the surrounding prose. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TKcC15gEuQBHMidPCFFYwn * fix(docs): use native enum names, canonical engine labels, and surface metadata failures Final review fix wave covering four findings: - PostgreSQL named enums (e.g. `ConversationStatus`) now keep their own type name and synthesized: false, instead of being renamed to `{table}_{column}` and losing identity. A type shared by several columns now dedupes to a single Enum block instead of colliding or duplicating. `synthesize_enum` and `render_type` route through one shared `enum_type_name` helper so the two can't drift apart again. - `database_type` (and the CommentsUnsupported/NoForeignKeyMetadata warnings) now use the same canonical engine label already used throughout table_structure_sql's own warning prose, instead of a raw Rust Debug string (`Postgres`, `SqlServer`, `MongoDb`). - An index-fetch failure during collection now surfaces as a TableSkipped warning instead of silently degrading to an empty index list, which relations.rs uses to infer relationship cardinality. - A schema-enumeration failure now surfaces as a warning instead of silently proceeding against schema "". - Removed the redundant Arc; buffer_unordered already caps concurrency at MAX_CONCURRENT_TABLES. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TKcC15gEuQBHMidPCFFYwn * feat(docs): add annotation file model * test(docs): assert full round-trip fidelity for the annotation model * feat(docs): add per-engine annotation key folding * fix(docs): do not fold identifiers on case-sensitive engines * feat(docs): load and validate the notes file * fix(docs): report a version mismatch before unknown-field errors * feat(docs): merge annotations into the schema snapshot * feat(docs): detect orphaned annotations without deleting them * feat(docs): add --notes to dbx dbml * feat(docs): remember a notes file path per connection Adds docs_notes_path to ConnectionConfig so the desktop app (Part 3) can persist where a connection's documentation notes file lives. The CLI is unaffected — it takes an explicit --notes path. ConnectionConfig has a hand-written Deserialize impl that delegates to a mirror struct, ConnectionConfigData, and converts via From. Adding the field only to ConnectionConfig would compile but never populate from stored JSON, since ConnectionConfigData's fields are what serde actually reads. The field is threaded through all three places: ConnectionConfig, ConnectionConfigData, and the From impl, mirroring the existing `color` field. * test(docs): verify annotations against a live database * test(docs): assert exactly one orphaned annotation * feat(docs): add snapshot types for the docs viewer * fix(docs): correct snapshot type nullability and add missing column fields * feat(docs): add a real-output fixture and drift conformance test The fixture is generated by dump_docs_fixture.rs from a live collect_snapshot run against the keycloak database in the shared local-infra stack, with annotations applied so a LOCAL note, column note, group and orphanedNotes warning are all present. fixtureConformance.spec.ts asserts against that real JSON rather than a hand-written literal, so a change to the Rust snapshot shape breaks the test instead of silently drifting from the hand-maintained types.ts. Keycloak is used because its schema is public open-source knowledge, so the committed fixture carries no private schema. The kept tables are an explicit allowlist rather than an alphabetical slice, because the conformance test needs a connected foreign-key subgraph: protocol_mapper has two foreign keys to different tables and composite_role has two to the same one. * feat(docs): add index grouping for the docs viewer * feat(docs): describe snapshot warnings for the viewer * feat(docs): add client-side search for the docs viewer * test(docs): make the search fixture able to fail * feat(docs): expose group hue as a CSS custom property * docs(plan): add annotations and viewer plans, correct Task 7 Parts 2 and 3a were planned after the first plan was committed and were never tracked. docs/superpowers/ is gitignored, so both needed -f, matching how the existing specs and plans in that directory were added. The viewer plan's Task 7 is corrected against the installed marked@18.0.4. Its original text carried four defects, found by probing the library rather than by review: an assertion that fails against a correct implementation, a pre-escaping approach that double-escapes entities, a javascript: blocklist with live bypasses (entity-encoded, vbscript:, data:text/html, and , which was never covered at all), and a link renderer whose text property is raw markdown source rather than parsed HTML — an XSS hole found while verifying the fix for the previous defect. The viewer plan also gains a corrections appendix grouping defects by failure mode. The annotations plan is committed as written; its defects are recorded in a follow-up. * feat(docs): render note markdown with raw HTML escaped * fix(docs): drop protocol-relative URLs in note markdown The URL allowlist permitted anything starting with / so relative paths work, and //evil.com qualifies. Over https that grants nothing a note author could not do with an ordinary https link, but the Part 3b standalone export is opened via file://, where //host/path is a UNC path. On Windows that opens an SMB connection and leaks an NTLM hash, with no click required since images auto-load, and it is plantable from a COMMENT ON value. Found by an adversarial probe of the committed module rather than by review; the backslash form was already dropped, only the slash form slipped through. * docs(plan): add corrections appendix to the annotations plan Groups the defects found executing Part 2 by failure mode, matching the form of the viewer plan's appendix. The notable one is Mode A: docs_notes_path had to be added in three places because ConnectionConfig has a serde mirror struct, and adding it in one place compiles, passes a round-trip test written against the same struct, and then reads None forever after every load. Also records a controller hypothesis that turned out to be wrong, since checking it cost one read. * fix(docs): reject backslash protocol-relative URLs in notes The // guard from the previous commit was itself a blocklist: /\evil.com starts with a single slash, so it passed, and the WHATWG URL spec treats /\ identically to // for special schemes. Browsers normalise the backslash, so it reaches the same file:// UNC path and the same no-click NTLM leak. Rejecting both separator characters in both positions closes it. Found by probing what String(raw).trim() leaves unnormalised before the prefix checks; the third defect on this file found by probing rather than by reading. * feat(docs): add docs viewer components Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TKcC15gEuQBHMidPCFFYwn * test(docs): pin each theme's legacy colour base independently The ordering check indexOf(hsl) < indexOf(@supports) quantified over any occurrence, so deleting the light .docs-group block left the dark block's hsl satisfying it. The test passed while every table group rendered colourless on light-theme WebViews without oklch. Asserting each selector's own base block catches deleting either one. Found by the task implementer, which deleted one block and then both to show the guard pinned 'some base exists' rather than 'each selector has a base'. * fix(docs): escape single quotes in note attribute values escapeHtml covered & < > and double quotes but not single quotes. Not exploitable today because every attribute in this file is double-quoted, but that is a formatting convention enforced nowhere and living in a different part of the file from the escaper. A future edit writing title='...' would turn a formatting choice into an attribute breakout. Raised by review as latent fragility rather than a defect; fixed because the escaper should be correct on its own rather than correct-given-an-invariant. * test(docs): match single-quoted v-html bindings in the contract guard The guard matched /v-html\s*=\s*"([^"]*)"/ — double quotes only. A binding written `v-html='table.note'` produced zero matches and passed, handing a database COMMENT ON value straight to the DOM with the renderNote sanitiser bypassed. Both quote styles are valid Vue and nothing in the repo enforces one, so the guard had a hole exactly where it mattered most. Verified by temporarily rewriting a real WikiIndex binding as `v-html='table.note'`: the test now fails with "WikiIndex.vue: v-html must render renderNote output: expected 'table.note' to contain 'renderNote'". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TKcC15gEuQBHMidPCFFYwn * fix(docs): cap search results per kind instead of overall DocsSearch sliced searchDocs output to 40 AFTER concatenation, and the concatenation order is tables -> columns -> groups -> enums. Columns always flooded the list, so the cap deleted the tail — every group and enum hit. Against the real fixture, "e" produced 155 hits (9 table, 133 column, 1 group, 12 enum) and rendered 9 tables, 31 columns and nothing else; groups and enums were structurally unreachable through search. Cap each kind against its own limit inside docsSearch.ts, where the logic is tested, and drop the slice from the template so exactly one place limits results. Ranking is unchanged: tables still precede columns. Verified by reverting to a single .slice(0, 40) over the concatenation: the new tests fail with "enums must survive a column flood: expected false to be true" and "expected 30 to be 20". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TKcC15gEuQBHMidPCFFYwn * fix(docs): move the index card note out of its button The card rendered renderNote output with v-html inside the