yarramate 0.22.0 → 0.23.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,12 +1,13 @@
1
1
  format: yarramate/question-catalogue/v1
2
2
  id: core-enrichment
3
- version: "0.8"
3
+ version: "0.9"
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, application, technology,
9
- and implementation waves plus cross-cutting hygiene. Each question
8
+ The guided design path: motivation, interaction, business, application,
9
+ technology, and implementation waves plus cross-cutting hygiene. Each
10
+ question
10
11
  states the decision its answer changes; a question that cannot is
11
12
  deleted, not softened. Adequacy is enforced by linkage depth and
12
13
  attestation, never by reading words. Versioning is additive within a
@@ -18,6 +19,12 @@ waves:
18
19
  - id: motivation
19
20
  name: Motivation
20
21
  description: Why the system exists and what constrains it.
22
+ - id: interaction
23
+ name: Interaction
24
+ description: >-
25
+ Load-bearing hops: the behavior a component is assigned to, the
26
+ serving/triggering/flow mechanism, payload, trust, reliability, and
27
+ capacity. Hygiene waits.
21
28
  - id: business
22
29
  name: Business
23
30
  description: Who acts, what is served, and what information matters.
@@ -333,6 +340,398 @@ questions:
333
340
  attestation with topic "adequacy" (revoke by deleting it; Git
334
341
  reviews both).
335
342
 
343
+ # ---- interaction ---------------------------------------------------------
344
+ - id: authn-standard-missing
345
+ wave: interaction
346
+ since: "0.9"
347
+ scope: workspace
348
+ trigger:
349
+ - condition: exists-linkage
350
+ kinds:
351
+ - yarramate/core@0.1#serving
352
+ - yarramate/core@0.1#flow
353
+ - yarramate/core@0.1#triggering
354
+ direction: either
355
+ counterpartKinds:
356
+ - yarramate/core@0.1#applicationComponent
357
+ - yarramate/core@0.1#applicationInterface
358
+ - yarramate/core@0.1#businessActor
359
+ - condition: no-subject-of-kind
360
+ kinds:
361
+ - yarramate/policy@0.1#authentication-constraint
362
+ question: >-
363
+ What is the default authentication mechanism for interactions in this
364
+ architecture?
365
+ askPlain: >-
366
+ When one system talks to another here, how do they prove who they
367
+ are? Pick the default now so each hop is not inventing its own.
368
+ materiality: >-
369
+ An unauthenticated hop to another system is a trust-boundary decision
370
+ nobody recorded. Implementers will pick a mechanism in code, and the
371
+ next hop will assume a different one. This question does not cover
372
+ authorization or transport security.
373
+ authority: human
374
+ resolution: >-
375
+ Add an authentication-constraint (the standard, or a not-applicable
376
+ subject with the reason in its description) in a document that
377
+ selects yarramate/policy@0.1.
378
+
379
+ - id: ratelimit-standard-missing
380
+ wave: interaction
381
+ since: "0.9"
382
+ scope: workspace
383
+ trigger:
384
+ - condition: exists-linkage
385
+ kinds:
386
+ - yarramate/core@0.1#serving
387
+ direction: either
388
+ counterpartKinds:
389
+ - yarramate/core@0.1#businessActor
390
+ - condition: no-subject-of-kind
391
+ kinds:
392
+ - yarramate/policy@0.1#rate-limit-constraint
393
+ question: >-
394
+ Where is rate limiting a requirement, and what is the default rule?
395
+ askPlain: >-
396
+ Do caller-facing APIs have a capacity cap? If yes, what is it? If
397
+ no, say so so we do not keep asking per hop.
398
+ materiality: >-
399
+ Caller-facing hops without a capacity rule are sized by whoever
400
+ implements first. An explicit none is a decision; silence is not.
401
+ authority: human
402
+ resolution: >-
403
+ Add a rate-limit-constraint for the default cap, or a
404
+ ratelimit-not-applicable subject. Bind hop-specific values with
405
+ expects on the binding.
406
+
407
+ - id: reliability-standard-missing
408
+ wave: interaction
409
+ since: "0.9"
410
+ scope: workspace
411
+ trigger:
412
+ - condition: exists-linkage
413
+ kinds:
414
+ - yarramate/core@0.1#flow
415
+ - yarramate/core@0.1#triggering
416
+ direction: either
417
+ counterpartKinds:
418
+ - yarramate/core@0.1#applicationComponent
419
+ - yarramate/core@0.1#applicationInterface
420
+ - yarramate/core@0.1#applicationProcess
421
+ - yarramate/core@0.1#applicationFunction
422
+ - yarramate/core@0.1#applicationInteraction
423
+ - yarramate/core@0.1#applicationEvent
424
+ - yarramate/core@0.1#businessActor
425
+ - condition: no-subject-of-kind
426
+ kinds:
427
+ - yarramate/policy@0.1#reliability-constraint
428
+ question: >-
429
+ What is the default delivery, retry, and idempotency rule for
430
+ asynchronous or content-moving interactions?
431
+ askPlain: >-
432
+ When a message or payload does not arrive, do we retry, and what
433
+ makes a retry safe to repeat?
434
+ materiality: >-
435
+ Retry without idempotency duplicates work; no retry without a
436
+ recorded choice leaves timeout behaviour to each implementer.
437
+ authority: human
438
+ resolution: >-
439
+ Add a reliability-constraint (the standard, or not-applicable with
440
+ the reason in its description).
441
+
442
+ - id: hop-unrealised
443
+ wave: interaction
444
+ since: "0.9"
445
+ scope: subject
446
+ subjects:
447
+ kinds:
448
+ - yarramate/core@0.1#applicationComponent
449
+ statuses:
450
+ - planned
451
+ - current
452
+ trigger:
453
+ - condition: has-linkage
454
+ kinds:
455
+ - yarramate/core@0.1#serving
456
+ - yarramate/core@0.1#flow
457
+ - yarramate/core@0.1#triggering
458
+ direction: either
459
+ counterpartKinds:
460
+ - yarramate/core@0.1#applicationComponent
461
+ - yarramate/core@0.1#applicationInterface
462
+ - yarramate/core@0.1#businessActor
463
+ - condition: missing-linkage
464
+ kinds:
465
+ - yarramate/core@0.1#assignment
466
+ direction: outgoing
467
+ counterpartKinds:
468
+ - yarramate/core@0.1#applicationProcess
469
+ - yarramate/core@0.1#applicationFunction
470
+ - yarramate/core@0.1#applicationInteraction
471
+ - yarramate/core@0.1#applicationEvent
472
+ - yarramate/core@0.1#applicationInterface
473
+ question: >-
474
+ {subject.name} participates in an application interaction, but
475
+ nothing it is assigned to is that interaction. What process,
476
+ function, interaction, event, or interface is the hop?
477
+ askPlain: >-
478
+ {subject.name} talks to something else, but the model never names
479
+ the work it does in that talk. What is the actual step?
480
+ materiality: >-
481
+ A component-to-component edge with no assigned behavior is
482
+ inventory. Protocol, trust, payload, and failure have nowhere to
483
+ bind until the hop is a subject.
484
+ authority: either
485
+ resolution: >-
486
+ Add an application process, function, interaction, event, or
487
+ interface, assign this component to it, and relate that behavior
488
+ with serving, triggering, or flow to its counterpart. Do not answer
489
+ by deleting the component or adding a realization instead.
490
+
491
+ - id: interaction-protocol-unbound
492
+ wave: interaction
493
+ since: "0.9"
494
+ scope: subject
495
+ subjects:
496
+ kinds:
497
+ - yarramate/core@0.1#applicationProcess
498
+ - yarramate/core@0.1#applicationFunction
499
+ - yarramate/core@0.1#applicationInteraction
500
+ - yarramate/core@0.1#applicationEvent
501
+ - yarramate/core@0.1#applicationInterface
502
+ statuses:
503
+ - planned
504
+ - current
505
+ trigger:
506
+ - condition: has-linkage
507
+ kinds:
508
+ - yarramate/core@0.1#serving
509
+ direction: either
510
+ counterpartKinds:
511
+ - yarramate/core@0.1#applicationComponent
512
+ - yarramate/core@0.1#applicationInterface
513
+ - yarramate/core@0.1#applicationProcess
514
+ - yarramate/core@0.1#applicationFunction
515
+ - yarramate/core@0.1#applicationInteraction
516
+ - yarramate/core@0.1#applicationEvent
517
+ - yarramate/core@0.1#businessActor
518
+ - condition: missing-constraint
519
+ kinds:
520
+ - yarramate/policy@0.1#mechanism-constraint
521
+ question: >-
522
+ {subject.name} is a serving hop. Which protocol is it — REST/HTTP,
523
+ SOAP, GraphQL, or something else?
524
+ askPlain: >-
525
+ When {subject.name} is called, what is the wire protocol?
526
+ materiality: >-
527
+ Serving names the pattern, not the contract. A REST hop and a SOAP
528
+ hop fail, version, and authenticate differently.
529
+ authority: either
530
+ resolution: >-
531
+ Bind a mechanism-constraint on this behavior (or a not-applicable
532
+ subject if the serving kind is already the whole answer).
533
+
534
+ - id: interaction-content-unknown
535
+ wave: interaction
536
+ since: "0.9"
537
+ scope: subject
538
+ subjects:
539
+ kinds:
540
+ - yarramate/core@0.1#applicationProcess
541
+ - yarramate/core@0.1#applicationFunction
542
+ - yarramate/core@0.1#applicationInteraction
543
+ - yarramate/core@0.1#applicationEvent
544
+ - yarramate/core@0.1#applicationInterface
545
+ statuses:
546
+ - planned
547
+ - current
548
+ trigger:
549
+ - condition: missing-flow-content
550
+ question: >-
551
+ {subject.name} sends a flow with no content. What moves?
552
+ askPlain: >-
553
+ {subject.name} sends something on. What is that something called?
554
+ materiality: >-
555
+ A flow with no content is an unnamed payload. Retry, scanning, and
556
+ the counterpart contract have nothing to name.
557
+ authority: either
558
+ resolution: >-
559
+ Set content on the flow relationship.
560
+
561
+ - id: interaction-contract-unknown
562
+ wave: interaction
563
+ since: "0.9"
564
+ scope: subject
565
+ subjects:
566
+ kinds:
567
+ - yarramate/core@0.1#applicationProcess
568
+ - yarramate/core@0.1#applicationFunction
569
+ - yarramate/core@0.1#applicationInteraction
570
+ - yarramate/core@0.1#applicationEvent
571
+ - yarramate/core@0.1#applicationInterface
572
+ statuses:
573
+ - planned
574
+ - current
575
+ trigger:
576
+ - condition: has-linkage
577
+ kinds:
578
+ - yarramate/core@0.1#serving
579
+ - yarramate/core@0.1#flow
580
+ - yarramate/core@0.1#triggering
581
+ direction: either
582
+ counterpartKinds:
583
+ - yarramate/core@0.1#applicationComponent
584
+ - yarramate/core@0.1#applicationInterface
585
+ - yarramate/core@0.1#applicationProcess
586
+ - yarramate/core@0.1#applicationFunction
587
+ - yarramate/core@0.1#applicationInteraction
588
+ - yarramate/core@0.1#applicationEvent
589
+ - yarramate/core@0.1#businessActor
590
+ - condition: missing-linkage
591
+ kinds:
592
+ - yarramate/core@0.1#access
593
+ direction: outgoing
594
+ counterpartKinds:
595
+ - yarramate/core@0.1#dataObject
596
+ - yarramate/core@0.1#contract
597
+ question: >-
598
+ {subject.name} participates in a hop but accesses no contract or
599
+ data object. What information does it read or write?
600
+ askPlain: >-
601
+ What record or contract is {subject.name} working with?
602
+ materiality: >-
603
+ Schema ownership, identifiers, and classification have nowhere to
604
+ attach until the hop names the information it moves.
605
+ authority: either
606
+ resolution: >-
607
+ Add access from this behavior to a dataObject or contract.
608
+
609
+ - id: interaction-trust-unbound
610
+ wave: interaction
611
+ since: "0.9"
612
+ scope: subject
613
+ subjects:
614
+ kinds:
615
+ - yarramate/core@0.1#applicationProcess
616
+ - yarramate/core@0.1#applicationFunction
617
+ - yarramate/core@0.1#applicationInteraction
618
+ - yarramate/core@0.1#applicationEvent
619
+ - yarramate/core@0.1#applicationInterface
620
+ statuses:
621
+ - planned
622
+ - current
623
+ trigger:
624
+ - condition: has-linkage
625
+ kinds:
626
+ - yarramate/core@0.1#serving
627
+ - yarramate/core@0.1#flow
628
+ - yarramate/core@0.1#triggering
629
+ direction: either
630
+ counterpartKinds:
631
+ - yarramate/core@0.1#applicationComponent
632
+ - yarramate/core@0.1#applicationInterface
633
+ - yarramate/core@0.1#applicationProcess
634
+ - yarramate/core@0.1#applicationFunction
635
+ - yarramate/core@0.1#applicationInteraction
636
+ - yarramate/core@0.1#applicationEvent
637
+ - yarramate/core@0.1#businessActor
638
+ - condition: missing-constraint
639
+ kinds:
640
+ - yarramate/policy@0.1#authentication-constraint
641
+ question: >-
642
+ How is trust established for {subject.name}?
643
+ askPlain: >-
644
+ Who does {subject.name} authenticate as, and with what?
645
+ materiality: >-
646
+ An unauthenticated hop to another system is a trust-boundary
647
+ decision nobody recorded. This is authentication only — not
648
+ authorization or transport security.
649
+ authority: either
650
+ resolution: >-
651
+ Bind an authentication-constraint on this behavior, or a
652
+ not-applicable subject if this hop is deliberately unauthenticated.
653
+
654
+ - id: interaction-reliability-unbound
655
+ wave: interaction
656
+ since: "0.9"
657
+ scope: subject
658
+ subjects:
659
+ kinds:
660
+ - yarramate/core@0.1#applicationProcess
661
+ - yarramate/core@0.1#applicationFunction
662
+ - yarramate/core@0.1#applicationInteraction
663
+ - yarramate/core@0.1#applicationEvent
664
+ - yarramate/core@0.1#applicationInterface
665
+ statuses:
666
+ - planned
667
+ - current
668
+ trigger:
669
+ - condition: has-linkage
670
+ kinds:
671
+ - yarramate/core@0.1#triggering
672
+ - yarramate/core@0.1#flow
673
+ direction: either
674
+ counterpartKinds:
675
+ - yarramate/core@0.1#applicationComponent
676
+ - yarramate/core@0.1#applicationInterface
677
+ - yarramate/core@0.1#applicationProcess
678
+ - yarramate/core@0.1#applicationFunction
679
+ - yarramate/core@0.1#applicationInteraction
680
+ - yarramate/core@0.1#applicationEvent
681
+ - yarramate/core@0.1#businessActor
682
+ - condition: missing-constraint
683
+ kinds:
684
+ - yarramate/policy@0.1#reliability-constraint
685
+ question: >-
686
+ What happens when {subject.name} fails — retry, idempotency,
687
+ dead-letter, compensation?
688
+ askPlain: >-
689
+ If {subject.name} does not finish, do we retry, and what makes a
690
+ retry safe?
691
+ materiality: >-
692
+ A content-moving hop without a reliability rule duplicates or
693
+ drops work at the first timeout.
694
+ authority: either
695
+ resolution: >-
696
+ Bind a reliability-constraint. A named failure process is stronger
697
+ and may come later; the constraint is the 0.9 closer.
698
+
699
+ - id: interaction-capacity-unbound
700
+ wave: interaction
701
+ since: "0.9"
702
+ scope: subject
703
+ subjects:
704
+ kinds:
705
+ - yarramate/core@0.1#applicationProcess
706
+ - yarramate/core@0.1#applicationFunction
707
+ - yarramate/core@0.1#applicationInteraction
708
+ - yarramate/core@0.1#applicationEvent
709
+ - yarramate/core@0.1#applicationInterface
710
+ statuses:
711
+ - planned
712
+ - current
713
+ trigger:
714
+ - condition: has-linkage
715
+ kinds:
716
+ - yarramate/core@0.1#serving
717
+ direction: either
718
+ counterpartKinds:
719
+ - yarramate/core@0.1#businessActor
720
+ - condition: missing-constraint
721
+ kinds:
722
+ - yarramate/policy@0.1#rate-limit-constraint
723
+ question: >-
724
+ What is the capacity rule for {subject.name}?
725
+ askPlain: >-
726
+ How hard may callers hit {subject.name}, or is there no cap?
727
+ materiality: >-
728
+ An Experience-facing hop without a capacity rule is sized by
729
+ whoever implements first. Explicit none is a decision.
730
+ authority: either
731
+ resolution: >-
732
+ Bind a rate-limit-constraint, with expects on the binding for the
733
+ numeric cap, or a not-applicable subject.
734
+
336
735
  # ---- business ------------------------------------------------------------
