@canonical/anatomy-dsl 0.2.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,19 @@
1
- @prefix anatomy: <http://anatomy-dsl.example.org/ontology#> .
1
+ @prefix anatomy: <https://anatomy.canonical.com/> .
2
2
  @prefix sh: <http://www.w3.org/ns/shacl#> .
3
3
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
4
4
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
5
5
  @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
6
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
7
+ # The token graph. A consumed symbol is a dt: IRI, and SymbolListShape
8
+ # constrains every member of anatomy:consumes to one.
9
+ @prefix dt: <https://dt.canonical.com/> .
10
+ # The design system graph. Bound because ds:TokenBinding reuses
11
+ # anatomy:styleKey and anatomy:styleState, and a reader of these shapes needs
12
+ # the other end of that reuse in front of them.
13
+ @prefix ds: <https://ds.canonical.com/> .
6
14
 
7
15
  # ═══════════════════════════════════════════════════════════════
8
- # SHACL SHAPES FOR ANATOMY DSL 0.2.0
16
+ # SHACL SHAPES FOR ANATOMY DSL 0.4.0
9
17
  # ═══════════════════════════════════════════════════════════════
10
18
  #
11
19
  # Constraints, patterns, enumerations, and encoding conventions.
@@ -13,13 +21,21 @@
13
21
  #
14
22
  # Shapes:
15
23
  # SpecificationShape — root document
16
- # NamedNodeShape — uri required, encoding pattern
17
- # AnonymousNodeShape — role required
24
+ # NamedNodeShape — uri required, encoding pattern, props allowed,
25
+ # edges and styles well-formed
26
+ # AnonymousNodeShape — role required, props forbidden,
27
+ # edges and styles well-formed
18
28
  # EdgeShape — edgeTarget XOR edgeSwitch
19
- # RelationShape — cardinality pattern, optional slotName
29
+ # RelationShape — cardinality pattern, optional slotName,
30
+ # optional traversal projection (field required)
20
31
  # SwitchShape — discriminator enum, minimum cases
21
32
  # SwitchCaseShape — case node required
22
- # StyleShape — reified key-value tuple (one key, one value)
33
+ # StyleShape — reified key-value tuple (one key, one value,
34
+ # optional closed-vocabulary interaction state);
35
+ # targeted by PREDICATE, since style tuples carry
36
+ # no rdf:type — see the shape for why
37
+ # ProjectionShape — at least one of projectionType/projectionField
38
+ # PropShape — reified name-value tuple (one name, one value)
23
39
  # ═══════════════════════════════════════════════════════════════
24
40
 
25
41
  # ───────────────────────────────────────────────────────────────
@@ -70,6 +86,36 @@ anatomy:NamedNodeShape a sh:NodeShape ;
70
86
  sh:path anatomy:role ;
71
87
  sh:maxCount 0 ;
72
88
  sh:message "NamedNode must not have a role (use AnonymousNode instead)." ;
89
+ ] ;
90
+
91
+ sh:property [
92
+ sh:path anatomy:hasProjection ;
93
+ sh:maxCount 1 ;
94
+ sh:class anatomy:Projection ;
95
+ sh:message "NamedNode admits at most one projection." ;
96
+ ] ;
97
+
98
+ sh:property [
99
+ sh:path anatomy:hasProp ;
100
+ sh:class anatomy:Prop ;
101
+ sh:message "hasProp must point at a Prop." ;
102
+ ] ;
103
+
104
+ sh:property [
105
+ sh:path anatomy:hasEdge ;
106
+ sh:class anatomy:Edge ;
107
+ sh:message "hasEdge must point at an Edge." ;
108
+ ] ;
109
+
110
+ sh:property [
111
+ sh:path anatomy:hasStyle ;
112
+ # Written as a negation rather than as sh:nodeKind
113
+ # sh:BlankNodeOrIRI, which is the same constraint: sem 0.1.0-alpha.2
114
+ # reports every blank-node style tuple as a violation of the nodeKind
115
+ # form, while rdf-validate-shacl accepts them. The negation is what
116
+ # the message says anyway, and both engines agree on it.
117
+ sh:not [ sh:nodeKind sh:Literal ] ;
118
+ sh:message "hasStyle must point at a style tuple, not a literal." ;
73
119
  ] .
