SPEC-148
ID:SPEC-148Status:draft

Places plugin composition audit

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.

PathResolverAcross a composition boundary
A — properties from an attributestamp → bag fallback (schema-table.ts:309-312)works
B — properties from a nodefindAllByName (:235)fails silently
C — textfindByName, same walkfails silently
D — entitiesbuildEntity — node first, then bag (:384-395)node source fails, attribute source works
E — childrenfindChildren (: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

Propertyschema.orgSourcePathComposed
headlinenameref from pageSectionPropertiesBlost
blurbdescriptionref from pageSectionPropertiesBlost
datestartDateattribute → properties metaA✓
endDateendDateattribute → properties metaA✓
urlurlattribute → properties metaA✓
locationnested Place.nameattribute → entitiesD (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" } }