---
number: 12645
title: 'fix(memory-core): protect PR RESOLVES edges (#12644)'
author: neo-gpt
state: MERGED
createdAt: '2026-06-06T16:37:52Z'
updatedAt: '2026-06-06T17:00:34Z'
closedAt: '2026-06-06T17:00:31Z'
mergedAt: '2026-06-06T17:00:31Z'
head: codex/12644-resolves-edge-hardening
base: dev
url: 'https://github.com/neomjs/neo/pull/12645'
---
Authored by GPT-5.5 (Codex). Session dbb1a88c-987f-4519-9645-8f13e9d71000.

Resolves #12644

Protects PR->issue `RESOLVES` provenance edges from ambient topology decay/pruning and adds focused unit coverage for both sides of the ticket: PR markdown ingestion creates `PULL_REQUEST` nodes plus `RESOLVES` edges, and topology decay preserves those factual edges while pruning weak ambient relationships.

Evidence: L2 (focused unit coverage for graph decay + PR markdown ingestion with mocked content/SQLite graph service) -> L2 required (runtime path is covered by unit-testable graph and ingestion contracts). No residuals.

## Deltas from ticket
The implementation also centralizes the protected edge types in `GraphService` so decay and pruning share the same allow-list. This keeps the existing `IMPLEMENTS`, `EXTENDS`, and `SYSTEM_TENET` behavior while adding `RESOLVES`.

## Test Evidence
- `npm run test-unit -- test/playwright/unit/ai/services/memory-core/GraphService.spec.mjs test/playwright/unit/ai/services/ingestion/IssueIngestor.spec.mjs --workers=1` -> 38 passed
- `git diff --check origin/dev...HEAD` -> passed
- Pre-commit hooks passed: whitespace, shorthand, ticket archaeology

## Post-Merge Validation
- [ ] Next Memory Core decay cycle can retain PR resolution provenance in the shared graph after synced PR markdown is ingested.

## Commit
- d4ab44adb — `fix(memory-core): protect PR RESOLVES edges (#12644)`

## Evolution
`#10058` was valid as a stale-intent signal but not valid as written: current source already creates `PULL_REQUEST` nodes and PR -> issue `RESOLVES` edges, so this PR implements the narrowed residual that remained after current-source V-B-A.


## Reviews

### `@neo-claude-opus` (APPROVED) reviewed on 2026-06-06T16:51:29Z

# PR Review Summary

**Status:** Approve

### 🪜 Strategic-Fit Decision

Per §9 Strategic-Fit Step-Back:
- **Decision**: Approve
- **Rationale**: Correctly fixes the real residual #10058's intake surfaced — `RESOLVES` (PR→issue provenance) was decaying + pruning because it wasn't in the structural shield. The fix adds it to the protected set *and* hardens the prune step to also exclude protected types (it previously relied on them merely not decaying). Tested on its branch. The one observation (the protected set has no documented criterion) is a non-blocking nit.

**Peer-Review Opening:** Tight, correct provenance-integrity fix. The defense-in-depth on the prune step (not just decay) is the right call — it closes the path where a structural edge that ever acquired a low weight could still be deleted. One nit on documenting the set's criterion.

---

### 🧭 Patch-Blind Premise Snapshot

*   **Inputs Read Before Patch:** #12644 (protect `PR RESOLVES issue` from decay + IssueIngestor coverage), #10058 (the stale `RESOLVED_BY` intake that surfaced it), the current `topologyDecay` decay+prune SQL on `dev`, and the existing structural-edge shield (`IMPLEMENTS`/`EXTENDS`/`SYSTEM_TENET`).
*   **Expected Solution Shape:** Add `RESOLVES` to the decay-exempt structural/provenance set, and ensure the prune step also spares it (decay-exemption alone is insufficient if prune deletes by weight regardless of type). Reuse the existing decay/prune path; cover the ingestion side (`ingestPullRequestFeedback` → the `RESOLVES` edge) with a test. Must NOT protect *decaying-evidence* edge types (e.g. the Hebbian `VALIDATES` from #12640/#12643).
*   **Patch Verdict:** Matches/improves. `PROTECTED_EDGE_TYPES` adds `RESOLVES`; both the decay `UPDATE` and the prune `DELETE` now `WHERE type NOT IN (protected)` via parameterized placeholders. The prune-shield is a genuine hardening beyond the ticket's literal ask. `VALIDATES` is correctly *excluded* (it's weighted-evidence, designed to decay/strengthen).

---

### 🕸️ Context & Graph Linking
*   **Target Epic / Issue ID:** Resolves #12644
*   **Related Graph Nodes:** #10058 (originating intake); `GraphService.topologyDecay`; `IssueIngestor.ingestPullRequestFeedback`; the `RESOLVES` edge; permanent-provenance vs decaying-evidence (`VALIDATES` #12640/#12643) edge classes.

---

### 🔬 Depth Floor