74
120
 
75
121
  # ───────────────────────────────────────────────────────────────
@@ -94,6 +140,36 @@ anatomy:AnonymousNodeShape a sh:NodeShape ;
94
140
  sh:path anatomy:uri ;
95
141
  sh:maxCount 0 ;
96
142
  sh:message "AnonymousNode must not have a uri (use NamedNode instead)." ;
143
+ ] ;
144
+
145
+ sh:property [
146
+ sh:path anatomy:hasProjection ;
147
+ sh:maxCount 1 ;
148
+ sh:class anatomy:Projection ;
149
+ sh:message "AnonymousNode admits at most one projection." ;
150
+ ] ;
151
+
152
+ sh:property [
153
+ sh:path anatomy:hasProp ;
154
+ sh:maxCount 0 ;
155
+ sh:message "Props belong to components: anonymous nodes have no prop surface to pin — use a NamedNode." ;
156
+ ] ;
157
+
158
+ sh:property [
159
+ sh:path anatomy:hasEdge ;
160
+ sh:class anatomy:Edge ;
161
+ sh:message "hasEdge must point at an Edge." ;
162
+ ] ;
163
+
164
+ sh:property [
165
+ sh:path anatomy:hasStyle ;
166
+ # Written as a negation rather than as sh:nodeKind
167
+ # sh:BlankNodeOrIRI, which is the same constraint: sem 0.1.0-alpha.2
168
+ # reports every blank-node style tuple as a violation of the nodeKind
169
+ # form, while rdf-validate-shacl accepts them. The negation is what
170
+ # the message says anyway, and both engines agree on it.
171
+ sh:not [ sh:nodeKind sh:Literal ] ;
172
+ sh:message "hasStyle must point at a style tuple, not a literal." ;
97
173
  ] .
98
174
 
99
175
  # ───────────────────────────────────────────────────────────────
@@ -161,6 +237,28 @@ anatomy:RelationShape a sh:NodeShape ;
161
237
  sh:path anatomy:slotName ;
162
238
  sh:maxCount 1 ;
163
239
  sh:datatype xsd:string ;
240
+ ] ;
241
+
242
+ # A relation projection is a traversal: the field is mandatory.
243
+ # Type narrowing belongs on the child node, never on the relation.
244
+ sh:property [
245
+ sh:path anatomy:hasProjection ;
246
+ sh:maxCount 1 ;
247
+ sh:class anatomy:Projection ;
248
+ sh:node [
249
+ a sh:NodeShape ;
250
+ sh:property [
251
+ sh:path anatomy:projectionField ;
252
+ sh:minCount 1 ;
253
+ sh:message "Relation projections must name the traversal field." ;
254
+ ] ;
255
+ sh:property [
256
+ sh:path anatomy:projectionType ;
257
+ sh:maxCount 0 ;
258
+ sh:message "Type narrowing lives on the child node, not the relation." ;
259
+ ] ;
260
+ ] ;
261
+ sh:message "Relation admits at most one projection, and it must be a traversal (projectionField, no projectionType)." ;
164
262
  ] .
165
263
 
166
264
  # ───────────────────────────────────────────────────────────────
@@ -209,24 +307,511 @@ anatomy:SwitchCaseShape a sh:NodeShape ;
209
307
  # STYLE
210
308
  # ───────────────────────────────────────────────────────────────
211
309
 
310
+ # Targeted by PREDICATE as well as by class, and the predicate target is the
311
+ # one that does the work. Prop and Projection tuples carry an explicit
312
+ # rdf:type (":hasProp [ a :Prop ; … ]"), so sh:targetClass reaches them;
313
+ # style tuples do not (":hasStyle [ :styleKey … ; :styleValue … ]"), so a
314
+ # class target alone matched NOTHING the transform emits and every
315
+ # constraint below was inert — the CTI key pattern and the styleState
316
+ # vocabulary included. sh:targetObjectsOf reaches them where they are.
317
+ #
318
+ # The alternative is to emit "a :Style" from the transform and rewrite the
319
+ # golden pairs. That is the more uniform data model and worth doing on its
320
+ # own terms; this shape is correct either way, since both targets stay.
212
321
  anatomy:StyleShape a sh:NodeShape ;
