Skip to content

core: strict-mode entity filter reclassifies out-of-vocabulary entities instead of dropping them - #313

Merged
verveguy merged 8 commits into
mainfrom
fabrik/issue-312
Aug 2, 2026
Merged

core: strict-mode entity filter reclassifies out-of-vocabulary entities instead of dropping them#313
verveguy merged 8 commits into
mainfrom
fabrik/issue-312

Conversation

@verveguy

@verveguy verveguy commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes #312

Summary

Completes the reclassify-not-drop symmetry ADR-0310 established for edges: under strict ontology mode, an entity extracted with a type outside the declared vocabulary is now retained with a new Unclassified label instead of being dropped, with its original type preserved and recoverable from attributes.original_entity_type. This also removes the cascade where the strict-mode entity filter's "no entities remain" branch unconditionally cleared all edges for a chunk — since entities are never removed for their type anymore, that branch is now dead and has been deleted.

Key changes

  • ExtractedEntity gets a new original_entity_type: Option<String> field, unconditionally cleared before any mode-specific filtering (same untrusted-JSON discipline already applied to the edge-side original_relation_type).
  • The strict-mode entity filter in episode.rs reclassifies instead of dropping: an empty/absent type (or literal "Entity") resolves as a plain untyped Entity (unchanged, no reclassification); an in-vocabulary type is rewritten to canonical form as before; anything else gets entity_type = "Unclassified" with the original preserved.
  • AddEpisodeResult gains entities_reclassified_unclassified, tallied directly in the Phase A filter loop (no Phase C deferral needed, unlike the edge-side tally — an entity, once reclassified, is never subsequently dropped). Surfaced in knowledge_process_chunk's IPC response.
  • make_insert_row's attributes field now emits {"original_entity_type": "<orig>"} when applicable, instead of always "{}".
  • Dedup is unaffected — Phase B resolution is keyed by name, not type, so a reclassified entity dedups identically to any other entity, including merging into a pre-existing entity stored under its correct declared type.
  • New ADR-0312 documents the decision, citing ADR-0310 as precedent (not authority) and independently arguing the edge-endpoint-cascade and schema-closure reasons this holds for entities.
  • README's ontology docs (format example + Modes table) updated to describe the new behavior.

Test plan

  • cargo fmt --all clean
  • cargo test — 913 passed, 8 ignored (full workspace suite)
  • cargo clippy --all-targets -- -D warnings clean
  • New/updated tests in crates/core/tests/ontology_integration.rs: reclassify-not-drop disposition (labels + attributes), per-run tally over N out-of-vocabulary entities, edge survival with a reclassified endpoint, empty/"Entity"-type passthrough, and dedup against a pre-existing declared-type entity
  • crates/core/tests/ipc_parity.rs asserts entities_reclassified_unclassified is numeric in the knowledge_process_chunk response

@verveguy

verveguy commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

🏭 Fabrik — stage: Implement
branch: fabrik/issue-312 | commit: ebbcc76 | main: 552be21 | 2026-08-02 14:17 UTC

Task checklist updated. Now let's emit the PR creation marker.

