delete date from Extra, like every other lifted key

Extra holds what the parser does not name (ADR-0002), and title, aliases, tags,
order, slug and draft are all deleted once lifted. date was the one exception,
so a template reading .Extra.date got the raw YAML value sitting beside the
parsed time — two spellings of the same fact, which is how they drift.

One line, and a test that names every lifted key rather than just this one, so
the next field added to Bundle has somewhere to fail if it forgets.

Latent item cleared. It had been waiting on "whatever next reads Extra
generically"; nothing does yet, but the fix was smaller than the row describing
it.
This commit is contained in:
Claude Opus 5
2026-08-01 23:26:56 +06:00
committed by bdeshi
parent 482a862ff5
commit 4f706f1c35
4 changed files with 46 additions and 25 deletions
-1
View File
@@ -119,7 +119,6 @@ with a stated reason. A list nothing drains is a graveyard of known defects.
|---|---|---| |---|---|---|
| No mechanical check that the counters are *correct* | Accepted at the Arc 1 boundary: the coupling gate makes forgetting them impossible, which is the real failure mode, and checking the values needs code to count | The first page-level transform (queue 15), now that the transform counter means something narrower | | No mechanical check that the counters are *correct* | Accepted at the Arc 1 boundary: the coupling gate makes forgetting them impossible, which is the real failure mode, and checking the values needs code to count | The first page-level transform (queue 15), now that the transform counter means something narrower |
| No mechanical gate on the untrusted boundary (ADR-0003) | Scheduled to Arc 3: nothing untrusted is read yet. Half of it is mechanical in a different way since ADR-0060 — `verify.sh` requires `html.WithUnsafe()` in exactly one file, so a second pipeline trusting its input cannot appear unnoticed — but there is still no check that a *future* untrusted source stays out of shortcode and template evaluation | The comment path — a test that untrusted input reaches no shortcode or template evaluation | | No mechanical gate on the untrusted boundary (ADR-0003) | Scheduled to Arc 3: nothing untrusted is read yet. Half of it is mechanical in a different way since ADR-0060 — `verify.sh` requires `html.WithUnsafe()` in exactly one file, so a second pipeline trusting its input cannot appear unnoticed — but there is still no check that a *future* untrusted source stays out of shortcode and template evaluation | The comment path — a test that untrusted input reaches no shortcode or template evaluation |
| `date` stays in `Extra` after being lifted onto `Bundle.Date`, unlike `title`, `aliases`, `tags` and `order`, which are deleted | Spotted while adding `order`; the theme contract says `Extra` holds what the parser does not name, so one of the two is wrong. Harmless today — a template reading `.Extra.date` gets the raw YAML value | Whatever next reads `Extra` generically: feeds (queue 14) or `check` (17) |
| A gallery's images carry no `alt` | `width`/`height` now come from the original (ADR-0042), so only alt text is missing, and a filename does not supply one. An empty `alt` is honest for a picture the page has already introduced | Captions per gallery entry — a sidecar or a frontmatter list — if the reference theme ever needs them | | A gallery's images carry no `alt` | `width`/`height` now come from the original (ADR-0042), so only alt text is missing, and a filename does not supply one. An empty `alt` is honest for a picture the page has already introduced | Captions per gallery entry — a sidecar or a frontmatter list — if the reference theme ever needs them |
| Sequence resolution rescans the index on every bundle request — two passes over every key, each doing a `Lookup` | Measured at the same time as the pictures (ADR-0044): a whole page is ~63µs, so this is not what costs anything. Remembering it would be a cache with no measurement behind it | A page render exceeding a few milliseconds, which is also what would revive the parked cache model | | Sequence resolution rescans the index on every bundle request — two passes over every key, each doing a `Lookup` | Measured at the same time as the pictures (ADR-0044): a whole page is ~63µs, so this is not what costs anything. Remembering it would be a cache with no measurement behind it | A page render exceeding a few milliseconds, which is also what would revive the parked cache model |
| The root listing's `<title>` repeats itself — "A Khosra Demo · A Khosra Demo" | Spotted 2026-08-01 by looking at the served page, not by any test: `base.html` joins page title and site title unconditionally, and at the root those are the same string. Cosmetic, and the fix is one `if` in a template — theme layer, not engine | The first time the reference theme is worked on (Phase G4 touches it), or sooner if a feed or OpenGraph title inherits the same doubling | | The root listing's `<title>` repeats itself — "A Khosra Demo · A Khosra Demo" | Spotted 2026-08-01 by looking at the served page, not by any test: `base.html` joins page title and site title unconditionally, and at the root those are the same string. Cosmetic, and the fix is one `if` in a template — theme layer, not engine | The first time the reference theme is worked on (Phase G4 touches it), or sooner if a feed or OpenGraph title inherits the same doubling |
+24 -24
View File
@@ -22,9 +22,9 @@ check.go 45 · main.go 192 · new.go 42 · wire.go 27
- new.go:12 func runNew(args []string) - new.go:12 func runNew(args []string)
- wire.go:17 func extenders(partial render.Partial) []goldmark.Extender - wire.go:17 func extenders(partial render.Partial) []goldmark.Extender
## internal/content — 1042 lines + 537 test ## internal/content — 1043 lines + 558 test
clock.go 12 · content.go 450 · doc.go 5 · extras.go 92 · settings.go 59 · site.go 424 clock.go 12 · content.go 451 · doc.go 5 · extras.go 92 · settings.go 59 · site.go 424
- clock.go:9 var now = time.Now - clock.go:9 var now = time.Now
- clock.go:12 func Now() time.Time { return now() } - clock.go:12 func Now() time.Time { return now() }
@@ -35,28 +35,28 @@ clock.go 12 · content.go 450 · doc.go 5 · extras.go 92 · settings.go 59 · s
- content.go:94 func Scan(fsys fs.FS) ([]Bundle, error) - content.go:94 func Scan(fsys fs.FS) ([]Bundle, error)
- content.go:103 func ScanReport(fsys fs.FS) ([]Bundle, []Problem, error) - content.go:103 func ScanReport(fsys fs.FS) ([]Bundle, []Problem, error)
- content.go:142 func Parse(name string, data []byte) (Bundle, error) - content.go:142 func Parse(name string, data []byte) (Bundle, error)
- content.go:182 func (b Bundle) Published(at time.Time) bool - content.go:183 func (b Bundle) Published(at time.Time) bool
- content.go:191 func (b Bundle) Assets() (string, bool) - content.go:192 func (b Bundle) Assets() (string, bool)
- content.go:201 func stringList(v any) []string - content.go:202 func stringList(v any) []string
- content.go:223 func asTime(v any) time.Time - content.go:224 func asTime(v any) time.Time
- content.go:239 func asInt(v any) int - content.go:240 func asInt(v any) int
- content.go:252 func terms(v any) []string - content.go:253 func terms(v any) []string
- content.go:276 func TagSlug(tag string) string - content.go:277 func TagSlug(tag string) string
- content.go:285 func Normalise(s string) string { return norm.NFC.String(s) } - content.go:286 func Normalise(s string) string { return norm.NFC.String(s) }
- content.go:291 func splitName(name string) (key, lang string, ok bool) - content.go:292 func splitName(name string) (key, lang string, ok bool)
- content.go:313 func isLangTag(s string) bool - content.go:314 func isLangTag(s string) bool
- content.go:331 func isPartial(base string) bool - content.go:332 func isPartial(base string) bool
- content.go:343 func skipDir(base string) bool - content.go:344 func skipDir(base string) bool
- content.go:351 func splitFrontmatter(data []byte) (front, body []byte) - content.go:352 func splitFrontmatter(data []byte) (front, body []byte)
- content.go:366 func trimLeadingFence(data []byte, fence string) ([]byte, bool) - content.go:367 func trimLeadingFence(data []byte, fence string) ([]byte, bool)
- content.go:383 func dropCollisions(all []Bundle) ([]Bundle, []Problem) - content.go:384 func dropCollisions(all []Bundle) ([]Bundle, []Problem)
- content.go:405 const PerPage = 10 - content.go:406 const PerPage = 10
- content.go:411 func URL(key, lang string) string - content.go:412 func URL(key, lang string) string
- content.go:424 func TagURL(section, slug, lang string, page int) string - content.go:425 func TagURL(section, slug, lang string, page int) string
- content.go:434 const DerivedPrefix = "/derived/" - content.go:435 const DerivedPrefix = "/derived/"
- content.go:437 func DerivedURL(name string) string { return DerivedPrefix + name } - content.go:438 func DerivedURL(name string) string { return DerivedPrefix + name }
- content.go:440 const TagsSegment = "tags" - content.go:441 const TagsSegment = "tags"
- content.go:444 func PageURL(key, lang string, page int) string - content.go:445 func PageURL(key, lang string, page int) string
- extras.go:14 const ExtrasDir = "extras" - extras.go:14 const ExtrasDir = "extras"
- extras.go:17 type Entry struct - extras.go:17 type Entry struct
- extras.go:33 func Extras(fsys fs.FS, b Bundle) []Entry - extras.go:33 func Extras(fsys fs.FS, b Bundle) []Entry
+1
View File
@@ -158,6 +158,7 @@ func Parse(name string, data []byte) (Bundle, error) {
b.Aliases = stringList(b.Extra["aliases"]) b.Aliases = stringList(b.Extra["aliases"])
delete(b.Extra, "aliases") delete(b.Extra, "aliases")
b.Date = asTime(b.Extra["date"]) b.Date = asTime(b.Extra["date"])
delete(b.Extra, "date")
b.Tags = terms(b.Extra["tags"]) b.Tags = terms(b.Extra["tags"])
delete(b.Extra, "tags") delete(b.Extra, "tags")
b.Order = asInt(b.Extra["order"]) b.Order = asInt(b.Extra["order"])
+21
View File
@@ -203,3 +203,24 @@ func TestAnUnderscoreFileIsAPartialNotABundle(t *testing.T) {
} }
} }
} }
// Extra holds what the parser does not name (ADR-0002). Every lifted key is deleted from it, and `date` was
// the one exception — a template reading `.Extra.date` got the raw YAML value beside the parsed one.
func TestALiftedKeyLeavesExtra(t *testing.T) {
b, err := Parse("posts/x.md", []byte("---\ntitle: T\ndate: 2026-03-08\ntags: [a]\norder: 10\n"+
"aliases: [old/x]\nslug: s\ndraft: true\nkeeps: me\n---\nbody\n"))
if err != nil {
t.Fatal(err)
}
for _, lifted := range []string{"title", "date", "tags", "order", "aliases", "slug", "draft"} {
if _, still := b.Extra[lifted]; still {
t.Errorf("%q is on the Bundle, so it must not also be in Extra: %v", lifted, b.Extra)
}
}
if b.Date.IsZero() {
t.Error("the date should still have been lifted")
}
if b.Extra["keeps"] != "me" {
t.Errorf("a key the parser does not name stays: %v", b.Extra)
}
}