Relationships
Related 16
Branches 2
Summary
The second plugin audited against SPEC-145, after SPEC-147's storytelling. Places is 960 lines across three runes and three children, and the result differs from storytelling's in a way worth having in writing: it does not retire, it splits. Two runes compose; one never can, because it is not a domain rune at all.
It also establishes the per-property audit method, because per-rune verdicts proved unstable — twice in the course of this audit a rune was called composable or blocked on the strength of one property, when its other properties took different paths.
Why per-property, not per-rune
A schema row's source resolves through one of five paths, and only one of them is broken by composition (SPEC-146, Problem 2). Which path a property takes is a property of that property, not of its rune — so a rune with four properties can be three parts fine and one part broken, and a per-rune verdict is a coin toss on which property you happened to look at first.
| Path | Resolver | Across a composition boundary |
|---|---|---|
A — properties from an attribute | stamp → bag fallback (schema-table.ts:309-312) | works |
B — properties from a node | findAllByName (:235) | fails silently |
C — text | findByName, same walk | fails silently |
D — entities | buildEntity — node first, then bag (:384-395) | node source fails, attribute source works |
E — children | findChildren (:528) | works, and over-matches (SPEC-146 Problem 1) |
event is the clearest demonstration in the codebase, and its own source comment says so: "location … exists only as an attribute value, so the applier rebuilds a carrier from the field bag rather than stamping a node. headline / blurb come from pageSectionProperties and survive as refs, so those stamp in place. One table, three of the mechanism's resolution paths."
The audit
event — three paths in one table, one of them blocked
| Property | schema.org | Source | Path | Composed |
|---|---|---|---|---|
headline | name | ref from pageSectionProperties | B | lost |
blurb | description | ref from pageSectionProperties | B | lost |
date | startDate | attribute → properties meta | A | ✓ |
endDate | endDate | attribute → properties meta | A | ✓ |
url | url | attribute → properties meta | A | ✓ |
location | nested Place.name | attribute → entities | D (attribute) | ✓ |
Measured against today's applier, the same table over a declared and a composed tree:
// declared { "@type": "Event", "name": "Tech Conference 2025", "description": "Three days of talks.", "startDate": "2025-06-15", "endDate": "2025-06-17", "url": "https://example.com/register", "location": { "@type": "Place", "name": "San Francisco, CA" } } // composed — the header placed inside a {% card %} { "@type": "Event", "startDate": "2025-06-15", "endDate": "2025-06-17", "url": "https://example.com/register", "location": { "@type": "Place", "name": "San Francisco, CA" } }