Implemented reclassify-not-drop for strict-mode entities (mirroring ADR-0310's edge-side fix): out-of-vocabulary entity types now get the Unclassified label with the original type preserved in attributes, and the edge-clearing cascade on empty-entity chunks was removed. Added entities_reclassified_unclassified tally, 6 new/updated tests, ADR-0312, and README updates. Full local gate (fmt/test/clippy) passes; branch pushed and PR marker emitted.


Used 100/100 turns, 0k input / 39k output tokens.

@verveguy
verveguy marked this pull request as ready for review August 2, 2026 14:17
Copilot AI review requested due to automatic review settings August 2, 2026 14:17

@handarbeit-pruefer handarbeit-pruefer 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.

Confirmed — this all lines up correctly. I'm done reviewing; I have no findings to report.

I reviewed the entity-side reclassify-not-drop implementation (crates/core/src/episode.rs, types.rs, handlers.rs), the new tests in ontology_integration.rs and ipc_parity.rs, README updates, and ADR-0312. The change mirrors the edge-side ADR-0310 pattern closely: original_entity_type is unconditionally cleared right after extraction (guarding against untrusted-JSON injection) before being legitimately re-set only in the strict-mode reclassify branch; the empty/"Entity" passthrough, in-vocabulary canonicalization, and out-of-vocabulary → Unclassified reclassification are all correctly ordered; the dead edges.clear() cascade is fully removed; the tally is counted once in Phase A with no double-counting risk (verified against the dedup-merge test, which intentionally still counts a merged-but-reclassified entity); and attributes.original_entity_type is only ever populated on the reclassified path. Cross-checked the ADR's claim that reclassified entities surface automatically via knowledge_reprocess_entity_types's OffOntology scope — confirmed in corrections.rs, which flags any label absent from the ontology's type-name set. Docs and tests are consistent with the implementation throughout. I found no bugs, correctness issues, or design problems worth flagging.

Copilot AI 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.

Pull request overview

This PR updates strict ontology mode to reclassify out-of-vocabulary entities to Unclassified instead of dropping them, preserving the original extracted type in attributes.original_entity_type and exposing a new per-run tally (entities_reclassified_unclassified) via IPC. It also adds ADR-0312 and updates docs/tests to reflect and lock in the new behavior.

Changes:

  • Rework strict-mode entity-type filtering in add_episode to reclassify rather than drop, and persist the original type in entity attributes.
  • Add ExtractedEntity.original_entity_type plus AddEpisodeResult.entities_reclassified_unclassified, and surface the tally in knowledge_process_chunk.
  • Add ADR-0312 + README updates, and expand integration tests to cover disposition, tallying, endpoint survival, passthrough, and dedup.

Reviewed changes

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

Show a summary per file
File Description
specs/312-strict-mode-still-deletes/spec.md Spec Kit feature spec capturing the reclassify-not-drop decision and acceptance scenarios.
README.md Updates strict-mode ontology documentation to describe entity reclassification + preservation.
docs/adr/index.md Adds ADR-0312 entry to the ADR index.
docs/adr/0312-entity-strict-mode-reclassifies-not-drops.md Records the strict-mode entity reclassification decision and rationale.
crates/core/src/types.rs Adds ExtractedEntity.original_entity_type for preserving rejected strict-mode types.
crates/core/src/episode.rs Implements entity reclassification + attribute emission + new tally in AddEpisodeResult.
crates/core/src/handlers.rs Surfaces entities_reclassified_unclassified in knowledge_process_chunk response JSON.
crates/core/src/extractor.rs Updates mock extractor entity literals to include the new field.
crates/core/src/llm_router.rs Updates tests to include the new field in extracted entities.
crates/core/tests/ontology_integration.rs Replaces drop-expectation tests with reclassify/tally/endpoint/dedup/passthrough assertions.
crates/core/tests/ipc_parity.rs Asserts entities_reclassified_unclassified is numeric in IPC response.
crates/core/tests/extraction_quality.rs Updates test extractor outputs for new entity field.
crates/core/tests/edge_endpoint_resolution.rs Updates helper extraction results for new entity field.
crates/core/tests/dedup_auto_heal_integration.rs Updates test extractor outputs for new entity field.
crates/core/tests/cross_episode_dedup.rs Updates test extraction results for new entity field.
crates/core/tests/concurrent_rw_integration.rs Updates entity literals for new field in concurrency test.
crates/eval/tests/harness_integration.rs Updates eval harness fixtures to include new entity field.
crates/eval/src/scoring.rs Updates scoring tests’ entity construction for new field.
crates/eval/src/runner.rs Updates runner tests’ entity construction for new field.
crates/eval/src/pairwise.rs Updates pairwise tests’ entity construction for new field.
crates/eval/src/metrics.rs Updates metrics tests’ entity construction for new field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/core/src/episode.rs
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@verveguy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: df9dc2d9-8855-4284-a616-dfecaac3adca

📥 Commits

Reviewing files that changed from the base of the PR and between 8e76e22 and c1dba4e.

📒 Files selected for processing (22)
  • crates/core/src/episode.rs
  • crates/core/src/extractor.rs
  • crates/core/src/handlers.rs
  • crates/core/src/llm_router.rs
  • crates/core/src/types.rs
  • crates/core/tests/concurrent_rw_integration.rs
  • crates/core/tests/cross_episode_dedup.rs
  • crates/core/tests/dedup_auto_heal_integration.rs
  • crates/core/tests/edge_endpoint_resolution.rs
  • crates/core/tests/extraction_quality.rs
  • crates/core/tests/ipc_parity.rs
  • crates/core/tests/ontology_integration.rs
  • crates/eval/src/metrics.rs
  • crates/eval/src/pairwise.rs
  • crates/eval/src/runner.rs
  • crates/eval/src/scoring.rs
  • crates/eval/tests/harness_integration.rs
  • docs/adr/0312-entity-strict-mode-reclassifies-not-drops.md
  • docs/adr/index.md
  • docs/llms-full.txt
  • docs/ontology.md
  • specs/312-strict-mode-still-deletes/spec.md
📝 Walkthrough

Walkthrough

Strict ontology mode retains unsupported non-empty entity types by relabeling them Unclassified. The original type is stored in attributes.original_entity_type. Entity edges remain available after reclassification. AddEpisodeResult and knowledge_process_chunk expose the per-run reclassification count. Tests cover retention, edge resolution, empty types, normalization, and deduplication. README documentation, ADR-0312, and the issue specification describe the behavior.

Assessment against linked issues

Objective Addressed Explanation
Retain unsupported entities as Unclassified and preserve their original type [#312]
Preserve edges and support deduplication [#312]
Expose per-run reclassification counts [#312]
Preserve existing mode behavior and document the design decision [#312]

Possibly related PRs


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

@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: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c25a08b-827d-46e8-b12e-69a0358465aa

📥 Commits

Reviewing files that changed from the base of the PR and between 552be21 and ebbcc76.

📒 Files selected for processing (21)
  • README.md
  • crates/core/src/episode.rs
  • crates/core/src/extractor.rs
  • crates/core/src/handlers.rs
  • crates/core/src/llm_router.rs
  • crates/core/src/types.rs
  • crates/core/tests/concurrent_rw_integration.rs
  • crates/core/tests/cross_episode_dedup.rs
  • crates/core/tests/dedup_auto_heal_integration.rs
  • crates/core/tests/edge_endpoint_resolution.rs
  • crates/core/tests/extraction_quality.rs
  • crates/core/tests/ipc_parity.rs
  • crates/core/tests/ontology_integration.rs
  • crates/eval/src/metrics.rs
  • crates/eval/src/pairwise.rs
  • crates/eval/src/runner.rs
  • crates/eval/src/scoring.rs
  • crates/eval/tests/harness_integration.rs
  • docs/adr/0312-entity-strict-mode-reclassifies-not-drops.md
  • docs/adr/index.md
  • specs/312-strict-mode-still-deletes/spec.md

Comment thread crates/core/src/episode.rs
Comment thread docs/adr/0312-entity-strict-mode-reclassifies-not-drops.md Outdated
Comment thread README.md Outdated
Comment thread specs/312-strict-mode-still-deletes/spec.md Outdated
Comment thread specs/312-strict-mode-still-deletes/spec.md
@verveguy

verveguy commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

🏭 Fabrik — stage: Review
branch: fabrik/issue-312 | commit: ebbcc76 | main: 552be21 | 2026-08-02 14:23 UTC

Nothing to push since I made no changes. The implementation is already clean and complete.

Review Findings

Verified

  • Rebased onto latest main (already up to date)
  • No external PR review feedback pending
  • cargo fmt --all -- --check — clean
  • cargo test — 913 passed, 8 ignored, 0 failed
  • cargo clippy --all-targets -- -D warnings — no issues
  • Correctness: strict-mode entity filter reclassifies instead of dropping (FR-001/002), Unclassified sentinel substitutes the label rather than appending (preserves schema closure), original type preserved in attributes.original_entity_type (FR-003), tally added and threaded to IPC response (FR-004), old drop-locking test replaced (FR-005), edge-clearing cascade deleted (FR-006), empty/"Entity" type correctly bypasses reclassification (FR-007), dedup path unaffected since it's name-keyed not type-keyed (FR-008)
  • Security: original_entity_type is unconditionally cleared before any mode-specific logic runs, exactly mirroring the pattern that took three review round-trips to get right on the edge side in core: strict ontology mode drops declared aliases and never tells the model the constraint #310 — applied correctly on the first pass here
  • Test coverage: 6 new/updated tests cover all FR/user-story acceptance scenarios, including reclassify-count tallying, edge-endpoint survival, empty-type passthrough, and dedup-against-declared-type
  • All ExtractedEntity construction call sites across the workspace (11 files) were updated for the new field — confirmed by full compile success
  • Documentation: README's inline YAML comment and Modes table updated; ADR-0312 written with independent reasoning (not just analogy to ADR-0310) and correctly indexed in docs/adr/index.md

Blocking

None.

Reviewed the entity reclassify-not-drop implementation for issue #312. Full local gate passes (fmt, 913 tests, clippy with -D warnings), all FR-001–008 requirements are correctly implemented and tested, the untrusted-JSON clearing pattern that needed three fix rounds on the edge side (#310) was applied correctly on the first pass here, and docs/ADR-0312 are in place. No issues found; no fixes were needed. PR is ready for human review.


Used 31/100 turns, 0k input / 6k output tokens.

@verveguy

verveguy commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

🏭 Fabrik — stage: Review (review feedback addressed)
branch: fabrik/issue-312 | commit: 8e76e22 | main: 552be21 | 2026-08-02 14:35 UTC

Confirmed — same two placeholders exist in the issue body. I'll produce the updated issue body with those references fixed, matching the spec.md edits.

Review Comment Findings — Resolution

Fixed (real bugs, confirmed by code inspection):

  • Copilot + CodeRabbit (same underlying defect, two angles): entities_reclassified_unclassified could overcount — the reclassify loop tallied an out-of-vocabulary entity with an empty/whitespace name before the later retain(|e| !e.name.trim().is_empty()) dropped it, so it was counted but never persisted, directly contradicting the "no desync risk" claim in the code comment and ADR. Fixed by moving the empty-name retain before the reclassify loop.
  • Copilot: the empty/"Entity" passthrough branch left entity_type un-normalized, so a case/separator variant ("entity", "ENTITY") would bypass reclassification correctly but then leak as a spurious label via make_insert_row's raw-string (non-normalized) check. Fixed by normalizing entity_type on the passthrough path too.
  • Added two regression tests covering both fixes. Full local gate re-verified: cargo fmt --all -- --check, cargo test (915 passed, up from 913), cargo clippy --all-targets -- -D warnings — all clean.

Fixed (doc precision, quick wins):

  • ADR-0312: reworded a line so it doesn't start with #310 (markdownlint MD018 false-positive).
  • spec.md: replaced the docs/adr/0312-<slug>.md placeholder with the real ADR filename now that it exists (also updating the issue body below for the same reason).
  • README: qualified "never dropped" as "never dropped for its type alone" in both the YAML comment and Modes table, since edges can still be dropped later for unrelated reasons (self-referential, unresolvable endpoint) — that nuance was accurate but easy to misread.

Dismissed:

  • CodeRabbit's SC-004 narrowing suggestion — the criterion is a controlled before/after invariant tested on identical extraction inputs (as the added tests do), not a live-corpus non-deterministic claim; no implementation defect follows from it. Left as-is; a human can revisit if they disagree.

Not done: thread resolution via gh api graphql failed — GitHub API rate limit exceeded for this session. The fixes are pushed; threads can be resolved manually or on a later attempt.

Both fixes pushed as separate commits (3daf9b0, 8e76e22) to fabrik/issue-312.


Threads addressed:

  • crates/core/src/episode.rs:229 — resolved
  • crates/core/src/episode.rs:244 — resolved
  • docs/adr/0312-entity-strict-mode-reclassifies-not-drops.md:94 — resolved
  • README.md:269 — resolved
  • specs/312-strict-mode-still-deletes/spec.md:38 — resolved
  • specs/312-strict-mode-still-deletes/spec.md:127 — resolved

Resolved 6 review thread(s) across 6 comment(s).

@handarbeit-pruefer handarbeit-pruefer 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.

Confirms the claim. Reviewed everything thoroughly — implementation, tests, ADR, and README are internally consistent, with previous review findings already addressed. I found no correctness or security issues.

I reviewed the entity-side reclassify-not-drop change (crates/core/src/types.rs, crates/core/src/episode.rs), the new ADR-0312, README updates, and the test coverage in ontology_integration.rs/ipc_parity.rs. The implementation faithfully extends the ADR-0310 edge-side pattern to entities: the strict-mode filter reclassifies rather than drops, original_entity_type is unconditionally cleared before mode-specific filtering (defeating untrusted-JSON injection of that key), the dead edges.clear() cascade is fully removed, and edge cases (empty name before tally, "Entity"/case-variant passthrough not leaking into labels, merge-vs-insert tally semantics) are each covered by a dedicated test and matched by an explicit code comment. I did not find bugs, security issues, or design problems worth flagging — the one subtlety I traced (the reclassified-tally counting an entity that later merges away, discarding its Unclassified label/attributes) is deliberate, documented in the ADR's "Alternatives Considered" section, and asserted by strict_mode_reclassified_entity_dedups_against_declared_type.

@verveguy

verveguy commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

🏭 Fabrik — stage: Validate
branch: fabrik/issue-312 | commit: 8e76e22 | main: 552be21 | 2026-08-02 14:56 UTC

All gates pass. Writing the validation report.

Validation Report

Requirements: 8/8 passed

  • FR-001 (retain, not drop): episode.rs:220-257 — strict-mode entity filter no longer removes entities from extraction.entities; only rewrites entity_type.
  • FR-002 (Unclassified sentinel label, not raw string): ENTITY_UNCLASSIFIED = "Unclassified" (episode.rs:53), assigned in place of the rejected type; make_insert_row pushes it into labels alongside base Entity (episode.rs:519-535).
  • FR-003 (original type preserved in attributes): episode.rs:539-542 writes {"original_entity_type": "<orig>"}; no schema change, confirmed no migration touched.
  • FR-004 (per-run tally observable): AddEpisodeResult.entities_reclassified_unclassified (episode.rs:38), threaded to handle_process_chunk's IPC JSON (handlers.rs:440), asserted numeric in ipc_parity.rs:802-803.
  • FR-005 (old drop-locking test superseded): strict_mode_entity_type_still_drops_not_reclassifies replaced by strict_mode_entity_type_reclassifies_not_drops (ontology_integration.rs:1017-1020, comment cites the superseded test by name).
  • FR-006 (edge-clearing cascade removed): confirmed no extraction.edges.clear() call remains anywhere in episode.rs.
  • FR-007 (empty/"Entity" type passthrough, no reclassification): episode.rs:238-243, covered by strict_mode_empty_entity_type_resolves_as_plain_entity and strict_mode_entity_type_case_variant_of_entity_not_leaked_into_labels.
  • FR-008 (dedup unaffected, name-keyed): confirmed structurally (Phase B resolution keyed by get_entity_by_name_ci, type never consulted) and by test strict_mode_reclassified_entity_dedups_against_declared_type (ontology_integration.rs:1340).

Also verified: SC-003 (no open/freeform prompt changes — prompts/mod.rs untouched in diff), ADR-0312 exists and is indexed (docs/adr/index.md:65), README's YAML comment + Modes table updated (lines 265, 320), and the two post-Review regression fixes (tally/empty-name desync, un-normalized label leak) are present with dedicated tests.

Test Suite: PASSED

  • cargo fmt --all -- --check — clean
  • cargo test — 915 passed, 8 ignored, 0 failed (56 suites)
  • cargo clippy --all-targets -- -D warnings — clean

Regressions: None detected

Diff is scoped exactly to the entity reclassify path, its call-site plumbing (ExtractedEntity fixture updates across 11 files), and docs. No shared interface changes beyond additive fields.

Code completeness

No TODO/FIXME/debug leftovers in the diff. All plan tasks (Implement + Review fix commits) reflected in git log.

Branch state

Rebased onto latest origin/main (already up to date, no conflicts), working tree clean, nothing to push. PR #313: mergeable=MERGEABLE, mergeStateStatus=CLEAN.

Verdict: READY TO MERGE

Validation passed. PR mergeable: MERGEABLE, mergeStateStatus: CLEAN. All 8 requirements (FR-001–008) verified against code and tests; full local gate clean (915 tests passed, fmt/clippy clean); no regressions; branch rebased and pushed.


Used 21/100 turns, 0k input / 5k output tokens.

@handarbeit-pruefer handarbeit-pruefer 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.

Everything is in order. This is a well-implemented, thoroughly tested PR with no issues found.

I reviewed the strict-mode entity reclassification change (episode.rs, types.rs, handlers.rs), the removed edges-clearing cascade, the ADR-0312 rationale, and the new tests in ontology_integration.rs/ipc_parity.rs. The implementation is symmetric with the existing edge-side reclassify-not-drop logic (ADR-0310), correctly clears original_entity_type before any mode-specific filtering (matching the untrusted-JSON discipline for the edge field), preserves the raw (non-normalized) out-of-vocabulary label in attributes, and counts the tally at the correct point in the pipeline given entities are never dropped downstream. Dedup/merge semantics, PascalCase vs SCREAMING_SNAKE_CASE label conventions, and the removal of the dead "no entities remain → clear edges" branch all check out against the code and are backed by targeted tests covering the tally, dedup-merge interaction, empty-name edge case, and edge survival with a reclassified endpoint. No remote/main ref was available in this checkout to produce a line-scoped diff, so I reviewed the current state of the relevant files directly against the PR description; I found no bugs, correctness issues, or design problems worth flagging.

@verveguy

verveguy commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

🏭 Fabrik — stage: Validate
branch: fabrik/issue-312 | commit: c8147eb | main: 4a360f5 | 2026-08-02 21:55 UTC

All checks are consistent with an in-progress CI run (not a conflict). All local gates pass and requirements are verified. Writing the final report.

Validation Report

Requirements: 8/8 passed

  • FR-001 (retain, not drop): episode.rs:220-257 — strict-mode entity filter never removes entities from extraction.entities; only rewrites entity_type.
  • FR-002 (Unclassified sentinel, not raw string): ENTITY_UNCLASSIFIED = "Unclassified" (episode.rs:53), substituted for the rejected type; make_insert_row appends it to labels alongside base Entity.
  • FR-003 (original type preserved): episode.rs:539-542 writes {"original_entity_type": "<orig>"} into EntityRow.attributes; no schema change.
  • FR-004 (per-run tally observable): AddEpisodeResult.entities_reclassified_unclassified threaded to handle_process_chunk's IPC response (handlers.rs:440), asserted numeric in ipc_parity.rs:802-803.
  • FR-005 (old drop-locking test superseded): strict_mode_entity_type_reclassifies_not_drops (ontology_integration.rs:1020) replaces the old drop-asserting test.
  • FR-006 (edge-clearing cascade removed): confirmed no extraction.edges.clear() call remains anywhere in episode.rs.
  • FR-007 (empty/"Entity" type passthrough): episode.rs:238-243, covered by dedicated tests including the case-variant leak regression.
  • FR-008 (dedup unaffected): Phase B resolution is name-keyed, not type-keyed; covered by strict_mode_reclassified_entity_dedups_against_declared_type.

Also verified: no open/freeform prompt changes (SC-003), ADR-0312 present and indexed, README's YAML comment + Modes table updated, both post-Review regression fixes (tally/empty-name desync at episode.rs:211-218, un-normalized label leak at episode.rs:222-225) are in place with dedicated tests, and the docs precision fixes from the last rebase/CodeRabbit round are present.

Test Suite: PASSED

  • cargo fmt --all -- --check — clean
  • cargo test — 925 passed, 8 ignored, 0 failed (56 suites, 199s)
  • cargo clippy --all-targets -- -D warnings — clean

Regressions: None detected

Diff (origin/main..HEAD) is 21 files, scoped exactly to the entity reclassify path, its ExtractedEntity fixture threading (11 call sites), docs, and ADR. No shared-interface changes beyond additive fields.

Branch state

Rebased onto latest origin/main (already up to date, no conflicts), working tree clean, all commits pushed. PR #313: mergeable=MERGEABLE, mergeStateStatus=BLOCKED (CI's test (ubuntu-latest) check is currently pending on the freshly-pushed commit c8147eb — this is the same benign state noted in the prior Validate round, not a merge conflict; mergeStateStatus is only blocking per the gate if it's DIRTY).

Verdict: READY TO MERGE

Re-validated after the rebase that resolved the ADR-index conflict. All 8 requirements (FR-001–008) confirmed against code and tests; full local gate clean (925 tests, fmt/clippy clean); no regressions. PR #313 mergeable: MERGEABLE, mergeStateStatus: BLOCKED (CI still running on latest commit, not a conflict). Ready to merge once CI completes.


Used 5/100 turns, 0k input / 8k output tokens.

@verveguy
verveguy enabled auto-merge August 2, 2026 23:05
Mirrors ADR-0310's edge-side treatment: an out-of-vocabulary entity type
is rewritten to a new `Unclassified` sentinel label instead of being
dropped, with the original type preserved in `attributes` as
`{"original_entity_type": "<original>"}`. An empty/absent type (or the
literal "Entity") continues to resolve as a plain untyped Entity, not a
reclassification. The all-entities-empty cascade that used to clear
`extraction.edges` is removed, since entities are no longer removed for
their type. The per-run reclassification count is surfaced as
`entities_reclassified_unclassified` on `AddEpisodeResult` and in
`knowledge_process_chunk`'s response, mirroring
`edges_reclassified_unclassified` (issue #312).
…ures

Mechanical follow-up to the new ExtractedEntity field — updates every
existing struct-literal call site across crates/core and crates/eval
tests/fixtures so they compile against the extended struct.
Replaces strict_mode_entity_type_still_drops_not_reclassifies with
strict_mode_entity_type_reclassifies_not_drops, asserting the retained
entity's Unclassified label and recoverable attributes. Updates the
older strict_mode_entity_filtering_drops_out_of_vocab expectation to
match. Adds coverage for: the per-run reclassification tally over N
entities (User Story 3), an edge surviving with a reclassified endpoint
(User Story 2), the empty/"Entity"-type passthrough (FR-007), and
dedup against a pre-existing declared-type entity (FR-008/SC-006).
Also asserts entities_reclassified_unclassified is numeric in the
knowledge_process_chunk IPC response, mirroring the existing
edges_reclassified_unclassified assertion.
Documents the decision to reclassify out-of-vocabulary entity types to
Unclassified rather than dropping them, citing ADR-0310 as precedent
and independently arguing the edge-endpoint-cascade and schema-closure
reasons this holds for entities. Updates the ontology.yaml format
example and the Modes table's strict entity-types cell to describe the
new behavior instead of the old drop behavior.
…abel leak

Two real bugs from Copilot/CodeRabbit review on issue #312:

1. entities_reclassified_unclassified could overcount: the strict-mode
   reclassify loop ran before the empty-name retain, so an out-of-vocab
   entity with an empty/whitespace name was tallied then dropped, never
   reaching storage. Move the empty-name retain before the reclassify loop
   so only persisted entities are counted.

2. The empty/"Entity" passthrough branch left entity_type un-normalized,
   so a case/separator variant (e.g. "entity", "ENTITY") bypassed
   reclassification correctly but then leaked as a spurious label via
   make_insert_row's raw-string check. Normalize entity_type on the
   passthrough path too.

Adds regression tests for both.
- ADR-0312: reword a line so it doesn't start with "#310" (markdownlint
  MD018 false-positive heading trigger).
- spec.md: replace the docs/adr/0312-<slug>.md placeholder with the
  actual ADR filename now that it exists, in both references.
- README: qualify "never dropped" as "never dropped for its type alone"
  in both the strict-mode YAML comment and the Modes table, since edges
  can still be dropped later for unrelated reasons (self-referential,
  unresolvable endpoint).
The docs-only rebase resolution updated docs/ontology.md's strict-mode
description for issue #312 but left the generated llms-full.txt stale,
tripping the docs-drift CI check.
@verveguy

verveguy commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

🏭 Fabrik — stage: Validate
branch: fabrik/issue-312 | commit: c1dba4e | main: c44f064 | 2026-08-02 23:32 UTC

mergeable=MERGEABLE, mergeStateStatus=BLOCKED (not DIRTY) — this is the benign "CI still running on the fresh commit" state per the gate rules, not a conflict. All requirements were already verified in prior Validate rounds; I re-confirmed the local gate (fmt/test/clippy) passes on the rebased tree and fixed the one real issue found this round (stale generated docs). Per CLAUDE.md, I must not wait on CI — the engine handles that. Completing the stage now.

Validation Report

Requirements: 8/8 passed (re-confirmed against current code)

  • FR-001–008: All previously verified in prior Validate passes against episode.rs, ontology_integration.rs, ipc_parity.rs, handlers.rs — no code changes since, only a rebase and a docs-generation fix this round.
  • SC-001–006: unchanged, previously verified.

Test Suite: PASSED

  • cargo fmt --all -- --check — clean
  • cargo test — 931 passed, 8 ignored, 0 failed (56 suites, 252.67s)
  • cargo clippy --all-targets -- -D warnings — clean

Regressions: None detected

Diff scoped to the entity reclassify path, its fixture threading, docs, ADR, and (this round) the regenerated docs/llms-full.txt.

Issue found and fixed this round

The "Docs drift check" workflow's check-llms-full job was failing: an earlier Validate-stage rebase (resolving the README→docs/ontology.md migration conflict) updated docs/ontology.md's strict-mode wording for ADR-0312 but never regenerated the derived docs/llms-full.txt, so it drifted out of sync. Fixed by running scripts/generate-docs-llms-full.sh and committing the result (c1dba4e), pushed to fabrik/issue-312.

Branch state

Rebased onto latest origin/main (up to date, no conflicts), working tree clean, all commits pushed including the fix above.

Verdict: READY TO MERGE

Pre-completion gate: rebase clean against origin/main; PR #313 mergeable=MERGEABLE, mergeStateStatus=BLOCKED (CI re-running on the freshly-pushed commit c1dba4e — not a conflict; the docs-drift check that was failing is now fixed by this push).

Validation passed after fixing a stale-docs CI failure: docs/llms-full.txt hadn't been regenerated after an earlier rebase updated docs/ontology.md's ADR-0312 wording, tripping the docs-drift check — regenerated and pushed (c1dba4e). All 8 requirements (FR-001–008) re-verified, full local gate clean (931 tests, fmt/clippy clean), no regressions. PR #313 mergeable: MERGEABLE, mergeStateStatus: BLOCKED (CI running on latest commit, not a conflict).


Used 17/100 turns, 0k input / 3k output tokens.

@handarbeit-pruefer handarbeit-pruefer 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.

Not critical — I've already verified with build+clippy. I have enough to write the review.

Review

I reviewed the strict-mode entity reclassification change (crates/core/src/episode.rs, types.rs, handlers.rs), the removal of the "no entities remain → clear all edges" cascade, ADR-0312, and the accompanying tests in ontology_integration.rs/ipc_parity.rs. The implementation faithfully follows the edge-side precedent (ADR-0310): original_entity_type is unconditionally cleared before any mode-specific filtering to prevent hallucinated/injected JSON keys from surviving, the empty-name filter runs before the tally to keep entities_reclassified_unclassified in sync with what's actually persisted, and the reclassify logic correctly distinguishes "no type" from "in-vocabulary" from "out-of-vocabulary." I traced the label/attribute construction in make_insert_row, the dedup interaction (merge vs. insert), and the docs/ontology.md/ADR-0312 text against the code and found them consistent. cargo build and cargo clippy --all-targets -- -D warnings are clean. I did not find correctness bugs or security issues; only a couple of minor points worth a second look.

false
e.original_entity_type = Some(e.entity_type.clone());
e.entity_type = ENTITY_UNCLASSIFIED.to_string();
entities_reclassified_unclassified += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When a reclassified entity dedups by exact name-match into an existing entity (PhaseBResult::NameMatch), the Merge branch only updates summary, never labels/attributes — so entities_reclassified_unclassified counts this extraction event even though the Unclassified label/original_entity_type never reaches storage for it (confirmed by strict_mode_reclassified_entity_dedups_against_declared_type, which asserts the tally increments even on merge). This is called out and intentionally accepted in ADR-0312's consequences section, but it does mean the counter measures "reclassification decisions made during processing" rather than "entities persisted with the Unclassified label" — worth double-checking that downstream consumers of this IPC field (dashboards, alerts) are told which of the two it represents, since the name suggests the latter.

e.name, e.entity_type
);
false
e.original_entity_type = Some(e.entity_type.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: if the model hallucinates the literal type string "Unclassified" for an entity type that is itself out of vocabulary, original_entity_type gets set to Some("Unclassified") and entity_type is rewritten to the same value — a no-op-looking reclassification whose attributes.original_entity_type is indistinguishable from the sentinel itself. Harmless, but slightly confusing when later inspecting attributes to recover the "real" original type.

@handarbeit-pruefer handarbeit-pruefer 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.

I've reviewed the implementation thoroughly. Now let me finalize my findings.

Reviewed the entity-side reclassify-not-drop change in episode.rs, types.rs, handlers.rs, the new ADR-0312, README/docs updates, and the new/updated tests in ontology_integration.rs. The implementation is careful and well-documented: original_entity_type is scrubbed from untrusted extractor JSON before any mode-specific logic runs (mirroring the edge-side discipline), the empty/"Entity" passthrough avoids spurious reclassification, the tally is counted at the correct point (no Phase C deferral needed since entities are never subsequently dropped for type), and the dead "clear all edges" cascade is gone. Tests cover the disposition, per-run tally, edge survival on a reclassified endpoint, and dedup-merge into a declared-type entity. Two minor points below are worth a look but neither blocks anything.

Additional findings (could not anchor to diff)

crates/core/src/episode.rs:550: Pre-existing type-conflict logging, but worth flagging in the context of this PR: when a reclassified entity (extracted.entity_type == "Unclassified") name-matches an existing entity stored under its correct declared type (per the PR description's own dedup scenario — "merging into a pre-existing entity stored under its correct declared type"), this comparison (existing.labels won't contain "Unclassified") will fire a false-positive type conflict warning on every such merge, even though this is expected, documented behavior under reclassify-not-drop. Consider special-casing Unclassified here so the log isn't misleading.


The strict-mode entity filter in `episode.rs` never drops an entity for its `entity_type` alone.

1. **An entity whose normalized type is empty or the literal `"Entity"` passes through

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Neither this ADR nor the code adds any guard against an ontology author declaring an entity type literally named Unclassified in their vocabulary. If they do, a genuine in-vocabulary Unclassified-typed entity becomes indistinguishable from a reclassified out-of-vocabulary one (both get exactly the Unclassified label, and the genuine one won't have original_entity_type set, but there's no rejection/warning at ontology-load time to catch the collision early). Same gap likely exists for UNCLASSIFIED on the relation side from ADR-0310, so this may be an accepted pre-existing risk rather than new, but worth a one-line note in the ADR if so.

@verveguy
verveguy merged commit 2eb7722 into main Aug 2, 2026
11 checks passed
@verveguy
verveguy deleted the fabrik/issue-312 branch August 2, 2026 23:51
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.

core: strict mode still deletes out-of-vocabulary entities while edges are now preserved

2 participants