|
1 | 1 | package docs |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/json" |
4 | 5 | "fmt" |
5 | 6 | "os" |
6 | 7 | "path/filepath" |
7 | 8 | "sort" |
8 | 9 | "strings" |
9 | 10 |
|
10 | 11 | "github.com/tae2089/code-context-graph/internal/ccgref" |
| 12 | + "github.com/tae2089/code-context-graph/internal/ctxns" |
11 | 13 | "github.com/tae2089/code-context-graph/internal/model" |
12 | 14 | "github.com/tae2089/code-context-graph/internal/pathutil" |
13 | 15 | ) |
@@ -52,35 +54,9 @@ func (g *Generator) Lint() (*LintReport, error) { |
52 | 54 | report := &LintReport{} |
53 | 55 |
|
54 | 56 | // 1. Collect all .md doc files from the output directory. |
55 | | - docFiles := map[string]os.FileInfo{} // source path → FileInfo of .md |
56 | | - if _, err := os.Stat(g.OutDir); err == nil { |
57 | | - err := filepath.Walk(g.OutDir, func(path string, info os.FileInfo, err error) error { |
58 | | - if err != nil || info.IsDir() { |
59 | | - return err |
60 | | - } |
61 | | - if !strings.HasSuffix(path, ".md") { |
62 | | - return nil |
63 | | - } |
64 | | - |
65 | | - rel, _ := filepath.Rel(g.OutDir, path) |
66 | | - rel = filepath.ToSlash(rel) |
67 | | - |
68 | | - // Skip index.md — it's not a per-file doc. |
69 | | - if rel == "index.md" { |
70 | | - return nil |
71 | | - } |
72 | | - |
73 | | - // Strip .md suffix to get the source path. |
74 | | - srcPath := strings.TrimSuffix(rel, ".md") |
75 | | - if len(g.Exclude) > 0 && pathutil.MatchExcludes(g.Exclude, srcPath) { |
76 | | - return nil |
77 | | - } |
78 | | - docFiles[srcPath] = info |
79 | | - return nil |
80 | | - }) |
81 | | - if err != nil { |
82 | | - return nil, fmt.Errorf("walk docs dir: %w", err) |
83 | | - } |
| 57 | + docFiles, err := g.lintDocFiles() |
| 58 | + if err != nil { |
| 59 | + return nil, err |
84 | 60 | } |
85 | 61 |
|
86 | 62 | // 2. Collect all source file paths from the graph (distinct file_path |
@@ -301,6 +277,90 @@ func (g *Generator) Lint() (*LintReport, error) { |
301 | 277 | return report, nil |
302 | 278 | } |
303 | 279 |
|
| 280 | +// @intent collect only the Markdown files that belong to the active docs namespace. |
| 281 | +// @domainRule named namespaces trust their scoped manifest; without one, foreign docs in the shared output dir are ignored. |
| 282 | +func (g *Generator) lintDocFiles() (map[string]os.FileInfo, error) { |
| 283 | + docFiles := map[string]os.FileInfo{} // source path -> FileInfo of .md |
| 284 | + if m, ok, err := g.loadLintManifest(); err != nil { |
| 285 | + return nil, err |
| 286 | + } else if ok { |
| 287 | + for _, rel := range m.Files { |
| 288 | + rel = filepath.ToSlash(rel) |
| 289 | + if rel == "index.md" || !strings.HasSuffix(rel, ".md") { |
| 290 | + continue |
| 291 | + } |
| 292 | + srcPath := strings.TrimSuffix(rel, ".md") |
| 293 | + if len(g.Exclude) > 0 && pathutil.MatchExcludes(g.Exclude, srcPath) { |
| 294 | + continue |
| 295 | + } |
| 296 | + full, err := safeDocOutputPath(g.OutDir, rel) |
| 297 | + if err != nil { |
| 298 | + return nil, err |
| 299 | + } |
| 300 | + info, err := os.Stat(full) |
| 301 | + if err != nil { |
| 302 | + if os.IsNotExist(err) { |
| 303 | + continue |
| 304 | + } |
| 305 | + return nil, err |
| 306 | + } |
| 307 | + if !info.IsDir() { |
| 308 | + docFiles[srcPath] = info |
| 309 | + } |
| 310 | + } |
| 311 | + return docFiles, nil |
| 312 | + } |
| 313 | + if g.Namespace != "" && ctxns.Normalize(g.Namespace) != ctxns.DefaultNamespace { |
| 314 | + return docFiles, nil |
| 315 | + } |
| 316 | + if _, err := os.Stat(g.OutDir); err == nil { |
| 317 | + err := filepath.Walk(g.OutDir, func(path string, info os.FileInfo, err error) error { |
| 318 | + if err != nil || info.IsDir() { |
| 319 | + return err |
| 320 | + } |
| 321 | + if !strings.HasSuffix(path, ".md") { |
| 322 | + return nil |
| 323 | + } |
| 324 | + |
| 325 | + rel, _ := filepath.Rel(g.OutDir, path) |
| 326 | + rel = filepath.ToSlash(rel) |
| 327 | + |
| 328 | + // Skip index.md — it's not a per-file doc. |
| 329 | + if rel == "index.md" { |
| 330 | + return nil |
| 331 | + } |
| 332 | + |
| 333 | + // Strip .md suffix to get the source path. |
| 334 | + srcPath := strings.TrimSuffix(rel, ".md") |
| 335 | + if len(g.Exclude) > 0 && pathutil.MatchExcludes(g.Exclude, srcPath) { |
| 336 | + return nil |
| 337 | + } |
| 338 | + docFiles[srcPath] = info |
| 339 | + return nil |
| 340 | + }) |
| 341 | + if err != nil { |
| 342 | + return nil, fmt.Errorf("walk docs dir: %w", err) |
| 343 | + } |
| 344 | + } |
| 345 | + return docFiles, nil |
| 346 | +} |
| 347 | + |
| 348 | +// @intent load the active namespace manifest for lint without hiding whether it exists. |
| 349 | +func (g *Generator) loadLintManifest() (*manifest, bool, error) { |
| 350 | + data, err := os.ReadFile(g.manifestPath()) |
| 351 | + if err != nil { |
| 352 | + if os.IsNotExist(err) { |
| 353 | + return nil, false, nil |
| 354 | + } |
| 355 | + return nil, false, err |
| 356 | + } |
| 357 | + m := &manifest{} |
| 358 | + if err := json.Unmarshal(data, m); err != nil { |
| 359 | + return nil, false, err |
| 360 | + } |
| 361 | + return m, true, nil |
| 362 | +} |
| 363 | + |
304 | 364 | // ccgRefExists checks whether a parsed ccg:// @see ref resolves to graph data. |
305 | 365 | // @intent let docs lint validate cross-namespace refs while keeping local @see lookup semantics unchanged. |
306 | 366 | func (g *Generator) ccgRefExists(ref ccgref.Ref) (bool, error) { |
|
0 commit comments