Skip to content

Commit 349240a

Browse files
committed
refactor(hickey,lowy): derive NoteMatch.uri in ToJSON to remove redundant field
The uri field on NoteMatch was a derived value (noteUriPrefix <> path) stored alongside its source, with the rule enforced only inside noteMatchOf. Two consequences: * The pure result type imported wire-layer concerns (Emanote.MCP.Uri + Catalog ResourceKind) for a field every consumer could derive. * Two ways to drift: if noteUriPrefix ever changes, every site that reads NoteMatch.uri sees the stale value. Drop uri from the record and emit it from the manual ToJSON instance. Wire shape is unchanged (clients still receive uri); construction is concentrated in one place. The test asserts on the JSON payload now that the field no longer exists on the Haskell value.
1 parent db678c3 commit 349240a

2 files changed

Lines changed: 30 additions & 22 deletions

File tree

emanote/src/Emanote/MCP/Tools.hs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
{-# LANGUAGE DeriveAnyClass #-}
21
{-# LANGUAGE DuplicateRecordFields #-}
2+
{-# LANGUAGE NamedFieldPuns #-}
33

44
{- | MCP query tools (phase 3).
55
@@ -29,8 +29,7 @@ import Data.Aeson qualified as Aeson
2929
import Data.IxSet.Typed qualified as Ix
3030
import Data.Map.Strict qualified as Map
3131
import Data.Text qualified as T
32-
import Emanote.MCP.Catalog (ResourceKind (Note))
33-
import Emanote.MCP.Uri (kindToUri)
32+
import Emanote.MCP.Uri (noteUriPrefix)
3433
import Emanote.Model (Model)
3534
import Emanote.Model qualified as M
3635
import Emanote.Model.Graph qualified as G
@@ -73,35 +72,41 @@ tools readModel =
7372
data NoteMatch = NoteMatch
7473
{ path :: Text
7574
, title :: Text
76-
, uri :: Text
7775
}
7876
deriving stock (Eq, Show, Generic)
79-
deriving anyclass (ToJSON)
77+
78+
{- | The @uri@ field is derived from @path@ so there is no way for the two to
79+
diverge: drift in 'noteUriPrefix' propagates to every consumer through
80+
one place.
81+
-}
82+
instance ToJSON NoteMatch where
83+
toJSON NoteMatch {path, title} =
84+
Aeson.object
85+
[ "path" .= path
86+
, "title" .= title
87+
, "uri" .= (noteUriPrefix <> path)
88+
]
8089

8190
noteMatchOf :: N.Note -> NoteMatch
8291
noteMatchOf note =
83-
let p = toText $ R.lmlSourcePath (note ^. N.noteRoute)
84-
in NoteMatch
85-
{ path = p
86-
, title = Tit.toPlain (note ^. N.noteTitle)
87-
, uri = kindToUri (Note (toString p))
88-
}
92+
NoteMatch
93+
{ path = toText $ R.lmlSourcePath (note ^. N.noteRoute)
94+
, title = Tit.toPlain (note ^. N.noteTitle)
95+
}
8996

9097
{- | Build a 'NoteMatch' from a route. Falls back to a route-derived title
9198
when the note can't be looked up — used by callers that hold a route but
9299
not the 'N.Note' (e.g. backlink sources).
93100
-}
94101
noteMatchOfRoute :: Model -> R.LMLRoute -> NoteMatch
95102
noteMatchOfRoute model r =
96-
case M.modelLookupNoteByRoute' r model of
97-
Just note -> noteMatchOf note
98-
Nothing ->
99-
let p = toText $ R.lmlSourcePath r
100-
in NoteMatch
101-
{ path = p
102-
, title = Tit.toPlain (Tit.fromRoute r)
103-
, uri = kindToUri (Note (toString p))
104-
}
103+
maybe fallback noteMatchOf (M.modelLookupNoteByRoute' r model)
104+
where
105+
fallback =
106+
NoteMatch
107+
{ path = toText $ R.lmlSourcePath r
108+
, title = Tit.toPlain (Tit.fromRoute r)
109+
}
105110

106111
-- ---------------------------------------------------------------------------
107112
-- find_notes

emanote/test/Emanote/MCP/ToolsSpec.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module Emanote.MCP.ToolsSpec where
22

3+
import Data.Aeson qualified as Aeson
4+
import Data.Text qualified
35
import Emanote.MCP.Tools (NoteMatch (..), ResolveResult (..), findNotes, getBacklinks, resolveWikilink)
46
import Emanote.Model.Note qualified as MN
57
import Emanote.Model.Type qualified as M
@@ -57,9 +59,10 @@ spec = do
5759
it "treats a non-positive limit as zero" $ do
5860
findNotes "guide" 0 notebook `shouldBe` []
5961

60-
it "advertises an emanote:// URI for every match" $ do
62+
it "advertises an emanote:// URI for every match in the JSON payload" $ do
6163
let [hit] = findNotes "wiki" 20 notebook
62-
uri hit `shouldBe` "emanote://note/guide/wikilinks.md"
64+
json = decodeUtf8 @Text (Aeson.encode hit)
65+
json `shouldSatisfy` Data.Text.isInfixOf "\"uri\":\"emanote://note/guide/wikilinks.md\""
6366

6467
describe "getBacklinks" $ do
6568
let target = LMLRoute_Md (R ("guide" :| ["neuron"]))

0 commit comments

Comments
 (0)