From ad934fdf666050c7ba75273b2505d5e7a8d672ff Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 25 Jul 2026 08:52:15 -0500 Subject: [PATCH] chore(sdd): archive suggest-relations-provenance (#135) Merge the graph-projection, llm-edge-production, and contradiction-detection delta specs into the main specs tree and move the change folder to the archive. Closes the SDD cycle for #135 (provenance-mirror edges typed derived_from at projection + the contradiction candidate guard). --- .../archive-report.md | 152 ++++++++++++++++++ .../design.md | 0 .../explore.md | 0 .../proposal.md | 0 .../specs/contradiction-detection/spec.md | 0 .../specs/graph-projection/spec.md | 0 .../specs/llm-edge-production/spec.md | 0 .../tasks.md | 0 .../verify-report.md | 0 .../specs/contradiction-detection/spec.md | 31 ++++ openspec/specs/graph-projection/spec.md | 58 ++++++- openspec/specs/llm-edge-production/spec.md | 28 +++- 12 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 openspec/changes/archive/2026-07-25-suggest-relations-provenance/archive-report.md rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/design.md (100%) rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/explore.md (100%) rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/proposal.md (100%) rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/specs/contradiction-detection/spec.md (100%) rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/specs/graph-projection/spec.md (100%) rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/specs/llm-edge-production/spec.md (100%) rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/tasks.md (100%) rename openspec/changes/{suggest-relations-provenance => archive/2026-07-25-suggest-relations-provenance}/verify-report.md (100%) diff --git a/openspec/changes/archive/2026-07-25-suggest-relations-provenance/archive-report.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/archive-report.md new file mode 100644 index 0000000..bde8e14 --- /dev/null +++ b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/archive-report.md @@ -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 diff --git a/openspec/changes/suggest-relations-provenance/design.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/design.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/design.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/design.md diff --git a/openspec/changes/suggest-relations-provenance/explore.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/explore.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/explore.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/explore.md diff --git a/openspec/changes/suggest-relations-provenance/proposal.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/proposal.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/proposal.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/proposal.md diff --git a/openspec/changes/suggest-relations-provenance/specs/contradiction-detection/spec.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/specs/contradiction-detection/spec.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/specs/contradiction-detection/spec.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/specs/contradiction-detection/spec.md diff --git a/openspec/changes/suggest-relations-provenance/specs/graph-projection/spec.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/specs/graph-projection/spec.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/specs/graph-projection/spec.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/specs/graph-projection/spec.md diff --git a/openspec/changes/suggest-relations-provenance/specs/llm-edge-production/spec.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/specs/llm-edge-production/spec.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/specs/llm-edge-production/spec.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/specs/llm-edge-production/spec.md diff --git a/openspec/changes/suggest-relations-provenance/tasks.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/tasks.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/tasks.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/tasks.md diff --git a/openspec/changes/suggest-relations-provenance/verify-report.md b/openspec/changes/archive/2026-07-25-suggest-relations-provenance/verify-report.md similarity index 100% rename from openspec/changes/suggest-relations-provenance/verify-report.md rename to openspec/changes/archive/2026-07-25-suggest-relations-provenance/verify-report.md diff --git a/openspec/specs/contradiction-detection/spec.md b/openspec/specs/contradiction-detection/spec.md index 2253743..828848e 100644 --- a/openspec/specs/contradiction-detection/spec.md +++ b/openspec/specs/contradiction-detection/spec.md @@ -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 @@ -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`), diff --git a/openspec/specs/graph-projection/spec.md b/openspec/specs/graph-projection/spec.md index c7da1dd..989a850 100644 --- a/openspec/specs/graph-projection/spec.md +++ b/openspec/specs/graph-projection/spec.md @@ -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 @@ -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 diff --git a/openspec/specs/llm-edge-production/spec.md b/openspec/specs/llm-edge-production/spec.md index b48c43e..2d50cf3 100644 --- a/openspec/specs/llm-edge-production/spec.md +++ b/openspec/specs/llm-edge-production/spec.md @@ -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 @@ -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