213
322
  sh:targetClass anatomy:Style ;
323
+ sh:targetObjectsOf anatomy:hasStyle ;
214
324
  rdfs:label "Style Shape" ;
215
325
  skos:definition "Reified key-value tuple: exactly one key and one value." ;
216
326
 
327
+ # The key vocabulary is CLOSED, as styleState already is: one
328
+ # anatomy:StyleKey individual per canonical key in registry.ttl, whose
329
+ # roster is style-keys.yaml — measured over the reference stylesheets,
330
+ # with its provenance stated there and in registry.ttl. The sh:in below
331
+ # is PROJECTED from that roster by `bun run generate:shapes` and diffed
332
+ # in CI — never hand-edited.
333
+ #
334
+ # The pattern is the Path production of §4.1, not the old all-lowercase
335
+ # CTI one: a Key and a Symbol are one shape and the registry tells them
336
+ # apart, so a key may carry a camelCase segment
337
+ # (typography.lineHeight) exactly as a symbol may (color.focusRing).
217
338
  sh:property [
218
339
  sh:path anatomy:styleKey ;
219
340
  sh:minCount 1 ;
220
341
  sh:maxCount 1 ;
221
342
  sh:datatype xsd:string ;
222
- sh:pattern "^[a-z]+\\.[a-z]+(\\.[a-z]+)*$" ;
223
- sh:message "Style key must follow CTI dot-notation: category.type[.item]." ;
343
+ sh:pattern "^[a-z]+(\\.[A-Za-z0-9]+)+$" ;
344
+ sh:message "Style key must be a dotted path: a lowercase namespace then one or more segments." ;
345
+ # BEGIN GENERATED styleKey sh:in — bun run generate:shapes
346
+ sh:in (
347
+ "appearance.background"
348
+ "appearance.background.image"
349
+ "appearance.background.position"
350
+ "appearance.background.repeat"
351
+ "appearance.background.size"
352
+ "appearance.border.block.start.color"
353
+ "appearance.border.bottom.color"
354
+ "appearance.border.color"
355
+ "appearance.border.inline.end.color"
356
+ "appearance.border.style"
357
+ "appearance.border.top.width"
358
+ "appearance.border.width"
359
+ "appearance.native"
360
+ "appearance.opacity"
361
+ "appearance.outline.color"
362
+ "appearance.outline.offset"
363
+ "appearance.outline.style"
364
+ "appearance.outline.width"
365
+ "appearance.radius"
366
+ "appearance.visibility"
367
+ "interaction.cursor"
368
+ "interaction.pointerEvents"
369
+ "interaction.resize"
370
+ "interaction.select"
371
+ "layout.align"
372
+ "layout.alignContent"
373
+ "layout.alignSelf"
374
+ "layout.boxSizing"
375
+ "layout.container"
376
+ "layout.direction"
377
+ "layout.flex.basis"
378
+ "layout.flex.grow"
379
+ "layout.flex.shrink"
380
+ "layout.grid.area"
381
+ "layout.grid.autoFlow"
382
+ "layout.grid.autoRows"
383
+ "layout.grid.column"
384
+ "layout.grid.columns"
385
+ "layout.grid.row"
386
+ "layout.grid.rows"
387
+ "layout.justify"
388
+ "layout.listStyle"
389
+ "layout.objectFit"
390
+ "layout.offset.block.end"
391
+ "layout.offset.block.start"
392
+ "layout.offset.bottom"
393
+ "layout.offset.inline.start"
394
+ "layout.offset.left"
395
+ "layout.offset.right"
396
+ "layout.offset.top"
397
+ "layout.order"
398
+ "layout.overflow"
399
+ "layout.overflow.block"
400
+ "layout.overflow.inline"
401
+ "layout.overscroll"
402
+ "layout.position"
403
+ "layout.stack"
404
+ "layout.table.collapse"
405
+ "layout.table.layout"
406
+ "layout.table.spacing"
407
+ "layout.type"
408
+ "layout.verticalAlign"
409
+ "layout.wrap"
410
+ "motion.duration"
411
+ "motion.easing"
412
+ "motion.property"
413
+ "size.block"
414
+ "size.height"
415
+ "size.inline"
416
+ "size.max.block"
417
+ "size.max.height"
418
+ "size.max.width"
419
+ "size.min.block"
420
+ "size.min.height"
421
+ "size.min.width"
422
+ "size.width"
423
+ "spacing.external.block.end"
424
+ "spacing.external.block.start"
425
+ "spacing.external.bottom"
426
+ "spacing.external.inline.end"
427
+ "spacing.external.inline.start"
428
+ "spacing.external.left"
429
+ "spacing.external.right"
430
+ "spacing.external.top"
431
+ "spacing.gap"
432
+ "spacing.gap.block"
433
+ "spacing.gap.inline"
434
+ "spacing.internal.block.end"
435
+ "spacing.internal.block.start"
436
+ "spacing.internal.bottom"
437
+ "spacing.internal.inline.end"
438
+ "spacing.internal.inline.start"
439
+ "spacing.internal.left"
440
+ "spacing.internal.top"
441
+ "typography.align"
442
+ "typography.color"
443
+ "typography.decoration"
444
+ "typography.decoration.offset"
445
+ "typography.decoration.thickness"
446
+ "typography.direction"
447
+ "typography.font"
448
+ "typography.fontFamily"
449
+ "typography.letterSpacing"
450
+ "typography.lineHeight"
451
+ "typography.overflow"
452
+ "typography.size"
453
+ "typography.variant.caps"
454
+ "typography.variant.numeric"
455
+ "typography.weight"
456
+ "typography.whiteSpace"
457
+ "typography.wrap"
458
+ ) ;
459
+ # END GENERATED styleKey sh:in
224
460
  ] ;
225
461
 
462
+ # The authored spelling, verbatim, as the evidence of what the reference
463
+ # says: a scalar as written, a sequence in its flow form. The symbols are
464
+ # NOT re-derived from it — anatomy:consumes carries them — so the three
465
+ # constraints here are only that the retired notation cannot reach the
466
+ # graph through it.
226
467
  sh:property [
227
468
  sh:path anatomy:styleValue ;
228
469
  sh:minCount 1 ;
229
470
  sh:maxCount 1 ;
230
471
  sh:datatype xsd:string ;
231
472
  sh:message "Style requires exactly one value." ;
473
+ ] ;
474
+
475
+ sh:property [
476
+ sh:path anatomy:styleValue ;
477
+ # The slash-delimited path is retired (AT.01). Anchored on the whole
478
+ # value, so a literal that merely holds a slash between spaces
479
+ # ("1 / -1", a grid line the reference writes 42 times) is untouched.
480
+ sh:not [ sh:pattern "^[A-Za-z0-9-]+(/[A-Za-z0-9-]+)+\\??$" ] ;
481
+ sh:message "The slash-delimited token path is retired: write the symbol's own dotted name." ;
482
+ ] ;
483
+
484
+ sh:property [
485
+ sh:path anatomy:styleValue ;
486
+ # The trailing "?" marker is retired (AT.03). An element that
487
+ # resolves nowhere is a register row and a comment, not a sigil.
488
+ sh:not [ sh:pattern "\\?$" ] ;
489
+ sh:message "The trailing '?' optional marker is retired: an unresolved element is a register row and a comment." ;
490
+ ] ;
491
+
492
+ sh:property [
493
+ sh:path anatomy:styleValue ;
494
+ sh:not [ sh:pattern "(^|[./])\\$?root([./]|$)" ] ;
495
+ sh:message "'root' and '$root' are not value segments." ;
496
+ ] ;
497
+
498
+ # THE SEAM. An rdf:List of dt:TokenSymbol IRIs in fallback order, whose
499
+ # head is the primary symbol; the authored spelling stays in styleValue as
500
+ # evidence. Whether a key carries one is the registry's business, and the
501
+ # generated sh:or below is where that is said.
502
+ sh:property [
503
+ sh:path anatomy:consumes ;
504
+ sh:maxCount 1 ;
505
+ sh:node anatomy:SymbolListShape ;
506
+ sh:message "consumes must be one rdf:List of dt: token symbols, in fallback order." ;
507
+ ] ;
508
+
509
+ # Closed v1 vocabulary, registry-governed. Additions go through the
510
+ # registry (checked, visited, dragged, pending, error, read-only are
511
+ # candidates), never through loosening this list ad hoc. There is no
512
+ # "default" state: an absent styleState IS the default.
513
+ #
514
+ # A state value that differs from its base state's is a LINT, not a
515
+ # constraint (AT.11): the reference does it — Button's :disabled reads a
516
+ # different symbol — so it is reported with its ranks and never rejected,
517
+ # and the report lives in design-system's validator, not here.
518
+ sh:property [
519
+ sh:path anatomy:styleState ;
520
+ sh:maxCount 1 ;
521
+ sh:datatype xsd:string ;
522
+ sh:in ( "hover" "active" "focus" "disabled" "selected" ) ;
523
+ sh:message "styleState must be one of: hover, active, focus, disabled, selected (absent = default state)." ;
524
+ ] ;
525
+
526
+ # BEGIN GENERATED valueKind sh:or — bun run generate:shapes
527
+ sh:or (
528
+ [ # a primitive key: a literal, and nothing consumed
529
+ sh:property [
530
+ sh:path anatomy:styleKey ;
531
+ sh:in (
532
+ "appearance.background.image"
533
+ "appearance.background.position"
534
+ "appearance.background.repeat"
535
+ "appearance.background.size"
536
+ "appearance.border.style"
537
+ "appearance.native"
538
+ "appearance.opacity"
539
+ "appearance.outline.style"
540
+ "appearance.visibility"
541
+ "interaction.cursor"
542
+ "interaction.pointerEvents"
543
+ "interaction.resize"
544
+ "interaction.select"
545
+ "layout.align"
546
+ "layout.alignContent"
547
+ "layout.alignSelf"
548
+ "layout.boxSizing"
549
+ "layout.container"
550
+ "layout.direction"
551
+ "layout.flex.grow"
552
+ "layout.flex.shrink"
553
+ "layout.grid.area"
554
+ "layout.grid.autoFlow"
555
+ "layout.grid.autoRows"
556
+ "layout.grid.column"
557
+ "layout.grid.columns"
558
+ "layout.grid.row"
559
+ "layout.grid.rows"
560
+ "layout.justify"
561
+ "layout.listStyle"
562
+ "layout.objectFit"
563
+ "layout.order"
564
+ "layout.overflow"
565
+ "layout.overflow.block"
566
+ "layout.overflow.inline"
567
+ "layout.overscroll"
568
+ "layout.position"
569
+ "layout.stack"
570
+ "layout.table.collapse"
571
+ "layout.table.layout"
572
+ "layout.table.spacing"
573
+ "layout.type"
574
+ "layout.verticalAlign"
575
+ "layout.wrap"
576
+ "motion.property"
577
+ "typography.align"
578
+ "typography.decoration"
579
+ "typography.direction"
580
+ "typography.overflow"
581
+ "typography.variant.caps"
582
+ "typography.variant.numeric"
583
+ "typography.whiteSpace"
584
+ "typography.wrap"
585
+ ) ;
586
+ ] ;
587
+ sh:property [ sh:path anatomy:consumes ; sh:maxCount 0 ] ;
588
+ sh:property [
589
+ sh:path anatomy:styleValue ;
590
+ sh:not [ sh:pattern "^[a-z]+(\\.[A-Za-z0-9]+)+$" ] ;
591
+ ] ;
592
+ ]
593
+ [ # a token key: the symbols are consumed, always
594
+ sh:property [
595
+ sh:path anatomy:styleKey ;
596
+ sh:in (
597
+ "appearance.border.block.start.color"
598
+ "appearance.border.bottom.color"
599
+ "appearance.border.inline.end.color"
600
+ "layout.offset.block.end"
601
+ "layout.offset.block.start"
602
+ "layout.offset.inline.start"
603
+ "size.inline"
604
+ "size.max.block"
605
+ "size.min.block"
606
+ "size.min.height"
607
+ "spacing.gap.block"
608
+ "spacing.gap.inline"
609
+ "typography.decoration.offset"
610
+ ) ;
611
+ ] ;
612
+ sh:property [ sh:path anatomy:consumes ; sh:minCount 1 ] ;
613
+ ]
614
+ [ # an either key, consuming symbols
615
+ sh:property [
616
+ sh:path anatomy:styleKey ;
617
+ sh:in (
618
+ "appearance.background"
619
+ "appearance.border.color"
620
+ "appearance.border.top.width"
621
+ "appearance.border.width"
622
+ "appearance.outline.color"
623
+ "appearance.outline.offset"
624
+ "appearance.outline.width"
625
+ "appearance.radius"
626
+ "layout.flex.basis"
627
+ "layout.offset.bottom"
628
+ "layout.offset.left"
629
+ "layout.offset.right"
630
+ "layout.offset.top"
631
+ "motion.duration"
632
+ "motion.easing"
633
+ "size.block"
634
+ "size.height"
635
+ "size.max.height"
636
+ "size.max.width"
637
+ "size.min.width"
638
+ "size.width"
639
+ "spacing.external.block.end"
640
+ "spacing.external.block.start"
641
+ "spacing.external.bottom"
642
+ "spacing.external.inline.end"
643
+ "spacing.external.inline.start"
644
+ "spacing.external.left"
645
+ "spacing.external.right"
646
+ "spacing.external.top"
647
+ "spacing.gap"
648
+ "spacing.internal.block.end"
649
+ "spacing.internal.block.start"
650
+ "spacing.internal.bottom"
651
+ "spacing.internal.inline.end"
652
+ "spacing.internal.inline.start"
653
+ "spacing.internal.left"
654
+ "spacing.internal.top"
655
+ "typography.color"
656
+ "typography.decoration.thickness"
657
+ "typography.font"
658
+ "typography.fontFamily"
659
+ "typography.letterSpacing"
660
+ "typography.lineHeight"
661
+ "typography.size"
662
+ "typography.weight"
663
+ ) ;
664
+ ] ;
665
+ sh:property [ sh:path anatomy:consumes ; sh:minCount 1 ] ;
666
+ ]
667
+ [ # an either key, carrying a primitive
668
+ sh:property [
669
+ sh:path anatomy:styleKey ;
670
+ sh:in (
671
+ "appearance.background"
672
+ "appearance.border.color"
673
+ "appearance.border.top.width"
674
+ "appearance.border.width"
675
+ "appearance.outline.color"
676
+ "appearance.outline.offset"
677
+ "appearance.outline.width"
678
+ "appearance.radius"
679
+ "layout.flex.basis"
680
+ "layout.offset.bottom"
681
+ "layout.offset.left"
682
+ "layout.offset.right"
683
+ "layout.offset.top"
684
+ "motion.duration"
685
+ "motion.easing"
686
+ "size.block"
687
+ "size.height"
688
+ "size.max.height"
689
+ "size.max.width"
690
+ "size.min.width"
691
+ "size.width"
692
+ "spacing.external.block.end"
693
+ "spacing.external.block.start"
694
+ "spacing.external.bottom"
695
+ "spacing.external.inline.end"
696
+ "spacing.external.inline.start"
697
+ "spacing.external.left"
698
+ "spacing.external.right"
699
+ "spacing.external.top"
700
+ "spacing.gap"
701
+ "spacing.internal.block.end"
702
+ "spacing.internal.block.start"
703
+ "spacing.internal.bottom"
704
+ "spacing.internal.inline.end"
705
+ "spacing.internal.inline.start"
706
+ "spacing.internal.left"
707
+ "spacing.internal.top"
708
+ "typography.color"
709
+ "typography.decoration.thickness"
710
+ "typography.font"
711
+ "typography.fontFamily"
712
+ "typography.letterSpacing"
713
+ "typography.lineHeight"
714
+ "typography.size"
715
+ "typography.weight"
716
+ ) ;
717
+ ] ;
718
+ sh:property [ sh:path anatomy:consumes ; sh:maxCount 0 ] ;
719
+ sh:property [
720
+ sh:path anatomy:styleValue ;
721
+ sh:not [ sh:pattern "^[a-z]+(\\.[A-Za-z0-9]+)+$" ] ;
722
+ ] ;
723
+ ]
724
+ ) ;
725
+ sh:message "A style tuple must match its key's valueKind: a token key consumes symbols, a primitive key carries a literal and consumes none, an either key does one or the other." ;
726
+ # END GENERATED valueKind sh:or
727
+ rdfs:comment "The valueKind disjunction above is generated from registry.ttl." .
728
+
729
+ # ───────────────────────────────────────────────────────────────
730
+ # SYMBOL LIST
731
+ # ───────────────────────────────────────────────────────────────
732
+
733
+ anatomy:SymbolListShape a sh:NodeShape ;
734
+ rdfs:label "Symbol List Shape" ;
735
+ skos:definition """The shape of anatomy:consumes: every member of the
736
+ rdf:List is an IRI in the token graph's namespace whose local name is a
737
+ dotted symbol. The list is the fallback order, so its head is the primary
738
+ symbol; a member that is a literal, a blank node or an IRI outside dt: is
739
+ a violation. Whether a member RESOLVES — whether the token graph declares
740
+ that symbol — is not asked here: a name no stratum declares is kept as
741
+ consumed with a register row (AT.09), and the check belongs to the
742
+ consumer holding the strata.""" ;
743
+
744
+ sh:property [
745
+ sh:path ( [ sh:zeroOrMorePath rdf:rest ] rdf:first ) ;
746
+ sh:minCount 1 ;
747
+ sh:nodeKind sh:IRI ;
748
+ sh:pattern "^https://dt\\.canonical\\.com/[a-z]+(\\.[A-Za-z0-9]+)+$" ;
749
+ sh:message "Every consumed element must be a dt: token symbol IRI with a dotted local name." ;
750
+ ] .
751
+
752
+ # ───────────────────────────────────────────────────────────────
753
+ # PROJECTION
754
+ # ───────────────────────────────────────────────────────────────
755
+
756
+ anatomy:ProjectionShape a sh:NodeShape ;
757
+ sh:targetClass anatomy:Projection ;
758
+ rdfs:label "Projection Shape" ;
759
+ skos:definition "Fragment-style graph binding: a type condition, a field path, or both — never neither." ;
760
+
761
+ sh:property [
762
+ sh:path anatomy:projectionType ;
763
+ sh:maxCount 1 ;
764
+ sh:datatype xsd:string ;
765
+ sh:pattern "^[A-Z][A-Za-z0-9_]*$" ;
766
+ sh:message "Projection type must be a GraphQL type name (PascalCase)." ;
767
+ ] ;
768
+
769
+ sh:property [
770
+ sh:path anatomy:projectionField ;
771
+ sh:maxCount 1 ;
772
+ sh:datatype xsd:string ;
773
+ sh:pattern "^[_A-Za-z][_A-Za-z0-9]*(\\.[_A-Za-z][_A-Za-z0-9]*)*$" ;
774
+ sh:message "Projection field must be a dot-delimited path of GraphQL field names." ;
775
+ ] ;
776
+
777
+ sh:or (
778
+ [
779
+ sh:property [
780
+ sh:path anatomy:projectionType ;
781
+ sh:minCount 1 ;
782
+ ] ;
783
+ ]
784
+ [
785
+ sh:property [
786
+ sh:path anatomy:projectionField ;
787
+ sh:minCount 1 ;
788
+ ] ;
789
+ ]
790
+ ) ;
791
+ sh:message "Projection requires at least one of projectionType or projectionField." .
792
+
793
+ # ───────────────────────────────────────────────────────────────
794
+ # PROP
795
+ # ───────────────────────────────────────────────────────────────
796
+
797
+ anatomy:PropShape a sh:NodeShape ;
798
+ sh:targetClass anatomy:Prop ;
799
+ rdfs:label "Prop Shape" ;
800
+ skos:definition "Reified pinned-prop tuple: exactly one name and one value. Whether the prop exists on the referenced component is validated by consumers holding the design system graph, not here." ;
801
+
802
+ sh:property [
803
+ sh:path anatomy:propName ;
804
+ sh:minCount 1 ;
805
+ sh:maxCount 1 ;
806
+ sh:datatype xsd:string ;
807
+ sh:pattern "^[a-z][a-zA-Z0-9]*$" ;
808
+ sh:message "Prop name must be a camelCase identifier as defined on the component." ;
809
+ ] ;
810
+
811
+ sh:property [
812
+ sh:path anatomy:propValue ;
813
+ sh:minCount 1 ;
814
+ sh:maxCount 1 ;
815
+ sh:datatype xsd:string ;
816
+ sh:message "Prop requires exactly one value." ;
232
817
  ] .