yarramate 1.35.0 → 1.36.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.
@@ -605,6 +605,12 @@ export function runAskCommand(options, cwd) {
605
605
  topic,
606
606
  seeds: resolution.seeds,
607
607
  matched: resolution.matched,
608
+ // The same document the tools entry emits, field for field (ADR 0156),
609
+ // and `test/tools-entry.test.ts` compares them. The ranked candidates
610
+ // ride on both (#569).
611
+ ...(resolution.addressing === 'free-text'
612
+ ? { candidates: resolution.candidates }
613
+ : {}),
608
614
  ...(neighbourhood === undefined ? {} : { neighbourhood }),
609
615
  result: evaluated,
610
616
  };
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, Proje
21
21
  export { createFileSystemStore, type PendingWrite, type SourceStore, type StoredSource, type WriteConflict, type WriteOutcome, } from './source-store.js';
22
22
  export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, type ApplyOutcome, } from './apply-command.js';
23
23
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
24
+ export { conventionalRelationshipKind, CONVENTION_RULES, type RelationshipConvention, } from './relationship-convention.js';
24
25
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
25
26
  export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
26
27
  export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CataloguePatternVacancy, type CatalogueInput, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, }
19
19
  export { createFileSystemStore, } from './source-store.js';
20
20
  export { applyOperations, landOperations, posixDirectoryOf, } from './apply-command.js';
21
21
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
22
+ export { conventionalRelationshipKind, CONVENTION_RULES, } from './relationship-convention.js';
22
23
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
23
24
  export { deletionBlockers, describeDeletion, draftDeletion, } from './deletion-drafting.js';
24
25
  export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, conditionInput, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, } from './interrogate-command.js';
