Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Archive Report: suggest-relations-provenance (Closes #135)

**Date Archived**: 2026-07-25
**Status**: ARCHIVED
**Change**: suggest-relations-provenance
**GitHub Issue**: #135 (Closed)
**PR**: #179 (Merged)

## Summary

The `suggest-relations-provenance` change has been fully implemented, verified (PASS, 3/3 requirements, 14/14 scenarios), reviewed via full 4R (Risk/Reliability clean; Resilience 1 finding fixed with regression test), and archived. The change introduces provenance-mirror edge synthesis at graph-projection read time to automatically type edges that mirror frontmatter `provenance:` entries as `derived_from`, eliminating redundant LLM calls for deterministic fact derivations.

## Change Scope

**Title**: Type Provenance-Mirror Edges As `derived_from` At Projection Time

**Type**: Enhancement (graph analysis optimization)

**Capabilities Modified**:
- `graph-projection`: synthesize `relation_type="derived_from"` for untyped body-link edges whose target is a member of the source document's `provenance:` frontmatter list
- `llm-edge-production`: exclude provenance-mirror edges from suggestion candidates (now typed, not NULL)
- `contradiction-detection`: exclude `derived_from`-typed edges from contradiction candidate generation

## Delta Specs Merged

Three delta specs have been merged into the main specification tree:

| Domain | Requirement | Key Change |
|--------|-------------|-----------|
| `graph-projection` | Edge `relation_type` Populated From Frontmatter `relations:` And Provenance-Mirror Synthesis | Added synthesis of `derived_from` for provenance-mirror edges at projection read time |
| `llm-edge-production` | Read-Only Suggestion Of Relation Types For Untyped Links | Clarified exclusion of provenance-mirror edges (now typed) from suggestion candidates |
| `contradiction-detection` | Candidate Generation From Typed Graph Edges, Deduped | Added explicit exclusion of `derived_from`-typed edges from contradiction candidate pairs |

**Main Spec Files Updated**:
- `/Users/jasonssdev/Dev/Projects/openkos/openspec/specs/graph-projection/spec.md`
- `/Users/jasonssdev/Dev/Projects/openkos/openspec/specs/llm-edge-production/spec.md`
- `/Users/jasonssdev/Dev/Projects/openkos/openspec/specs/contradiction-detection/spec.md`

## Verification Results

**Verdict**: PASS

| Metric | Value |
|--------|-------|
| Requirements Implemented | 3/3 (100%) |
| Scenarios Passing | 14/14 (100%) |
| Test Suite | 2156 passed, 0 failed, exit 0 |
| Quality Gate | All checks passed (ruff, mypy) |
| Tasks Complete | 26/26 (100%) |
| Changed Lines | 524 insertions + 17 deletions (well under 400-line budget) |

**Key Compliance**:
- Graph projection correctly synthesizes `relation_type="derived_from"` for provenance-mirror edges via exact-id-match set membership on `provenance:` frontmatter
- Suggest-relations naturally excludes provenance-mirror edges (automatic, no code change to `edge_typing.py`)
- Contradiction detection properly guards `derived_from` edges from candidate generation (type-based, applies regardless of edge origin)
- Projection replaces (never duplicates) the edge row; edge count unchanged
- Malformed provenance frontmatter degrades gracefully without crashing

## Review Findings

**4R Review Results**:
- **Risk Lens**: Clean — no security, permission, data loss, or architectural issues
- **Reliability Lens**: Clean — all tests pass independently, TDD compliance verified
- **Resilience Lens**: 1 WARNING finding discovered and fixed
- Issue: `_pair_relation_types` mislabeled contradiction pairs containing both genuine and provenance-mirror edges
- Fix: Prefer genuine (non-`derived_from`) typed edges for relation labeling
- Regression Test: `test_find_contradictions_relation_label_prefers_genuine_type_over_derived_from` added and passing
- **Readability Lens**: 2 docstring fixes applied for accuracy
- `sqlite_graph.py`: Updated function docstring to reflect provenance-mirror synthesis
- `analysis.py`: Corrected `to_digraph` docstring re: `relation_type` population

**Post-Review Quality**: Full suite 2157 passed after all fixes (original 2156 + 1 new regression test)

## Implementation Details

**Changed Files** (from PR #179):
- `src/openkos/graph/sqlite_graph.py`: Synthesize `derived_from` for provenance-mirror edges
- `src/openkos/resolution/contradiction.py`: Guard against `derived_from` in candidate pairs
- `tests/unit/graph/test_sqlite_graph.py`: Provenance-mirror synthesis tests
- `tests/unit/resolution/test_edge_typing.py`: Candidate exclusion tests
- `tests/unit/resolution/test_contradiction.py`: Guard verification tests (including regression test)
- `tests/unit/cli/test_suggest_relations.py`: CLI integration tests
- `tests/unit/graph/test_analysis.py`: Non-regression test for edge count preservation

**Rollback Boundary**:
- Revert `sqlite_graph.py` and `contradiction.py` hunks
- Projection is a read-only derived cache rebuilt per run
- No migration required; next reindex restores prior `None` typing

## Traceability

**Engram Artifacts** (all complete and verified):
- Proposal (ID 1936): Intent, scope, approach, risks, success criteria
- Spec (ID 1937): THREE delta specs with all 14 scenarios
- Design (ID 1938): Technical approach, architecture decisions, downstream audit
- Tasks (ID 1939): 26 TDD phases (RED/GREEN), all complete
- Apply-Progress (ID 1940): Implementation with 3 post-4R-review fixes
- Verify-Report (ID 1941): PASS verdict with full spec compliance matrix
- Archive-Report (ID from this save): Final closure record

**GitHub References**:
- Issue: #135 (closed)
- PR: #179 (merged to main)
- Commit: Merged commit in main branch

## Archive Contents

All original artifacts preserved in `/Users/jasonssdev/Dev/Projects/openkos/openspec/changes/archive/2026-07-25-suggest-relations-provenance/`:

```
.
├── proposal.md
├── design.md
├── tasks.md
├── verify-report.md
├── explore.md
├── specs/
│ ├── graph-projection/spec.md
│ ├── llm-edge-production/spec.md
│ └── contradiction-detection/spec.md
└── archive-report.md (this file)
```

## SDD Cycle Completion

The `suggest-relations-provenance` change has completed the full SDD lifecycle:

1. ✅ **Propose** (ID 1936): Intent and scope defined, risks identified, approach chosen
2. ✅ **Spec** (ID 1937): Three delta specs written with 14 scenarios
3. ✅ **Design** (ID 1938): Technical decisions made, downstream audit completed
4. ✅ **Tasks** (ID 1939): 26 implementation tasks scheduled (TDD Red/Green/Regression/QA)
5. ✅ **Apply** (ID 1940): All tasks completed + 3 post-4R fixes, 2157 tests passing
6. ✅ **Verify** (ID 1941): 3/3 requirements, 14/14 scenarios, PASS verdict
7. ✅ **Archive** (today): Change closed, main specs synced, artifacts archived

The change is ready for release on the main branch.

## Accepted Consequences

As noted in the proposal: on today's ingest-only bundles, `suggest-relations` now returns **zero candidates** (every body-link edge is a provenance-mirror edge, now typed as `derived_from`). This is intentional and honest — there are no genuine concept↔concept untyped edges to suggest until a separate follow-up change (issue #131 track) supplies extraction-LLM or mining candidate edges. The verb becomes meaningful once that work lands.

## Next Steps

No further work required for this change. Deployment to production via normal merge pipeline.

If concept↔concept edge candidates are desired, file a new SDD change linked to #131 (stable concept IDs) to define and implement candidate generation from external sources.

---

**Archived by**: SDD Archive Phase (automated)
**Archive Date**: 2026-07-25
**Status**: Complete
31 changes: 31 additions & 0 deletions openspec/specs/contradiction-detection/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ config write (S4); a persisted OKF type for the judgment result; or a seeded
from typed graph edges (`relation_type is not None`) via `build_graph`.
Each unordered pair MUST be deduped by `frozenset({source_id, target_id})`
so symmetric, duplicate, and multi-edge pairs are judged exactly once.
Candidate generation MUST NOT surface edges whose `relation_type ==
"derived_from"`: a `derived_from` relationship is a derivation/provenance
link, never a contradiction candidate. This exclusion applies to EVERY
`derived_from` edge regardless of origin — both graph-projection-synthesized
provenance-mirror edges and any hand-authored `derived_from` entry in
`relations:` frontmatter — since candidate generation has no signal to
distinguish the two, and a derivation is never a contradiction candidate
either way.
(Previously: candidate generation admitted any edge with a non-`None`
`relation_type`, with no type-specific exclusion; this adds an explicit
`derived_from` exclusion so provenance-mirror edges — now typed
`derived_from` by graph projection instead of remaining `None` — do not
newly become contradiction candidates.)

#### Scenario: Symmetric and multi-edge pairs judged once

Expand All @@ -33,6 +46,24 @@ so symmetric, duplicate, and multi-edge pairs are judged exactly once.
- WHEN `find_contradictions` runs
- THEN exactly one judgment is produced for the pair, not two

#### Scenario: Provenance-only bundle yields zero contradiction candidates

- GIVEN a bundle whose only typed edges are provenance-mirror edges typed
`derived_from` by graph projection (concept-to-source links backed by
`provenance:` frontmatter membership)
- WHEN `find_contradictions` runs
- THEN zero candidate pairs are generated, no concept-to-source pair is
judged, and no LLM call is made — matching prior behavior when those rows
were untyped and already excluded

#### Scenario: Genuine typed contradiction-eligible edge is still surfaced

- GIVEN a bundle with two event concepts connected by a `related_to`-typed
edge (not `derived_from`)
- WHEN `find_contradictions` runs
- THEN that pair is included in the candidate set and judged, confirming
the exclusion applies only to `derived_from`, not to all typed edges

### Requirement: Per-Pair Verdict Shape With Cited Claims

Each judgment MUST carry `verdict` (`CONTRADICTS`/`CONSISTENT`/`UNCERTAIN`),
Expand Down
58 changes: 50 additions & 8 deletions openspec/specs/graph-projection/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,24 @@ produce an edge.
- WHEN the projection is built
- THEN neither link produces an edge, and building does not raise

### Requirement: Edge `relation_type` Populated From Frontmatter `relations:`
### Requirement: Edge `relation_type` Populated From Frontmatter `relations:` And Provenance-Mirror Synthesis

`build_graph` MUST populate an edge's `relation_type` from the source
document's `relations:` frontmatter entry whose `target` resolves to that
edge's target node id. WHEN no matching `relations:` entry exists for an
edge, `relation_type` MUST remain `NULL`, unchanged from before. The
existing untyped `[text](/id.md)` `_LINK_RE` edge-extraction path MUST
remain unchanged for objects without a `relations:` key.
edge, the system MUST synthesize `relation_type = "derived_from"` at
projection read time IF AND ONLY IF the edge's target node id is a MEMBER
of the source document's decoded `provenance:` frontmatter list (exact id
match; membership only — never derived from link text or heading). This
synthesis MUST NOT write to `relations:` frontmatter, MUST NOT mutate
bundle bytes, and MUST NOT change ingest byte-identity. WHEN no matching
`relations:` entry exists AND the target is not a `provenance:` member,
`relation_type` MUST remain `NULL`, unchanged from before. The existing
untyped `[text](/id.md)` `_LINK_RE` edge-extraction path MUST remain
unchanged for objects without a `relations:` key.
(Previously: absent a `relations:` match, `relation_type` always stayed
`NULL` regardless of `provenance:` frontmatter; this adds provenance-mirror
synthesis as a second, projection-only typing source.)

#### Scenario: Typed relation edge carries its relation_type

Expand All @@ -131,13 +141,45 @@ remain unchanged for objects without a `relations:` key.
- WHEN the projection is built
- THEN the edge to `concepts/x` has `relation_type == "depends_on"`

#### Scenario: Untyped-link edge remains NULL relation_type
#### Scenario: Untyped-link edge with no provenance match remains NULL

- GIVEN a document with no `relations:` key whose body contains a
bundle-relative markdown link
bundle-relative markdown link to a target that is NOT a member of the
document's `provenance:` frontmatter list (or `provenance:` is missing or
empty)
- WHEN the projection is built
- THEN the resulting edge's `relation_type` is `NULL`, matching prior
behavior
- THEN the resulting edge's `relation_type` is `NULL`

