Protocol

Look Conventions

The one canonical way to style a Kanonak ontology, and a decision guide for when to use a look, a transformation, or a view.

The look system turns a class into a rendered page with no hand-written HTML or CSS: you attach a look to a type, and every instance of that type renders through it. Whatever a type does not declare, it inherits from its superclass and ultimately from the universal floor on Resource — so every resource has a sane page even before you style anything. This package is the single source of truth for that system; the conventions below are the whole of it.

Start here — what are you trying to produce?

A readable PAGE for a resource (its HTML page + SVG identity)
   -> a LOOK.  Declare derivation.look on the CLASS: a ResourceView of bands.
        - want the resource's icon / glyph?            add look.semanticSvg
        - display name or summary is not the default
          rdfs.label / rdfs.comment?                   add look.displayLabel / displaySummary
        - want to theme its colors?                    add derivation.tokens
        - want a DIAGRAM (relations, a sequence,
          the schema)?                                 add a look.Diagram / look.Sequence /
                                                       look.EntityRelationship band — it reads
                                                       the graph; author no diagram data

A NON-page artifact (JSON, YAML, generated code) -- or a visual the
band catalogue genuinely cannot express (a bespoke chart or matrix)
   -> a TRANSFORMATION.  A tx.Transformation bound via derivation.derivations.

A QUERY that selects, filters, or reshapes instances into a result set
   -> a VIEW.  A view.View, materialized into an EphemeralPackage.

In one line: Look for pages, Derivations + a Transformation for non-page artifacts and novel visuals, and a View for queries. The look is the default for presentation; the other two are for when you are not rendering a resource's own page. Most publishers only ever need looks.

Layer
Guidance Layer
Author
Paul Fryer
Created Date
May 29, 2026

Conventions

#

Choosing an Approach

Three tools present or derive data, and they do not overlap. A look (Look) renders a resource as a page — the default, and almost always what you want. A transformation (Transformation, bound via Derivations) produces a non-page artifact (JSON, YAML, generated code) or a visual the band catalogue cannot express. A view (View) is a query that selects and reshapes instances into a result set. Reach for the look first; only drop to a transformation or a view when you are not rendering a resource's own page.

Has Recommended Rule#
TextRationale
#

To render a resource as a readable page, you SHOULD declare a Look and let the universal renderer produce the HTML, CSS, and SVG. Do NOT hand-author a Transformation that emits page HTML.

Declarative bands express the common page (hero + properties + cross-references) in a fraction of the YAML, with built-in cascade semantics for multi-publisher styling. Hand-rolled HTML transformations diverge across publishers and rot.

#

Author a Transformation (bound via Derivations) only for output that is NOT a resource page — a data format, a generated file, or a bespoke visualization the band catalogue cannot produce.

Transformations are the escape hatch for genuinely novel output. Using one where a look would do throws away the cascade and the shared band library.

#

Use a View when you need to select, filter, or aggregate instances into a NEW result shape — not to style an existing resource. A view materializes into a Ephemeral Package. The full data-View authoring guide is the kanonak.org/view-language protocol; this guide is the visual (look) side of the same view.View base.

A view is a query; a look is presentation. They share the view.View base — a look view is its visual specialization, a data view its projecting one — but the surfaces are disjoint (bands vs projections). Conflating them leads to looks that try to compute and views that try to render; route by intent and follow the matching guide.

#

Style Types, Not Instances

You attach a look to a class (or a package, or a publisher), and every instance of that type renders through it. You almost never style a single resource — you style the concept, and all its instances follow. Styling one instance directly is a rare, deliberate override, not the default move.

Has Required Rule#
TextRationale
#

A Look declaration SHOULD live on a class (or a Package / publisher), so it applies to every instance. Declaring it on an individual instance MUST be a deliberate override, not the way you style a kind of thing.

Styling the type is what makes the system scale: one declaration styles a thousand instances, and the class hierarchy carries it to subtypes. Per-instance looks are duplication waiting to drift.

#

When presentation should follow an instance's STATE — a status, a severity, a phase — declare a Instance Variant on the class's View rather than styling the affected instances. The variant's When Condition matches a property value, and every instance in that state follows it.