@@ -0,0 +1,25 @@
1
+ import { type RelationshipKind } from './profile.js';
2
+ /**
3
+ * One convention, as a named rule. The name is carried into the answer so a
4
+ * caller can say WHY, which is the difference between guidance a reviewer can
5
+ * argue with and a number they have to take on faith.
6
+ */
7
+ export interface RelationshipConvention {
8
+ readonly kind: RelationshipKind;
9
+ readonly rule: string;
10
+ readonly because: string;
11
+ }
12
+ /**
13
+ * The kind practice expects from `from` to `to`, or `null` where practice has
14
+ * no expectation.
15
+ *
16
+ * Never contradicts the table: an answer is returned only when the table also
17
+ * permits it, so guidance can never point at an edge `check` would refuse.
18
+ */
19
+ export declare const conventionalRelationshipKind: (from: string, to: string) => RelationshipConvention | null;
20
+ /** Every rule's name and sentence, for documentation and for the test that checks them. */
21
+ export declare const CONVENTION_RULES: readonly {
22
+ readonly name: string;
23
+ readonly kind: RelationshipKind;
24
+ readonly because: string;
25
+ }[];
@@ -0,0 +1,176 @@
1
+ import { conceptKinds, } from './profile.js';
2
+ import { isCoreConceptKindId, permittedRelationshipKinds, } from './relationship-matrix.js';
3
+ /**
4
+ * The relationship kind ArchiMate practice expects for a pair of core kinds,
5
+ * where practice has an expectation at all (#571).
6
+ *
7
+ * The relationship table is a PERMISSION table. It answers "may I draw this",
8
+ * and for an application component pointing at an application function it
9
+ * answers yes six times. Practice is much narrower: across this repository's
10
+ * own model and the ApertureX reference, 970 relationships over 72 core-kind
11
+ * pairs, the author used exactly one kind for 34 of the pairs the table lets
12
+ * them choose among, and used `assignment` for that particular pair 87 times
13
+ * out of 88. The engine knew the permission and never said the convention, so
14
+ * every palette offered six, every agent guessed, and a reviewer could never
15
+ * be told "this pair is usually X and yours is Y".
16
+ *
17
+ * ## Where these answers come from
18
+ *
19
+ * They are read off ArchiMate's own definitions of the relationships, NOT
20
+ * fitted to the two records. That direction matters. Both records agree, for
21
+ * instance, that a business actor points at a constraint with `association`,
22
+ * 87 times out of 87 - but that is one adopter's house convention for
23
+ * recording sign-off, not a rule of the language, and blessing it here would
24
+ * have told every other adopter their own style was wrong. The records are
25
+ * used as a CHECK: `test/relationship-convention.test.ts` asserts that every
26
+ * convention declared here matches real practice on a record that has enough
27
+ * of that pair to have a habit, and prints the disagreements when it does not.
28
+ *
29
+ * ## Silence is an answer
30
+ *
31
+ * `conventionalRelationshipKind` returns `null` far more often than not, and
32
+ * that is the point. A pair with no entry here is a pair where the language
33
+ * genuinely permits several readings and the author must choose: an artifact
34
+ * pointing at an application function is `realization` 41 times and
35
+ * `association` 16, a capability pointing at a goal splits evenly, and two
36
+ * application functions relate by composition, triggering, flow or serving
37
+ * depending on what the author means. Declaring a convention there would
38
+ * manufacture confidence the language does not have.
39
+ *
40
+ * Nothing here is a rule the compiler enforces. A permitted kind stays
41
+ * permitted; `check` never reads this file. It is guidance, offered to a
42
+ * palette, an agent, or a reviewer, and it says nothing when it has nothing
43
+ * to say.
44
+ */
45
+ const KIND_META = new Map(conceptKinds.map((kind) => [kind.id, { layer: kind.layer, aspect: kind.aspect }]));
46
+ /** The three service elements: external behaviour a layer offers outward. */
47
+ const SERVICES = new Set([
48
+ 'businessService',
49
+ 'applicationService',
50
+ 'technologyService',
51
+ ]);
52
+ /** The three interface elements: the point at which a service is offered. */
53
+ const INTERFACES = new Set([
54
+ 'businessInterface',
55
+ 'applicationInterface',
56
+ 'technologyInterface',
57
+ ]);
58
+ const isService = (kind) => SERVICES.has(kind);
59
+ const isInterface = (kind) => INTERFACES.has(kind);
60
+ const isInternalBehaviour = (kind) => KIND_META.get(kind)?.aspect === 'behavior' && !isService(kind);
61
+ /**
62
+ * Ordered; the first rule that matches answers, and order is load-bearing
63
+ * twice. A service touching passive structure is `access` before it is
64
+ * anything else, because a service is behaviour first and an offering second.
65
+ * And an interface pointing at a service of its own layer is `assignment`,
66
+ * the interface exposing that service, while an interface pointing at
67
+ * anything else is `serving`. Both times the specific rule comes first.
68
+ */
69
+ const RULES = [
70
+ {
71
+ name: 'behaviour-uses-data',
72
+ because: 'behaviour reads, writes or creates the passive structure it touches',
73
+ kind: 'access',
74
+ when: (from, to, f, t) => t.aspect === 'passive-structure' &&
75
+ (isInternalBehaviour(from) || isService(from)) &&
76
+ // An artifact is passive structure in the technology layer, and
77
+ // behaviour pointing at one is not "reading a file": both records write
78
+ // `association` there far more often than `access`. Left to the author.
79
+ to !== 'artifact',
80
+ },
81
+ {
82
+ name: 'interface-exposes-service',
83
+ because: 'an interface is assigned to the service it exposes, not to the behaviour that uses it',
84
+ kind: 'assignment',
85
+ when: (from, to, f, t) => isInterface(from) && isService(to) && f.layer === t.layer,
86
+ },
87
+ {
88
+ name: 'service-is-offered-outward',
89
+ because: 'a service is made available to whoever uses it',
90
+ kind: 'serving',
91
+ when: (from, _to, _f, t) => (isService(from) || isInterface(from)) &&
92
+ (t.aspect === 'active-structure' || t.aspect === 'behavior'),
93
+ },
94
+ {
95
+ name: 'grouping-collects-members',
96
+ because: 'a grouping aggregates what it collects; the members exist without it',
97
+ kind: 'aggregation',
98
+ when: (from) => from === 'grouping',
99
+ },
100
+ {
101
+ name: 'artifact-realizes-what-it-implements',
102
+ because: 'an artifact is the concrete file or deployable that fulfils the thing it stands for',
103
+ kind: 'realization',
104
+ when: (from, to, _f, t) => from === 'artifact' &&
105
+ (t.aspect === 'active-structure' || t.aspect === 'passive-structure') &&
106
+ to !== 'artifact',
107
+ },
108
+ {
109
+ name: 'active-structure-performs-behaviour',
110
+ because: 'an active structure element is assigned to the internal behaviour it performs',
111
+ kind: 'assignment',
112
+ when: (from, to, f, t) => f.aspect === 'active-structure' &&
113
+ isInternalBehaviour(to) &&
114
+ f.layer === t.layer,
115
+ },
116
+ {
117
+ name: 'internal-realizes-service',
118
+ because: 'the internal element is the concrete thing that fulfils the service its layer offers',
119
+ kind: 'realization',
120
+ when: (from, to, f, t) => isService(to) &&
121
+ f.layer === t.layer &&
122
+ (f.aspect === 'active-structure' || isInternalBehaviour(from)) &&
123
+ !isInterface(from),
124
+ },
125
+ {
126
+ name: 'technology-serves-application',
127
+ // The least certain rule here. The authoring reference lists this pair as
128
+ // "realizes or serves", two readings: realization when the node IS the
129
+ // implementation, serving when it hosts one. Both records are unanimous
130
+ // for `serving`, ten times in ten, so that is what is offered - and the
131
+ // other reading stays one click away, as every non-conventional kind does.
132
+ because: 'a technology element is not part of the application it runs; it serves it',
133
+ kind: 'serving',
134
+ when: (_from, _to, f, t) => f.layer === 'technology' &&
135
+ t.layer === 'application' &&
136
+ (t.aspect === 'active-structure' || t.aspect === 'behavior'),
137
+ },
138
+ ];
139
+ const cache = new Map();
140
+ /**
141
+ * The kind practice expects from `from` to `to`, or `null` where practice has
142
+ * no expectation.
143
+ *
144
+ * Never contradicts the table: an answer is returned only when the table also
145
+ * permits it, so guidance can never point at an edge `check` would refuse.
146
+ */
147
+ export const conventionalRelationshipKind = (from, to) => {
148
+ const key = `${from}>${to}`;
149
+ const cached = cache.get(key);
150
+ if (cached !== undefined)
151
+ return cached;
152
+ const answer = compute(from, to);
153
+ cache.set(key, answer);
154
+ return answer;
155
+ };
156
+ const compute = (from, to) => {
157
+ if (!isCoreConceptKindId(from) || !isCoreConceptKindId(to))
158
+ return null;
159
+ const fromMeta = KIND_META.get(from);
160
+ const toMeta = KIND_META.get(to);
161
+ if (fromMeta === undefined || toMeta === undefined)
162
+ return null;
163
+ for (const rule of RULES) {
164
+ if (!rule.when(from, to, fromMeta, toMeta))
165
+ continue;
166
+ // Guidance that the table forbids would be worse than none: it would send
167
+ // an author at an edge `check` refuses. A rule that lands outside the
168
+ // permission is treated as having no answer for this pair.
169
+ if (!permittedRelationshipKinds(from, to).has(rule.kind))
170
+ return null;
171
+ return { kind: rule.kind, rule: rule.name, because: rule.because };
172
+ }
173
+ return null;
174
+ };
175
+ /** Every rule's name and sentence, for documentation and for the test that checks them. */
176
+ export const CONVENTION_RULES = RULES.map(({ name, kind, because }) => ({ name, kind, because }));
@@ -93,6 +93,12 @@ export type AskSlice = AskResultBase & {
93
93
  readonly topic?: string;
94
94
  readonly seeds?: readonly string[];
95
95
  readonly matched?: number;
96
+ /**
97
+ * Every concept a term touched, ranked, present on a free-text slice (#569).
98
+ * The seeds are the first few of these; a caller with a judgment to spend
99
+ * reorders the list and asks again with `subjects` addressing.
100
+ */
101
+ readonly candidates?: readonly SeedCandidate[];
96
102
  readonly changed?: {
97
103
  readonly range: string;
98
104
  readonly concepts: readonly string[];
@@ -172,12 +178,46 @@ export type AskKinds = AskResultBase & {
172
178
  export type AskResult = AskOrientation | AskRoster | AskSlice | AskAdvice | AskWhere | AskNext | AskOpen | AskCompare | AskKinds;
173
179
  export declare const claimValue: (claims: readonly GraphClaim[], subject: string, predicate: string) => string | undefined;
174
180
  export declare const conceptEntries: (graph: SemanticGraph) => readonly ConceptEntry[];
181
+ /**
182
+ * How many ranked candidates `resolveSeeds` hands back beside the seeds. Thirty
183
+ * is the shortlist the #569 measurement reranked, and it is a cap rather than a
184
+ * target: most queries match fewer.
185
+ */
186
+ export declare const candidateLimit = 30;
187
+ export interface SeedCandidate {
188
+ readonly id: string;
189
+ /** How many distinct query terms this concept's text contains. The whole of the ranking. */
190
+ readonly terms: number;
191
+ }
175
192
  export interface SeedResolution {
176
193
  readonly addressing: 'free-text' | 'subjects';
177
194
  readonly seeds: readonly string[];
178
195
  readonly matched: number;
196
+ /**
197
+ * Every concept a term touched, ranked, not just the handful that seeded the
198
+ * slice (#569).
199
+ *
200
+ * The term count finds the right subject and then buries it. Measured on the
201
+ * Halcyon showcase against 32 questions written by someone other than the
202
+ * author: the subject the asker meant was inside this list 87% of the time
203
+ * and was the FIRST seed only 43% of the time. Retrieval was not the
204
+ * weakness; ordering was.
205
+ *
206
+ * The engine cannot fix that on its own, because deciding which of several
207
+ * term-matching concepts actually answers a question is a judgment and this
208
+ * is a deterministic CLI (ADR 0059). So it hands the list over instead. A
209
+ * caller with a judgment to spend - an agent, a host with a decision model -
210
+ * reorders these and calls back with `subjects` addressing, which already
211
+ * exists. A caller with none uses `seeds` exactly as before.
212
+ *
213
+ * Capped, so a one-word query against a large workspace cannot return the
214
+ * whole record: `candidateLimit`, which a caller may raise.
215
+ */
216
+ readonly candidates: readonly SeedCandidate[];
179
217
  }
180
- export declare const resolveSeeds: (terms: readonly string[], entries: readonly ConceptEntry[]) => SeedResolution;
218
+ export declare const resolveSeeds: (terms: readonly string[], entries: readonly ConceptEntry[], options?: {
219
+ readonly candidates?: number;
220
+ }) => SeedResolution;
181
221
  export declare const defaultNeighbourCap = 12;
182
222
  export interface SliceEvaluation {
183
223
  readonly result: ProjectionResult;
package/dist/tools/ask.js CHANGED
@@ -48,6 +48,12 @@ export const conceptEntries = (graph) => {
48
48
  .sort((left, right) => left.id.localeCompare(right.id));
49
49
  };
50
50
  const seedLimit = 5;
51
+ /**
52
+ * How many ranked candidates `resolveSeeds` hands back beside the seeds. Thirty
53
+ * is the shortlist the #569 measurement reranked, and it is a cap rather than a
54
+ * target: most queries match fewer.
55
+ */
56
+ export const candidateLimit = 30;
51
57
  // Free text is the default addressing mode: terms match concept ids,
52
58
  // names, alternative labels, and descriptions; matching concepts seed the
53
59
  // slice. Exact subject ids short-circuit to precise addressing: the
@@ -58,11 +64,19 @@ const seedLimit = 5;
58
64
  // aliases would be the one graded field in an otherwise ungraded match,
59
65
  // and the whole point of recording the team's actual word for a subject is
60
66
  // that it should find it.
61
- export const resolveSeeds = (terms, entries) => {
67
+ export const resolveSeeds = (terms, entries, options = {}) => {
62
68
  const known = new Set(entries.map(({ id }) => id));
63
69
  const unique = [...new Set(terms)];
64
70
  if (unique.every((term) => known.has(term))) {
65
- return { addressing: 'subjects', seeds: unique, matched: unique.length };
71
+ // Precise addressing named the subjects outright. There is nothing to
72
+ // rank, so the candidates ARE the seeds: a caller reranking whatever it is
73
+ // given never has to ask which addressing produced them.
74
+ return {
75
+ addressing: 'subjects',
76
+ seeds: unique,
77
+ matched: unique.length,
78
+ candidates: unique.map((id) => ({ id, terms: 1 })),
79
+ };
66
80
  }
67
81
  const lowered = [
68
82
  ...new Set(terms
@@ -84,6 +98,9 @@ export const resolveSeeds = (terms, entries) => {
84
98
  addressing: 'free-text',
85
99
  seeds: scored.slice(0, seedLimit).map(({ id }) => id),
86
100
  matched: scored.length,
101
+ candidates: scored
102
+ .slice(0, Math.max(seedLimit, options.candidates ?? candidateLimit))
103
+ .map(({ id, score }) => ({ id, terms: score })),
87
104
  };
88
105
  };
89
106
  // On a dense graph, hub seeds make 1-hop connected expansion reach most
@@ -461,6 +478,12 @@ export const askSlice = (workspace, query, options = {}) => guarded(() => {
461
478
  topic,
462
479
  seeds: resolution.seeds,
463
480
  matched: resolution.matched,
481
+ // Everything a term touched, ranked, so an agent can reorder the list
482
+ // and ask again with `subjects` rather than take the term count's word
483
+ // for which five mattered (#569). Only where there was ranking to do.
484
+ ...(resolution.addressing === 'free-text'
485
+ ? { candidates: resolution.candidates }
486
+ : {}),
464
487
  ...(neighbourhood === undefined ? {} : { neighbourhood }),
465
488
  result: evaluated,
466
489
  rendered: renderSlice(evaluated, compiled, options.budget),
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export { SHIPPED_CATALOGUE, MissingSourceError, type ToolWorkspace, type ToolResult, type ToolFailure, } from './tools/workspace.js';
11
11
  export { designStep, designStepDetailed, type DesignStep, type DesignStepDetailed, type DesignStepOptions, type DesignStepResult, } from './tools/design.js';
12
- export { askKinds, askNext, askOpen, askOrientation, askRoster, askSlice, conceptEntries, resolveSeeds, sliceProjection, type AskAdvice, type AskCompare, type AskKinds, type AskNext, type AskOpen, type AskOrientation, type AskResult, type AskResultBase, type AskRoster, type AskSlice, type AskWhere, type ConceptEntry, type NeighbourhoodOmission, type OpenQuestionRef, type RelationshipKindSummary, type RelationshipMatrixSummary, type RosterOptions, type SliceOptions, type SliceQuery, } from './tools/ask.js';
12
+ export { askKinds, askNext, askOpen, askOrientation, askRoster, askSlice, candidateLimit, conceptEntries, resolveSeeds, type SeedCandidate, type SeedResolution, sliceProjection, type AskAdvice, type AskCompare, type AskKinds, type AskNext, type AskOpen, type AskOrientation, type AskResult, type AskResultBase, type AskRoster, type AskSlice, type AskWhere, type ConceptEntry, type NeighbourhoodOmission, type OpenQuestionRef, type RelationshipKindSummary, type RelationshipMatrixSummary, type RosterOptions, type SliceOptions, type SliceQuery, } from './tools/ask.js';
13
13
  export { checkSources, checkWorkspace, type CheckCounts, type CheckEvaluation, type CheckInput, type CheckOptions, type CheckResult, } from './tools/check.js';
14
14
  export { applyBatch, type ApplyResult, type OperationsInput, } from './tools/apply.js';
15
15
  export { exportBriefs, exportGraph, exportLikeC4, exportGovernance, exportMarkdown, exportResponsibility, exportRtm, exportWorkbook, type BriefsOptions, type ExportedBriefs, } from './tools/export.js';
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export { SHIPPED_CATALOGUE, MissingSourceError, } from './tools/workspace.js';
11
11
  export { designStep, designStepDetailed, } from './tools/design.js';
12
- export { askKinds, askNext, askOpen, askOrientation, askRoster, askSlice, conceptEntries, resolveSeeds, sliceProjection, } from './tools/ask.js';
12
+ export { askKinds, askNext, askOpen, askOrientation, askRoster, askSlice, candidateLimit, conceptEntries, resolveSeeds, sliceProjection, } from './tools/ask.js';
13
13
  export { checkSources, checkWorkspace, } from './tools/check.js';
14
14
  export { applyBatch, } from './tools/apply.js';
15
15
  export { exportBriefs, exportGraph, exportLikeC4, exportGovernance, exportMarkdown, exportResponsibility, exportRtm, exportWorkbook, } from './tools/export.js';