#### Scenario: Provenance-mirror link to a source is synthesized as derived_from

- GIVEN a concept document with `provenance: [sources/foo]` and a
`## Related` body link `[foo](/sources/foo.md)`
- WHEN the projection is built
- THEN the edge from the concept to `sources/foo` has `relation_type ==
"derived_from"`, and no bundle file is modified

#### Scenario: Provenance-mirror link to a cited concept is also synthesized

- GIVEN a `query --save` concept document with `provenance: [concepts/bar]`
and a `## Related` body link `[bar](/concepts/bar.md)`
- WHEN the projection is built
- THEN the edge from the concept to `concepts/bar` has `relation_type ==
"derived_from"`, based on `provenance:` list membership, not the
`sources/` prefix

#### Scenario: Genuine concept-to-concept link outside provenance stays untyped

- GIVEN a concept document whose `provenance:` list does not include a link
target that the body also links to
- WHEN the projection is built
- THEN that edge's `relation_type` remains `NULL`

#### Scenario: Multi-entry provenance list types each matching target

- GIVEN a document with `provenance: [sources/a, sources/b]` and `##
Related` links to both `sources/a` and `sources/b`
- WHEN the projection is built
- THEN both edges have `relation_type == "derived_from"`

### Requirement: GraphStore Protocol Defines The Derived-Layer Surface