State-driven presentation is the one case that looks like it needs per-instance styling and does not. A variant is still a declaration on the TYPE — one rule covers every instance that enters the state, including ones authored later — so it scales the way the rest of the cascade does, while styling each affected instance by hand does not.

#

per-instance-styling

Avoid — styling one widget instance. Pull the look up onto the Widget class so every widget renders the same way.

Value
my-publisher.org/widgets/widget-0042: derivation.look: type: look.ResourceView bands: [ { type: look.Hero, look.title: rdfs.label } ]
#

The Resource View

The core pattern: a class declares Look whose value is a Resource View — an ordered list of bands, each rendering one region of the page from the resource's own properties. The band catalogue covers the common page: Hero (title + subtitle + type badges), Property List and Property Table (property values), Reference List / Referenced By (cross-references), Markdown (prose), Embedded Views (nested resources rendered through their own class looks), and more. Bands read existing properties — they do not carry content.

Has Required Rule#
TextRationale
#

A class's page look MUST be a Resource View whose bands is an ordered list of band instances, each pointing (via look.source or a band-specific slot) at a property of the resource. Render order follows list order.

An ordered list of declarative bands reading real properties is what lets the renderer produce a page with no per-class code, and keeps labels and values sourced from the graph (DRY).

#

styled-class

A complete Author page — a hero from rdfs.label / rdfs.comment, the list of works from catalog.wrote, and an incoming-reference grid — every region derived from existing properties.

Value
my-publisher.org/catalog/Author: derivation.look: type: look.ResourceView bands: - { type: look.Hero, look.title: rdfs.label, look.subtitle: rdfs.comment, look.badges: rdfs.type } - { type: look.PropertyList, look.source: catalog.wrote } - { type: look.ReferencedBy }
#

Path-Carrier Bands

The common bands above name a single property in a slot like Source or Title Property. The quantitative and series bands — Stat Row, Distribution, Timeline / Version Diff, Version Delta, Time Plot, a detail-row Reference List, and Diagram channels — need to reach a value that may live one or more steps INTO the instance. As of look@2.0.0 every such *Path slot (metricPath, statPath, mapPath, alphaPath/betaPath, lowerPath/upperPath, hueBy, laneBy, labelPath, badgePath, nodeNote, edgeValue) is a Expression — the SAME vocabulary the SVG tiers and data Views use — NOT a /-separated path string. There are exactly three shapes, chosen by what the step is:

  • A property on the evaluated node → a Property Read whose readSource is a Var Ref of input.
  • A field inside an embedded value → NEST the read: a tx.PropertyRead whose readSource is itself the tx.PropertyRead of the embedding property (e.g. read confidence, then estimateMean off it).
  • A field across a REFERENCE → a Traverse (through the reference property, its step reading off the traversed-to subject); to show a referenced resource's display name, traverse and read Label.

The node bound to input depends on the band: instance-level bands (Distribution, StatRow, VersionDelta) evaluate against the PAGE instance; series and diagram bands evaluate against each sub-resource reached through the band's track / source / entries / relation property. Either way input is the node, and every property is a real reference validated at author time.

Has Required Rule#
TextRationale
#

Every band *Path carrier MUST be a Expression evaluated against the band's node (bound to input) — never a /-separated path string. Use a Property Read for a direct or embedded field (nested for embeds) and a Traverse for a reference step.

A string path matched by stripped local name is a magic string: it fails silently to undefined at render time and is invisible to validation. A typed expression resolves every step through the object model, so kanonak validate (the LookBandPath rule) reports an out-of-scope or misspelled property as an error rather than rendering an empty band.

#

stat-and-distribution

A StatRow cell reading estimateMean out of the embedded confidence estimate — a nested PropertyRead, the embedded-field shape.

Value
# `confidence` is an embedded Estimate on the page instance; # reach its `estimateMean` with a NESTED PropertyRead. Thesis: derivation.look: type: look.ResourceView bands: - type: look.StatRow look.stats: - look.statLabel: "μ" look.statPath: type: tx.PropertyRead tx.readSource: type: tx.PropertyRead tx.readSource: { type: tx.VarRef, tx.varName: input } tx.readProp: confidence tx.readProp: estimateMean
#

