Skip to content

Commit 553fd7c

Browse files
committed
refactor: clear cmd/ purity grandfathered map
Move BuildVault from journal/cmd/obsidian to journal/core/obsidian, remove stale grandfathered entries (one, every already migrated). TestCmdDirPurity now passes with zero exceptions. Also adds gitnexus-analyze Makefile target. Spec: specs/cmd-purity-cleanup.md Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent 7e41040 commit 553fd7c

9 files changed

Lines changed: 313 additions & 278 deletions

File tree

.context/TASKS.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,34 @@ TASK STATUS LABELS:
2727

2828
### Misc
2929

30-
- [ ] gitnexus analyze --embeddings --skill : we need a make target, but also this
31-
butchers AGENTS.md and CLAUDE.md so those files need to be reverted and
32-
GITNEXUS.md should be updated accordingly. The make target should remind
33-
the user about it.
34-
35-
- [ ] SMB mount path support: add `CTX_BACKUP_SMB_MOUNT_PATH` env var so `ctx backup` can use fstab/systemd automounts instead of requiring GVFS. Spec: specs/smb-mount-path-support.md #priority:medium #added:2026-04-04-010000
30+
- [ ] SMB mount path support: add `CTX_BACKUP_MOUNT_PATH` env var so
31+
`ctx backup` can use fstab/systemd automounts instead of requiring GVFS.
32+
Spec: specs/smb-mount-path-support.md #priority:medium #added:2026-04-04-010000
3633

3734
### Architecture Docs
3835

39-
- [ ] Publish architecture docs to docs/: copy ARCHITECTURE.md, DETAILED_DESIGN domain files, and CHEAT-SHEETS.md to docs/reference/. Sanitize intervention points into docs/contributing/. Exclude DANGER-ZONES.md and ARCHITECTURE-PRINCIPAL.md (internal only). Spec: specs/publish-architecture-docs.md #priority:medium #added:2026-04-03-150000
36+
- [ ] Publish architecture docs to docs/: copy ARCHITECTURE.md,
37+
DETAILED_DESIGN domain files, and CHEAT-SHEETS.md to docs/reference/.
38+
Sanitize intervention points into docs/contributing/.
39+
Exclude DANGER-ZONES.md and ARCHITECTURE-PRINCIPAL.md (internal only).
40+
Spec: specs/publish-architecture-docs.md #priority:medium #added:2026-04-03-150000
4041

41-
- [ ] Update ctx-architecture skill to append discovered terms to GLOSSARY.md during Phase 3. Additive only, max 10 terms per run, project-specific only, alphabetical insertion, skip if GLOSSARY.md empty. Print added terms in convergence report. Spec: specs/publish-architecture-docs.md #priority:low #added:2026-04-03-153000
42+
- [ ] Update ctx-architecture skill to append discovered terms to GLOSSARY.md
43+
during Phase 3. Additive only, max 10 terms per run, project-specific only,
44+
alphabetical insertion, skip if GLOSSARY.md empty. Print added terms in
45+
convergence report. Spec: specs/publish-architecture-docs.md #priority:low #added:2026-04-03-153000
4246

4347
### Code Cleanup Findings
4448

4549

46-
- [ ] Extend flagbind helpers (IntFlag, DurationFlag, DurationFlagP, StringP, BoolP) and migrate ~50 call sites to unblock TestNoFlagBindOutsideFlagbind #added:2026-04-01-233250
50+
- [x] Extend flagbind helpers (IntFlag, DurationFlag, DurationFlagP, StringP,
51+
BoolP) and migrate ~50 call sites to unblock TestNoFlagBindOutsideFlagbind
52+
#added:2026-04-01-233250
4753

4854
- [ ] Implement journal compaction: Elastic-style tiered storage with tar.gz
4955
backup. Spec: specs/journal-compact.md #added:2026-03-31-110005
5056