Expand Down
28 changes: 27 additions & 1 deletion openspec/specs/llm-edge-production/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ graph projection and, for each, MUST print an LLM-suggested relation
file, index, or log. Every printed suggested type MUST be a value accepted
by the existing `validate_relation_type` check. The candidate set MUST be
restricted to untyped edges only; edges that already carry a `relation_type`
MUST NOT be listed as suggestion candidates.
MUST NOT be listed as suggestion candidates. Because graph projection now
synthesizes `relation_type = "derived_from"` for provenance-mirror edges
(edges whose target is a member of the source document's `provenance:`
frontmatter list), those edges carry a `relation_type` and MUST NOT be
listed as candidates, and MUST NOT trigger an LLM call.
(Previously: the candidate set excluded only edges typed via `relations:`
frontmatter; it now also excludes edges typed by provenance-mirror
projection synthesis, with no code path distinction required since both
sources populate the same `relation_type` field read by this requirement.)

#### Scenario: Verb lists every untyped edge with a valid suggestion

Expand All @@ -51,6 +59,24 @@ MUST NOT be listed as suggestion candidates.
- THEN only the untyped edge appears in the output; the already-typed edge
is not re-suggested

#### Scenario: Bundle with only provenance-mirror edges surfaces zero candidates

- GIVEN a bundle whose only body links are provenance-mirror edges (every
link target is a member of its source document's `provenance:`
frontmatter list, now typed `derived_from` by graph projection)
- WHEN the suggestion verb runs
- THEN it prints zero candidate edges, makes zero LLM calls, and reports
honestly that there is nothing to type

#### Scenario: A genuine untyped concept-to-concept edge is still surfaced

- GIVEN a bundle containing one provenance-mirror edge (now typed
`derived_from`) and one genuine untyped concept-to-concept edge whose
target is not a member of its source's `provenance:` list
- WHEN the suggestion verb runs
- THEN only the genuine untyped edge is printed as a candidate with an
LLM-suggested type and rationale; the provenance-mirror edge is absent

### Requirement: Fail-Closed LLM Parsing

The system MUST parse LLM output fail-closed: a malformed or partial
Expand Down