yarramate 0.7.0 → 0.8.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.
- package/catalogues/core-enrichment.yaml +284 -6
- package/dist/apply-command.js +201 -54
- package/dist/design-command.js +6 -0
- package/dist/interrogate-command.d.ts +2 -0
- package/dist/interrogate-command.js +4 -2
- package/package.json +1 -1
- package/schema/yarramate-design-step.schema.json +11 -0
- package/schema/yarramate-interrogation-report.schema.json +1 -0
- package/schema/yarramate-operations.schema.json +31 -0
- package/schema/yarramate-question-catalogue.schema.json +5 -0
- package/skills/yarramate-architecture/SKILL.md +5 -1
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
format: yarramate/question-catalogue/v1
|
|
2
2
|
id: core-enrichment
|
|
3
|
-
version: "0.
|
|
3
|
+
version: "0.4"
|
|
4
4
|
profile: yarramate/core@0.1
|
|
5
5
|
presentation:
|
|
6
6
|
title: Core enrichment interview
|
|
7
7
|
description: >-
|
|
8
|
-
The guided design path: motivation, business,
|
|
9
|
-
cross-cutting hygiene
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
The guided design path: motivation, business, application, technology,
|
|
9
|
+
and implementation waves plus cross-cutting hygiene. Each question
|
|
10
|
+
states the decision its answer changes; a question that cannot is
|
|
11
|
+
deleted, not softened. Adequacy is enforced by linkage depth and
|
|
12
|
+
attestation, never by reading words. Versioning is additive within a
|
|
13
|
+
major version: every question records the catalogue version it arrived
|
|
14
|
+
in (ADR 0063), and a completed interview honestly reopens when the
|
|
15
|
+
path deepens.
|
|
13
16
|
|
|
14
17
|
waves:
|
|
15
18
|
- id: motivation
|
|
@@ -23,6 +26,16 @@ waves:
|
|
|
23
26
|
description: >-
|
|
24
27
|
How declared services are realized, performed, and fed with
|
|
25
28
|
information.
|
|
29
|
+
- id: technology
|
|
30
|
+
name: Technology
|
|
31
|
+
description: >-
|
|
32
|
+
Where the declared applications actually run and what materializes
|
|
33
|
+
them.
|
|
34
|
+
- id: implementation
|
|
35
|
+
name: Implementation
|
|
36
|
+
description: >-
|
|
37
|
+
How the planned architecture becomes real: work, deliverables, and
|
|
38
|
+
the plateaus between here and there.
|
|
26
39
|
- id: hygiene
|
|
27
40
|
name: Model hygiene
|
|
28
41
|
description: Cross-cutting completeness that keeps every wave honest.
|
|
@@ -31,6 +44,7 @@ questions:
|
|
|
31
44
|
# ---- motivation ----------------------------------------------------------
|
|
32
45
|
- id: outcome-missing
|
|
33
46
|
wave: motivation
|
|
47
|
+
since: "0.1"
|
|
34
48
|
scope: workspace
|
|
35
49
|
trigger:
|
|
36
50
|
- condition: no-subject-of-kind
|
|
@@ -50,6 +64,7 @@ questions:
|
|
|
50
64
|
|
|
51
65
|
- id: stakeholders-missing
|
|
52
66
|
wave: motivation
|
|
67
|
+
since: "0.1"
|
|
53
68
|
scope: workspace
|
|
54
69
|
trigger:
|
|
55
70
|
- condition: no-subject-of-kind
|
|
@@ -68,6 +83,7 @@ questions:
|
|
|
68
83
|
|
|
69
84
|
- id: constraints-missing
|
|
70
85
|
wave: motivation
|
|
86
|
+
since: "0.1"
|
|
71
87
|
scope: workspace
|
|
72
88
|
trigger:
|
|
73
89
|
- condition: no-subject-of-kind
|
|
@@ -86,6 +102,7 @@ questions:
|
|
|
86
102
|
|
|
87
103
|
- id: goal-unrealized
|
|
88
104
|
wave: motivation
|
|
105
|
+
since: "0.1"
|
|
89
106
|
scope: subject
|
|
90
107
|
subjects:
|
|
91
108
|
kinds:
|
|
@@ -110,6 +127,7 @@ questions:
|
|
|
110
127
|
|
|
111
128
|
- id: goal-no-driver
|
|
112
129
|
wave: motivation
|
|
130
|
+
since: "0.3"
|
|
113
131
|
scope: subject
|
|
114
132
|
subjects:
|
|
115
133
|
kinds:
|
|
@@ -137,6 +155,7 @@ questions:
|
|
|
137
155
|
|
|
138
156
|
- id: driver-influences-nothing
|
|
139
157
|
wave: motivation
|
|
158
|
+
since: "0.3"
|
|
140
159
|
scope: subject
|
|
141
160
|
subjects:
|
|
142
161
|
kinds:
|
|
@@ -158,6 +177,7 @@ questions:
|
|
|
158
177
|
|
|
159
178
|
- id: stakeholder-unconcerned
|
|
160
179
|
wave: motivation
|
|
180
|
+
since: "0.3"
|
|
161
181
|
scope: subject
|
|
162
182
|
subjects:
|
|
163
183
|
kinds:
|
|
@@ -180,6 +200,7 @@ questions:
|
|
|
180
200
|
|
|
181
201
|
- id: requirement-unrealized
|
|
182
202
|
wave: motivation
|
|
203
|
+
since: "0.3"
|
|
183
204
|
scope: subject
|
|
184
205
|
subjects:
|
|
185
206
|
kinds:
|
|
@@ -203,6 +224,7 @@ questions:
|
|
|
203
224
|
|
|
204
225
|
- id: principle-unapplied
|
|
205
226
|
wave: motivation
|
|
227
|
+
since: "0.3"
|
|
206
228
|
scope: subject
|
|
207
229
|
subjects:
|
|
208
230
|
kinds:
|
|
@@ -225,6 +247,7 @@ questions:
|
|
|
225
247
|
|
|
226
248
|
- id: assessment-unlinked
|
|
227
249
|
wave: motivation
|
|
250
|
+
since: "0.3"
|
|
228
251
|
scope: subject
|
|
229
252
|
subjects:
|
|
230
253
|
kinds:
|
|
@@ -243,6 +266,7 @@ questions:
|
|
|
243
266
|
|
|
244
267
|
- id: motivation-unattested
|
|
245
268
|
wave: motivation
|
|
269
|
+
since: "0.3"
|
|
246
270
|
scope: subject
|
|
247
271
|
subjects:
|
|
248
272
|
kinds:
|
|
@@ -268,6 +292,7 @@ questions:
|
|
|
268
292
|
# ---- business ------------------------------------------------------------
|
|
269
293
|
- id: service-consumer-unknown
|
|
270
294
|
wave: business
|
|
295
|
+
since: "0.1"
|
|
271
296
|
scope: subject
|
|
272
297
|
subjects:
|
|
273
298
|
kinds:
|
|
@@ -291,6 +316,7 @@ questions:
|
|
|
291
316
|
|
|
292
317
|
- id: owner-missing
|
|
293
318
|
wave: business
|
|
319
|
+
since: "0.1"
|
|
294
320
|
scope: subject
|
|
295
321
|
subjects:
|
|
296
322
|
kinds:
|
|
@@ -313,6 +339,7 @@ questions:
|
|
|
313
339
|
|
|
314
340
|
- id: actor-unassigned
|
|
315
341
|
wave: business
|
|
342
|
+
since: "0.1"
|
|
316
343
|
scope: subject
|
|
317
344
|
subjects:
|
|
318
345
|
kinds:
|
|
@@ -335,6 +362,7 @@ questions:
|
|
|
335
362
|
|
|
336
363
|
- id: information-unaccessed
|
|
337
364
|
wave: business
|
|
365
|
+
since: "0.1"
|
|
338
366
|
scope: subject
|
|
339
367
|
subjects:
|
|
340
368
|
kinds:
|
|
@@ -358,6 +386,7 @@ questions:
|
|
|
358
386
|
|
|
359
387
|
- id: no-service-declared
|
|
360
388
|
wave: business
|
|
389
|
+
since: "0.1"
|
|
361
390
|
scope: workspace
|
|
362
391
|
trigger:
|
|
363
392
|
- condition: no-subject-of-kind
|
|
@@ -378,6 +407,7 @@ questions:
|
|
|
378
407
|
|
|
379
408
|
- id: service-realizes-no-motivation
|
|
380
409
|
wave: business
|
|
410
|
+
since: "0.3"
|
|
381
411
|
scope: subject
|
|
382
412
|
subjects:
|
|
383
413
|
kinds:
|
|
@@ -405,6 +435,7 @@ questions:
|
|
|
405
435
|
|
|
406
436
|
- id: process-untriggered
|
|
407
437
|
wave: business
|
|
438
|
+
since: "0.3"
|
|
408
439
|
scope: subject
|
|
409
440
|
subjects:
|
|
410
441
|
kinds:
|
|
@@ -427,6 +458,7 @@ questions:
|
|
|
427
458
|
|
|
428
459
|
- id: business-service-unrealized
|
|
429
460
|
wave: business
|
|
461
|
+
since: "0.3"
|
|
430
462
|
scope: subject
|
|
431
463
|
subjects:
|
|
432
464
|
kinds:
|
|
@@ -460,6 +492,7 @@ questions:
|
|
|
460
492
|
# ---- application ---------------------------------------------------------
|
|
461
493
|
- id: app-service-unrealized
|
|
462
494
|
wave: application
|
|
495
|
+
since: "0.3"
|
|
463
496
|
scope: subject
|
|
464
497
|
subjects:
|
|
465
498
|
kinds:
|
|
@@ -490,6 +523,7 @@ questions:
|
|
|
490
523
|
|
|
491
524
|
- id: component-realizes-nothing
|
|
492
525
|
wave: application
|
|
526
|
+
since: "0.3"
|
|
493
527
|
scope: subject
|
|
494
528
|
subjects:
|
|
495
529
|
kinds:
|
|
@@ -515,6 +549,7 @@ questions:
|
|
|
515
549
|
|
|
516
550
|
- id: behavior-unassigned
|
|
517
551
|
wave: application
|
|
552
|
+
since: "0.3"
|
|
518
553
|
scope: subject
|
|
519
554
|
subjects:
|
|
520
555
|
kinds:
|
|
@@ -546,6 +581,7 @@ questions:
|
|
|
546
581
|
|
|
547
582
|
- id: event-triggers-nothing
|
|
548
583
|
wave: application
|
|
584
|
+
since: "0.3"
|
|
549
585
|
scope: subject
|
|
550
586
|
subjects:
|
|
551
587
|
kinds:
|
|
@@ -568,6 +604,7 @@ questions:
|
|
|
568
604
|
|
|
569
605
|
- id: information-unowned
|
|
570
606
|
wave: application
|
|
607
|
+
since: "0.3"
|
|
571
608
|
scope: subject
|
|
572
609
|
subjects:
|
|
573
610
|
kinds:
|
|
@@ -589,6 +626,7 @@ questions:
|
|
|
589
626
|
|
|
590
627
|
- id: planned-design-unattested
|
|
591
628
|
wave: application
|
|
629
|
+
since: "0.3"
|
|
592
630
|
scope: subject
|
|
593
631
|
subjects:
|
|
594
632
|
kinds:
|
|
@@ -612,9 +650,246 @@ questions:
|
|
|
612
650
|
topic "design-review" (revoke by deleting it when the design
|
|
613
651
|
changes).
|
|
614
652
|
|
|
653
|
+
# ---- technology ----------------------------------------------------------
|
|
654
|
+
- id: component-unhosted
|
|
655
|
+
wave: technology
|
|
656
|
+
since: "0.4"
|
|
657
|
+
scope: subject
|
|
658
|
+
subjects:
|
|
659
|
+
kinds:
|
|
660
|
+
- yarramate/core@0.1#applicationComponent
|
|
661
|
+
kindMatching: exact
|
|
662
|
+
statuses:
|
|
663
|
+
- planned
|
|
664
|
+
- current
|
|
665
|
+
trigger:
|
|
666
|
+
- condition: missing-linkage
|
|
667
|
+
kinds:
|
|
668
|
+
- yarramate/core@0.1#serving
|
|
669
|
+
- yarramate/core@0.1#assignment
|
|
670
|
+
direction: incoming
|
|
671
|
+
counterpartKinds:
|
|
672
|
+
- yarramate/core@0.1#node
|
|
673
|
+
- yarramate/core@0.1#device
|
|
674
|
+
- yarramate/core@0.1#systemSoftware
|
|
675
|
+
- yarramate/core@0.1#technologyService
|
|
676
|
+
question: >-
|
|
677
|
+
Where does {subject.name} run?
|
|
678
|
+
materiality: >-
|
|
679
|
+
The hosting boundary decides latency, failure domain, scaling
|
|
680
|
+
model, and data residency; a component with no declared runtime is
|
|
681
|
+
deployed by whoever gets there first. Matching is exact by intent:
|
|
682
|
+
profile-derived module kinds inherit their deployable parent's
|
|
683
|
+
hosting rather than answering separately.
|
|
684
|
+
authority: either
|
|
685
|
+
resolution: >-
|
|
686
|
+
Add the node, device, or system software that hosts it with a
|
|
687
|
+
serving or assignment relationship into the component.
|
|
688
|
+
|
|
689
|
+
- id: node-serves-nothing
|
|
690
|
+
wave: technology
|
|
691
|
+
since: "0.4"
|
|
692
|
+
scope: subject
|
|
693
|
+
subjects:
|
|
694
|
+
kinds:
|
|
695
|
+
- yarramate/core@0.1#node
|
|
696
|
+
- yarramate/core@0.1#device
|
|
697
|
+
- yarramate/core@0.1#systemSoftware
|
|
698
|
+
trigger:
|
|
699
|
+
- condition: missing-relationship
|
|
700
|
+
kinds:
|
|
701
|
+
- yarramate/core@0.1#serving
|
|
702
|
+
- yarramate/core@0.1#assignment
|
|
703
|
+
direction: outgoing
|
|
704
|
+
question: >-
|
|
705
|
+
What does {subject.name} host or serve?
|
|
706
|
+
materiality: >-
|
|
707
|
+
Declared infrastructure that serves nothing is either cost without
|
|
708
|
+
purpose or missing the links that justify it; both change the
|
|
709
|
+
deployment budget.
|
|
710
|
+
authority: either
|
|
711
|
+
resolution: >-
|
|
712
|
+
Add serving or assignment relationships to the applications and
|
|
713
|
+
behavior it hosts, or remove it.
|
|
714
|
+
|
|
715
|
+
- id: technology-service-unrealized
|
|
716
|
+
wave: technology
|
|
717
|
+
since: "0.4"
|
|
718
|
+
scope: subject
|
|
719
|
+
subjects:
|
|
720
|
+
kinds:
|
|
721
|
+
- yarramate/core@0.1#technologyService
|
|
722
|
+
statuses:
|
|
723
|
+
- planned
|
|
724
|
+
- current
|
|
725
|
+
trigger:
|
|
726
|
+
- condition: missing-linkage
|
|
727
|
+
kinds:
|
|
728
|
+
- yarramate/core@0.1#realization
|
|
729
|
+
direction: incoming
|
|
730
|
+
counterpartKinds:
|
|
731
|
+
- yarramate/core@0.1#node
|
|
732
|
+
- yarramate/core@0.1#systemSoftware
|
|
733
|
+
- yarramate/core@0.1#technologyFunction
|
|
734
|
+
- yarramate/core@0.1#technologyProcess
|
|
735
|
+
question: >-
|
|
736
|
+
What provides {subject.name}?
|
|
737
|
+
materiality: >-
|
|
738
|
+
A technology service with no realizing node or behavior is a
|
|
739
|
+
dependency assumed rather than provided; outages and upgrades have
|
|
740
|
+
no owner in the model.
|
|
741
|
+
authority: either
|
|
742
|
+
resolution: >-
|
|
743
|
+
Add realization from the node, system software, or technology
|
|
744
|
+
behavior that provides the service.
|
|
745
|
+
|
|
746
|
+
- id: artifact-unassigned
|
|
747
|
+
wave: technology
|
|
748
|
+
since: "0.4"
|
|
749
|
+
scope: subject
|
|
750
|
+
subjects:
|
|
751
|
+
kinds:
|
|
752
|
+
- yarramate/core@0.1#artifact
|
|
753
|
+
kindMatching: exact
|
|
754
|
+
trigger:
|
|
755
|
+
- condition: missing-relationship
|
|
756
|
+
kinds:
|
|
757
|
+
- yarramate/core@0.1#assignment
|
|
758
|
+
- yarramate/core@0.1#realization
|
|
759
|
+
direction: any
|
|
760
|
+
question: >-
|
|
761
|
+
What deploys {subject.name}, and what does it materialize?
|
|
762
|
+
materiality: >-
|
|
763
|
+
An artifact with no deployment target and nothing it realizes is a
|
|
764
|
+
build output the architecture cannot place; release engineering
|
|
765
|
+
starts from exactly these links.
|
|
766
|
+
authority: either
|
|
767
|
+
resolution: >-
|
|
768
|
+
Assign the artifact to the node that deploys it and add realization
|
|
769
|
+
to the component or data it materializes.
|
|
770
|
+
|
|
771
|
+
# ---- implementation ------------------------------------------------------
|
|
772
|
+
- id: workpackage-delivers-nothing
|
|
773
|
+
wave: implementation
|
|
774
|
+
since: "0.4"
|
|
775
|
+
scope: subject
|
|
776
|
+
subjects:
|
|
777
|
+
kinds:
|
|
778
|
+
- yarramate/core@0.1#workPackage
|
|
779
|
+
trigger:
|
|
780
|
+
- condition: missing-relationship
|
|
781
|
+
kinds:
|
|
782
|
+
- yarramate/core@0.1#realization
|
|
783
|
+
- yarramate/core@0.1#aggregation
|
|
784
|
+
direction: outgoing
|
|
785
|
+
question: >-
|
|
786
|
+
What does {subject.name} produce?
|
|
787
|
+
materiality: >-
|
|
788
|
+
Work with no deliverable cannot be accepted or declared done; the
|
|
789
|
+
deliverable link is what makes progress reviewable rather than
|
|
790
|
+
reported.
|
|
791
|
+
authority: either
|
|
792
|
+
resolution: >-
|
|
793
|
+
Add realization from the work package to its deliverables.
|
|
794
|
+
|
|
795
|
+
- id: workpackage-unassigned
|
|
796
|
+
wave: implementation
|
|
797
|
+
since: "0.4"
|
|
798
|
+
scope: subject
|
|
799
|
+
subjects:
|
|
800
|
+
kinds:
|
|
801
|
+
- yarramate/core@0.1#workPackage
|
|
802
|
+
trigger:
|
|
803
|
+
- condition: missing-linkage
|
|
804
|
+
kinds:
|
|
805
|
+
- yarramate/core@0.1#assignment
|
|
806
|
+
direction: incoming
|
|
807
|
+
counterpartKinds:
|
|
808
|
+
- yarramate/core@0.1#businessActor
|
|
809
|
+
- yarramate/core@0.1#businessRole
|
|
810
|
+
question: >-
|
|
811
|
+
Who is committed to {subject.name}?
|
|
812
|
+
materiality: >-
|
|
813
|
+
An unassigned work package is a plan without capacity; commitment
|
|
814
|
+
is the difference between a roadmap and a wish list.
|
|
815
|
+
authority: human
|
|
816
|
+
resolution: >-
|
|
817
|
+
Add assignment from the actor or role that owns the work.
|
|
818
|
+
|
|
819
|
+
- id: deliverable-realizes-nothing
|
|
820
|
+
wave: implementation
|
|
821
|
+
since: "0.4"
|
|
822
|
+
scope: subject
|
|
823
|
+
subjects:
|
|
824
|
+
kinds:
|
|
825
|
+
- yarramate/core@0.1#deliverable
|
|
826
|
+
kindMatching: exact
|
|
827
|
+
trigger:
|
|
828
|
+
- condition: missing-relationship
|
|
829
|
+
kinds:
|
|
830
|
+
- yarramate/core@0.1#realization
|
|
831
|
+
direction: outgoing
|
|
832
|
+
question: >-
|
|
833
|
+
What does {subject.name} make real?
|
|
834
|
+
materiality: >-
|
|
835
|
+
A deliverable that realizes no architecture element is effort
|
|
836
|
+
disconnected from the design; its acceptance criteria cannot be
|
|
837
|
+
stated architecturally.
|
|
838
|
+
authority: either
|
|
839
|
+
resolution: >-
|
|
840
|
+
Add realization from the deliverable to the plateau, requirement,
|
|
841
|
+
or element it brings into being.
|
|
842
|
+
|
|
843
|
+
- id: plateau-aggregates-nothing
|
|
844
|
+
wave: implementation
|
|
845
|
+
since: "0.4"
|
|
846
|
+
scope: subject
|
|
847
|
+
subjects:
|
|
848
|
+
kinds:
|
|
849
|
+
- yarramate/core@0.1#plateau
|
|
850
|
+
trigger:
|
|
851
|
+
- condition: missing-relationship
|
|
852
|
+
kinds:
|
|
853
|
+
- yarramate/core@0.1#aggregation
|
|
854
|
+
- yarramate/core@0.1#composition
|
|
855
|
+
direction: outgoing
|
|
856
|
+
question: >-
|
|
857
|
+
Which architecture does {subject.name} stabilize?
|
|
858
|
+
materiality: >-
|
|
859
|
+
A plateau that aggregates nothing describes no state anyone can
|
|
860
|
+
stand on; migration planning needs to know what is stable when.
|
|
861
|
+
authority: either
|
|
862
|
+
resolution: >-
|
|
863
|
+
Aggregate the elements that are current during this plateau, or use
|
|
864
|
+
architecture states instead and remove it.
|
|
865
|
+
|
|
866
|
+
- id: gap-unaddressed
|
|
867
|
+
wave: implementation
|
|
868
|
+
since: "0.4"
|
|
869
|
+
scope: subject
|
|
870
|
+
subjects:
|
|
871
|
+
kinds:
|
|
872
|
+
- yarramate/core@0.1#gap
|
|
873
|
+
trigger:
|
|
874
|
+
- condition: missing-relationship
|
|
875
|
+
kinds:
|
|
876
|
+
- yarramate/core@0.1#association
|
|
877
|
+
- yarramate/core@0.1#realization
|
|
878
|
+
direction: any
|
|
879
|
+
question: >-
|
|
880
|
+
What closes {subject.name}?
|
|
881
|
+
materiality: >-
|
|
882
|
+
A named gap nothing addresses is a known risk with no plan; either
|
|
883
|
+
work closes it or the acceptance of it should be on record.
|
|
884
|
+
authority: human
|
|
885
|
+
resolution: >-
|
|
886
|
+
Associate the gap with the plateaus it separates and the
|
|
887
|
+
deliverable that closes it, or record the decision to accept it.
|
|
888
|
+
|
|
615
889
|
# ---- hygiene -------------------------------------------------------------
|
|
616
890
|
- id: concept-isolated
|
|
617
891
|
wave: hygiene
|
|
892
|
+
since: "0.1"
|
|
618
893
|
scope: subject
|
|
619
894
|
subjects:
|
|
620
895
|
kinds:
|
|
@@ -640,6 +915,7 @@ questions:
|
|
|
640
915
|
|
|
641
916
|
- id: concept-undescribed
|
|
642
917
|
wave: hygiene
|
|
918
|
+
since: "0.1"
|
|
643
919
|
scope: subject
|
|
644
920
|
subjects:
|
|
645
921
|
kinds:
|
|
@@ -663,6 +939,7 @@ questions:
|
|
|
663
939
|
|
|
664
940
|
- id: status-missing
|
|
665
941
|
wave: hygiene
|
|
942
|
+
since: "0.1"
|
|
666
943
|
scope: subject
|
|
667
944
|
subjects:
|
|
668
945
|
kinds:
|
|
@@ -685,6 +962,7 @@ questions:
|
|
|
685
962
|
|
|
686
963
|
- id: states-undefined
|
|
687
964
|
wave: hygiene
|
|
965
|
+
since: "0.1"
|
|
688
966
|
scope: workspace
|
|
689
967
|
trigger:
|
|
690
968
|
- condition: no-state-defined
|
package/dist/apply-command.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { isSeq, parseDocument } from 'yaml';
|
|
3
|
+
import { isMap, isScalar, isSeq, parseDocument, stringify, } from 'yaml';
|
|
4
4
|
import Ajv2020Module from 'ajv/dist/2020.js';
|
|
5
5
|
import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
|
|
6
6
|
import { compileWorkspace } from './compiler.js';
|
|
@@ -11,48 +11,170 @@ import operationsSchema from '../schema/yarramate-operations.schema.json' with {
|
|
|
11
11
|
};
|
|
12
12
|
const Ajv2020 = Ajv2020Module.default;
|
|
13
13
|
const validateOperations = new Ajv2020({ allErrors: true }).compile(operationsSchema);
|
|
14
|
-
// Scalar fields replace; list fields append
|
|
15
|
-
//
|
|
14
|
+
// Scalar fields replace; list fields append; `remove` retracts (ADR 0062).
|
|
15
|
+
// An answer enriches what is there and may explicitly take back what it
|
|
16
|
+
// asserted — it never silently shrinks anything.
|
|
16
17
|
const SCALAR_CONCEPT_FIELDS = ['kind', 'name', 'description', 'status', 'owner'];
|
|
17
18
|
const LIST_CONCEPT_FIELDS = ['constraints', 'references', 'presentIn', 'attestations'];
|
|
18
19
|
const SCALAR_RELATIONSHIP_FIELDS = ['kind', 'from', 'to', 'name', 'description', 'status', 'mode', 'content'];
|
|
19
20
|
const LIST_RELATIONSHIP_FIELDS = ['references', 'presentIn'];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// The splice layer. Every operation becomes a minimal text edit against the
|
|
23
|
+
// document's current source, so bytes the batch never touched stay
|
|
24
|
+
// byte-identical — an apply diff is exactly the answer it landed (#114).
|
|
25
|
+
// The atomic compile gate below validates the spliced text itself, so any
|
|
26
|
+
// splice defect rejects the batch loudly instead of corrupting a document.
|
|
27
|
+
const lineStartOf = (source, offset) => source.lastIndexOf('\n', offset - 1) + 1;
|
|
28
|
+
const indentAt = (source, offset) => offset - lineStartOf(source, offset);
|
|
29
|
+
// End of the last line a node occupies, extended through its newline.
|
|
30
|
+
const lineEndAfter = (source, offset) => {
|
|
31
|
+
const newline = source.indexOf('\n', Math.max(offset - 1, 0));
|
|
32
|
+
return newline === -1 ? source.length : newline + 1;
|
|
33
|
+
};
|
|
34
|
+
const reindent = (text, indent) => text.split('\n').join(`\n${' '.repeat(indent)}`);
|
|
35
|
+
// A plain value as YAML source. lineWidth 0 keeps strings we author on one
|
|
36
|
+
// line; genuinely multi-line strings become block scalars and are re-indented
|
|
37
|
+
// by the caller.
|
|
38
|
+
const valueText = (value) => stringify(value, { lineWidth: 0 }).trimEnd();
|
|
39
|
+
const pairFor = (map, key) => map.items.find((pair) => isScalar(pair.key) && pair.key.value === key);
|
|
40
|
+
const nodeRange = (node) => {
|
|
41
|
+
const range = node.range;
|
|
42
|
+
if (range === undefined) {
|
|
43
|
+
throw new Error('YAML node has no source range');
|
|
44
|
+
}
|
|
45
|
+
return range;
|
|
46
|
+
};
|
|
47
|
+
// Renders `items` as block sequence entries at the given marker indent.
|
|
48
|
+
const sequenceEntries = (items, markerIndent) => {
|
|
49
|
+
const rendered = stringify(items, { lineWidth: 0 }).trimEnd();
|
|
50
|
+
return `${' '.repeat(markerIndent)}${reindent(rendered, markerIndent)}`;
|
|
51
|
+
};
|
|
52
|
+
const splice = (source, start, end, text) => source.slice(0, start) + text + source.slice(end);
|
|
53
|
+
// Insert a newline-terminated block at a line boundary, tolerating a
|
|
54
|
+
// source that does not end in a newline.
|
|
55
|
+
const insertBlock = (source, insertAt, block) => splice(source, insertAt, insertAt, insertAt > 0 && source[insertAt - 1] !== '\n' ? `\n${block}` : block);
|
|
56
|
+
// Node ranges may extend past their trailing newline to the start of the
|
|
57
|
+
// next line; anchor insertions on the last CONTENT line instead.
|
|
58
|
+
const afterContentLine = (source, offset) => {
|
|
59
|
+
let anchor = offset;
|
|
60
|
+
while (anchor > 0 && source[anchor - 1] === '\n')
|
|
61
|
+
anchor -= 1;
|
|
62
|
+
return lineEndAfter(source, anchor);
|
|
63
|
+
};
|
|
64
|
+
// Where a new field of an item lands: after the last line of the item's
|
|
65
|
+
// final pair.
|
|
66
|
+
const itemFieldInsertAt = (source, map) => {
|
|
67
|
+
const lastPair = map.items[map.items.length - 1];
|
|
68
|
+
const anchor = lastPair.value === null || lastPair.value === undefined
|
|
69
|
+
? nodeRange(lastPair.key)[2]
|
|
70
|
+
: nodeRange(lastPair.value)[1];
|
|
71
|
+
return afterContentLine(source, anchor);
|
|
72
|
+
};
|
|
73
|
+
// Appends one item to a top-level block collection (`concepts:` or
|
|
74
|
+
// `relationships:`), creating or converting the collection when needed.
|
|
75
|
+
const appendCollectionItem = (source, collection, item) => {
|
|
76
|
+
const document = parseDocument(source);
|
|
77
|
+
const root = document.contents;
|
|
78
|
+
if (!isMap(root))
|
|
79
|
+
throw new Error('Document root is not a mapping');
|
|
80
|
+
const pair = pairFor(root, collection);
|
|
81
|
+
if (pair === undefined) {
|
|
82
|
+
const base = source.endsWith('\n') ? source : `${source}\n`;
|
|
83
|
+
return `${base}${collection}:\n${sequenceEntries([item], 2)}\n`;
|
|
84
|
+
}
|
|
85
|
+
const sequence = pair.value;
|
|
86
|
+
if (isSeq(sequence) && !sequence.flow && sequence.items.length > 0) {
|
|
87
|
+
const lastItem = sequence.items[sequence.items.length - 1];
|
|
88
|
+
const markerIndent = indentAt(source, nodeRange(lastItem)[0]) - 2;
|
|
89
|
+
const insertAt = lineEndAfter(source, nodeRange(lastItem)[2]);
|
|
90
|
+
return insertBlock(source, insertAt, `${sequenceEntries([item], markerIndent)}\n`);
|
|
25
91
|
}
|
|
92
|
+
if (!isSeq(sequence)) {
|
|
93
|
+
// `concepts:` with no value at all: append the block under the key.
|
|
94
|
+
const insertAt = lineEndAfter(source, nodeRange(pair.key)[2]);
|
|
95
|
+
return insertBlock(source, insertAt, `${sequenceEntries([item], 2)}\n`);
|
|
96
|
+
}
|
|
97
|
+
// Empty or flow collection: replace the whole value with a block
|
|
98
|
+
// sequence carrying any existing entries plus the new one, consuming
|
|
99
|
+
// the space that separated it from the key.
|
|
100
|
+
const existing = sequence.items.length > 0
|
|
101
|
+
? sequence.toJSON()
|
|
102
|
+
: [];
|
|
103
|
+
let [start] = nodeRange(sequence);
|
|
104
|
+
const valueEnd = nodeRange(sequence)[1];
|
|
105
|
+
while (start > 0 && source[start - 1] === ' ')
|
|
106
|
+
start -= 1;
|
|
107
|
+
return splice(source, start, valueEnd, `\n${sequenceEntries([...existing, item], 2)}`);
|
|
26
108
|
};
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
109
|
+
const itemMap = (source, collection, id) => {
|
|
110
|
+
const document = parseDocument(source);
|
|
111
|
+
const root = document.contents;
|
|
112
|
+
if (!isMap(root))
|
|
113
|
+
return undefined;
|
|
114
|
+
const pair = pairFor(root, collection);
|
|
115
|
+
if (pair === undefined || !isSeq(pair.value))
|
|
30
116
|
return undefined;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
117
|
+
const found = pair.value.items.find((candidate) => isMap(candidate) &&
|
|
118
|
+
candidate.items.some((field) => isScalar(field.key) &&
|
|
119
|
+
field.key.value === 'id' &&
|
|
120
|
+
isScalar(field.value) &&
|
|
121
|
+
field.value.value === id));
|
|
122
|
+
return found === undefined ? undefined : { map: found };
|
|
35
123
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
124
|
+
// The indent item fields sit at, read off the item's own first field.
|
|
125
|
+
const fieldIndentOf = (source, map) => indentAt(source, nodeRange(map.items[0].key)[0]);
|
|
126
|
+
const setScalarField = (source, map, key, value) => {
|
|
127
|
+
const indent = fieldIndentOf(source, map);
|
|
128
|
+
const rendered = reindent(valueText(value), indent + 2);
|
|
129
|
+
const pair = pairFor(map, key);
|
|
130
|
+
if (pair === undefined) {
|
|
131
|
+
return insertBlock(source, itemFieldInsertAt(source, map), `${' '.repeat(indent)}${key}: ${rendered}\n`);
|
|
40
132
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
133
|
+
const [start, valueEnd] = nodeRange(pair.value);
|
|
134
|
+
return splice(source, start, valueEnd, rendered);
|
|
135
|
+
};
|
|
136
|
+
const appendListField = (source, map, key, additions) => {
|
|
137
|
+
const indent = fieldIndentOf(source, map);
|
|
138
|
+
const pair = pairFor(map, key);
|
|
139
|
+
if (pair === undefined) {
|
|
140
|
+
return insertBlock(source, itemFieldInsertAt(source, map), `${' '.repeat(indent)}${key}:\n${sequenceEntries(additions, indent + 2)}\n`);
|
|
141
|
+
}
|
|
142
|
+
const sequence = pair.value;
|
|
143
|
+
if (isSeq(sequence) && !sequence.flow && sequence.items.length > 0) {
|
|
144
|
+
const lastItem = sequence.items[sequence.items.length - 1];
|
|
145
|
+
const markerIndent = indentAt(source, nodeRange(lastItem)[0]) - 2;
|
|
146
|
+
const insertAt = lineEndAfter(source, nodeRange(lastItem)[2]);
|
|
147
|
+
return insertBlock(source, insertAt, `${sequenceEntries(additions, markerIndent)}\n`);
|
|
54
148
|
}
|
|
149
|
+
const existing = isSeq(sequence) && sequence.items.length > 0
|
|
150
|
+
? sequence.toJSON()
|
|
151
|
+
: [];
|
|
152
|
+
const merged = [...existing, ...additions];
|
|
153
|
+
const [start, valueEnd] = isSeq(sequence)
|
|
154
|
+
? nodeRange(sequence)
|
|
155
|
+
: nodeRange(pair.value);
|
|
156
|
+
if (isSeq(sequence) && sequence.flow) {
|
|
157
|
+
const flow = stringify(merged, {
|
|
158
|
+
collectionStyle: 'flow',
|
|
159
|
+
lineWidth: 0,
|
|
160
|
+
}).trimEnd();
|
|
161
|
+
return splice(source, start, valueEnd, flow);
|
|
162
|
+
}
|
|
163
|
+
return splice(source, start, valueEnd, `\n${sequenceEntries(merged, indent + 2)}`);
|
|
164
|
+
};
|
|
165
|
+
// Retraction (#115): delete the field's whole entry, from the start of its
|
|
166
|
+
// key line through the end of its value's last line.
|
|
167
|
+
const removeField = (source, map, key) => {
|
|
168
|
+
const pair = pairFor(map, key);
|
|
169
|
+
if (pair === undefined)
|
|
170
|
+
return undefined;
|
|
171
|
+
const start = lineStartOf(source, nodeRange(pair.key)[0]);
|
|
172
|
+
const valueEnd = pair.value === null || pair.value === undefined
|
|
173
|
+
? nodeRange(pair.key)[2]
|
|
174
|
+
: nodeRange(pair.value)[2];
|
|
175
|
+
return splice(source, start, lineEndAfter(source, valueEnd), '');
|
|
55
176
|
};
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
56
178
|
export function runApplyCommand(options, cwd) {
|
|
57
179
|
const json = options.includes('--json');
|
|
58
180
|
const rest = options.filter((option) => option !== '--json');
|
|
@@ -91,7 +213,7 @@ export function runApplyCommand(options, cwd) {
|
|
|
91
213
|
// Documents are addressed by their manifest paths; an operation aimed
|
|
92
214
|
// anywhere else is rejected before anything is touched.
|
|
93
215
|
const workspaceDocuments = new Map(workspace.documents.map((path) => [resolve(cwd, path), path]));
|
|
94
|
-
const
|
|
216
|
+
const candidates = new Map();
|
|
95
217
|
const counts = {
|
|
96
218
|
addedConcepts: 0,
|
|
97
219
|
addedRelationships: 0,
|
|
@@ -112,35 +234,63 @@ export function runApplyCommand(options, cwd) {
|
|
|
112
234
|
locate(`Operation ${index} targets "${operation.document}", which is not a document of workspace "${workspace.id}"`),
|
|
113
235
|
]);
|
|
114
236
|
}
|
|
115
|
-
let
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
parsed.set(absolute, document);
|
|
237
|
+
let source = candidates.get(absolute);
|
|
238
|
+
if (source === undefined) {
|
|
239
|
+
source = readFileSync(absolute, 'utf8');
|
|
119
240
|
}
|
|
120
241
|
if (operation.op === 'add-concept') {
|
|
121
|
-
|
|
242
|
+
source = appendCollectionItem(source, 'concepts', operation.concept);
|
|
122
243
|
counts.addedConcepts += 1;
|
|
123
244
|
}
|
|
124
245
|
else if (operation.op === 'add-relationship') {
|
|
125
|
-
|
|
246
|
+
source = appendCollectionItem(source, 'relationships', operation.relationship);
|
|
126
247
|
counts.addedRelationships += 1;
|
|
127
248
|
}
|
|
128
249
|
else {
|
|
129
250
|
const collection = operation.op === 'update-concept' ? 'concepts' : 'relationships';
|
|
130
|
-
const payload = operation.op === 'update-concept'
|
|
251
|
+
const payload = (operation.op === 'update-concept'
|
|
131
252
|
? operation.concept
|
|
132
|
-
: operation.relationship;
|
|
133
|
-
const
|
|
134
|
-
|
|
253
|
+
: operation.relationship);
|
|
254
|
+
const scalars = operation.op === 'update-concept'
|
|
255
|
+
? SCALAR_CONCEPT_FIELDS
|
|
256
|
+
: SCALAR_RELATIONSHIP_FIELDS;
|
|
257
|
+
const lists = operation.op === 'update-concept'
|
|
258
|
+
? LIST_CONCEPT_FIELDS
|
|
259
|
+
: LIST_RELATIONSHIP_FIELDS;
|
|
260
|
+
const id = payload.id;
|
|
261
|
+
const removals = operation.remove ?? [];
|
|
262
|
+
const contradiction = removals.find((key) => payload[key] !== undefined);
|
|
263
|
+
if (contradiction !== undefined) {
|
|
135
264
|
return failed([
|
|
136
|
-
locate(`Operation ${index}
|
|
265
|
+
locate(`Operation ${index} both sets and removes "${contradiction}" on "${id}"`),
|
|
137
266
|
]);
|
|
138
267
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
268
|
+
const located = itemMap(source, collection, id);
|
|
269
|
+
if (located === undefined) {
|
|
270
|
+
return failed([
|
|
271
|
+
locate(`Operation ${index} updates "${id}", which does not exist in ${operation.document}`),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
for (const key of scalars) {
|
|
275
|
+
if (payload[key] === undefined)
|
|
276
|
+
continue;
|
|
277
|
+
source = setScalarField(source, itemMap(source, collection, id).map, key, payload[key]);
|
|
278
|
+
}
|
|
279
|
+
for (const key of lists) {
|
|
280
|
+
const additions = payload[key];
|
|
281
|
+
if (additions === undefined || additions.length === 0)
|
|
282
|
+
continue;
|
|
283
|
+
source = appendListField(source, itemMap(source, collection, id).map, key, additions);
|
|
284
|
+
}
|
|
285
|
+
for (const key of removals) {
|
|
286
|
+
const removed = removeField(source, itemMap(source, collection, id).map, key);
|
|
287
|
+
if (removed === undefined) {
|
|
288
|
+
return failed([
|
|
289
|
+
locate(`Operation ${index} removes "${key}", which is not set on "${id}"`),
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
292
|
+
source = removed;
|
|
293
|
+
}
|
|
144
294
|
if (operation.op === 'update-concept') {
|
|
145
295
|
counts.updatedConcepts += 1;
|
|
146
296
|
}
|
|
@@ -148,13 +298,10 @@ export function runApplyCommand(options, cwd) {
|
|
|
148
298
|
counts.updatedRelationships += 1;
|
|
149
299
|
}
|
|
150
300
|
}
|
|
301
|
+
candidates.set(absolute, source);
|
|
151
302
|
}
|
|
152
303
|
// The atomic gate: the whole candidate workspace must compile before a
|
|
153
304
|
// single byte is written; any diagnostic rejects the entire batch.
|
|
154
|
-
const candidates = new Map([...parsed.entries()].map(([absolute, document]) => [
|
|
155
|
-
absolute,
|
|
156
|
-
document.toString({ lineWidth: 0 }),
|
|
157
|
-
]));
|
|
158
305
|
const compilation = compileWorkspace([...workspace.profiles, ...workspace.documents].map((path) => {
|
|
159
306
|
const absolute = resolve(cwd, path);
|
|
160
307
|
return {
|
|
@@ -167,7 +314,7 @@ export function runApplyCommand(options, cwd) {
|
|
|
167
314
|
for (const [absolute, source] of candidates) {
|
|
168
315
|
writeFileSync(absolute, source, 'utf8');
|
|
169
316
|
}
|
|
170
|
-
const touched = [...
|
|
317
|
+
const touched = [...candidates.keys()]
|
|
171
318
|
.map((absolute) => workspaceDocuments.get(absolute))
|
|
172
319
|
.sort();
|
|
173
320
|
if (json) {
|
package/dist/design-command.js
CHANGED
|
@@ -33,6 +33,7 @@ const selectStep = (report, subjectFilter) => {
|
|
|
33
33
|
question: question.question,
|
|
34
34
|
materiality: question.materiality,
|
|
35
35
|
resolution: question.resolution,
|
|
36
|
+
...(question.since === undefined ? {} : { since: question.since }),
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
const subjects = subjectFilter === undefined
|
|
@@ -49,6 +50,7 @@ const selectStep = (report, subjectFilter) => {
|
|
|
49
50
|
question: first.question,
|
|
50
51
|
materiality: question.materiality,
|
|
51
52
|
resolution: question.resolution,
|
|
53
|
+
...(question.since === undefined ? {} : { since: question.since }),
|
|
52
54
|
subject: {
|
|
53
55
|
id: first.id,
|
|
54
56
|
...(first.name === undefined ? {} : { name: first.name }),
|
|
@@ -56,6 +58,10 @@ const selectStep = (report, subjectFilter) => {
|
|
|
56
58
|
...(subjects.length > 1
|
|
57
59
|
? { remainingSubjects: subjects.length - 1 }
|
|
58
60
|
: {}),
|
|
61
|
+
// The full roster sharing this question (#116): when one policy
|
|
62
|
+
// answer covers many subjects, the harness can collect it once
|
|
63
|
+
// and land one apply batch instead of interviewing N times.
|
|
64
|
+
openSubjects: subjects.map(({ id }) => id),
|
|
59
65
|
};
|
|
60
66
|
}
|
|
61
67
|
}
|
|
@@ -44,6 +44,7 @@ export interface CatalogueQuestion {
|
|
|
44
44
|
readonly materiality: string;
|
|
45
45
|
readonly resolution: string;
|
|
46
46
|
readonly authority: 'human' | 'agent' | 'either';
|
|
47
|
+
readonly since?: string;
|
|
47
48
|
}
|
|
48
49
|
export interface QuestionCatalogue {
|
|
49
50
|
readonly format: 'yarramate/question-catalogue/v1';
|
|
@@ -74,6 +75,7 @@ interface ReportQuestion {
|
|
|
74
75
|
readonly question: string;
|
|
75
76
|
readonly materiality: string;
|
|
76
77
|
readonly resolution: string;
|
|
78
|
+
readonly since?: string;
|
|
77
79
|
readonly subjects?: readonly OpenSubject[];
|
|
78
80
|
}
|
|
79
81
|
export interface InterrogationReport {
|
|
@@ -175,6 +175,7 @@ export function evaluateCatalogue(catalogue, graph, profileContext) {
|
|
|
175
175
|
question: question.question.trim(),
|
|
176
176
|
materiality: question.materiality.trim(),
|
|
177
177
|
resolution: question.resolution.trim(),
|
|
178
|
+
...(question.since === undefined ? {} : { since: question.since }),
|
|
178
179
|
};
|
|
179
180
|
if (question.scope === 'workspace') {
|
|
180
181
|
const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext));
|
|
@@ -253,12 +254,13 @@ export function renderInterrogationReport(report) {
|
|
|
253
254
|
lines.push(` closed ${question.id}`);
|
|
254
255
|
continue;
|
|
255
256
|
}
|
|
257
|
+
const sinceMarker = question.since === undefined ? '' : ` [since ${question.since}]`;
|
|
256
258
|
if (question.subjects === undefined) {
|
|
257
|
-
lines.push(` OPEN ${question.id} — ${question.question}`);
|
|
259
|
+
lines.push(` OPEN ${question.id}${sinceMarker} — ${question.question}`);
|
|
258
260
|
lines.push(` why: ${question.materiality}`);
|
|
259
261
|
continue;
|
|
260
262
|
}
|
|
261
|
-
lines.push(` OPEN ${question.id} (${question.subjects.length} ${question.subjects.length === 1 ? 'subject' : 'subjects'})`);
|
|
263
|
+
lines.push(` OPEN ${question.id}${sinceMarker} (${question.subjects.length} ${question.subjects.length === 1 ? 'subject' : 'subjects'})`);
|
|
262
264
|
for (const subject of question.subjects) {
|
|
263
265
|
lines.push(` ask: "${subject.question}" [authority: ${question.authority}]`);
|
|
264
266
|
}
|
package/package.json
CHANGED
|
@@ -139,6 +139,17 @@
|
|
|
139
139
|
"remainingSubjects": {
|
|
140
140
|
"type": "integer",
|
|
141
141
|
"minimum": 1
|
|
142
|
+
},
|
|
143
|
+
"openSubjects": {
|
|
144
|
+
"type": "array",
|
|
145
|
+
"minItems": 1,
|
|
146
|
+
"items": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"minLength": 1
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"since": {
|
|
152
|
+
"type": "string"
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
}
|
|
@@ -264,6 +264,22 @@
|
|
|
264
264
|
"type": "object"
|
|
265
265
|
}
|
|
266
266
|
]
|
|
267
|
+
},
|
|
268
|
+
"remove": {
|
|
269
|
+
"type": "array",
|
|
270
|
+
"minItems": 1,
|
|
271
|
+
"items": {
|
|
272
|
+
"enum": [
|
|
273
|
+
"name",
|
|
274
|
+
"description",
|
|
275
|
+
"status",
|
|
276
|
+
"owner",
|
|
277
|
+
"constraints",
|
|
278
|
+
"references",
|
|
279
|
+
"presentIn",
|
|
280
|
+
"attestations"
|
|
281
|
+
]
|
|
282
|
+
}
|
|
267
283
|
}
|
|
268
284
|
}
|
|
269
285
|
},
|
|
@@ -294,6 +310,21 @@
|
|
|
294
310
|
"type": "object"
|
|
295
311
|
}
|
|
296
312
|
]
|
|
313
|
+
},
|
|
314
|
+
"remove": {
|
|
315
|
+
"type": "array",
|
|
316
|
+
"minItems": 1,
|
|
317
|
+
"items": {
|
|
318
|
+
"enum": [
|
|
319
|
+
"name",
|
|
320
|
+
"description",
|
|
321
|
+
"status",
|
|
322
|
+
"mode",
|
|
323
|
+
"content",
|
|
324
|
+
"references",
|
|
325
|
+
"presentIn"
|
|
326
|
+
]
|
|
327
|
+
}
|
|
297
328
|
}
|
|
298
329
|
}
|
|
299
330
|
}
|
|
@@ -392,6 +392,11 @@
|
|
|
392
392
|
"resolution": {
|
|
393
393
|
"description": "Guidance for how an accepted answer should land as native authoring (which claim, relationship, or state to add).",
|
|
394
394
|
"$ref": "#/$defs/nonEmptyText"
|
|
395
|
+
},
|
|
396
|
+
"since": {
|
|
397
|
+
"description": "The catalogue version this question first appeared in. Lets consumers distinguish 'the catalogue deepened' from 'the model regressed' when a completed interview reopens (ADR 0063).",
|
|
398
|
+
"type": "string",
|
|
399
|
+
"pattern": "^[0-9]+\\.[0-9]+$"
|
|
395
400
|
}
|
|
396
401
|
},
|
|
397
402
|
"allOf": [
|
|
@@ -129,7 +129,11 @@ yarramate design .yarramate/workspace.yaml
|
|
|
129
129
|
.yarramate/workspace.yaml`), then re-run `design` — the next question is
|
|
130
130
|
recomputed from the model, so the loop is resumable across sessions and
|
|
131
131
|
agents with no handover. Use `--subject <id>` to focus the interview on
|
|
132
|
-
one element.
|
|
132
|
+
one element. When a step reports many `openSubjects` sharing one
|
|
133
|
+
question (ownership is the classic case), do not interview N times:
|
|
134
|
+
collect the policy answer once — "who owns what, by area" — and land it
|
|
135
|
+
across every listed subject as one apply batch. The interview is
|
|
136
|
+
complete when `design` says so.
|
|
133
137
|
5. Create:
|
|
134
138
|
- an alternatives projection for the decision;
|
|
135
139
|
- a bounded target projection for implementation agents.
|