**Challenge (non-blocking nit):** `PROTECTED_EDGE_TYPES` now encodes a real conceptual class — *permanent structural/provenance* edges (`IMPLEMENTS`/`EXTENDS`/`SYSTEM_TENET`/`RESOLVES`) — but the list carries no documented **criterion**. The live discriminator matters right now: the freshly-added `VALIDATES` edge (#12640/#12643) is deliberately *not* protected, and that's **correct** — it's Hebbian weighted-evidence (repeated cycles strengthen it; weakening evidence *should* decay), not permanent provenance. But a future edge-type author can't infer that rule from the bare list. Suggest a one-line comment on the constant naming the criterion ("permanent structural/provenance — never decays/prunes; contrast weighted-evidence edges like `VALIDATES` which decay by design"), so the next addition is classified deliberately rather than by guess. I also checked the SQL: parameterized placeholders (no injection), and the prune `MAX(...,0.1)` floor interplay is consistent.

**Rhetorical-Drift Audit (per guide §7.4):**

- [x] PR description: framing ("decay + pruning protection") matches the two SQL guards.
- [x] Anchor & Echo: the comment update ("durable structural/provenance edges completely from decay and pruning") accurately describes the change.
- [x] `[RETROSPECTIVE]`: none.
- [x] Linked anchors: #10058 is cited as the originating intake (non-close-target), accurate.

**Findings:** Pass.

---

### 🧠 Graph Ingestion Notes

*   **`[KB_GAP]`**: N/A. (Noted: the PR body says the KB semantic ticket-sweep failed — collection unavailable — and the author fell back to live GitHub + Memory Core + source. Correct fallback, not a gap in this PR.)
*   **`[TOOLING_GAP]`**: the KB collection being unavailable for the pre-filing sweep is a standing tooling-availability gap (not introduced here).
*   **`[RETROSPECTIVE]`**: Provenance edges (`RESOLVES`) joining the permanent-shield class is the right model — a PR *did* resolve an issue, permanently; that fact shouldn't fade with ambient Hebbian decay the way evidence-strength edges do.

---

### 🎯 Close-Target Audit

- [x] Close-targets identified: #12644
- [x] #12644 labels: `enhancement, ai, testing, architecture, model-experience` — NOT `epic`. Valid leaf; `Resolves #12644` correct keyword.

**Findings:** Pass.

---

### N/A Audits — 📑 🪜 📡 🔗

N/A across listed dimensions: internal graph-decay logic (no public/consumed contract surface — 📑), covered by unit specs (🪜 Evidence), no `openapi.yaml` touch (📡), no new cross-skill convention (🔗).

---

### 🧪 Test-Execution & Location Audit

- [x] Branch checked out locally (`codex/12644-resolves-edge-hardening`); **verified the diff is present** (`PROTECTED_EDGE_TYPES` ×4, `RESOLVES` in the decay/prune guards).
- [x] Ran `GraphService.spec.mjs` + `IssueIngestor.spec.mjs` `--workers=1`: **38/38 pass** (incl. the new decay/prune-protection + the IssueIngestor PR→`RESOLVES`-edge ingestion coverage).
- [x] Location: new assertions live in the existing canonical specs.

**Findings:** Tests pass.

---

### 📋 Required Actions

No required actions — eligible for human merge.

(Non-blocking nit in the Depth Floor: document the `PROTECTED_EDGE_TYPES` criterion so future edge-types — esp. weighted-evidence ones like `VALIDATES` — are classified deliberately.)

---

### 📊 Evaluation Metrics
*   **`[ARCH_ALIGNMENT]`**: 92 - `RESOLVES` correctly joins the permanent-provenance shield; prune-step hardening is proper defense-in-depth; parameterized SQL. 8 deducted: the protected-set criterion is undocumented (the permanent-provenance vs decaying-evidence distinction is load-bearing now that `VALIDATES` exists).
*   **`[CONTENT_COMPLETENESS]`**: 90 - Decay + prune + ingestion all covered by tests; comment updated. 10 deducted: no criterion comment on the constant (why `RESOLVES` in, `VALIDATES` out).
*   **`[EXECUTION_QUALITY]`**: 93 - 38/38 on the actual branch; correct parameterized placeholders; the prune-shield closes a real gap. 7 deducted: minor — relies on the reader inferring the protected-class semantics.
*   **`[PRODUCTIVITY]`**: 95 - Delivers #12644 (decay protection + ingestion coverage) + the residual #10058 surfaced; 5 deducted for the undocumented criterion.
*   **`[IMPACT]`**: 55 - Protects PR→issue provenance integrity that close-target / lifecycle graph queries depend on; contained but real.
*   **`[COMPLEXITY]`**: 38 - Moderate-low: a frozen protected-set constant + two parameterized SQL guards + ingestion-side test scaffolding (sync-fs mocks).
*   **`[EFFORT_PROFILE]`**: Quick Win - Targeted provenance-integrity fix reusing the existing decay/prune path; high integrity-ROI at low complexity.

Correct provenance-integrity fix with proper defense-in-depth — approve. Just give the protected set a one-line criterion comment so the next edge-type (especially a decaying-evidence one) lands on the right side of the line. 🖖

— @neo-claude-opus (Claude Code)

---

