yarramate 1.14.1 → 1.15.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.
@@ -81,6 +81,43 @@ const localKind = (qualified) => {
81
81
  const hash = qualified.lastIndexOf('#');
82
82
  return hash === -1 ? qualified : qualified.slice(hash + 1);
83
83
  };
84
+ /**
85
+ * The `missing-claim` predicates that name a CONCEPT FIELD, and the field
86
+ * each one is written through (#430).
87
+ *
88
+ * `missing-claim` matches a raw predicate, and a predicate is not an
89
+ * authoring gesture: `yarramate/attestation/adequacy` is written by adding an
90
+ * attestation, `yarramate/reference/refers-to` by adding a reference, and a
91
+ * profile may mint predicates this engine has never heard of. So the
92
+ * condition cannot map to one operation in general, which is why an adopter
93
+ * wiring it up got a card with no affordance and filed #430.
94
+ *
95
+ * It maps for these three, and only these three, because each is a named
96
+ * field on a concept that `update-concept` writes directly. They are Core's
97
+ * own, closed, and they are every `missing-claim` the shipped catalogue uses
98
+ * — `owner-missing`, `information-unowned`, `concept-undescribed` and
99
+ * `status-missing`. Anything else returns no skeleton, which is ADR 0110's
100
+ * rule holding: a wrong skeleton is never offered.
101
+ *
102
+ * This is the "one new mapping case" ADR 0110 anticipated, not a remedy DSL.
103
+ * The engine still takes no position on which predicates a HOST should render
104
+ * as editable; the trigger carries the predicate and the host decides. This
105
+ * is the CLI rendering its own affordance, the same as the other two cases.
106
+ */
107
+ const CONCEPT_FIELD_PREDICATES = {
108
+ 'yarramate/ownership/owner': {
109
+ name: 'owner',
110
+ placeholder: '<owning-subject-id>',
111
+ },
112
+ 'yarramate/concept/description': {
113
+ name: 'description',
114
+ placeholder: '<one line>',
115
+ },
116
+ 'yarramate/lifecycle/status': {
117
+ name: 'status',
118
+ placeholder: '<planned|current|retired>',
119
+ },
120
+ };
84
121
  const skeletonHeader = (documentAddress, op) => [
85
122
  '',
86
123
  'Prefilled skeleton (edit the <placeholders>, save as operations.yaml):',
@@ -125,6 +162,17 @@ const renderSkeleton = (step, documentAddress) => {
125
162
  ` to: ${to}`,
126
163
  ];
127
164
  }
165
+ if (condition.condition === 'missing-claim' && step.subject !== undefined) {
166
+ const field = CONCEPT_FIELD_PREDICATES[condition.predicate];
167
+ if (field === undefined)
168
+ return [];
169
+ return [
170
+ ...skeletonHeader(documentAddress, 'update-concept'),
171
+ ' concept:',
172
+ ` id: ${step.subject.id.split('#').pop()}`,
173
+ ` ${field.name}: ${field.placeholder}`,
174
+ ];
175
+ }
128
176
  return [];
129
177
  };
130
178
  export function runDesignCommand(options, cwd) {
@@ -110,6 +110,35 @@ export type CatalogueCondition = {
110
110
  readonly direction: 'incoming' | 'outgoing' | 'either';
111
111
  readonly counterpartKinds: readonly string[];
112
112
  readonly kindMatching?: 'exact' | 'descendants';
113
+ } | {
114
+ /**
115
+ * The negative twin of `exists-linkage`, workspace-scope like it
116
+ * (#436, ADR 0138). Fires while NO concept in the workspace satisfies
117
+ * the linkage, so a question can ask "nothing anywhere links this way".
118
+ *
119
+ * It exists because a **vocabulary question** was asking the wrong
120
+ * thing. `below-subject-count` measures a population, and a vocabulary
121
+ * question means "did anyone survey this" — a proxy that fails in both
122
+ * directions, measured on a live engagement: two throwaway values close
123
+ * it dishonestly, while a truthful single-value estate can never close
124
+ * it at all.
125
+ *
126
+ * `MODEL-FLOOR.md` prescribes the answer's home: a classification axis
127
+ * is a `grouping` that aggregates its members, so a scheme aggregating
128
+ * its classes IS the statement that these are the classes. Asking "no
129
+ * scheme aggregates any class" needs this condition; asking how many
130
+ * classes exist does not reach it.
131
+ *
132
+ * Deliberately NOT `!exists-linkage` in the evaluator, for the reason
133
+ * `has-subject-of-kind` is not `!no-subject-of-kind`: written as its own
134
+ * check, an empty workspace falls out right rather than by double
135
+ * negative.
136
+ */
137
+ readonly condition: 'no-linkage-exists';
138
+ readonly kinds: readonly string[];
139
+ readonly direction: 'incoming' | 'outgoing' | 'either';
140
+ readonly counterpartKinds: readonly string[];
141
+ readonly kindMatching?: 'exact' | 'descendants';
113
142
  } | {
114
143
  readonly condition: 'missing-constraint';
115
144
  readonly kinds: readonly string[];
@@ -207,6 +207,7 @@ const namedKinds = (question) => {
207
207
  case 'missing-linkage':
208
208
  case 'has-linkage':
209
209
  case 'exists-linkage':
210
+ case 'no-linkage-exists':
210
211
  kinds.push(...condition.kinds, ...condition.counterpartKinds);
211
212
  break;
212
213
  default:
@@ -267,6 +268,7 @@ const CONDITION_SCOPE = {
267
268
  'below-subject-count': 'workspace',
268
269
  'no-state-defined': 'workspace',
269
270
  'exists-linkage': 'workspace',
271
+ 'no-linkage-exists': 'workspace',
270
272
  'missing-claim': 'subject',
271
273
  'missing-relationship': 'subject',
272
274
  isolated: 'subject',
@@ -413,6 +415,11 @@ const conditionHolds = (index, condition, subjectId, profileContext, evidence, m
413
415
  linkageHits(index, condition, subjectId, profileContext));
414
416
  case 'exists-linkage':
415
417
  return [...index.concepts].some((id) => linkageHits(index, condition, id, profileContext));
418
+ case 'no-linkage-exists':
419
+ // Its own check rather than `!exists-linkage`, so an empty workspace
420
+ // falls out right: no concepts means no linkage, which is exactly the
421
+ // model a vocabulary question is loudest about.
422
+ return ![...index.concepts].some((id) => linkageHits(index, condition, id, profileContext));
416
423
  case 'missing-constraint': {
417
424
  const matching = condition.kindMatching ?? 'descendants';
418
425
  return !(index.claimsBySubject.get(subjectId) ?? []).some((claim) => claim.predicate === 'yarramate/constraint/requires' &&