a published surface is not only markdown
add appended .md unconditionally, so openapi.json became openapi.json.md. Reported from use against a repository that publishes an OpenAPI document beside its prose. It is now supplied only when the name carries no extension of its own. Two consequences came with it that the report could not have seen. A facet's name was built by stripping the extension, so openapi.json would have taken openapi.usages.md and collided with the facet for a sibling openapi.md; only a trailing .md is stripped now. And the unlocked walk matched *.md only, so a hand-fetched .json under .loom/externals was adopted, unlocked, and invisible to check; it now walks everything that is not the lock file or a dotfile. The assumption was never decided, it was typed. externals says a repository publishes documents and publication says prose in whatever form its readers need. Neither says markdown and three places in this tool did. Also records that the wrong diagnosis cost more than the bug: the repository is private, a scratch config with no token made every fetch look broken, and loomctl config would have said so in one line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
This commit is contained in:
@@ -1185,3 +1185,40 @@ first thing to batch.**
|
|||||||
had to repair a lock by hand. **No test we wrote would have caught it**: both
|
had to repair a lock by hand. **No test we wrote would have caught it**: both
|
||||||
sites round-tripped fine in isolation, and the field only vanishes across the
|
sites round-tripped fine in isolation, and the field only vanishes across the
|
||||||
handoff between two of them.*
|
handoff between two of them.*
|
||||||
|
|
||||||
|
## 2026-09-11 — a published surface is not only markdown
|
||||||
|
|
||||||
|
**`loomctl external add <repo> openapi.json` asked for `openapi.json.md`.** *`.md`
|
||||||
|
was appended unconditionally, which is right for `cart` and wrong for anything
|
||||||
|
carrying its own extension.* **Reported from use against a repository publishing
|
||||||
|
an OpenAPI document beside its prose.**
|
||||||
|
|
||||||
|
**Fixed: `.md` is supplied only when the name has no extension at all**, *never
|
||||||
|
appended to one that has its own.*
|
||||||
|
|
||||||
|
**Two consequences went with it, and neither would have been noticed from the
|
||||||
|
report:**
|
||||||
|
|
||||||
|
**Facets.** *A facet's name was built by stripping the extension, so
|
||||||
|
`openapi.json` would have taken `openapi.usages.md`* — **colliding with the facet
|
||||||
|
for a sibling `openapi.md`.** *Only a trailing `.md` is stripped now, so it is
|
||||||
|
`openapi.json.usages.md`, and `sibling-facets`' shape survives for documents that
|
||||||
|
are not prose.*
|
||||||
|
|
||||||
|
**The unlocked walk.** *It matched `*.md` only, so a hand-fetched `.json` under
|
||||||
|
`.loom/externals/` was invisible to `check`* — **adopted, unlocked, and never
|
||||||
|
reported.** *It now walks everything that is not the lock file or a dotfile.*
|
||||||
|
|
||||||
|
> **The assumption was never decided; it was typed.** *`externals` says a
|
||||||
|
> repository publishes **documents**, and `publication` says prose **in whatever
|
||||||
|
> form its readers need**.* **Neither says markdown, and three places in this tool
|
||||||
|
> did.**
|
||||||
|
|
||||||
|
**Belief that could be shown wrong:** *that extension-or-not is a good enough
|
||||||
|
rule.* **A name like `v1.2-notes` would be read as carrying an extension**, *and
|
||||||
|
the 404 path now lists the real surface, which is how somebody would find out.*
|
||||||
|
|
||||||
|
*Incidental, and worth recording because it cost a wrong diagnosis: the repository
|
||||||
|
is private, and a scratch test with a token-less config made every fetch look
|
||||||
|
broken.* **`loomctl config` says whether a token was found, and reading it first
|
||||||
|
would have been faster than reasoning.**
|
||||||
|
|||||||
Vendored
+5
-1
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.hypertheory-labs.dev/loom/loom-cli/internal/config"
|
"git.hypertheory-labs.dev/loom/loom-cli/internal/config"
|
||||||
@@ -32,7 +33,10 @@ func AddByName(root, ownerRepo, name string, out io.Writer) error {
|
|||||||
if strings.Count(ownerRepo, "/") != 1 {
|
if strings.Count(ownerRepo, "/") != 1 {
|
||||||
return fmt.Errorf("want owner/repo, got %q", ownerRepo)
|
return fmt.Errorf("want owner/repo, got %q", ownerRepo)
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(name, ".md") {
|
// A published surface is not only markdown — an OpenAPI document is a
|
||||||
|
// document. Supply .md only when the name carries no extension at all,
|
||||||
|
// and never append one to a name that already has its own.
|
||||||
|
if path.Ext(name) == "" {
|
||||||
name += ".md"
|
name += ".md"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
-1
@@ -430,7 +430,9 @@ func unlockedDocs(root string, locked map[string]bool) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".md") {
|
// Every file here is an adopted document or something we wrote beside
|
||||||
|
// one — except the lock itself, and anything else beginning with a dot.
|
||||||
|
if d.IsDir() || strings.HasPrefix(d.Name(), ".") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
rel, err := filepath.Rel(base, p)
|
rel, err := filepath.Rel(base, p)
|
||||||
|
|||||||
Vendored
+41
-1
@@ -1,6 +1,10 @@
|
|||||||
package external
|
package external
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestLocalPath(t *testing.T) {
|
func TestLocalPath(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -70,3 +74,39 @@ func TestIsFacet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublishedNameExtension(t *testing.T) {
|
||||||
|
// A published surface is not only markdown, and .md must never be appended
|
||||||
|
// to a name that already carries an extension of its own.
|
||||||
|
for _, tc := range []struct{ in, want string }{
|
||||||
|
{"cart", "cart.md"},
|
||||||
|
{"cart.md", "cart.md"},
|
||||||
|
{"recovering-data", "recovering-data.md"},
|
||||||
|
{"openapi.json", "openapi.json"},
|
||||||
|
{"entitlement-examples.json", "entitlement-examples.json"},
|
||||||
|
} {
|
||||||
|
got := tc.in
|
||||||
|
if path.Ext(got) == "" {
|
||||||
|
got += ".md"
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("%q -> %q, want %q", tc.in, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFacetNameKeepsANonMarkdownExtension(t *testing.T) {
|
||||||
|
// openapi.json.usages.md, never openapi.usages.md — which would collide with
|
||||||
|
// the facet for a sibling openapi.md.
|
||||||
|
for _, tc := range []struct{ doc, want string }{
|
||||||
|
{"h/o/r/cart.md", "h/o/r/cart.usages.md"},
|
||||||
|
{"h/o/r/openapi.json", "h/o/r/openapi.json.usages.md"},
|
||||||
|
} {
|
||||||
|
if got := strings.TrimSuffix(tc.doc, ".md") + ".usages.md"; got != tc.want {
|
||||||
|
t.Errorf("facet for %q = %q, want %q", tc.doc, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isFacet("h/o/r/openapi.json.usages.md") {
|
||||||
|
t.Error("a facet beside a non-markdown document should still read as a facet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.hypertheory-labs.dev/loom/loom-cli/internal/lock"
|
"git.hypertheory-labs.dev/loom/loom-cli/internal/lock"
|
||||||
)
|
)
|
||||||
@@ -59,7 +60,7 @@ func poladLockPath(root string) string {
|
|||||||
// usually survive unchanged — what moves is the manifests, the config, the code
|
// usually survive unchanged — what moves is the manifests, the config, the code
|
||||||
// that a usage named, which is why a usage names them.
|
// that a usage named, which is why a usage names them.
|
||||||
func usagesFor(root, rel string) (string, bool) {
|
func usagesFor(root, rel string) (string, bool) {
|
||||||
p := rel[:len(rel)-len(filepath.Ext(rel))] + ".usages.md"
|
p := strings.TrimSuffix(rel, ".md") + ".usages.md"
|
||||||
_, err := os.Stat(filepath.Join(root, lock.Dir, filepath.FromSlash(p)))
|
_, err := os.Stat(filepath.Join(root, lock.Dir, filepath.FromSlash(p)))
|
||||||
return p, err == nil
|
return p, err == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ package orient
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -93,7 +92,10 @@ func Generate(root, out string) (string, error) {
|
|||||||
// facetsFor finds the files written beside an adopted document.
|
// facetsFor finds the files written beside an adopted document.
|
||||||
func facetsFor(root, rel string) map[string]string {
|
func facetsFor(root, rel string) map[string]string {
|
||||||
found := map[string]string{}
|
found := map[string]string{}
|
||||||
stem := strings.TrimSuffix(rel, path.Ext(rel))
|
// Only a trailing .md is stripped: openapi.json keeps its extension, so
|
||||||
|
// its facet is openapi.json.usages.md and cannot collide with a facet for
|
||||||
|
// a sibling openapi.md.
|
||||||
|
stem := strings.TrimSuffix(rel, ".md")
|
||||||
for _, kind := range []string{"usages", "gaps"} {
|
for _, kind := range []string{"usages", "gaps"} {
|
||||||
p := stem + "." + kind + ".md"
|
p := stem + "." + kind + ".md"
|
||||||
if _, err := os.Stat(filepath.Join(root, lock.Dir, filepath.FromSlash(p))); err == nil {
|
if _, err := os.Stat(filepath.Join(root, lock.Dir, filepath.FromSlash(p))); err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user