Skip to content

fix(conflicts): scan --dry-run --semantic persists verdicts and reports inserted: 0 #590

Description

@piaortiz

📋 Pre-flight Checks

  • I have searched existing issues and this is not a duplicate
  • I understand this issue needs status:approved before a PR can be opened

📝 Bug Description

engram conflicts scan --dry-run --semantic writes to the database. It inserted 13 relation rows and mutated 4 pre-existing pending relations, while reporting dry_run: true and inserted: 0.

The inserted counter appears to track only the structural pass. The --semantic pass persists its verdicts regardless of --dry-run, and its writes are neither counted nor prevented.

This makes the flag actively misleading: an operator running --dry-run on a large store to preview the blast radius will mutate the relation graph instead. There is no conflicts delete subcommand, so the rows cannot be reverted through the CLI.

🔄 Steps to Reproduce

  1. Use a project with existing observations and at least one unjudged candidate pair.
  2. Record the baseline:
    engram conflicts stats --project <P>
    
  3. Run the structural dry-run and confirm it is inert:
    engram conflicts scan --project <P> --dry-run
    
  4. Run the semantic dry-run:
    export ENGRAM_AGENT_CLI=claude
    engram conflicts scan --project <P> --dry-run --semantic \
      --max-semantic 34 --concurrency 3 --timeout-per-call 60 --yes
    
  5. Compare against the baseline:
    engram conflicts stats --project <P>
    engram conflicts list --project <P> --limit 200
    

✅ Expected Behavior

With --dry-run, no rows are inserted, updated, or transitioned out of pending, in either the structural or the semantic pass. Ideally the dry-run also prints the verdicts it would persist, so the preview has value on its own. Today the verdicts are only observable because they were written.

❌ Actual Behavior

The semantic pass persisted its verdicts. The command reported the opposite.

Command output:

Conflicts Scan (project: docs)
  inspected:        452
  candidates_found: 34
  already_related:  0
  inserted:         0
  dry_run:          true
  semantic_judged:  21
  semantic_skipped: 13
  semantic_errors:  0

conflicts stats before:

  By judgment_status:
    pending:     70
    judged:      57

  By relation type:
    not_conflict:        55
    pending:             70
    related:             2

conflicts stats after (same command, nothing else run in between):

  By judgment_status:
    pending:     66
    judged:      74

  By relation type:
    compatible:          4
    not_conflict:        54
    pending:             66
    related:             13
    scoped:              3

Total relations went from 127 to 140. Relation types compatible and scoped did not exist in this project before the run. pending dropped by 4, so existing rows were transitioned, not just appended.

The 13 new rows are ids 288–300, all with created_at between 01:37:22 and 01:39:15 UTC — the exact window of the scan:

id=300  rel=scoped      status=judged  created=2026-07-10 01:39:15
id=299  rel=related     status=judged  created=2026-07-10 01:39:04
id=298  rel=compatible  status=judged  created=2026-07-10 01:38:51
...
id=288  rel=related     status=judged  created=2026-07-10 01:37:22

Observations were not affected — only the relation graph.

Note: 21 pairs were judged but only 13 rows appeared. The gap is consistent with #490 (not_conflict verdicts persist nothing), so the 8 missing rows are likely not_conflict. That is a separate bug; it is mentioned only because it makes the row delta look smaller than the number of writes actually attempted.

Operating System

Linux (Ubuntu/Debian)

Engram Version

1.19.0

Agent / Client

Claude Code

Shell

bash

📋 Relevant Logs

$ engram conflicts show 300
Conflict Detail
  relation_id:     300
  sync_id:         rel-0ded0339935af39a
  relation:        scoped
  judgment_status: judged
  created_at:      2026-07-10 01:39:15
  updated_at:      2026-07-10 01:39:15

  source_id:       obs-8f839d61c753db4d
  source_title:    sdd/auditoria-cargas/apply-progress

  target_id:       obs-3228c1d6c8c7ed05
  target_title:    sdd/auditoria-cargas/tasks

💡 Additional Context

Two things make this costlier than a normal flag bug:

  1. --semantic spends model tokens. It requires ENGRAM_AGENT_CLI and spawns one headless agent process per candidate pair. The run above issued 21 model calls. An operator reaching for --dry-run to estimate that cost before committing will pay the cost and take the writes.

  2. The writes are not reversible from the CLI. engram conflicts exposes list, show, stats, scan, and deferred — there is no delete.

Suggested fixes, in order of value:

  • Thread the dry-run flag into the semantic pass so it evaluates and reports without persisting.
  • Have the dry-run print the verdicts it would write (source, target, relation), which is the output an operator actually wants from a preview.
  • Make inserted count every row the command would touch, including semantic writes, so the counter cannot disagree with the store.
  • Consider conflicts delete <relation_id> for recovery.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions