Skip to content

Commit 3a76e8b

Browse files
committed
feat: load docs from runtime git cache
1 parent d894dc3 commit 3a76e8b

12 files changed

Lines changed: 803 additions & 13 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
IMPLEMENTATION.md
22
AGENTS.md
3-
.idea
3+
.idea
4+
.atlas/

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ forj marketplace make:job sync-catalog
4747

4848
## Development
4949

50+
```bash
51+
make build
52+
make release-check
53+
make test
54+
make vet
55+
```
56+
57+
At runtime, Atlas reads docs from `GOFORJ_DOCS_PATH` when set. Otherwise it
58+
clones or refreshes `github.com/goforj/docs` in the user's cache directory,
59+
loads the Markdown tree into memory, and serves MCP docs tools from memory.
60+
Atlas uses the `git` executable when it is available and silently falls back to
61+
native Go git support when it is not.
62+
63+
Atlas is consumed by GoForj as a Go module, not as a prebuilt binary. A release
64+
should run `make release-check`, tag the module, and then bump GoForj to that
65+
tag. The normal docs path is a local git cache loaded into memory by the MCP
66+
server, so Atlas does not need to commit a copied docs tree.
67+
68+
Equivalent direct validation:
69+
5070
```bash
5171
GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test ./...
5272
```

docs/cache.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package docs
2+
3+
import (
4+
"context"
5+
"errors"
6+
"sync"
7+
)
8+
9+
// CachingProvider loads docs once and serves them from memory afterward.
10+
type CachingProvider struct {
11+
Provider Provider
12+
13+
mu sync.Mutex
14+
loaded bool
15+
manifest Manifest
16+
documents []Document
17+
}
18+
19+
// Manifest returns cached docs metadata.
20+
func (p *CachingProvider) Manifest(ctx context.Context) (Manifest, error) {
21+
if err := p.load(ctx); err != nil {
22+
return Manifest{}, err
23+
}
24+
return p.manifest, nil
25+
}
26+
27+
// Documents returns cached Markdown documents.
28+
func (p *CachingProvider) Documents(ctx context.Context) ([]Document, error) {
29+
if err := p.load(ctx); err != nil {
30+
return nil, err
31+
}
32+
return append([]Document(nil), p.documents...), nil
33+
}
34+
35+
// load keeps docs retrieval fast after the first successful provider read.
36+
func (p *CachingProvider) load(ctx context.Context) error {
37+
p.mu.Lock()
38+
defer p.mu.Unlock()
39+
40+
if p.loaded {
41+
return nil
42+
}
43+
44+
if p.Provider == nil {
45+
return errors.New("docs provider is not configured")
46+
}
47+
documents, err := p.Provider.Documents(ctx)
48+
if err != nil {
49+
return err
50+
}
51+
manifest, err := p.Provider.Manifest(ctx)
52+
if err != nil {
53+
return err
54+
}
55+
p.documents = append([]Document(nil), documents...)
56+
p.manifest = manifest
57+
p.loaded = true
58+
return nil
59+
}

docs/cache_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package docs
2+
3+
import (
4+
"context"
5+
"testing"
6+
)
7+
8+
func TestCachingProviderLoadsDocumentsOnce(t *testing.T) {
9+
source := &countingProvider{
10+
manifest: Manifest{Version: "test", Revision: "rev1"},
11+
documents: []Document{
12+
{Path: "index.md", Title: "Home", Content: "# Home\n"},
13+
},
14+
}
15+
provider := &CachingProvider{Provider: source}
16+
17+
for i := 0; i < 3; i++ {
18+
documents, err := provider.Documents(context.Background())
19+
if err != nil {
20+
t.Fatalf("documents: %v", err)
21+
}
22+
if len(documents) != 1 {
23+
t.Fatalf("expected one document, got %d", len(documents))
24+
}
25+
}
26+
if source.documentCalls != 1 {
27+
t.Fatalf("expected one source document load, got %d", source.documentCalls)
28+
}
29+
}
30+
31+
type countingProvider struct {
32+
manifest Manifest
33+
documents []Document
34+
manifestCalls int
35+
documentCalls int
36+
}
37+
38+
func (p *countingProvider) Manifest(context.Context) (Manifest, error) {
39+
p.manifestCalls++
40+
return p.manifest, nil
41+
}
42+
43+
func (p *countingProvider) Documents(context.Context) ([]Document, error) {
44+
p.documentCalls++
45+
return p.documents, nil
46+
}

docs/fallback.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package docs
2+
3+
import (
4+
"context"
5+
"errors"
6+
)
7+
8+
// DefaultProvider returns Atlas docs using a local override or cached git docs.
9+
func DefaultProvider(version string) Provider {
10+
providers := []Provider{}
11+
if provider, ok := ProviderFromEnv(version); ok {
12+
providers = append(providers, provider)
13+
}
14+
providers = append(providers, NewGitProvider(version))
15+
return &CachingProvider{Provider: FallbackProvider{Providers: providers}}
16+
}
17+
18+
// FallbackProvider tries docs providers in order until one returns documents.
19+
type FallbackProvider struct {
20+
Providers []Provider
21+
}
22+
23+
// Manifest returns metadata for the first provider that can return documents.
24+
func (p FallbackProvider) Manifest(ctx context.Context) (Manifest, error) {
25+
provider, err := p.provider(ctx)
26+
if err != nil {
27+
return Manifest{}, err
28+
}
29+
return provider.Manifest(ctx)
30+
}
31+
32+
// Documents returns documents from the first available provider.
33+
func (p FallbackProvider) Documents(ctx context.Context) ([]Document, error) {
34+
provider, err := p.provider(ctx)
35+
if err != nil {
36+
return nil, err
37+
}
38+
return provider.Documents(ctx)
39+
}
40+
41+
// provider picks the first source with real documents so broken overrides do not hide git docs.
42+
func (p FallbackProvider) provider(ctx context.Context) (Provider, error) {
43+
var lastErr error
44+
for _, provider := range p.Providers {
45+
if provider == nil {
46+
continue
47+
}
48+
documents, err := provider.Documents(ctx)
49+
if err != nil {
50+
lastErr = err
51+
continue
52+
}
53+
if len(documents) > 0 {
54+
return provider, nil
55+
}
56+
}
57+
if lastErr != nil {
58+
return nil, lastErr
59+
}
60+
return nil, errors.New("no docs providers available")
61+
}

docs/fs.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (p FSProvider) Manifest(ctx context.Context) (Manifest, error) {
5252

5353
// Documents loads Markdown documents from the provider root.
5454
func (p FSProvider) Documents(ctx context.Context) ([]Document, error) {
55-
root, err := filepath.Abs(p.Root)
55+
root, err := filepath.Abs(docsRoot(p.Root))
5656
if err != nil {
5757
return nil, err
5858
}
@@ -113,3 +113,15 @@ func firstHeading(content string, fallback string) string {
113113
}
114114
return fallback
115115
}
116+
117+
func docsRoot(root string) string {
118+
if dirExists(filepath.Join(root, "docs")) {
119+
return filepath.Join(root, "docs")
120+
}
121+
return root
122+
}
123+
124+
func dirExists(path string) bool {
125+
info, err := os.Stat(path)
126+
return err == nil && info.IsDir()
127+
}

0 commit comments

Comments
 (0)