51-
- [ ] Refactor 28 grandfathered cmd/ purity violations found by
57+
- [x] Refactor 28 grandfathered cmd/ purity violations found by
5258
TestCmdDirPurity: move unexported helpers, exported non-Cmd/Run functions,
5359
and types from cmd/ directories to core/. See grandfathered map in
5460
compliance_test.go for the full list. #priority:medium #added:2026-03-31-005115

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ register-mcp:
228228
@./hack/register-gemini-search.sh
229229
@./hack/register-gitnexus.sh
230230

231+
## gitnexus-analyze: Updates gitnexus embeddings and skill.
232+
gitnexus-analyze:
233+
gitnexus analyze --embeddings --skill
234+
echo "GitNexus updated AGENTS.md and CLAUDE.md -- DO NOT COMMIT THEM!"
235+
231236
## gemini-search: Register gemini-search MCP server with Claude Code
232237
gemini-search:
233238
@./hack/register-gemini-search.sh

internal/cli/journal/cmd/obsidian/doc.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// Package obsidian implements the ctx journal obsidian subcommand.
88
//
99
// [Cmd] builds the cobra.Command with --output flag. [Run]
10-
// generates an Obsidian vault from journal entries with wikilinks,
11-
// topic pages, and frontmatter. [BuildVault] handles the file
12-
// generation pipeline.
10+
// delegates to core/obsidian.BuildVault to generate an
11+
// Obsidian vault from journal entries.
1312
package obsidian

internal/cli/journal/cmd/obsidian/run.go

Lines changed: 2 additions & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -7,260 +7,25 @@
77
package obsidian
88

99
import (
10-
"fmt"
11-
"os"
1210
"path/filepath"
1311

1412
"github.com/spf13/cobra"
1513

16-
"github.com/ActiveMemory/ctx/internal/assets/tpl"
17-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/consolidate"
18-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/format"
19-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/frontmatter"
20-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/moc"
21-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/parse"
22-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/reduce"
23-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/section"
24-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/turn"
25-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/wikilink"
26-
"github.com/ActiveMemory/ctx/internal/cli/journal/core/wrap"
14+
coreObsidian "github.com/ActiveMemory/ctx/internal/cli/journal/core/obsidian"
2715
"github.com/ActiveMemory/ctx/internal/config/dir"
28-
"github.com/ActiveMemory/ctx/internal/config/file"
29-
"github.com/ActiveMemory/ctx/internal/config/fs"
30-
"github.com/ActiveMemory/ctx/internal/config/obsidian"
31-
errFs "github.com/ActiveMemory/ctx/internal/err/fs"
32-
errJournal "github.com/ActiveMemory/ctx/internal/err/journal"
33-
"github.com/ActiveMemory/ctx/internal/io"
3416
"github.com/ActiveMemory/ctx/internal/rc"
35-
"github.com/ActiveMemory/ctx/internal/write/err"
36-
writeObsidian "github.com/ActiveMemory/ctx/internal/write/obsidian"
3717
)
3818