badge-via-traverse

A ReferenceList badge that follows each evidence item's strength reference and reads the target's rdfs.label — the reference-step shape (Traverse + a read of the traversed-to subject).

Value
# `strength` is a REFERENCE; show the referenced resource's label. - type: look.ReferenceList look.entries: [supports] look.badgePath: type: tx.Traverse tx.traverseSource: { type: tx.VarRef, tx.varName: input } tx.through: strength tx.step: type: tx.PropertyRead tx.readSource: { type: tx.VarRef, tx.varName: input } tx.readProp: rdfs.label
#

string-path

Invalid as of look@2.0.0 — a /-separated string path. The slot is a tx.Expression; author a nested tx.PropertyRead (read confidence, then betaAlpha) instead. A string here is silently dropped (the slot is an object property) and the band renders empty.

Value
- type: look.Distribution look.alphaPath: "confidence/betaAlpha" # WRONG: string path look.betaPath: "confidence/betaBeta"
#

Diagrams Read the Graph

Three bands draw pictures, and they share one discipline: nothing is drawn that the graph does not already state. Nodes are the resources' own identity tiles, labels resolve through the display lenses, and every indicator is read from the ontology — direction from the triple and from Domain / Range, a reply from Inverse Of, an undirected edge from Symmetric Property, cardinality from Min Count / Max Count on a targeting shape or from Functional Property. The look layer only says WHICH relations to show, and how they look where the ontology is silent.

BandSourceDraws
Diagramthe page instance's relations (facetsrelation, encode)a cluster, a hub-and-spoke, or a DAG (edges), with channels mapping a value to thickness / hue
Sequencea prose property on the page instance (source) + connectorslifelines and messages, ordered by the prose's [[references]], joined by the graph
Entity Relationshipthe ontology itselfclasses, attributes, relationships, cardinalities, inheritance

A Sequence is the one to reach for when someone asks for a sequence diagram, and it is deliberately NOT a data model of steps. The prose supplies the ORDER and the PARTICIPANTS (every [[reference]], in document order); the graph supplies the EDGES and their DIRECTION (a message is drawn for each declared connector that actually holds between two consecutively-referenced participants, in the direction the triple states — so passive voice cannot mislead); the band's connectors overlay supplies the SELECTION (which object properties count as messages). The three never overlap, and the domain ontology imports nothing about sequences. A [[property]] reference placed between two participant references narrows that pair to that edge. A pair the graph does not join draws nothing — the graph is the filter — and kanonak validate (the SequenceNarrative rule) reports it, so prose↔graph drift is an author-time diagnostic, not a missing arrow.

The EntityRelationship band has no slots at all: on a Package page it draws every class the package declares; on a class page — whose look comes from Class, since a class's own Look describes its INSTANCES — it draws that class and the classes its object properties reach.

Has Required Rule#
TextRationale
#

A diagram band MUST NOT be given data of its own. Do not model "steps", "messages", "edges" or "cardinality" so that a picture can be drawn: a Sequence reads object properties the ontology already declares, in the order a prose property's [[references]] tell it; a Entity Relationship reads Min Count / Max Count and Functional Property for cardinality. If a diagram is wrong, the graph is wrong — fix the triple, the shape, or the prose.

A diagram with its own data is a second copy of the model that drifts from the first. Reading the graph makes the picture a proof of the data — and lets kanonak validate report drift between the narrative and the triples, which no hand-drawn diagram can do.

#

A Sequence MUST declare look.source naming a property whose Range is Markdown, and look.connectors listing one or more Connector whose look.relation is an object property between the participants. Direction is taken from the triple, never from the sentence.

Order can only come from prose (a list has order but no narrative; a set has neither), and a message can only be a triple that holds. Both are checked at validate time — a non-prose source, a missing connector list, a relation that is not a property, and an unknown lineStyle / nodeTier are each an error on the declaring class.

Has Recommended Rule#
TextRationale
#

A Sequence SHOULD be placed beside a Markdown band over the SAME look.source property, so the narrative and the diagram render from one statement.

The prose is the diagram's annotation. Rendering both from one property means they cannot disagree, and a reader can follow the numbered messages back into the sentences that name them.

#

Prefer the ontology's own structure to a look slot: declare Inverse Of to get a dotted reply, Symmetric Property to get an undirected edge, a SHACL shape or Functional Property to get a cardinality. Use lineStyle (solid / dotted / open / thick) only for what OWL cannot say — an asynchronous or a critical-path message — and look.nodeTier on the band or Diagram Tier on the participants' class only to size the node tiles.

Structure declared once serves every consumer — the ER diagram, the sequence, kanonak validate --conformance, generated code — while a look slot serves one picture. Reserving the overlay for what the ontology cannot express keeps the two from contradicting each other.

#

sequence-from-a-narrative

Three messages — payment → fraud (calls), fraud → payment (responds to, dotted because it is the owl.inverseOf a connector), payment → audit (notifies, open arrowhead) — every one a triple that holds, in the order the prose names the participants. "The fraud service answers the payment service" draws fraud → payment because that is the direction of the respondsTo statement, whatever voice the sentence uses.

Value
Service: type: rdfs.Class look.displayLabel: serviceName # lifeline and message labels use the lens calls: { type: owl.ObjectProperty, rdfs.domain: Service, rdfs.range: Service, rdfs.label: calls } respondsTo: { type: owl.ObjectProperty, rdfs.domain: Service, rdfs.range: Service, rdfs.label: responds to, owl.inverseOf: calls } # → the dotted reply, derived notifies: { type: owl.ObjectProperty, rdfs.domain: Service, rdfs.range: Service, rdfs.label: notifies } narrative: { type: owl.DatatypeProperty, rdfs.domain: Flow, rdfs.range: prose.Markdown, rdfs.label: Narrative } Flow: type: rdfs.Class derivation.look: type: look.ResourceView look.bands: - type: look.Markdown # the prose … look.source: narrative - type: look.Sequence # … and the diagram, from ONE property look.source: narrative look.nodeTier: chip look.connectors: - { look.relation: calls } - { look.relation: respondsTo } - { look.relation: notifies, look.lineStyle: open } # async: the one thing OWL cannot say payment-service: { type: Service, serviceName: Payment Service, calls: fraud-service, notifies: audit-service } fraud-service: { type: Service, serviceName: Fraud Service, respondsTo: payment-service } audit-service: { type: Service, serviceName: Audit Service } checkout-flow: type: Flow narrative: | The [[payment-service]] asks the [[fraud-service]] to screen the order, and the [[fraud-service]] answers the [[payment-service]] with a verdict. On success the [[payment-service]] tells the [[audit-service]] to record it.
#

entity-relationship-on-package-and-class

No slots to author: the package page draws every class the package declares, each class page draws its neighbourhood, and the 1..1 on serviceName is read from the SHACL shape that kanonak validate --conformance also enforces.

Value
my-package: type: Package derivation.look: type: look.PackageView look.bands: - type: look.EntityRelationship # the whole package's schema rdfs.Class: # augment the imported class: every class PAGE derivation.look: type: look.ResourceView look.bands: - type: look.EntityRelationship # this class and the classes it reaches service-shape: # cardinality comes from here, not from the band type: sh.NodeShape sh.targetClass: Service sh.property: - { type: sh.PropertyShape, sh.path: serviceName, sh.minCount: 1, sh.maxCount: 1 }
#

steps-modeled-for-the-picture

Avoid — a Step class invented so a sequence can be drawn. It duplicates relations the services already have (calls, respondsTo) and will drift from them. Write the narrative as prose with [[references]] and let a look.Sequence read the existing object properties.

Value
Step: type: rdfs.Class stepFrom: { type: owl.ObjectProperty, rdfs.domain: Step, rdfs.range: Service } stepTo: { type: owl.ObjectProperty, rdfs.domain: Step, rdfs.range: Service } stepOrder: { type: owl.DatatypeProperty, rdfs.domain: Step, rdfs.range: xsd.integer } checkout-flow: type: Flow steps: - { type: Step, stepFrom: payment-service, stepTo: fraud-service, stepOrder: 1 } - { type: Step, stepFrom: fraud-service, stepTo: payment-service, stepOrder: 2 }
#

