oficina / estilo Public

Address credits provenance and accessibility review findings

Paul Campbell pushed by paul@keymail.dev a670e6bc7917b62a79a07e1178b8ce964b482c1e parent f5fa112
7 files changed, +85 −7
  • credits/catalog/catalog.go +5 −2
  • credits/catalog/catalog_test.go +25 −0
  • credits/credits.go +3 −2
  • credits/credits_test.go +19 −0
  • credits/icons_test.go +28 −0
  • credits/templates/credits.html +3 −1
  • implementation/open-source-credits.json +2 −2

CI passed — run details

diff --git a/credits/catalog/catalog.go b/credits/catalog/catalog.go
index cae740f..ace6336 100644
--- a/credits/catalog/catalog.go
+++ b/credits/catalog/catalog.go
@@ -88,7 +88,7 @@ func Collect(input io.Reader, exclude map[string]bool) (Inventory, error) {
return Inventory{}, err
}
for _, entry := range files {
- if !noticeFilename(entry.Name()) {
+ if entry.IsDir() || !noticeFilename(entry.Name()) {
continue
}
full := filepath.Join(dir, entry.Name())
@@ -103,7 +103,7 @@ func Collect(input io.Reader, exclude map[string]bool) (Inventory, error) {
if err != nil {
return Inventory{}, err
}
- if !utf8.Valid(raw) || len(raw) == 0 {
+ if !utf8.Valid(raw) || strings.TrimSpace(string(raw)) == "" {
return Inventory{}, errors.New("credits: empty or non-UTF-8 notice")
}
relative, err := filepath.Rel(m.Dir, full)
@@ -126,6 +126,9 @@ func Collect(input io.Reader, exclude map[string]bool) (Inventory, error) {
func noticeFilename(name string) bool {
name = strings.ToUpper(name)
+ if strings.HasSuffix(name, ".GO") {
+ return false
+ }
for _, prefix := range []string{"LICENSE", "LICENCE", "COPYING", "NOTICE"} {
if name == prefix || strings.HasPrefix(name, prefix+".") || strings.HasPrefix(name, prefix+"-") {
return true
diff --git a/credits/catalog/catalog_test.go b/credits/catalog/catalog_test.go
index 26cedb6..ec222b7 100644
--- a/credits/catalog/catalog_test.go
+++ b/credits/catalog/catalog_test.go
@@ -99,3 +99,28 @@ func TestCollectRefusesUnverifiableProvenance(t *testing.T) {
t.Fatal("truncated package stream accepted")
}
}
+
+func TestCollectSkipsSourceAndDirectoriesButRetainsNotices(t *testing.T) {
+ m := moduleFixture(t, "example.org/component")
+ for _, name := range []string{"license.go", "NOTICE_test.go", "NOTICE.go"} {
+ if err := os.WriteFile(filepath.Join(m.Dir, name), []byte("package example"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := os.Mkdir(filepath.Join(m.Dir, "license-tool"), 0700); err != nil {
+ t.Fatal(err)
+ }
+ got, err := Collect(encodePackages(t, Package{Dir: m.Dir, Module: m}), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(got.Components) != 1 || len(got.Components[0].Notices) != 2 {
+ t.Fatal("source or directory was collected as notice", got)
+ }
+ if err := os.WriteFile(filepath.Join(m.Dir, "NOTICE"), []byte(" \n\t"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Collect(encodePackages(t, Package{Dir: m.Dir, Module: m}), nil); err == nil {
+ t.Fatal("blank notice accepted")
+ }
+}
diff --git a/credits/credits.go b/credits/credits.go
index 4ef02b1..5db2e22 100644
--- a/credits/credits.go
+++ b/credits/credits.go
@@ -19,6 +19,7 @@ type Component struct {
Notices []Notice
}
type PageData struct {
- Title string
- Components []Component
+ Title string
+ MissingNoticeLabel string
+ Components []Component
}
diff --git a/credits/credits_test.go b/credits/credits_test.go
index d7c70d8..514a24e 100644
--- a/credits/credits_test.go
+++ b/credits/credits_test.go
@@ -49,3 +49,22 @@ func TestCallerSuppliesLocalizedTitleAndNoticesRemainOriginal(t *testing.T) {
}
}
}
+
+func TestNoticeFormattingAndMissingLabel(t *testing.T) {
+ tpl := template.Must(template.ParseFS(Templates(), "templates/*.html"))
+ var out bytes.Buffer
+ data := PageData{Title: "Credits", MissingNoticeLabel: "<missing>", Components: []Component{{Name: "first", Notices: []Notice{{Name: "LICENSE", Text: "\nOriginal notice"}}}, {Name: "second"}}}
+ if err := tpl.ExecuteTemplate(&out, "estilo/credits", data); err != nil {
+ t.Fatal(err)
+ }
+ html := out.String()
+ if !strings.Contains(html, "<pre>\n\nOriginal notice</pre>") {
+ t.Fatal("HTML parser would remove original first newline")
+ }
+ if strings.Contains(html, "<h2>") {
+ t.Fatal("notice filename became an unowned heading")
+ }
+ if strings.Count(html, "&lt;missing&gt;") != 1 || strings.Contains(html, "<missing>") {
+ t.Fatal("missing-notice label lost, repeated or interpreted")
+ }
+}
diff --git a/credits/icons_test.go b/credits/icons_test.go
new file mode 100644
index 0000000..d2a0dcf
--- /dev/null
+++ b/credits/icons_test.go
@@ -0,0 +1,28 @@
+package credits
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "os"
+ "testing"
+)
+
+func TestLucideNoticeMatchesRecordedProvenance(t *testing.T) {
+ raw, err := os.ReadFile("notices/lucide-provenance.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var record struct {
+ Source string `json:"source"`
+ SHA256 string `json:"sha256"`
+ Bytes int `json:"bytes"`
+ }
+ if err := json.Unmarshal(raw, &record); err != nil {
+ t.Fatal(err)
+ }
+ sum := sha256.Sum256([]byte(lucideNotice))
+ if record.Source != "https://raw.githubusercontent.com/lucide-icons/lucide/a79b2d131dab2bf20cb224bd0937b439a9c4fa99/LICENSE" || record.Bytes != len(lucideNotice) || record.SHA256 != hex.EncodeToString(sum[:]) {
+ t.Fatal("Lucide notice differs from original provenance")
+ }
+}
diff --git a/credits/templates/credits.html b/credits/templates/credits.html
index b30fc09..3316265 100644
--- a/credits/templates/credits.html
+++ b/credits/templates/credits.html
@@ -6,7 +6,9 @@
<li><details>
<summary><bdi>{{.Name}}</bdi>{{if .Version}} <bdi estilo-credits-version>{{.Version}}</bdi>{{end}}</summary>
{{if .SourceURL}}<p><a href="{{.SourceURL}}" referrerpolicy="no-referrer" rel="noreferrer"><bdi>{{.SourceURL}}</bdi></a></p>{{end}}
- {{range .Notices}}<h2>{{.Name}}</h2><pre>{{.Text}}</pre>{{end}}
+ {{if not .Notices}}{{if $.MissingNoticeLabel}}<p>{{$.MissingNoticeLabel}}</p>{{end}}{{end}}
+ {{range .Notices}}<p><strong>{{.Name}}</strong></p><pre>
+{{.Text}}</pre>{{end}}
</details></li>
{{end}}
</ul>
diff --git a/implementation/open-source-credits.json b/implementation/open-source-credits.json
index 996932d..e107e81 100644
--- a/implementation/open-source-credits.json
+++ b/implementation/open-source-credits.json
@@ -3,9 +3,9 @@
"user_request": "I want to add attribution to all the open source we're using ... would that go in a help menu?",
"suite_requirement": "Extract reusable components and maintain consistent UI/UX via Estilo.",
"copy_review": "User waived copy review for this round. Component contains no fixed user-facing strings; callers supply translated title and original upstream notices.",
- "first_consumer": "Home credits page; adoption in progress. Not an agreed suite interface until a second actual consumer adopts it.",
+ "first_consumer": "Home and Calendar implemented and browser-checked; Meet, Memoria, Docs and Sheets also consume the shared primitive.",
"behavior": "Native details list of component name/version, source link, and complete original notice text. No JavaScript. Template autoescapes all notice data and source URL. Bare estilo attributes define wrapping and minimum24px controls. Theme colors remain inherited and unchanged. External source links suppress referrers.",
"verification": "Full Estilo make test passed. Exact Home-rendered page passed native Chromium320/1200px overflow and24px target checks; Enter opens notices while page scripting is disabled. Browser evidence is in oficina-provisioning-20260911/home-credits-browser. Component and catalog tests cover escaping, translated title, bidi, original root/bundled/nested notices, deterministic order, explicit missing notices and invalid source provenance.",
- "review": "Pending Claude Fable after quota reset; no merge or deployment. A rendered credits list is not a claim that dependency/license inventory is complete.",
+ "review": "Base f5fa112 approved for source and preview by claude-fable-5-1. This follow-up addresses catalog source/directory false positives, verifies Lucide source digest, preserves a leading notice newline, removes repeated unowned notice headings, and lets callers supply a translated missing-notice label. Follow-up review pending.",
"inventory": "cmd/oficina-notices reads go list -deps -json for an app production package under the target build environment. It includes that Go toolchain LICENSE, selected module root and package-ancestor notice files, and an explicit missing-notices list. Exclusions are exact named first-party modules. Replacements and unverifiable provenance refuse; this does not claim to inventory separate JavaScript/npm or binary assets. Explicit -lucide adds the original upstream ISC/MIT notice for vendored Lucide/Feather glyphs; its upstream file revision/hash are recorded, without inventing an icon version."
}