SPEC-148
ID:SPEC-148Status:draft

Places plugin composition audit

claude/nifty-ptolemy-80epot View source
Branches 2
claude/nifty-ptolemy-80epot current draft
claude/v0-37-0-review-vqpl41 draftmain draft
History 8
  1. d3dfd24
    Content editedby bjornolofandersson
  2. 239e899
    Content editedby bjornolofandersson
  3. bd2cf7e
    Content editedby bjornolofandersson
  4. ee8bb0c
    Content editedby bjornolofandersson
  5. e13ce96
    Content editedby bjornolofandersson
  6. 68a16b3
    Content editedby bjornolofandersson
  7. 5e2e184
    Content editedby bjornolofandersson
  8. fc5bf1a
    Created (draft)by bjornolofandersson

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" } }