cardinality-on-the-band

Avoid — the band has no cardinality slot by design. Declare a sh.PropertyShape with sh.minCount / sh.maxCount (or make the property owl.FunctionalProperty); the diagram and the conformance validator then read the same constraint.

Value
- type: look.EntityRelationship look.cardinality: # no such slot, and there must not be one - { look.relation: serviceName, look.min: 1, look.max: 1 }
#

Visual Identity

A resource's icon is declared with Semantic SVG: one responsive SVG with four visibility tiers — chip, icon, card, full — so the same concept reads as a favicon, a sidebar tile, a grid card, and a hero figure without per-context variants. Each tier is a Expression (typically a Concat over SVG string literals interleaved with Property Read / Display Label), so every substitution resolves through the object model and is validated at author time. The floor on Resource supplies a first-letter glyph, so a class needs this only to override the default.

Has Recommended Rule#
TextRationale
#

A custom Semantic SVG SHOULD author its tiers as Expression values, never as raw string templates, so each embedded property reference is resolved and validated rather than leaking unrendered.

A typed expression fails at kanonak validate time when a referenced property is wrong; a string template fails silently at render time, leaking an unsubstituted placeholder.

#

Display Lenses

Cards, nav links, hero titles, and SVG glyphs all read a resource's display name and summary through two lenses: Display Label and Display Summary. The defaults on Resource are Label and Comment. A class whose name or summary lives in a domain-specific property (say teamName) declares the lens once, and every renderer reads the right value — no need to duplicate the value into rdfs.label.

Has Recommended Rule#
TextRationale
#

When a class's display name or one-line summary is NOT in Label / Comment, it SHOULD declare Display Label / Display Summary pointing at the property that holds it, rather than duplicating the value.

The lens keeps one value in one place and teaches every renderer to find it; duplicating into rdfs.label invites the two copies to drift apart.

#

team-name-lens

The Team class tells every renderer that a team's display name comes from teamName and its summary from teamCharter.

Value
my-publisher.org/org/Team: look.displayLabel: org.teamName look.displaySummary: org.teamCharter
#

The Cascade and the Floor

Looks cascade. A resource's look, semanticSvg, display lenses, and tokens each resolve by walking outward from the resource — the closest declaration wins — and bottoming out at the universal floor on Resource (published as kanonak.org/universal-look). So you only declare what differs from your superclass, and a class that declares nothing still renders a sensible page. Class-hierarchy overrides merge per facet; a per-instance override replaces.

Tokens resolve through five levels, closest first:

LevelDeclared onUse it for
instancethe resource itselfa deliberate one-off
varianta matching Instance Variant on its Viewpresentation that follows state
classany class in its type chainthe normal place to style
publisheryour Publisher resourcethe site-wide palette
floorResourcethe defaults you inherit

Set the palette at the publisher level and refine per class — a class override beats the site palette, and the site palette beats the floor. Two things worth knowing before you theme: the stock floor is a DARK theme, so a light palette must set every colour token or land half-applied; and every rendered stylesheet carries a provenance header naming which level supplied each token, which is the fastest way to answer "why is this colour not what I set?".

Has Required Rule#
TextRationale
#

Every resource MUST render even when its class declares no look: the universal floor on Resource provides the default page, glyph, and lenses. A class declares a look only to refine that default.

A guaranteed floor is what makes the system safe to adopt incrementally — you style what matters and inherit the rest, rather than authoring a full page for every class up front.

Has Recommended Rule#
TextRationale
#

A site-wide palette SHOULD be declared as Tokens on the publisher's Publisher resource, not repeated per class and not by overriding the Resource floor. Override the floor only to change what an UNSTYLED resource inherits everywhere, including resources from packages you import.

The publisher level exists so one declaration themes a site while leaving each class free to refine it. Repeating a palette per class is duplication waiting to drift. Overriding the floor does work — it is where look-tokens puts the defaults — but it is a blunter instrument: it restyles every resource the render closure reaches, so a change intended for your own pages also lands on imported vocabularies rendered alongside them.