3919
// Run generates an Obsidian vault from journal entries.
4020
//
41-
// Pipeline:
42-
// 1. Scan entries (reuse core.ScanJournalEntries)
43-
// 2. Create output dirs (entries/, topics/, files/, types/, .obsidian/)
44-
// 3. Write .obsidian/app.json
45-
// 4. Transform and write entries (normalize, convert links, transform
46-
// frontmatter, add related footer)
47-
// 5. Build indices (reuse core.BuildTopicIndex etc.)
48-
// 6. Generate and write MOC pages
49-
// 7. Generate and write Home.md
50-
//
5121
// Parameters:
5222
// - cmd: Cobra command for output stream
5323
// - output: Output directory for the vault
5424
//
5525
// Returns:
5626
// - error: Non-nil if generation fails
5727
func Run(cmd *cobra.Command, output string) error {
58-
return BuildVault(
28+
return coreObsidian.BuildVault(
5929
cmd, filepath.Join(rc.ContextDir(), dir.Journal), output,
6030
)
6131
}
62-
63-
// BuildVault generates an Obsidian vault from journal entries in
64-
// journalDir and writes the output to the output directory.
65-
//
66-
// Parameters:
67-
// - cmd: Cobra command for output stream
68-
// - journalDir: Path to the source journal directory
69-
// - output: Output directory for the vault
70-
//
71-
// Returns:
72-
// - error: Non-nil if generation fails
73-
func BuildVault(cmd *cobra.Command, journalDir, output string) error {
74-
if _, statErr := os.Stat(journalDir); os.IsNotExist(statErr) {
75-
return errJournal.NoDir(journalDir)
76-
}
77-
78-
entries, scanErr := parse.ScanJournalEntries(journalDir)
79-
if scanErr != nil {
80-
return errJournal.Scan(scanErr)
81-
}
82-
83-
if len(entries) == 0 {
84-
return errJournal.NoEntries(journalDir)
85-
}
86-
87-
// Create output directory structure
88-
dirs := []string{
89-
output,
90-
filepath.Join(output, obsidian.DirEntries),
91-
filepath.Join(output, obsidian.DirConfig),
92-
filepath.Join(output, dir.JournTopics),
93-
filepath.Join(output, dir.JournalFiles),
94-
filepath.Join(output, dir.JournalTypes),
95-
}
96-
for _, d := range dirs {
97-
if mkErr := io.SafeMkdirAll(d, fs.PermExec); mkErr != nil {
98-
return errFs.Mkdir(d, mkErr)
99-
}
100-
}
101-
102-
// Write .obsidian/app.json
103-
appConfigPath := filepath.Join(
104-
output, obsidian.DirConfig, obsidian.AppConfigFile,
105-
)
106-
if writeErr := io.SafeWriteFile(
107-
appConfigPath, []byte(obsidian.AppConfig), fs.PermFile,
108-
); writeErr != nil {
109-
return errFs.FileWrite(appConfigPath, writeErr)
110-
}
111-
112-
// Write README
113-
readmePath := filepath.Join(output, file.Readme)
114-
if writeErr := io.SafeWriteFile(
115-
readmePath,
116-
[]byte(fmt.Sprintf(tpl.ObsidianReadme, journalDir)),
117-
fs.PermFile,
118-
); writeErr != nil {
119-
return errFs.FileWrite(readmePath, writeErr)
120-
}
121-
122-
// Build indices for MOC pages and related footer
123-
regularEntries := moc.FilterRegularEntries(entries)
124-
125-
topicEntries := moc.FilterEntriesWithTopics(entries)
126-
topics := section.BuildTopicIndex(topicEntries)
127-
128-
keyFileEntries := moc.FilterEntriesWithKeyFiles(entries)
129-
keyFiles := section.BuildKeyFileIndex(keyFileEntries)
130-
131-
typeEntries := moc.FilterEntriesWithType(entries)
132-
sessionTypes := section.BuildTypeIndex(typeEntries)
133-
134-
// Build topic lookup for related footer
135-
topicIndex := moc.BuildTopicLookup(topicEntries)
136-
137-
// Transform and write entries
138-
for _, entry := range entries {
139-
src := entry.Path
140-
dst := filepath.Join(output, obsidian.DirEntries, entry.Filename)
141-
142-
content, readErr := io.SafeReadUserFile(filepath.Clean(src))
143-
if readErr != nil {
144-
err.WarnFile(cmd, entry.Filename, readErr)
145-
continue
146-
}
147-
148-
// Normalize content (read-only - do NOT write back to source)
149-
normalized := wrap.Content(
150-
turn.MergeConsecutive(
151-
consolidate.ToolRuns(
152-
reduce.CleanToolOutputJSON(
153-
reduce.StripSystemReminders(string(content)),
154-
),
155-
),
156-
),
157-
)
158-
159-
// Transform for Obsidian
160-
sourcePath := filepath.Join(
161-
dir.Context, dir.Journal, entry.Filename,
162-
)
163-
transformed := frontmatter.Transform(normalized, sourcePath)
164-
transformed = wikilink.ConvertMarkdownLinks(transformed)
165-
transformed += moc.GenerateRelatedFooter(
166-
entry, topicIndex, obsidian.MaxRelated,
167-
)
168-
169-
if writeErr := io.SafeWriteFile(
170-
dst, []byte(transformed), fs.PermFile,
171-
); writeErr != nil {
172-
err.WarnFile(cmd, entry.Filename, writeErr)
173-
continue
174-
}
175-
}
176-
177-
// Write topic MOC and pages
178-
if len(topics) > 0 {
179-
topicsDir := filepath.Join(output, dir.JournTopics)
180-
mocPath := filepath.Join(output, obsidian.MOCTopics)
181-
if writeErr := io.SafeWriteFile(
182-
mocPath, []byte(moc.ObsidianTopics(topics)),
183-
fs.PermFile,
184-
); writeErr != nil {
185-
return errFs.FileWrite(mocPath, writeErr)
186-
}
187-
188-
for _, t := range topics {
189-
if !t.Popular {
190-
continue
191-
}
192-
pagePath := filepath.Join(topicsDir, t.Name+file.ExtMarkdown)
193-
if writeErr := io.SafeWriteFile(
194-
pagePath, []byte(moc.GenerateObsidianTopicPage(t)),
195-
fs.PermFile,
196-
); writeErr != nil {
197-
err.WarnFile(cmd, pagePath, writeErr)
198-
}
199-
}
200-
}
201-
202-
// Write key files MOC and pages
203-
if len(keyFiles) > 0 {
204-
filesDir := filepath.Join(output, dir.JournalFiles)
205-
mocPath := filepath.Join(output, obsidian.MOCFiles)
206-
if writeErr := io.SafeWriteFile(
207-
mocPath, []byte(moc.ObsidianFiles(keyFiles)),
208-
fs.PermFile,
209-
); writeErr != nil {
210-
return errFs.FileWrite(mocPath, writeErr)
211-
}
212-
213-
for _, kf := range keyFiles {
214-
if !kf.Popular {
215-
continue
216-
}
217-
slug := format.KeyFileSlug(kf.Path)
218-
pagePath := filepath.Join(filesDir, slug+file.ExtMarkdown)
219-
if writeErr := io.SafeWriteFile(
220-
pagePath, []byte(moc.GenerateObsidianFilePage(kf)),
221-
fs.PermFile,
222-
); writeErr != nil {
223-
err.WarnFile(cmd, pagePath, writeErr)
224-
}
225-
}
226-
}
227-
228-
// Write types MOC and pages
229-
if len(sessionTypes) > 0 {
230-
typesDir := filepath.Join(output, dir.JournalTypes)
231-
mocPath := filepath.Join(output, obsidian.MOCTypes)
232-
if writeErr := io.SafeWriteFile(
233-
mocPath, []byte(moc.ObsidianTypes(sessionTypes)),
234-
fs.PermFile,
235-
); writeErr != nil {
236-
return errFs.FileWrite(mocPath, writeErr)
237-
}
238-
239-
for _, st := range sessionTypes {
240-
pagePath := filepath.Join(typesDir, st.Name+file.ExtMarkdown)
241-
if writeErr := io.SafeWriteFile(
242-
pagePath,
243-
[]byte(moc.GenerateObsidianTypePage(st)), fs.PermFile,
244-
); writeErr != nil {
245-
err.WarnFile(cmd, pagePath, writeErr)
246-
}
247-
}
248-
}
249-
250-
// Write Home.md
251-
homePath := filepath.Join(output, obsidian.MOCHome)
252-
if writeErr := io.SafeWriteFile(
253-
homePath,
254-
[]byte(moc.Home(
255-
regularEntries,
256-
len(topics) > 0, len(keyFiles) > 0, len(sessionTypes) > 0,
257-
)),
258-
fs.PermFile,
259-
); writeErr != nil {
260-
return errFs.FileWrite(homePath, writeErr)
261-
}
262-
263-
writeObsidian.InfoGenerated(cmd, len(entries), output)
264-
265-
return nil
266-
}

0 commit comments

Comments
 (0)