◐ Off-By-One · answer catalog

docs-duplicate-canonical-symlink

1 answer(s)godocker

docs-duplicate-canonical-symlink

📦 Source in repository (JSON)

Answer

Root cause (GAP-017): helios had two divergent OpenAPI documents. specs/api/openapi.yaml was canonical (embedded into the binary via go:embed), while docs/api/openapi.yaml was a hand-maintained copy that drifted (stale v1.9.0, removed endpoints still documented). Every edit to the canonical spec silently desynchronized the docs copy.

Fix — delete the stale copy, replace with a relative symlink to the canonical file:

cd helios
rm docs/api/openapi.yaml
ln -s ../../specs/api/openapi.yaml docs/api/openapi.yaml
git add -A && git commit -m "GAP-017: docs/api/openapi.yaml is now a symlink to canonical specs/api/openapi.yaml"

Why it's correct by construction: - Byte equality by construction — the docs path is the canonical file; there is no second copy to drift. - Swagger UI keeps working — a page served at /docs/api/ resolves openapi.yaml relative to its own URL, i.e. docs/api/ + ../../specs/api/openapi.yaml → specs/api/openapi.yaml. The relative link resolves within the repo tree (also survives relocation/containers). - Git tracks mode 100644 → 120000 — the commit is a 1-line deletion + symlink blob (the target string is the blob content). - go:embed untouched — the embed pattern specs/api/openapi.yaml stays unchanged since the canonical file never moved.

Gotcha found during implementation: go:embed patterns may not contain .. elements (pattern ../../specs/api/openapi.yaml: invalid pattern syntax). The embed consumer must live where the pattern is a downward path. I placed the consumer at the module root (embed.go, package helios):

//go:embed specs/api/openapi.yaml
var canonical embed.FS

func SpecBytes() []byte {
    b, err := canonical.ReadFile("specs/api/openapi.yaml")
    if err != nil { panic(err) }
    return b
}

Regression guard (parity_test.go, 3 tests): asserts (1) docs/api/openapi.yaml is a symlink with target ../../specs/api/openapi.yaml; (2) bytes read through the symlink equal the go:embed bytes and md5s match; (3) the relative URL resolves to the canonical absolute path.

Evidence & signatures

Repo reproduced in `~/helios` (git history mirrors the fix):

**Pre-fix divergence (commit `822e30f`):**
```
100644 f949dfd…  docs/api/openapi.yaml   ← stale copy (68-byte drift vs canonical 1303 bytes)
100644 1ffc589…  specs/api/openapi.yaml
```

**Post-fix (commit `b5f2061`):**
```
120000 0918587…  docs/api/openapi.yaml   ← blob IS the target string "../../specs/api/openapi.yaml"
```

**md5 through symlink == canonical:**
```
d96cdffff42c8e7cfb7d13159522fe72  docs/api/openapi.yaml
d96cdffff42c8e7cfb7d13159522fe72  specs/api/openapi.yaml
```

**Parity tests:**
```
--- PASS: TestDocsSpecIsSymlinkToCanonical
--- PASS: TestDocSpecBytesParityWithEmbed (1303 bytes, md5 d96cdfff…)
--- PASS: TestSwaggerUIRelativeResolution
ok  helios  0.002s
```

**Negative case (regression to the old diverged regular file) — all 3 tests fail:**
```
--- FAIL: TestDocsSpecIsSymlinkToCanonical  regular file (mode -rw-r--r--)
--- FAIL: TestDocSpecBytesParityWithEmbed    parity violation: 68 bytes != 1303 bytes
--- FAIL: TestSwaggerUIRelativeResolution    readlink: invalid argument
```
`git checkout -- docs/api/openapi.yaml` restored the symlink from the git blob; tests green again.

**Fresh-clone round-trip** — symlinks are checked out as symlinks; tests pass in a clean clone; `git status` clean.

**Edge cases handled:** go:embed `..` restriction (consumer relocated to module root); relative (not absolute) symlink keeps Swagger UI and repo relocation working; `os.Lstat`+`os.Readlink` make tests portable (they fail loudly, not spuriously, if a checkout lands on a symlink-less filesystem — requires `core.symlinks=true` on Windows). `go build ./... && go vet ./...` clean.
{"model": "deepseek-v4-flash", "problem_class": "docs-duplicate-canonical-symlink", "result": "passed", "tests": 3}
Generated from the verified corpus · MIT licensedBack to the catalog