| 1 |
|
package mcpsrv |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"context" |
| 5 |
|
"fmt" |
| 6 |
|
|
| 7 |
|
"sourcecraft.dev/bigbes/sr-ht-spec/service" |
| 8 |
|
) |
| 9 |
|
|
| 10 |
|
type readInput struct { |
| 11 |
|
Space string `json:"space" jsonschema:"the space to read from, written \"~owner/name\" as spec_list and spec_search report it"` |
| 12 |
|
Document string `json:"document" jsonschema:"which document: its frontmatter id (\"SPEC-0007\"), or its path with or without the \".md\" extension (\"specs/0007-storage\")"` |
| 13 |
|
Rev string `json:"rev,omitempty" jsonschema:"pin the read to one immutable revision, given as a git object name (lowercase hex, as returned in the rev field of any result). Omit this to read the space's approved head, which is what an agent almost always wants: the reviewed text. Branch names are not accepted."` |
| 14 |
|
} |
| 15 |
|
|
| 16 |
|
type readOutput struct { |
| 17 |
|
Space string `json:"space"` |
| 18 |
|
// ID is how this document is addressed: its frontmatter id when that is |
| 19 |
|
// well-formed and unique in the space, otherwise its path without the |
| 20 |
|
// extension. |
| 21 |
|
ID string `json:"id"` |
| 22 |
|
// DocID is the frontmatter id when the document has a well-formed one, and |
| 23 |
|
// empty otherwise. It differs from ID exactly when the document has no |
| 24 |
|
// usable id, which is the case an agent proposing an edit needs to see. |
| 25 |
|
DocID string `json:"doc_id,omitempty"` |
| 26 |
|
Path string `json:"path"` |
| 27 |
|
// Rev is the commit this content was read at. It is immutable: pass it back |
| 28 |
|
// as the rev argument to re-read these exact bytes. It is also the value |
| 29 |
|
// the Phase 3 write plane takes as If-Match when it is an approved head. |
| 30 |
|
Rev string `json:"rev"` |
| 31 |
|
// Blob is the sha of this document's content, and changes only when the |
| 32 |
|
// content does. |
| 33 |
|
Blob string `json:"blob"` |
| 34 |
|
// Pinned reports how Rev was chosen. False means the caller named no |
| 35 |
|
// revision and this is the space's approved head as of this call — the |
| 36 |
|
// reviewed, canonical text. True means the caller pinned a revision, and |
| 37 |
|
// whether that revision is on the approved branch is the caller's business: |
| 38 |
|
// this service does not claim it either way. |
| 39 |
|
Pinned bool `json:"pinned"` |
| 40 |
|
Title string `json:"title,omitempty"` |
| 41 |
|
Section string `json:"section,omitempty"` |
| 42 |
|
// Status is the document's authored lifecycle marker (draft, review, |
| 43 |
|
// superseded). It is not approval state: a document is approved by being |
| 44 |
|
// reachable from the approved ref, never by its frontmatter. |
| 45 |
|
Status string `json:"status,omitempty"` |
| 46 |
|
Summary string `json:"summary,omitempty"` |
| 47 |
|
Tags []string `json:"tags,omitempty"` |
| 48 |
|
// Markdown is the whole document, frontmatter included, exactly as stored. |
| 49 |
|
// It is the text to edit and send back when proposing a change. |
| 50 |
|
Markdown string `json:"markdown"` |
| 51 |
|
} |
| 52 |
|
|
| 53 |
25 |
func readHandler(ctx context.Context, b Backend, in readInput) (readOutput, error) { |
| 54 |
25 |
if err := requireRead(ctx); err != nil { |
| 55 |
1 |
return readOutput{}, err |
| 56 |
1 |
} |
| 57 |
24 |
ref, err := parseSpace(in.Space) |
| 58 |
24 |
if err != nil { |
| 59 |
1 |
return readOutput{}, err |
| 60 |
1 |
} |
| 61 |
23 |
rev, err := parseRev(in.Rev) |
| 62 |
23 |
if err != nil { |
| 63 |
4 |
return readOutput{}, err |
| 64 |
4 |
} |
| 65 |
19 |
sp, err := b.Docs.OpenSpace(ctx, ref) |
| 66 |
19 |
if err != nil { |
| 67 |
2 |
return readOutput{}, missingOrDenied(err, "spec_read", noSpace(ref)) |
| 68 |
2 |
} |
| 69 |
17 |
arc, bodies, resolved, err := archiveAt(ctx, b, sp, rev) |
| 70 |
17 |
if err != nil { |
| 71 |
2 |
return readOutput{}, missingOrDenied(err, "spec_read", noRevision(ref, rev)) |
| 72 |
2 |
} |
| 73 |
15 |
page, err := resolvePage(arc, in.Document) |
| 74 |
15 |
if err != nil { |
| 75 |
3 |
return readOutput{}, err |
| 76 |
3 |
} |
| 77 |
12 |
body, ok := bodies[page.Path] |
| 78 |
12 |
if !ok { |
| 79 |
0 |
// The archive is built from these very bodies, so a page without one |
| 80 |
0 |
// is a broken invariant rather than a missing document — this service's |
| 81 |
0 |
// fault and not an answer about the corpus, so it goes to the agent as a |
| 82 |
0 |
// protocol error and the detail goes to the log. Returning an empty |
| 83 |
0 |
// markdown field would be indistinguishable from an empty document. |
| 84 |
0 |
return readOutput{}, internalError( |
| 85 |
0 |
fmt.Errorf("document %s at %s in %s has no body", page.Path, resolved, ref), "spec_read") |
| 86 |
0 |
} |
| 87 |
12 |
return readOutput{ |
| 88 |
12 |
Space: ref.String(), |
| 89 |
12 |
ID: page.ID, |
| 90 |
12 |
DocID: page.DocID, |
| 91 |
12 |
Path: page.Path, |
| 92 |
12 |
Rev: resolved, |
| 93 |
12 |
Blob: page.Blob, |
| 94 |
12 |
Pinned: rev != service.ApprovedRev, |
| 95 |
12 |
Title: page.Title, |
| 96 |
12 |
Section: page.Section, |
| 97 |
12 |
Status: string(page.Status), |
| 98 |
12 |
Summary: page.Summary, |
| 99 |
12 |
Tags: page.Tags, |
| 100 |
12 |
Markdown: string(body), |
| 101 |
12 |
}, nil |
| 102 |
|
} |