Skip to content

Stop silently destroying properties; give properties/callouts a real section_id (#90) - #97

Merged
mobileskyfi merged 4 commits into
mainfrom
fix/90-property-section-identity
Jul 16, 2026
Merged

Stop silently destroying properties; give properties/callouts a real section_id (#90)#97
mobileskyfi merged 4 commits into
mainfrom
fix/90-property-section-identity

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Closes #90.

The issue's proposed fix doesn't work

#90 suggests making UNIQUE use section_id so it stops destroying rows. Measured against the whole corpus (365 cached pages, re-parsed with the real extractor), that still destroys 87 rows. Splitting the drops into exact duplicates (identical name/type/default/description — safe) vs genuinely distinct properties (real loss):

UNIQUE key dropped exact dupes distinct rows destroyed
Today — (page, name, section) text 165 24 141
#90's proposal — (page, name, section_id) 87 13 74
Hybrid — (page, name, section_id, section) 51 1 50
No UNIQUE at all 0 0

The reason is structural: the manual documents the same property name more than once within a single section, with different meanings. dot1x defines interface twice under Server — once for the server table, once for the client one — which matches #92's census finding that 12% of fragments hold more than one table. (page, name, section) encodes an assumption the corpus does not honor.

The strongest evidence arrived by accident: trying to reinstate the old index on real data, SQLite refuses to build it. The constraint has never been satisfiable by this corpus.

So the constraint is removed, not re-keyed. Extractors now assert parsed == stored (V-extractor-no-silent-drops) and fail the build on any gap.

Also worth noting: #90's headline "165" bundles 24 harmless exact duplicates. The real damage was 141 distinct properties.

Results

Grounded against the CI-built v0.11.0-rc.97 artifact, and a full rebuild from the 365-page cache.

  • All 4,575 parsed properties stored (shipped rc.97 holds 4,416 of 4,581). The extractor's own Properties: line counted attempted inserts, so it was reporting rows the DB did not contain.
  • Property→section attribution: 72% → 99.7% resolvable (was 72% unique / 16% ambiguous / 11% no-match). 0 dangling, 0 cross-page.
  • Callouts had no section attribution at all; now 89% resolve.
  • ppp-aaa's name is back to its three real meanings ("PPP profile name", "Name used for authentication", "User name supplied at the authentication stage") instead of only the first.

Remaining NULLs are genuine, not failures: 12 properties on loop-protect/services sit under a page H1 that parseSections deliberately drops as a title-duplicate; 104 callouts are page-level :::warning blocks above the first heading. Forcing an attribution would be worse than admitting there is none.

The h4–h6 decision (#90 asks for this to be stated)

A property under an h4–h6 folds to its enclosing h1–h3 ancestor. sections stays the retrieval unit and routeros_get_page is unchanged.

Grounded rather than assumed:

  • 87% of properties already sit directly under an h1–h3; only 588 (12.9%) fold at all.
  • Only 19 of 467 property-bearing sections (4%) span more than one heading after folding.
  • Minting h4–h6 rows would add 815 sections (+28%) of which only 74 bear properties — 91% pure overhead for this problem.
  • Minting also forces a containment decision the flat sections table can't model: an h2's text today includes its h4 children's prose, so you must either duplicate it or silently shrink what get_page returns for an existing section. That's a retrieval-unit redesign, not a data-loss fix.

With UNIQUE gone the fold destroys nothing — it costs only join precision, and section still names the h4 verbatim, so the decision stays reversible from the same parse.

Second commit: section_anchor

Storing every property surfaces what the constraint had been hiding by deletion: 111 name/section groups (276 rows) are indistinguishable by section, since it is heading text that repeats. lookup_property(name="name") on PPP AAA would return three rows all reading "Properties".

That ambiguity is created by the first commit, so this finishes it rather than deferring — otherwise dropping the constraint leaves lookup_property measurably worse (three confusing rows instead of one wrong-but-clean one). lookupProperty and related.properties now return section_anchor, unique within a page and feedable straight back into routeros_get_page(page, section=…). Round-trip verified against the real corpus.

Verification

  • The guard was proven, not assumed. Reintroducing the old code path (INSERT OR IGNORE + the constraint) reproduces the loss exactlyparsed 4575, stored 4410 — 165 row(s) lost — and exits 1. Clean builds exit 0.
  • v8 → v9 migration tested on the real shipped artifact: 4,416 rows preserved, UNIQUE gone, section_id present on both tables, FTS still queryable (rowids preserved across the rebuild, so the external-content index stays valid), integrity_check ok, foreign_key_check clean.
  • Anchor tests use ppp-aaa as Properties: 165 rows silently destroyed on insert; section identity stored as heading text (properties + callouts) #90 asks (new fixture), plus dot1x for callout attribution — it demonstrates both the resolved case and the legitimate-NULL case.
  • Full suite green: 862 pass / 0 fail. Biome, markdownlint, cspell clean.

Scope notes

  • Schema v9. Confirmed with the maintainer; 0.11 is deliberately open for these latent bugs.
  • Legacy extract-properties.ts deliberately untouched — Confluence-era, not in release.yml.
  • MANUAL.md schema block, VALIDATION.md (V-extractor-no-silent-drops), and CHANGELOG.md updated.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed an issue where properties and callouts could be silently omitted during content extraction.
    • Correctly associates properties and callouts with their enclosing documentation sections.
    • Supports multiple properties with the same name while preserving their distinct meanings.
  • Improvements

    • Property lookup and search results now include a section anchor for precise context.
    • Extraction now detects data mismatches and fails builds instead of silently losing content.
  • Documentation

    • Updated schema and validation documentation to describe section attribution and extraction safeguards.

mobileskyfi and others added 2 commits July 15, 2026 18:30
…real section_id (#90)

`properties.section` stored heading text, and UNIQUE(page_id, name, section) plus
INSERT OR IGNORE dropped any property whose page repeated a heading. The shipped
v0.11.0-rc.97 DB holds 4,416 of 4,581 parsed properties and nothing logged the gap —
the extractor's own "Properties:" line counted attempted inserts, so it reported rows
the DB did not contain.

Measuring every candidate key against the whole corpus showed the obvious fix is not
enough. Re-keying on section_id still destroys 87 rows, because the manual documents
one property name several times within a single section (dot1x defines `interface`
twice under "Server" — once for the server table, once for the client one). Section is
not an identity here at all; SQLite in fact refuses to build the index on real data.
Only removing the constraint reaches zero loss:

  today  (page,name,section-text)     165 dropped, 141 distinct
  issue  (page,name,section_id)        87 dropped,  74 distinct
  hybrid (page,name,section_id,text)   51 dropped,  50 distinct
  none                                   0

So the constraint is removed rather than re-keyed, and extractors now assert
parsed == stored (V-extractor-no-silent-drops) so a future drop fails the build
instead of shipping. Verified by reintroducing the old code path: it reproduces the
165-row loss exactly and now exits 1.

Attribution: properties/callouts gain section_id, correlated by line range in the pass
all three parsers already share. A property under an h4–h6 folds to its enclosing
h1–h3 section, so `sections` stays the retrieval unit and get_page is unchanged;
`section` keeps the raw h4 text, so the finer heading stays recoverable and the
decision reversible. Property attribution goes from 72% resolvable to 99.7%; callouts
had none at all and now resolve 89%. Remaining NULLs are genuinely above any heading
(page-level warnings, properties under a title-duplicate H1) and stay NULL rather than
being forced into a section.

Schema v9. Migration rebuilds properties in place, preserving rowids so the
external-content FTS index stays valid; verified against the real rc.97 artifact
(4,416 rows preserved, integrity_check ok, foreign_key_check clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rt (#90)

Storing every property surfaces a fact the old UNIQUE constraint hid by deleting it:
111 name/section groups (276 rows) are indistinguishable by `section`, because it holds
heading TEXT and repeats. lookup_property(name="name") on PPP AAA now returns three
rows all reading "Properties" — the profile name, the login name, and the active-user
name — with nothing to tell them apart.

That ambiguity is created by the previous commit, so this finishes it rather than
deferring: without the anchor, dropping the constraint would leave lookup_property
measurably worse (three confusing rows instead of one wrong-but-clean one).

lookupProperty and searchAll's related.properties now carry section_anchor (the
section's anchor_id: properties, properties-1, properties-2), unique within a page and
feedable straight back into routeros_get_page(page, section=...) to read that exact
fragment. Verified round-trip against the real corpus: each of ppp-aaa's three `name`
rows resolves to its own section. Null when a property sits above any heading — the key
is always present so consumers can branch on it.

routeros_lookup_property's description now teaches the workflow, per the
mcp-tool-descriptions rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 01:41
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3fa416e9-97ed-4d11-a6e7-21026333ccea

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Schema v9 adds section attribution for properties and callouts. The extractor resolves rows by source line, persists section IDs, and fails on count mismatches. Lookup and search results now expose section anchors for repeated property names.

Changes

Section-aware extraction

Layer / File(s) Summary
Schema v9 section attribution
src/db.ts, src/paths.ts, MANUAL.md
Adds nullable section foreign keys for properties and callouts, removes the prior property uniqueness constraint, adds indexes, and documents the v9 schema.
Line-based section attribution
src/extract-docusaurus.ts, src/extract-docusaurus.test.ts
Tracks source coordinates, maps properties and callouts to enclosing h1–h3 sections, and tests repeated headings, nested headings, and unsectioned content.
Attributed persistence and drop detection
src/extract-docusaurus.ts, VALIDATION.md, CHANGELOG.md
Persists section IDs with plain inserts, validates parsed-versus-stored counts, and records the silent-drop fix.
Section-aware lookup results
src/query.ts, src/query.test.ts, src/mcp.ts, CHANGELOG.md
Returns section_anchor from property lookup and search results and documents using it to retrieve the matching page section.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Docusaurus
  participant Extractor
  participant SQLite
  participant LookupTool
  Docusaurus->>Extractor: provide Markdown pages
  Extractor->>SQLite: store sections, properties, and callouts with section_id
  Extractor->>SQLite: compare parsed and stored row counts
  LookupTool->>SQLite: query property and section anchor
  SQLite-->>LookupTool: return section_anchor
Loading

Possibly related PRs

  • tikoci/rosetta#13: Introduced the Docusaurus extraction pipeline extended by this section-attribution change.

Suggested labels: area:docusaurus

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title matches the main change: removing silent property loss and adding section_id attribution for properties/callouts.
Linked Issues check ✅ Passed The PR removes lossy inserts, adds nullable section_id attribution, validates parsed-vs-stored counts, and covers repeated headings.
Out of Scope Changes check ✅ Passed All changes support schema v9, attribution, validation, or lookup disambiguation; no unrelated edits stand out.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/90-property-section-identity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a long-standing data integrity bug in the Docusaurus extractor + SQLite schema where properties rows were silently dropped due to UNIQUE(page_id, name, section) combined with INSERT OR IGNORE. It removes the unsatisfiable uniqueness constraint, adds real section attribution (section_id) for both properties and callouts, and introduces a hard guard to fail extraction when parsed rows don’t match what was actually stored.

Changes:

  • Schema v9: drop the properties uniqueness constraint, add properties.section_id + callouts.section_id, and migrate existing v8 DBs while preserving FTS rowids.
  • Extraction: attribute properties/callouts to h1–h3 sections via line-range mapping (folding h4–h6 to the nearest h1–h3 ancestor), and assert parsed == stored (V-extractor-no-silent-drops).
  • Query/tool output: return section_anchor to disambiguate same-named properties within a page and enable direct round-trip into routeros_get_page(..., section=...).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
VALIDATION.md Adds V-extractor-no-silent-drops to make “parsed vs stored” divergence a build-breaking invariant.
src/query.ts Adds section_anchor to property lookup rows and to related.properties so same-named properties are distinguishable.
src/query.test.ts Extends fixtures + tests to validate same-page disambiguation via section_anchor and round-trip into getPage(section=...).
src/paths.ts Bumps SCHEMA_VERSION to 9 and documents the v9 migration intent.
src/mcp.ts Updates routeros_lookup_property description/workflow to incorporate section_anchor as the disambiguator.
src/extract-docusaurus.ts Implements section attribution via line coordinates, adds section_id wiring for inserts, and enforces parsed==stored with an exit-1 guard.
src/extract-docusaurus.test.ts Adds fixtures/tests for repeated headings (ppp-aaa), h4–h6 folding behavior, and callout attribution.
src/db.ts Reorders schema creation to support new FKs; migrates v8→v9 for properties (rebuild) and callouts (add column) while preserving FTS invariants.
MANUAL.md Updates schema documentation to describe section_id and the deliberate h4–h6 folding behavior.
fixtures/docusaurus/ppp-aaa.md Adds a realistic repeated-heading fixture used to pin the previously-lost-property behavior.
CHANGELOG.md Documents the user-visible behavior fix (no more silent drops; new section_anchor surface).

Comment thread src/extract-docusaurus.test.ts
…oisoning the db.ts singleton

Two defects surfaced by CI on this branch, neither caused by the #90 change itself.

1. A stray NUL byte landed in a template literal I added (`${p.name}<NUL>${p.section}`,
   intended to be a space). Syntactically valid, so tests/typecheck/biome all passed —
   but it made the whole file read as binary: `file` reported "data" and grep silently
   matched nothing in it. Replaced with the intended space. Scanned the other 270
   source/doc files; this was the only one.

2. `import { EXCERPT_MARK_* } from "./query.ts"` was static. query.ts transitively loads
   db.ts, and an ESM static import is hoisted ABOVE this file's own
   `process.env.DB_PATH = ":memory:"` — so it opened the real on-disk ros-help.db and
   poisoned the db.ts singleton for the rest of the run. query.test.ts's V-db-wipe-guard
   caught it ("DB singleton is at .../ros-help.db"), which is exactly its job: without it,
   beforeAll()'s DELETEs would run against a real database.

   Pre-existing and order-dependent — it only bites when this file is the first to touch
   db.ts, and bun's test-file order is not stable (CI started with schema-roundtrip, local
   with extract-videos), so it hid until this branch drew the unlucky order. A re-run went
   green, which is precisely why it needed fixing rather than re-rolling.

   Verified the hoist directly: `process.env.DB_PATH=":memory:"` followed by a static
   db.ts import resolves to the repo-root path, not ":memory:". Not reproducible locally
   under bun 1.3.13 (CI runs 1.3.14), so the fix is grounded in the proven hoist and the
   repo's own rule rather than a local repro of the cross-file poisoning —
   .github/instructions/extractor-import-side-effects.instructions.md, hazard 2, mandates
   the dynamic form for this exact reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/extract-docusaurus.test.ts`:
- Around line 344-354: Add a storage regression test alongside the existing
collision test that uses the parsed page fixture to persist both dot1x interface
properties under the server section, then query the stored properties and assert
both rows remain. Ensure the fixture uses the same name and sectionAnchor with
distinct section IDs so a UNIQUE(page_id, name, section_id) constraint violation
is detected.

In `@src/extract-docusaurus.ts`:
- Around line 759-765: Update the rebuild flow around the section, callouts,
properties deletion block and insertAll so all deletes, inserts, FTS rebuilds,
and parsed/stored count validation execute within one transaction. Move
validation inside that transaction and throw when counts mismatch, allowing any
insert error or validation failure to roll back the destructive changes instead
of committing partial or invalid data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1f195084-c875-47dc-a26a-e27d9055a66a

📥 Commits

Reviewing files that changed from the base of the PR and between 2a5c5a8 and 2943dbd.

⛔ Files ignored due to path filters (1)
  • fixtures/docusaurus/ppp-aaa.md is excluded by !fixtures/**
📒 Files selected for processing (10)
  • CHANGELOG.md
  • MANUAL.md
  • VALIDATION.md
  • src/db.ts
  • src/extract-docusaurus.test.ts
  • src/extract-docusaurus.ts
  • src/mcp.ts
  • src/paths.ts
  • src/query.test.ts
  • src/query.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Bun and TypeScript, including Bun-native runtime APIs such as bun:sqlite, Bun.serve, and bunx.

Rosetta uses Bun and TypeScript; prefer bun and bun test rather than Node/npm-oriented substitutes.

Files:

  • src/paths.ts
  • src/mcp.ts
  • src/query.test.ts
  • src/db.ts
  • src/extract-docusaurus.test.ts
  • src/extract-docusaurus.ts
  • src/query.ts
*

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep this Copilot instructions file short; put substantive rules in narrow instruction files under .github/instructions/*.instructions.md.

Files:

  • VALIDATION.md
  • CHANGELOG.md
  • MANUAL.md
VALIDATION.md

📄 CodeRabbit inference engine (CLAUDE.md)

Put load-bearing invariants and their CI proof in VALIDATION.md.

Files:

  • VALIDATION.md
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer each document's canonical home and do not duplicate operational detail, schema blocks, or long rule lists in the routing index.

Files:

  • VALIDATION.md
  • CHANGELOG.md
  • MANUAL.md
src/{query,mcp,browse}.ts

📄 CodeRabbit inference engine (AGENTS.md)

Route MCP, query, classifier, TUI, and canonicalizer behavior through shared core code, usually src/query.ts; keep src/mcp.ts and src/browse.ts thin adapters.

Files:

  • src/mcp.ts
  • src/query.ts
CHANGELOG.md

📄 CodeRabbit inference engine (CLAUDE.md)

Record user-visible shipped changes in CHANGELOG.md under [Unreleased] or release sections.

Files:

  • CHANGELOG.md
{README.md,src/setup.ts,MANUAL.md}

📄 CodeRabbit inference engine (AGENTS.md)

Keep public client setup snippets aligned across README.md, src/setup.ts, and MANUAL.md, including the Codex command codex mcp add rosetta -- bunx @tikoci/rosetta``.

Files:

  • MANUAL.md
MANUAL.md

📄 CodeRabbit inference engine (CLAUDE.md)

Put installation, operations, release/re-extraction procedures, and schema reference in MANUAL.md.

Files:

  • MANUAL.md
🔇 Additional comments (16)
src/paths.ts (1)

105-114: LGTM!

src/mcp.ts (1)

727-738: LGTM!

src/query.test.ts (2)

136-160: LGTM!


901-928: LGTM!

src/db.ts (4)

119-157: LGTM!


167-211: LGTM!


241-252: LGTM!


277-277: LGTM!

src/query.ts (3)

372-379: LGTM!


1060-1069: LGTM!


1093-1116: LGTM!

VALIDATION.md (1)

50-50: LGTM!

CHANGELOG.md (1)

57-58: LGTM!

MANUAL.md (1)

371-372: LGTM!

Also applies to: 391-404

src/extract-docusaurus.test.ts (1)

8-17: LGTM!

Also applies to: 33-33, 46-53, 271-343, 357-402

src/extract-docusaurus.ts (1)

78-112: LGTM!

Also applies to: 295-320, 333-355, 456-487, 504-516

Comment thread src/extract-docusaurus.test.ts
Comment thread src/extract-docusaurus.ts
… the constraint (#90)

CodeRabbit review: the existing tests only covered repeated heading TEXT across distinct
anchors (ppp-aaa), which a UNIQUE(page_id, name, section_id) regression would survive.
That is a real gap — and it is the gap over the PR's central claim, since re-keying on
section_id is exactly the fix #90 proposed and the measurement rejected.

Adds the harder case at both layers:

- parse (extract-docusaurus.test.ts): dot1x's "Server" section documents `interface` more
  than once, with different descriptions — a server table and a client table under one
  heading. Confirmed against the fixture: 3 `interface` rows share the `server` anchor.
- storage (query.test.ts): two rows sharing name AND section_id both persist. If anyone
  re-adds the constraint, the fixture INSERT throws and beforeAll() fails loudly.

Proven, not assumed: temporarily re-adding UNIQUE(page_id, name, section_id) to db.ts makes
the suite fail with "SQLiteError: UNIQUE constraint failed: properties.page_id,
properties.name, properties.section_id". It passes 311/311 without.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Properties: 165 rows silently destroyed on insert; section identity stored as heading text (properties + callouts)

2 participants