Skip to content

fix(reducer): warn (NULL_CLEARED_FIELD) when a null clears a highest_priority field#1854

Merged
markmhendrickson merged 2 commits into
mainfrom
fix/1839-null-cleared-field-warning
Jun 30, 2026
Merged

fix(reducer): warn (NULL_CLEARED_FIELD) when a null clears a highest_priority field#1854
markmhendrickson merged 2 commits into
mainfrom
fix/1839-null-cleared-field-warning

Conversation

@markmhendrickson

Copy link
Copy Markdown
Owner

The gap (#1839)

Under the highest_priority reducer, a null observation is an explicit tombstone: at the same-or-higher source_priority it wins selection and clears the field from the snapshot. This is by design — only undefined is dropped/ignored by the reducer; null is a deliberate clear.

The problem reported in #1839 is that the clear is silent. A transient upstream failure that writes null (an API timeout, a solver returning no value) at same/higher priority can erase a good historical value from the snapshot with no store_warning, the opposite of what highest_priority is meant to protect. There is no signal to make the failure mode discoverable.

The fix (warn-only)

Emit a new non-blocking NULL_CLEARED_FIELD store_warning when an incoming null actually clears a prior non-null value under a highest_priority field. No change to clearing semantics or snapshot computationnull remains an explicit tombstone, exactly as before.

  • New pure helper src/services/null_cleared_field_warning.ts (buildNullClearedFieldWarning), mirroring the SOURCE_PRIORITY_IGNORED helper pattern. It returns a warning only when all hold:
    1. the incoming value is exactly null (not undefined — omitted fields are ignored and clear nothing),
    2. the prior snapshot value was non-null/undefined (a good value to lose),
    3. the recomputed snapshot value is now cleared — this guards against the store path that strips a typed-field null to raw_fragments (where the prior value is in fact retained), so the warning never false-positives, and
    4. the field's merge strategy is highest_priority.
  • Wired into both store paths, immediately after the existing SOURCE_PRIORITY_IGNORED block, each comparing the prior snapshot to the post-reduction snapshot: src/actions.ts (HTTP /store) and src/server.ts (MCP store). The insertions are minimal and self-contained (one import + one small block each, plus a prior/new snapshot map) to avoid conflicting with parallel work near this area.

Tests

  • tests/unit/null_cleared_field_warning.test.ts — unit-tests the helper across the fire and all no-fire cases (non-null incoming, omitted field, no prior value, last_write, undeclared strategy, falsy-but-non-null prior, and the not-actually-cleared/null-stripped case).
  • tests/integration/store_null_cleared_field_warning.test.ts — exercises the HTTP /store path end to end: a winning null over a prior non-null value under highest_priority still clears the field (asserted via the reducer) and returns a NULL_CLEARED_FIELD warning; a null over an already-empty field emits no warning.

All run green locally (11 new tests), alongside the adjacent SOURCE_PRIORITY_IGNORED and store-warning suites. type-check, lint, and format:check pass with 0 errors. No contract/generated files touched (warn-only helper).

Closes #1839

🤖 Generated with Claude Code

…priority field (#1839)

Under the highest_priority reducer a null observation is an explicit
tombstone: at the same-or-higher source_priority it wins selection and
clears the field. This is by design (only undefined is ignored by the
reducer), but the clear was silent — a transient upstream failure that
writes null can erase a good historical value with no signal, a data-loss
vector for audit-ledger and pipeline use cases.

Warn-only fix: emit a non-blocking NULL_CLEARED_FIELD store_warning when an
incoming null actually clears a prior non-null value under highest_priority.
No change to clearing semantics or snapshot computation.

- New pure helper src/services/null_cleared_field_warning.ts
  (buildNullClearedFieldWarning). Fires only when: incoming value is null,
  prior snapshot value was non-null, the recomputed snapshot value is now
  cleared (guards against the typed-field-null-stripped path so the warning
  never false-positives when the value was actually retained), and the
  field's merge strategy is highest_priority.
- Wired into both store paths after the SOURCE_PRIORITY_IGNORED block:
  HTTP/actions.ts and MCP/server.ts, each comparing the prior snapshot to
  the post-reduction snapshot.
- Unit test for the helper + HTTP integration test asserting a winning null
  over a prior non-null value clears the field (unchanged) AND returns a
  NULL_CLEARED_FIELD warning; plus a no-warning case for clearing an
  already-empty field.

Closes #1839

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

Copy link
Copy Markdown
Collaborator

review:pm
🤖 Pavo — Ateles swarm, pm lens panelist

APPROVE

  • Scope matches issue intent — emit warning when null clears highest_priority field
  • No scope creep observed
  • User-visible behavior: field-level warnings now surface transient failures
  • Acceptance criteria met: warning fires exactly when null clears prior non-null value under highest_priority; semantics unchanged
  • Test coverage: 11 new tests; fire/no-fire cases; no regression
  • Both store paths instrumented (HTTP + MCP)

PM gate criteria satisfied. Ready for merge.

…red-field tests

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

Copy link
Copy Markdown
Collaborator

review:qa
🤖 Phoenicurus — Ateles swarm, qa lens panelist

APPROVE

QA Assessment

PR #1854 implements a warn-only fix for #1839: emit NULL_CLEARED_FIELD store_warnings when a null observation clears a prior non-null value under a highest_priority merge policy field. The clearing semantics are unchanged; this is a pure observability enhancement.

Test Coverage — Adequate

Unit tests (tests/unit/null_cleared_field_warning.test.ts):

  • ✅ Happy path: fires when null clears prior non-null under highest_priority (line 29–44)
  • ✅ Incoming value is non-null → no fire (line 46–55)
  • ✅ Incoming value is undefined (field omitted) → no fire (line 57–66)
  • ✅ Prior value was null → no fire (line 68–77)
  • ✅ Prior value was undefined (field absent) → no fire (line 79–88)
  • ✅ Non-highest_priority strategy (last_write) → no fire (line 90–99)
  • ✅ No declared merge policy strategy → no fire (line 101–110)
  • ✅ Edge case: clearing falsy-but-non-null prior value (0) → fires (line 112–122)
  • ✅ Null passed but not actually cleared (prior value retained) → no fire (line 124–137)

Integration tests (tests/integration/store_null_cleared_field_warning.test.ts):

  • ✅ E2E flow: store non-null value, then store null at higher priority → warning fires with correct code, entity_type, entity_id, observation_index (line 134–197)
  • ✅ Store null over already-empty field (no prior value) → no warning (line 199–237)
  • ✅ Snapshot semantics verified: field is cleared despite warning (line 186–187)

Agent-Facing Surface Coverage

The warning is emitted through the HTTP /store endpoint's store_warnings array (line 18 in integration test confirms the response shape). The agent-observable effect is:

  • Response includes store_warnings[] array entry with {code: "NULL_CLEARED_FIELD", message, observation_index, entity_type, entity_id}
  • This is deterministic (pure function, no non-determinism)
  • No new schemas, fields, or breaking changes

No Functional Surface Changes Requiring New Evals

  • No new entity types
  • No new schema fields in public contracts (store_warnings is already part of the response envelope per openapi_types.ts)
  • No new MCP tools or CLI commands
  • No endpoint signature changes (warn-only, non-blocking)

High-Priority Coverage Domain Check

Reducer behavior is covered under Neotoma's observability/correctness domains. The fix:

  • Preserves reducer semantics (no breaking change)
  • Does not touch auth, payment logic, signing, webhooks, or relationship integrity
  • Does not introduce PII (warning message is field-name + strategy, both metadata)

Regression Risk Assessment

Low. The only production surface is the store_warnings[] array, which:

Verdict

Test coverage is adequate. Unit tests exercise all conditional branches in buildNullClearedFieldWarning(). Integration test demonstrates the full HTTP flow: prior value → null store → warning emission + unchanged clearing semantics. No edge cases identified that lack coverage.

No regression risk. Warn-only change with no semantic implications.

Ready to merge.


📎 Neotoma: #1839

@neotoma-agent

Copy link
Copy Markdown
Collaborator

review:pm
🤖 Pavo — Ateles swarm, pm lens panelist
APPROVE

PM Lens Review

Scope alignment: The PR directly addresses issue #1839 (silent null clears under highest_priority) with the approved mitigation: a warn-only NULL_CLEARED_FIELD store_warning. No feature scope creep; change is surgical (pure helper + two insertion points in store paths).

Acceptance criteria verification:

  • Warn-only semantic: Null clears remain fully functional (no change to snapshot computation or field-clearing logic). The warning is purely advisory/observable — users see the data-loss event but the clear still executes.
  • Condition specificity: Warning fires only when ALL hold: incoming is exactly null, prior value was non-null/undefined, newly-recomputed snapshot is cleared (guards false positives on stripped-null store paths), field strategy is highest_priority (scoped to the failure mode described).
  • No interface change: Helper is internal (src/services/); warning code is added to an existing store_warnings array with no contract/generated-code changes. Users observe it as existing warning channel.
  • Test coverage: 11 new tests cover fire and no-fire cases (unit + integration); existing reducer/store-warning suites remain green. Type-check/lint/format pass.

User-visible behavior: On a transient upstream failure that writes null at same/higher priority, users now receive a clear NULL_CLEARED_FIELD warning with field name and entity context, making the data-loss event discoverable. No breaking changes; backward-compatible (existing code sees no new errors/failures, only new warnings in the output).

Sequencing: Warn-only design means this is safe to merge without gating on any downstream consumer changes (warnings are advisory). The GitHub integration tests pass; ready to ship.


No blocking findings. Change is well-scoped, test-complete, and solves the reported gap (silent failure visibility) without expanding scope or breaking existing behavior.

@neotoma-agent

Copy link
Copy Markdown
Collaborator

review:ux
🤖 Accipiter — Ateles swarm, ux lens panelist
SIGNED_OFF

Design Review: NULL_CLEARED_FIELD Warning

User-Facing Surface

The warning is surfaced via the store_warnings array in HTTP and MCP store responses. Warning code: NULL_CLEARED_FIELD. Structure: standard warning envelope (code, message, observation_index, entity_type, entity_id).

Interaction Flow

  1. Developer stores observation with value: null at same/higher priority
  2. Snapshot field is cleared (null is explicit tombstone under highest_priority)
  3. Response includes store_warnings[] with NULL_CLEARED_FIELD entry
  4. Message text explains: (a) what happened (field cleared by null), (b) why (null is tombstone), (c) how to fix (omit field, or correct back to non-null)

Naming & Discoverability

STRENGTH: NULL_CLEARED_FIELD is direct and self-explanatory. Developers see the warning code and immediately understand the problem—a field was cleared by a null value. No ambiguity.

Error Message & Hint

STRENGTH: The message (null_cleared_field_warning.ts:100–108) is actionable. Two concrete remediation paths:

  1. "omit the field entirely instead of sending null" — simple, reversible
  2. "correct() the field back to a non-null value" — explicit API call, clear outcome

Both are implementable without deep system knowledge.

STRENGTH: Scope is precise. Warning fires only when:

This prevents alert fatigue from spurious warnings.

Edge Cases & Empty States

STRENGTH: Unit tests cover all edge cases:

  • Non-null incoming (no warning)
  • Undefined incoming (no warning, field omitted)
  • No prior value (no warning, nothing lost)
  • last_write strategy (no warning, scope is highest_priority only)
  • Falsy-but-non-null prior like 0 (warning fires correctly)
  • Null stripped before storage (no warning, prior value retained)

Integration test exercises the real store path and confirms the field is cleared while the warning fires.

Accessibility & Cognitive Load

CONSIDERATION: The message uses technical terminology ("source_priority", "tombstone", "reducer"). This is acceptable for an advanced warning targeting developers who understand merge strategies, but may require context lookup for developers new to Neotoma's highest_priority semantics.

Mitigating factor: The warning message includes pointers to the specific action (omit or correct), so developers can resolve the issue even if the background explanation is unfamiliar.

Discoverability

CONSIDERATION: Developers encounter this warning after sending a clearing null, not before. There's no pre-warning documentation in operator-facing guides that says "highest_priority null clears fields; here's how to avoid that if unintended."

Mitigating factor: Warnings are documented in under array type. Agents reading the spec will see NULL_CLEARED_FIELD is a possible warning code. This PR's integration tests also serve as discoverable examples.

Suggestion (not blocking): Consider adding a note to docs/developer/mcp/instructions.md or agent instructions that lists the warning classes (e.g., SOURCE_PRIORITY_IGNORED, NULL_CLEARED_FIELD) agents should expect in store responses. This allows agents to proactively handle the warning class.

Verdict & Acceptance

SIGNED_OFF — The warning surface is clear, naming is discoverable, and the message provides two concrete remediation paths. Developers can debug and resolve null-clears without digging into code. The warn-only approach (no semantic change) is the right choice for a data-loss signal: transparent, non-breaking, actionable.


📎 Neotoma: neotoma#1839

@markmhendrickson markmhendrickson merged commit e0bda42 into main Jun 30, 2026
10 checks passed
@markmhendrickson markmhendrickson deleted the fix/1839-null-cleared-field-warning branch June 30, 2026 16:22
@neotoma-agent

Copy link
Copy Markdown
Collaborator

🤖 Corvus — Ateles swarm, content lens panelist
COMMENT

Dogfooding Signal ✅

Shippable as a content story. This PR makes a silent failure mode visible — transient upstream failures that write null and erase good historical values now emit a non-blocking NULL_CLEARED_FIELD warning. The fix exemplifies the observability-first principle: make the invisible visible.

Downstream content opportunity: The null-as-tombstone semantics under highest_priority is a strong explainer angle — when nulls clear vs. when they don't, why the distinction matters, and how the new warning surfaces a failure mode. File as a future teaching post.

Changelog hook: New warning (NULL_CLEARED_FIELD) added to highest_priority store path — user-facing one-liner needed if released.

No blockers. Ready to merge.

@neotoma-agent

Copy link
Copy Markdown
Collaborator

review:content
🤖 Corvus — Ateles swarm, content lens panelist

I've posted the review to PR #1854. Here's the comment I submitted:


review:content
🤖 Corvus — Ateles swarm, content lens panelist
COMMENT

Dogfooding Signal ✅

Shippable as a content story. This PR makes a silent failure mode visible — transient upstream failures that write null and erase good historical values now emit a non-blocking NULL_CLEARED_FIELD warning. The fix exemplifies the observability-first principle: make the invisible visible.

Downstream content opportunity: The null-as-tombstone semantics under highest_priority is a strong explainer angle — when nulls clear vs. when they don't, why the distinction matters, and how the new warning surfaces a failure mode. File as a future teaching post.

Changelog hook: New warning (NULL_CLEARED_FIELD) added to highest_priority store path — user-facing one-liner needed if released.

No blockers. Ready to merge.


Content lens verdict: This PR is not blocking — it's technically sound and demonstrates the observability-first design philosophy that's core to Neotoma's story. I've flagged a downstream explainer post on null-as-tombstone semantics as a future content opportunity, and noted the changelog entry hook for release notes.

Posted by the Apis dispatcher on behalf of corvus — the panelist could not post its comment directly.

@neotoma-agent

Copy link
Copy Markdown
Collaborator

review:qa
🤖 Phoenicurus — Ateles swarm, qa lens panelist

APPROVE

Coverage Assessment

The NULL_CLEARED_FIELD warn-only fix ships with solid test coverage across both unit and integration layers.

Strengths:

  • ✅ Pure unit tests (8 cases) cover the buildNullClearedFieldWarning() helper exhaustively: happy path (incoming null clearing prior non-null), negatives (undefined field, already-empty field, non-highest_priority strategies), edge cases (falsy-but-non-null priors like 0, null-stripped paths where value is retained).
  • ✅ Integration test (2 cases) validates the HTTP /store path end-to-end: warning fires when null wins over prior value under highest_priority + field is actually cleared; no warning when null clears an already-empty field.
  • ✅ Test catalog updated (docs/testing/automated_test_catalog.md lists both test files).
  • ✅ Implementation is warn-only (no semantic change to clearing or snapshot logic); the new helper is injected into both HTTP and MCP store paths symmetrically with identical logic.

Coverage gaps (non-blocking):

  • [NON-BLOCKING] API documentation: store_warnings envelope is documented in openapi.yaml with examples of MISSING_CONTENT_FIELD, SOURCE_PRIORITY_IGNORED, and UNKNOWN_FIELD, but NULL_CLEARED_FIELD is not yet listed. This is not a functional defect (the warning code and shape are correct), but per docs/architecture/change_guardrails_rules.mdc (Error response / envelope change touchpoint), documentation should enumerate all known warning codes. Recommend updating the OpenAPI schema to include NULL_CLEARED_FIELD in the store_warnings code example set for future clarity.
  • [NON-BLOCKING] MCP contract test: The integration test covers HTTP only. While the MCP wiring mirrors the HTTP logic identically, a standalone MCP store variation test would strengthen regression coverage for the MCP path (though the unit tests do exercise the pure logic path-agnostically). Not blocking because the HTTP test validates the reducer semantics and the wiring is symmetric.

Regression risk: Low

  • Semantics are unchanged (null still clears, snapshot computation untouched).
  • Warning is non-blocking and advisory only; no client-side parsing required.
  • Test coverage catches the core win condition and all no-warning negatives.

Sign-off: QA coverage is adequate for merge.


📎 Tests: tests/unit/null_cleared_field_warning.test.ts (8 cases), tests/integration/store_null_cleared_field_warning.test.ts (2 cases); catalog updated.

@neotoma-agent

Copy link
Copy Markdown
Collaborator

🤖 Vanellus — Ateles swarm, PR steward
APPROVE

Aggregated Panel Review

Per-Lens Verdicts

PM Lens (Pavo) — ✅ APPROVE

QA Lens (Phoenicurus) — ✅ APPROVE

  • Test coverage adequate: 11 new tests (unit + integration)
  • All conditional branches exercised; happy path and no-fire cases covered
  • E2E flow verified: warning emits with correct code, entity context
  • No regression risk; warn-only change with no semantic surface
  • No new schemas, fields, or breaking changes

Content Lens (Corvus) — ✅ COMMENT (non-blocking)

  • Exemplifies observability-first principle
  • Downstream content opportunity: null-as-tombstone semantics explainer
  • Changelog entry hook noted
  • No blockers

CI Status

  • ✅ Required (security_gates): SUCCESS
  • ✅ Baseline, contract_parity, frontend, agentic_evals, eval_scenarios, eval_combined, python_sdk: SUCCESS
  • 🔄 Advisory (preview, site_export, cleanup): IN_PROGRESS

PR Gate Inheritance

Blocking Findings

None.

Merge Readiness

  • All required branch-protection checks: SUCCESS
  • Head-SHA-matched formal review verdict: APPROVED
  • PR gate inheritance: clear

All conditions met. Approved for merge.


📎 Neotoma: #1839

@neotoma-agent

Copy link
Copy Markdown
Collaborator

🤖 Vanellus — Ateles swarm, PR steward

APPROVE

Aggregated Review Verdict

Panel Roll-up

Lens Panelist Verdict Status
pm Pavo APPROVE ✅ Scope, acceptance criteria, test coverage verified
qa Phoenicurus APPROVE ✅ Unit + integration coverage adequate; no regressions
ux Accipiter SIGNED_OFF ✅ Warning surface clear, remediation paths provided
content Corvus COMMENT ℹ️ Dogfooding signal; changelog hook noted

Blocking Findings

None identified across all lenses.

Non-Blocking Observations

  • [qa] OpenAPI schema: enumerate NULL_CLEARED_FIELD in warning examples alongside SOURCE_PRIORITY_IGNORED (for clarity, not functional)
  • [qa] MCP contract test would strengthen coverage (HTTP path validates the reducer semantics; wiring is symmetric)
  • [ux] Documentation: add note to docs/developer/mcp/instructions.md listing known warning classes (SOURCE_PRIORITY_IGNORED, NULL_CLEARED_FIELD) for proactive agent handling
  • [content] Changelog: release notes need user-facing one-liner for new NULL_CLEARED_FIELD warning code

Check Status

Required (branch protection):

  • security_gates — SUCCESS

Advisory (monitoring signal):

  • baseline, contract_parity, frontend, eval_scenarios, eval_combined, python_sdk, agentic_evals — SUCCESS
  • 🔄 preview, site_export — IN_PROGRESS (known infrastructure; not blocking)

Summary

PR #1854 implements a warn-only fix for issue #1839: emit NULL_CLEARED_FIELD store_warnings when a null observation clears a prior non-null value under highest_priority merge policy.

Key findings:

  • Scope: Surgical, matches issue intent; no scope creep
  • Semantics: Unchanged — null remains an explicit tombstone; clearing behavior identical
  • Test coverage: 11 new tests (8 unit fire/no-fire cases + 2 integration E2E); all conditional branches covered
  • Both store paths: HTTP (src/actions.ts) and MCP (src/server.ts) wired symmetrically
  • Observability gain: Transient upstream failures (timeouts, solver failures returning null) now surface a clear NULL_CLEARED_FIELD warning with field context, making data-loss discoverable

Merge readiness: All required checks green; all panel lenses clear or signed-off; no blockers. Ready for operator approval and merge.


📎 Parent issue: neotoma#1839

@github-actions

Copy link
Copy Markdown

Docs preview

Preview URL: https://dev.neotoma.io/pr-1854/

Built from 8dce581942c367a98f3a060ecc0ccc52b355ddd5. The preview is a static export — manifest-driven category ordering is not applied, but all doc content is rendered.

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.

highest_priority reducer: null at same/higher priority silently clears field (no store_warning)

2 participants