337
736
  - id: service-consumer-unknown
338
737
  wave: business
package/dist/compiler.js CHANGED
@@ -7,6 +7,7 @@ import documentSchema from '../schema/yarramate-document.schema.json' with { typ
7
7
  import profileSchema from '../schema/yarramate-profile.schema.json' with { type: 'json'
8
8
  };
9
9
  import { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue } from './graph-claims.js';
10
+ import { shippedPolicyIdentity, shippedPolicySource, } from './shipped-profile.js';
10
11
  const coreProfile = 'yarramate/core@0.1';
11
12
  const require = createRequire(import.meta.url);
12
13
  const ajv2020Module = require('ajv/dist/2020.js');
@@ -283,6 +284,46 @@ function compileWorkspaceResolved(parsed) {
283
284
  const identity = `${value.id}@${value.version}`;
284
285
  pendingProfiles.push({ input, value, identity, positionFor });
285
286
  }
287
+ const alreadyDeclaresPolicy = pendingProfiles.some(({ identity }) => identity === shippedPolicyIdentity);
288
+ if (!alreadyDeclaresPolicy) {
289
+ const selected = documentInputs.some(({ entry }) => {
290
+ const value = entry.value;
291
+ return value.profile === shippedPolicyIdentity;
292
+ });
293
+ const extended = pendingProfiles.some(({ value }) => value.extends === shippedPolicyIdentity);
294
+ if (selected || extended) {
295
+ const input = {
296
+ path: 'yarramate:profile:yarramate/policy@0.1',
297
+ source: shippedPolicySource,
298
+ };
299
+ const { entry, fresh } = parseWorkspaceSource(input);
300
+ const value = entry.value;
301
+ if (entry.schemaDiagnostics.length > 0) {
302
+ profileDiagnostics.push(...entry.schemaDiagnostics);
303
+ }
304
+ else if (!validateProfile(value)) {
305
+ for (const error of validateProfile.errors ?? []) {
306
+ profileDiagnostics.push({
307
+ severity: 'error',
308
+ code: 'YM201',
309
+ message: `Profile schema violation: ${describeSchemaViolation(error)}`,
310
+ path: input.path,
311
+ pointer: error.instancePath || '/',
312
+ line: 1,
313
+ column: 1,
314
+ });
315
+ }
316
+ }
317
+ else {
318
+ pendingProfiles.push({
319
+ input,
320
+ value,
321
+ identity: shippedPolicyIdentity,
322
+ positionFor: positionReader(input.source, entry.positions, fresh),
323
+ });
324
+ }
325
+ }
326
+ }
286
327
  let unresolvedProfiles = pendingProfiles.sort((left, right) => left.identity.localeCompare(right.identity) ||
287
328
  left.input.path.localeCompare(right.input.path));
288
329
  while (unresolvedProfiles.length > 0) {
@@ -18,6 +18,7 @@ type CatalogueCondition = {
18
18
  } | {
19
19
  readonly condition: 'no-subject-of-kind';
20
20
  readonly kinds: readonly string[];
21
+ readonly kindMatching?: 'exact' | 'descendants';
21
22
  } | {
22
23
  readonly condition: 'no-state-defined';
23
24
  } | {
@@ -26,6 +27,24 @@ type CatalogueCondition = {
26
27
  readonly direction: 'incoming' | 'outgoing';
27
28
  readonly counterpartKinds: readonly string[];
28
29
  readonly kindMatching?: 'exact' | 'descendants';
30
+ } | {
31
+ readonly condition: 'has-linkage';
32
+ readonly kinds: readonly string[];
33
+ readonly direction: 'incoming' | 'outgoing' | 'either';
34
+ readonly counterpartKinds: readonly string[];
35
+ readonly kindMatching?: 'exact' | 'descendants';
36
+ } | {
37
+ readonly condition: 'exists-linkage';
38
+ readonly kinds: readonly string[];
39
+ readonly direction: 'incoming' | 'outgoing' | 'either';
40
+ readonly counterpartKinds: readonly string[];
41
+ readonly kindMatching?: 'exact' | 'descendants';
42
+ } | {
43
+ readonly condition: 'missing-constraint';
44
+ readonly kinds: readonly string[];
45
+ readonly kindMatching?: 'exact' | 'descendants';
46
+ } | {
47
+ readonly condition: 'missing-flow-content';
29
48
  } | {
30
49
  readonly condition: 'missing-reference';
31
50
  readonly predicate: string;
@@ -160,6 +160,54 @@ const relationshipKindMatches = (predicate, selectedKinds, matching, profileCont
160
160
  profileContext?.relationshipKindLineages
161
161
  .get(predicate)
162
162
  ?.includes(selected) === true));
163
+ const profileIdentityOfKind = (qualifiedKind) => {
164
+ const separator = qualifiedKind.indexOf('#');
165
+ return separator === -1 ? qualifiedKind : qualifiedKind.slice(0, separator);
166
+ };
167
+ const namedKinds = (question) => {
168
+ const kinds = [...(question.subjects?.kinds ?? [])];
169
+ for (const condition of question.trigger) {
170
+ switch (condition.condition) {
171
+ case 'missing-relationship':
172
+ case 'no-subject-of-kind':
173
+ case 'missing-constraint':
174
+ kinds.push(...condition.kinds);
175
+ break;
176
+ case 'missing-linkage':
177
+ case 'has-linkage':
178
+ case 'exists-linkage':
179
+ kinds.push(...condition.kinds, ...condition.counterpartKinds);
180
+ break;
181
+ default:
182
+ break;
183
+ }
184
+ }
185
+ return kinds;
186
+ };
187
+ const questionIsApplicable = (question, selectedProfiles) => {
188
+ const selected = new Set(selectedProfiles);
189
+ return namedKinds(question).every((kind) => selected.has(profileIdentityOfKind(kind)));
190
+ };
191
+ const linkageHits = (index, condition, subjectId, profileContext) => {
192
+ const matching = condition.kindMatching ?? 'descendants';
193
+ return index.relationshipClaims.some((claim) => {
194
+ if (!('ref' in claim.object))
195
+ return false;
196
+ if (!relationshipKindMatches(claim.predicate, condition.kinds, matching, profileContext)) {
197
+ return false;
198
+ }
199
+ const counterparts = [];
200
+ if ((condition.direction === 'outgoing' || condition.direction === 'either') &&
201
+ claim.subject === subjectId) {
202
+ counterparts.push(claim.object.ref);
203
+ }
204
+ if ((condition.direction === 'incoming' || condition.direction === 'either') &&
205
+ claim.object.ref === subjectId) {
206
+ counterparts.push(claim.subject);
207
+ }
208
+ return counterparts.some((counterpart) => kindMatches(index.kindOf.get(counterpart), condition.counterpartKinds, matching, profileContext));
209
+ });
210
+ };
163
211
  const conditionHolds = (index, condition, subjectId, profileContext) => {
164
212
  switch (condition.condition) {
165
213
  case 'missing-claim':
@@ -184,31 +232,43 @@ const conditionHolds = (index, condition, subjectId, profileContext) => {
184
232
  return (!index.relationshipClaims.some(({ subject, object }) => subject === subjectId ||
185
233
  ('ref' in object && object.ref === subjectId)) &&
186
234
  !index.referenceClaims.some(({ object }) => 'ref' in object && object.ref === subjectId));
187
- case 'no-subject-of-kind':
188
- return ![...index.concepts].some((id) => condition.kinds.includes(index.kindOf.get(id) ?? ''));
235
+ case 'no-subject-of-kind': {
236
+ const matching = condition.kindMatching ?? 'descendants';
237
+ return ![...index.concepts].some((id) => kindMatches(index.kindOf.get(id), condition.kinds, matching, profileContext));
238
+ }
189
239
  case 'no-state-defined':
190
240
  return !index.hasStates;
191
- case 'missing-linkage': {
241
+ case 'missing-linkage':
192
242
  // The linkage-depth primitive: the subject lacks a relationship of
193
243
  // these kinds, in this direction, whose counterpart is of one of
194
244
  // these kinds. Both relationship and counterpart kinds resolve
195
245
  // through profile lineage by default, matching the selector rule.
246
+ return !linkageHits(index, condition, subjectId, profileContext);
247
+ case 'has-linkage':
248
+ return (subjectId !== undefined &&
249
+ linkageHits(index, condition, subjectId, profileContext));
250
+ case 'exists-linkage':
251
+ return [...index.concepts].some((id) => linkageHits(index, condition, id, profileContext));
252
+ case 'missing-constraint': {
196
253
  const matching = condition.kindMatching ?? 'descendants';
197
- return !index.relationshipClaims.some((claim) => {
254
+ return !(index.claimsBySubject.get(subjectId) ?? []).some((claim) => claim.predicate === 'yarramate/constraint/requires' &&
255
+ 'ref' in claim.object &&
256
+ kindMatches(index.kindOf.get(claim.object.ref), condition.kinds, matching, profileContext));
257
+ }
258
+ case 'missing-flow-content': {
259
+ if (subjectId === undefined)
260
+ return false;
261
+ const matching = 'descendants';
262
+ return index.relationshipClaims.some((claim) => {
198
263
  if (!('ref' in claim.object))
199
264
  return false;
200
- if (!relationshipKindMatches(claim.predicate, condition.kinds, matching, profileContext)) {
265
+ if (!relationshipKindMatches(claim.predicate, ['yarramate/core@0.1#flow'], matching, profileContext)) {
201
266
  return false;
202
267
  }
203
- const counterpart = condition.direction === 'outgoing'
204
- ? claim.subject === subjectId
205
- ? claim.object.ref
206
- : undefined
207
- : claim.object.ref === subjectId
208
- ? claim.subject
209
- : undefined;
210
- return (counterpart !== undefined &&
211
- kindMatches(index.kindOf.get(counterpart), condition.counterpartKinds, matching, profileContext));
268
+ if (claim.subject !== subjectId && claim.object.ref !== subjectId) {
269
+ return false;
270
+ }
271
+ return !(index.claimsBySubject.get(claim.id) ?? []).some(({ predicate, object }) => predicate === 'yarramate/flow/content' && 'value' in object);
212
272
  });
213
273
  }
214
274
  case 'missing-reference':
@@ -278,10 +338,11 @@ export function evaluateCatalogue(catalogue, graph, profileContext) {
278
338
  const index = indexGraph(graph);
279
339
  let open = 0;
280
340
  let openQuestions = 0;
341
+ const applicableQuestions = catalogue.questions.filter((question) => questionIsApplicable(question, graph.profiles));
281
342
  const waves = catalogue.waves.map((wave) => ({
282
343
  id: wave.id,
283
344
  name: wave.name,
284
- questions: catalogue.questions
345
+ questions: applicableQuestions
285
346
  .filter((question) => question.wave === wave.id)
286
347
  .map((question) => {
287
348
  const base = {
@@ -325,7 +386,7 @@ export function evaluateCatalogue(catalogue, graph, profileContext) {
325
386
  format: 'yarramate/interrogation-report/v1',
326
387
  catalogue: `${catalogue.id}@${catalogue.version}`,
327
388
  summary: {
328
- questions: catalogue.questions.length,
389
+ questions: applicableQuestions.length,
329
390
  openQuestions,
330
391
  open,
331
392
  },
@@ -0,0 +1,2 @@
1
+ export declare const shippedPolicyIdentity = "yarramate/policy@0.1";
2
+ export declare const shippedPolicySource: string;
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export const shippedPolicyIdentity = 'yarramate/policy@0.1';
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+ export const shippedPolicySource = readFileSync(join(here, '..', 'profiles', 'yarramate-policy.yaml'), 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yarramate",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Tool-neutral semantic architecture engine and guided methodology",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "assets/likec4",
27
27
  "schema",
28
28
  "catalogues",
29
+ "profiles",
29
30
  "skills/yarramate-architecture",
30
31
  "docs/CONSUMING-YARRAMATE.md"
31
32
  ],
@@ -0,0 +1,18 @@
1
+ format: yarramate/profile/v1
2
+ id: yarramate/policy
3
+ version: "0.1"
4
+ extends: yarramate/core@0.1
5
+ conceptKinds:
6
+ - id: authentication-constraint
7
+ name: Authentication constraint
8
+ parent: yarramate/core@0.1#constraint
9
+ - id: rate-limit-constraint
10
+ name: Rate-limit constraint
11
+ parent: yarramate/core@0.1#constraint
12
+ - id: reliability-constraint
13
+ name: Reliability constraint
14
+ parent: yarramate/core@0.1#constraint
15
+ - id: mechanism-constraint
16
+ name: Mechanism constraint
17
+ parent: yarramate/core@0.1#constraint
18
+ relationshipKinds: []
@@ -290,7 +290,139 @@
290
290
  "enum": [
291
291
  "exact",
292
292
  "descendants"
293
+ ],
294
+ "default": "descendants"
295
+ }
296
+ }
297
+ },
298
+ {
299
+ "type": "object",
300
+ "additionalProperties": false,
301
+ "required": [
302
+ "condition",
303
+ "kinds",
304
+ "direction",
305
+ "counterpartKinds"
306
+ ],
307
+ "properties": {
308
+ "condition": {
309
+ "const": "has-linkage"
310
+ },
311
+ "kinds": {
312
+ "type": "array",
313
+ "minItems": 1,
314
+ "uniqueItems": true,
315
+ "items": {
316
+ "$ref": "#/$defs/qualifiedKind"
317
+ }
318
+ },
319
+ "direction": {
320
+ "enum": [
321
+ "outgoing",
322
+ "incoming",
323
+ "either"
293
324
  ]
325
+ },
326
+ "counterpartKinds": {
327
+ "type": "array",
328
+ "minItems": 1,
329
+ "uniqueItems": true,
330
+ "items": {
331
+ "$ref": "#/$defs/qualifiedKind"
332
+ }
333
+ },
334
+ "kindMatching": {
335
+ "enum": [
336
+ "exact",
337
+ "descendants"
338
+ ],
339
+ "default": "descendants"
340
+ }
341
+ }
342
+ },
343
+ {
344
+ "type": "object",
345
+ "additionalProperties": false,
346
+ "required": [
347
+ "condition",
348
+ "kinds",
349
+ "direction",
350
+ "counterpartKinds"
351
+ ],
352
+ "properties": {
353
+ "condition": {
354
+ "const": "exists-linkage"
355
+ },
356
+ "kinds": {
357
+ "type": "array",
358
+ "minItems": 1,
359
+ "uniqueItems": true,
360
+ "items": {
361
+ "$ref": "#/$defs/qualifiedKind"
362
+ }
363
+ },
364
+ "direction": {
365
+ "enum": [
366
+ "outgoing",
367
+ "incoming",
368
+ "either"
369
+ ]
370
+ },
371
+ "counterpartKinds": {
372
+ "type": "array",
373
+ "minItems": 1,
374
+ "uniqueItems": true,
375
+ "items": {
376
+ "$ref": "#/$defs/qualifiedKind"
377
+ }
378
+ },
379
+ "kindMatching": {
380
+ "enum": [
381
+ "exact",
382
+ "descendants"
383
+ ],
384
+ "default": "descendants"
385
+ }
386
+ }
387
+ },
388
+ {
389
+ "type": "object",
390
+ "additionalProperties": false,
391
+ "required": [
392
+ "condition",
393
+ "kinds"
394
+ ],
395
+ "properties": {
396
+ "condition": {
397
+ "const": "missing-constraint"
398
+ },
399
+ "kinds": {
400
+ "type": "array",
401
+ "minItems": 1,
402
+ "uniqueItems": true,
403
+ "items": {
404
+ "$ref": "#/$defs/qualifiedKind"
405
+ }
406
+ },
407
+ "kindMatching": {
408
+ "enum": [
409
+ "exact",
410
+ "descendants"
411
+ ],
412
+ "default": "descendants"
413
+ }
414
+ }
415
+ },
416
+ {
417
+ "type": "object",
418
+ "additionalProperties": false,
419
+ "description": "The subject is an endpoint of at least one flow relationship that has no yarramate/flow/content claim.",
420
+ "required": [
421
+ "condition"
422
+ ],
423
+ "properties": {
424
+ "condition": {
425
+ "const": "missing-flow-content"
294
426
  }
295
427
  }
296
428
  },
@@ -140,15 +140,24 @@ yarramate design .yarramate/workspace.yaml
140
140
  or read catalogue files. Answer one question at a time: questions the
141
141
  model or evidence can answer, answer from your authority; questions
142
142
  marked `human`, relay verbatim with their materiality. Land each answer
143
- as one atomic batch (`yarramate apply <operations.yaml>
144
- .yarramate/workspace.yaml`), then re-run `design` — the next question is
145
- recomputed from the model, so the loop is resumable across sessions and
146
- agents with no handover. Use `--subject <id>` to focus the interview on
147
- one element. When a step reports many `openSubjects` sharing one
148
- question (ownership is the classic case), do not interview N times:
149
- collect the policy answer once "who owns what, by area" — and land it
150
- across every listed subject as one apply batch. The interview is
151
- complete when `design` says so.
143
+ as subjects and relationships in one atomic batch (`yarramate apply
144
+ <operations.yaml> .yarramate/workspace.yaml`), never as description
145
+ alone. Then re-run `design` the next question is recomputed from the
146
+ model, so the loop is resumable across sessions and agents with no
147
+ handover. Use `--subject <id>` to focus the interview on one element.
148
+ When a step reports many `openSubjects` sharing one question (ownership
149
+ is the classic case), do not interview N times: collect the policy
150
+ answer once "who owns what, by area" and land it across every listed
151
+ subject as one apply batch.
152
+
153
+ After motivation, work the `interaction` wave (hops, mechanism, payload,
154
+ trust, reliability, capacity) before ownership and hygiene. Policy
155
+ subjects (`authentication-constraint`, `rate-limit-constraint`, and
156
+ siblings) live in a document that selects `yarramate/policy@0.1`; hops
157
+ bind them with qualified `constraints[].ref`. One batch may create and
158
+ bind. When `design --json` returns a step whose `wave` is not
159
+ `interaction` for the slice in focus, stop, render the brief, and do not
160
+ drain `owner-missing`. The interview is complete when `design` says so.
152
161
  5. Create:
153
162
  - an alternatives projection for the decision;
154
163
  - a bounded target projection for implementation agents.
@@ -200,6 +200,16 @@ rationale in the description and it becomes the non-goal record.
200
200
  Ownership is one accountable reference, not approval workflow. Constraints are
201
201
  identified references, not a policy engine or free-form metadata bag.
202
202
 
203
+ Distinct NFR rules (authentication, rate limit, reliability, serving
204
+ protocol) use `yarramate/policy@0.1` kinds, not Core `constraint` alone.
205
+ Select that profile on the document that declares the policy subjects;
206
+ other documents may keep their existing profile and bind with a
207
+ qualified `constraints[].ref`. Do not copy a profile file into the
208
+ workspace. A hop that must not have a rate limit binds a
209
+ `ratelimit-not-applicable` subject of kind `rate-limit-constraint` with
210
+ the reason in its description. Numeric caps go in `expects` on the
211
+ binding.
212
+
203
213
  ## Attestations
204
214
 
205
215
  ```yaml