yarramate 1.20.0 → 1.21.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/dist/adapters/likec4-prepare.js +1 -1
- package/dist/adapters/visual/protocol-contract.d.ts +15 -0
- package/dist/adapters/visual/session-server.js +12 -6
- package/dist/adapters/visual/workspace-model.d.ts +4 -4
- package/dist/adapters/visual/workspace-model.js +17 -5
- package/dist/ask-command.js +2 -2
- package/dist/check-command.js +12 -1
- package/dist/export-command.js +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/projection.d.ts +52 -4
- package/dist/projection.js +175 -9
- package/dist/schema-validators.generated.js +316 -246
- package/dist/subject-references.js +8 -0
- package/dist/visual-app/assets/index-Ch27Sr5z.js +394 -0
- package/dist/visual-app/index.html +1 -1
- package/dist/visual-app-lib/editor.js +24953 -24799
- package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +15 -0
- package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +4 -4
- package/dist/visual-app-lib/types/projection.d.ts +52 -4
- package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +23 -0
- package/dist/visual-app-lib/types/visual-app/session-client.d.ts +7 -1
- package/dist/visual-app-lib/types/visual-app/state.d.ts +2 -0
- package/package.json +1 -1
- package/schema/yarramate-projection.schema.json +9 -0
- package/dist/visual-app/assets/index-EegLnUfX.js +0 -394
|
@@ -141,7 +141,7 @@ export function prepareLikeC4Export(input) {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
const projectionResult = evaluateProjection(compilation.graph, projection.projection, compilation.profileContext);
|
|
144
|
+
const projectionResult = evaluateProjection(compilation.graph, projection.projection, compilation.profileContext, compilation.patternMemberships);
|
|
145
145
|
const comparison = input.comparison === undefined
|
|
146
146
|
? undefined
|
|
147
147
|
: compareArchitectureStates(compilation.graph, input.comparison.from, input.comparison.to);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `node:path`, or the schema documents into its bundle.
|
|
10
10
|
*/
|
|
11
11
|
import type { CanvasGraph } from "../../graph-projection.js";
|
|
12
|
+
import type { NestingKind } from "../../nesting.js";
|
|
12
13
|
import type { YarramateApplyResult, YarramateOperation } from "../../operations.js";
|
|
13
14
|
import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
|
|
14
15
|
/**
|
|
@@ -163,6 +164,20 @@ export interface VisualKindOption {
|
|
|
163
164
|
}
|
|
164
165
|
export interface VisualFilterQueryPayload {
|
|
165
166
|
readonly query: ProjectionQuery;
|
|
167
|
+
/**
|
|
168
|
+
* The nesting the canvas is drawing with, when the browser knows it (#473
|
|
169
|
+
* phase 2).
|
|
170
|
+
*
|
|
171
|
+
* Only `query.instances` reads it, and it must: the closure of an instance IS
|
|
172
|
+
* the containment tree, so evaluating it under a different nesting than the
|
|
173
|
+
* canvas answers a different question. On the ApertureX reference that is 15
|
|
174
|
+
* subjects against 2.
|
|
175
|
+
*
|
|
176
|
+
* Optional, so an older browser and every filter that names no instance keep
|
|
177
|
+
* working unchanged; absent, the evaluator falls back to the default nesting
|
|
178
|
+
* exactly as it did before.
|
|
179
|
+
*/
|
|
180
|
+
readonly nesting?: readonly NestingKind[];
|
|
166
181
|
}
|
|
167
182
|
export interface VisualFilterResultPayload {
|
|
168
183
|
readonly query: ProjectionQuery;
|
|
@@ -690,9 +690,15 @@ export const startVisualServer = async (options) => {
|
|
|
690
690
|
if (!started.ok)
|
|
691
691
|
standingDiagnostics = started.diagnostics;
|
|
692
692
|
}
|
|
693
|
-
const filterMatchedIds = (query) => compiledWorkspace === undefined
|
|
693
|
+
const filterMatchedIds = (query, nesting) => compiledWorkspace === undefined
|
|
694
694
|
? []
|
|
695
|
-
: matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext
|
|
695
|
+
: matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext,
|
|
696
|
+
// A view's query can name `instances`, and the facet resolves to the
|
|
697
|
+
// instance alone without these (ADR 0144).
|
|
698
|
+
compiledWorkspace.patternMemberships,
|
|
699
|
+
// And it resolves the WRONG closure without the nesting the canvas is
|
|
700
|
+
// drawing with, which is a wrong number rather than a missing one.
|
|
701
|
+
nesting);
|
|
696
702
|
/**
|
|
697
703
|
* Why a query dropped what it dropped, as the editor's "excluded, and why"
|
|
698
704
|
* list reads it (#248).
|
|
@@ -704,9 +710,9 @@ export const startVisualServer = async (options) => {
|
|
|
704
710
|
* the reason the editor shows and the set the canvas draws cannot come from
|
|
705
711
|
* two readings of the same query.
|
|
706
712
|
*/
|
|
707
|
-
const filterExclusions = (query) => compiledWorkspace === undefined
|
|
713
|
+
const filterExclusions = (query, nesting) => compiledWorkspace === undefined
|
|
708
714
|
? []
|
|
709
|
-
: exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
|
|
715
|
+
: exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext, compiledWorkspace.patternMemberships, nesting);
|
|
710
716
|
let listening = false;
|
|
711
717
|
let bootstrapSpent = false;
|
|
712
718
|
let agentAttached = false;
|
|
@@ -1243,8 +1249,8 @@ export const startVisualServer = async (options) => {
|
|
|
1243
1249
|
kind: "filter-result",
|
|
1244
1250
|
result: {
|
|
1245
1251
|
query: event.payload.query,
|
|
1246
|
-
matchedIds: filterMatchedIds(event.payload.query),
|
|
1247
|
-
excluded: filterExclusions(event.payload.query),
|
|
1252
|
+
matchedIds: filterMatchedIds(event.payload.query, event.payload.nesting),
|
|
1253
|
+
excluded: filterExclusions(event.payload.query, event.payload.nesting),
|
|
1248
1254
|
},
|
|
1249
1255
|
});
|
|
1250
1256
|
return;
|
|
@@ -2,7 +2,7 @@ import type { Diagnostic } from "../../compiler.js";
|
|
|
2
2
|
import type { PendingWrite, SourceStore } from "../../source-store.js";
|
|
3
3
|
import type { ResolvedWorkspace } from "../../workspace.js";
|
|
4
4
|
import type { VisualDiagnostic, VisualViewOperation } from "./protocol-contract.js";
|
|
5
|
-
import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
|
|
5
|
+
import type { NestingKind, ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
|
|
6
6
|
import type { ResolvedProfileContext, SemanticGraph } from "../../compiler.js";
|
|
7
7
|
import { type CataloguePatternMembership, type CataloguePatternVacancy } from "../../interrogate-command.js";
|
|
8
8
|
import type { VisualKindOption, VisualViewSummary } from "./protocol-contract.js";
|
|
@@ -41,7 +41,7 @@ export declare const kindOptionsOf: (lineages: ReadonlyMap<string, readonly stri
|
|
|
41
41
|
* subject count. A view over three components with two relationships between
|
|
42
42
|
* them would read as five, and the reviewer counting boxes would find three.
|
|
43
43
|
*/
|
|
44
|
-
export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => number;
|
|
44
|
+
export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => number;
|
|
45
45
|
/**
|
|
46
46
|
* Folds one interrogation report into what the canvas draws (#292).
|
|
47
47
|
*
|
|
@@ -109,9 +109,9 @@ export declare const renderedWorkspaceOf: (compiled: {
|
|
|
109
109
|
readonly views: readonly VisualViewSummary[];
|
|
110
110
|
};
|
|
111
111
|
/** Every subject a query draws, concepts and relationships alike. */
|
|
112
|
-
export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => readonly string[];
|
|
112
|
+
export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly string[];
|
|
113
113
|
/** Every concept a query dropped, and the facet that dropped it (#248). */
|
|
114
|
-
export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => readonly ProjectionExclusion[];
|
|
114
|
+
export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly ProjectionExclusion[];
|
|
115
115
|
/**
|
|
116
116
|
* One saved view, as the rail reads it. `subjectCount` is the caller's,
|
|
117
117
|
* because counting needs a compiled graph and a session builds its first list
|
|
@@ -45,7 +45,7 @@ export const kindOptionsOf = (lineages) => [...lineages.keys()].map((id) => ({
|
|
|
45
45
|
* subject count. A view over three components with two relationships between
|
|
46
46
|
* them would read as five, and the reviewer counting boxes would find three.
|
|
47
47
|
*/
|
|
48
|
-
export const conceptCountOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.filter(({ type }) => type === "concept").length;
|
|
48
|
+
export const conceptCountOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.filter(({ type }) => type === "concept").length;
|
|
49
49
|
export const interrogationOverlayOf = (compiled,
|
|
50
50
|
/**
|
|
51
51
|
* The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
|
|
@@ -118,7 +118,14 @@ dismissed = []) => {
|
|
|
118
118
|
export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismissed) => {
|
|
119
119
|
const refreshedViews = views.map((view) => ({
|
|
120
120
|
...view,
|
|
121
|
-
subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext
|
|
121
|
+
subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext,
|
|
122
|
+
// Without these an `instances` view counts 1 and the rail says so, which
|
|
123
|
+
// is a wrong number rather than a missing one (ADR 0144).
|
|
124
|
+
compiled.patternMemberships,
|
|
125
|
+
// Each view's OWN nesting, because each view's closure is its own. A rail
|
|
126
|
+
// sitting beside the canvas must not count a different tree than the
|
|
127
|
+
// canvas draws.
|
|
128
|
+
view.presentation?.nesting),
|
|
122
129
|
}));
|
|
123
130
|
const interrogation = catalogue === undefined
|
|
124
131
|
? undefined
|
|
@@ -148,18 +155,23 @@ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismis
|
|
|
148
155
|
};
|
|
149
156
|
};
|
|
150
157
|
/** Every subject a query draws, concepts and relationships alike. */
|
|
151
|
-
export const matchedIdsOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.map(({ id }) => id);
|
|
158
|
+
export const matchedIdsOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.map(({ id }) => id);
|
|
152
159
|
/** Every concept a query dropped, and the facet that dropped it (#248). */
|
|
153
|
-
export const exclusionsOf = (graph, query, profileContext) => explainProjection(graph, adHoc(query), profileContext);
|
|
160
|
+
export const exclusionsOf = (graph, query, profileContext, memberships, nesting) => explainProjection(graph, adHoc(query, nesting), profileContext, memberships);
|
|
154
161
|
/**
|
|
155
162
|
* A query on its own is not a projection, and every evaluator here wants one.
|
|
156
163
|
* The id is a placeholder that never reaches a document.
|
|
157
164
|
*/
|
|
158
|
-
const adHoc = (query) => ({
|
|
165
|
+
const adHoc = (query, nesting) => ({
|
|
159
166
|
format: "yarramate/projection/v1",
|
|
160
167
|
id: "ad-hoc",
|
|
161
168
|
version: "0",
|
|
162
169
|
query,
|
|
170
|
+
// `query.instances` resolves its closure through the view's nesting, so an
|
|
171
|
+
// ad-hoc projection that dropped the nesting would answer a DIFFERENT
|
|
172
|
+
// question than the canvas is drawing: 2 subjects against 15 on the
|
|
173
|
+
// ApertureX reference, with nothing to say it had (#473 phase 2).
|
|
174
|
+
...(nesting === undefined ? {} : { presentation: { nesting } }),
|
|
163
175
|
});
|
|
164
176
|
/**
|
|
165
177
|
* One saved view, as the rail reads it. `subjectCount` is the caller's,
|
package/dist/ask-command.js
CHANGED
|
@@ -784,7 +784,7 @@ export function runAskCommand(options, cwd) {
|
|
|
784
784
|
});
|
|
785
785
|
if (!loaded.ok)
|
|
786
786
|
continue;
|
|
787
|
-
const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext);
|
|
787
|
+
const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext, compilation.patternMemberships);
|
|
788
788
|
for (const subject of membership.subjects) {
|
|
789
789
|
covered.add(subject.id);
|
|
790
790
|
}
|
|
@@ -857,7 +857,7 @@ export function runAskCommand(options, cwd) {
|
|
|
857
857
|
});
|
|
858
858
|
if (!loaded.ok)
|
|
859
859
|
return failed(loaded.diagnostics);
|
|
860
|
-
const evaluated = evaluateProjection(graph, loaded.projection, compilation.profileContext);
|
|
860
|
+
const evaluated = evaluateProjection(graph, loaded.projection, compilation.profileContext, compilation.patternMemberships);
|
|
861
861
|
const result = {
|
|
862
862
|
format: 'yarramate/ask-result/v1',
|
|
863
863
|
workspace: workspace.id,
|
package/dist/check-command.js
CHANGED
|
@@ -214,7 +214,18 @@ export function runCheckCommand(options, cwd) {
|
|
|
214
214
|
// does not build would bury the real failure under its consequences.
|
|
215
215
|
const referenceDiagnostics = result.ok
|
|
216
216
|
? loadedProjections.flatMap(({ source, loaded }) => loaded.ok
|
|
217
|
-
? projectionReferenceDiagnostics(source, loaded.projection, result.graph, result.profileContext
|
|
217
|
+
? projectionReferenceDiagnostics(source, loaded.projection, result.graph, result.profileContext,
|
|
218
|
+
// Instance-hood from BOTH lists. A membership row exists only
|
|
219
|
+
// for a BOUND slot, so an instance whose slots are all empty
|
|
220
|
+
// has none - and judging it by bindings alone would call a real
|
|
221
|
+
// instance "not an instance" on the day it was authored, before
|
|
222
|
+
// anything was wired into it. The honest question is not "did
|
|
223
|
+
// it bind anything" but "does the model know it as an
|
|
224
|
+
// instance" (rule 2).
|
|
225
|
+
new Set([
|
|
226
|
+
...(result.patternMemberships ?? []).map(({ instance }) => instance),
|
|
227
|
+
...(result.patternVacancies ?? []).map(({ instance }) => instance),
|
|
228
|
+
]))
|
|
218
229
|
: [])
|
|
219
230
|
: [];
|
|
220
231
|
const optionalDiagnostics = sortDiagnostics([
|
package/dist/export-command.js
CHANGED
|
@@ -250,7 +250,10 @@ export function runExportCommand(options, cwd) {
|
|
|
250
250
|
});
|
|
251
251
|
if (!loadedProjection.ok)
|
|
252
252
|
return failed(loadedProjection.diagnostics);
|
|
253
|
-
result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext
|
|
253
|
+
result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext,
|
|
254
|
+
// An AUTHORED projection can name `instances`, and the facet resolves
|
|
255
|
+
// to the instance alone without these (ADR 0144).
|
|
256
|
+
compilation.patternMemberships);
|
|
254
257
|
}
|
|
255
258
|
if (kind === 'xlsx') {
|
|
256
259
|
// A workbook an architect can work in (#355). It takes a PROJECTION,
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { deriveAttestationStaleness } from './attestation-staleness.js';
|
|
|
11
11
|
export { deriveArtifactCoverage, type ArtifactCoverage, type CoverageScopePattern, } from './artifact-coverage.js';
|
|
12
12
|
export { buildRtm, renderRtmMarkdown, type RequirementsTraceabilityMatrix, type RtmAttestation, type RtmContextEntry, type RtmDescopedEntry, type RtmEvidenceVerdict, type RtmLineageEntry, type RtmRealizer, type RtmRow, type RtmSource, } from './rtm.js';
|
|
13
13
|
export type { CompilationCache, CompilationResult, ContextualCompilationResult, IncrementalCompilationResult, ParsedWorkspaceSource, PatternMembership, PatternVacancy, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
|
|
14
|
-
export { canonicalProjection, evaluateProjection, explainProjection, loadProjection, renderProjectionMarkdown, type ConceptFacet, type ProjectionExclusion, } from './projection.js';
|
|
14
|
+
export { canonicalProjection, evaluateProjection, explainProjection, instanceClosureOf, loadProjection, renderProjectionMarkdown, type ConceptFacet, type ProjectionExclusion, } from './projection.js';
|
|
15
15
|
export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, type AdapterMapping, type AdapterMappingLoadResult, type AdapterMappingValidationResult, type AdapterMappingsValidationResult, type AdapterSubjectMapping, } from './adapter-mapping.js';
|
|
16
16
|
export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, ProjectionResult, } from './projection.js';
|
|
17
17
|
export { createFileSystemStore, type PendingWrite, type SourceStore, type StoredSource, type WriteConflict, type WriteOutcome, } from './source-store.js';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { constraintExpectsPredicate, reconcileEvidenceReports, } from './reconci
|
|
|
10
10
|
export { deriveAttestationStaleness } from './attestation-staleness.js';
|
|
11
11
|
export { deriveArtifactCoverage, } from './artifact-coverage.js';
|
|
12
12
|
export { buildRtm, renderRtmMarkdown, } from './rtm.js';
|
|
13
|
-
export { canonicalProjection, evaluateProjection, explainProjection, loadProjection, renderProjectionMarkdown, } from './projection.js';
|
|
13
|
+
export { canonicalProjection, evaluateProjection, explainProjection, instanceClosureOf, loadProjection, renderProjectionMarkdown, } from './projection.js';
|
|
14
14
|
export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, } from './adapter-mapping.js';
|
|
15
15
|
export { createFileSystemStore, } from './source-store.js';
|
|
16
16
|
export { applyOperations, landOperations, posixDirectoryOf, } from './apply-command.js';
|
package/dist/projection.d.ts
CHANGED
|
@@ -6,6 +6,25 @@ export interface ProjectionDefinition {
|
|
|
6
6
|
readonly version: string;
|
|
7
7
|
readonly query: {
|
|
8
8
|
readonly subjects?: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* Pattern instances whose CONTENTS this query wants (#473, ADR 0144).
|
|
11
|
+
*
|
|
12
|
+
* Each id names an instance, and the facet selects that instance together
|
|
13
|
+
* with everything the fold tree would draw inside it - the same closure
|
|
14
|
+
* `presentation.fold: instances` collapses into one box, read through the
|
|
15
|
+
* view's own `nesting`. A view that wanted the box wanted what is in it.
|
|
16
|
+
*
|
|
17
|
+
* This is the ONE facet that adds rather than narrows, and it is why
|
|
18
|
+
* `subjects` and this are read as a single identity facet whose values
|
|
19
|
+
* combine with OR (`docs/PROJECTIONS.md`). Every other field still ANDs
|
|
20
|
+
* over the union: `instances` says which subjects are in play, and `kinds`
|
|
21
|
+
* or `statuses` narrow that set the way they always did.
|
|
22
|
+
*
|
|
23
|
+
* Hand-listing the members instead is what this replaces, and the list goes
|
|
24
|
+
* stale the moment the pattern binds another slot. The names here are the
|
|
25
|
+
* instance, not its parts, so the view follows the model.
|
|
26
|
+
*/
|
|
27
|
+
readonly instances?: readonly string[];
|
|
9
28
|
/**
|
|
10
29
|
* Subjects this query would otherwise select and the author has taken out
|
|
11
30
|
* (#267, ADR 0122). A facet view states a rule, and every interesting rule
|
|
@@ -90,6 +109,7 @@ export { DEFAULT_NESTING, type NestingKind } from './nesting.js';
|
|
|
90
109
|
*/
|
|
91
110
|
export { DEFAULT_FOLD, type FoldMode } from './fold-tree.js';
|
|
92
111
|
import type { NestingKind } from './nesting.js';
|
|
112
|
+
import { type FoldMembership } from './fold-tree.js';
|
|
93
113
|
/**
|
|
94
114
|
* Which way a view runs, and the default. Split out for the same reason as the
|
|
95
115
|
* nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
|
|
@@ -118,12 +138,40 @@ export declare function canonicalProjection(projection: ProjectionDefinition): P
|
|
|
118
138
|
* A facet of a query, named the way the query names it. What
|
|
119
139
|
* {@link explainProjection} reports as the reason a subject is not in a view.
|
|
120
140
|
*/
|
|
121
|
-
export type ConceptFacet = 'exclude' | 'states' | 'subjects' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
|
|
141
|
+
export type ConceptFacet = 'exclude' | 'states' | 'subjects' | 'instances' | 'documents' | 'kinds' | 'layers' | 'statuses' | 'excludeStatuses' | 'owners' | 'constraints';
|
|
122
142
|
/** One subject a query dropped, and the facet that dropped it. */
|
|
123
143
|
export interface ProjectionExclusion {
|
|
124
144
|
readonly id: string;
|
|
125
145
|
readonly facet: ConceptFacet;
|
|
126
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* How a query decides about concepts, built once and shared by the two things
|
|
149
|
+
* that ask.
|
|
150
|
+
*
|
|
151
|
+
* `evaluateProjection` asks whether a subject is in; `explainProjection` asks
|
|
152
|
+
* why one is out. They must never be able to disagree, which is why there is
|
|
153
|
+
* one selector rather than a filter here and a reason-finder somewhere else.
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* Which subjects each named pattern instance holds, transitively.
|
|
157
|
+
*
|
|
158
|
+
* The SAME tree the canvas folds, computed from the same module over the same
|
|
159
|
+
* inputs, because a view that names an instance must select exactly what the
|
|
160
|
+
* box would have contained. Two implementations of "what is inside this
|
|
161
|
+
* instance" would be two answers to one question, and the one the reader sees
|
|
162
|
+
* is whichever they happened to open (rule: a test must go through the seam it
|
|
163
|
+
* is testing - so must a second caller).
|
|
164
|
+
*
|
|
165
|
+
* Nesting comes from THIS view's `presentation.nesting`, not the default, for
|
|
166
|
+
* the reason folding reads it too: containment is a property of the view, and a
|
|
167
|
+
* view that nests on composition alone holds less than one that also nests on
|
|
168
|
+
* assignment.
|
|
169
|
+
*
|
|
170
|
+
* Without `memberships` the closure is the named instances ALONE. That is a
|
|
171
|
+
* degradation, not an answer, and it is why `check` refuses rather than
|
|
172
|
+
* quietly returning a smaller view (#447, #450).
|
|
173
|
+
*/
|
|
174
|
+
export declare function instanceClosureOf(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext, memberships?: readonly FoldMembership[]): ReadonlyMap<string, string>;
|
|
127
175
|
/** A query facet naming something the model does not have. */
|
|
128
176
|
export interface UnmatchedSelector {
|
|
129
177
|
readonly facet: string;
|
|
@@ -170,7 +218,7 @@ export declare function unmatchedSelectors(graph: SemanticGraph, projection: Pro
|
|
|
170
218
|
* caller so `check`, and anything that adopts this later, refuse in identical
|
|
171
219
|
* words with an identical code.
|
|
172
220
|
*/
|
|
173
|
-
export declare function projectionReferenceDiagnostics(source: WorkspaceSource, projection: ProjectionDefinition, graph: SemanticGraph, profileContext?: ResolvedProfileContext): readonly Diagnostic[];
|
|
221
|
+
export declare function projectionReferenceDiagnostics(source: WorkspaceSource, projection: ProjectionDefinition, graph: SemanticGraph, profileContext?: ResolvedProfileContext, instances?: ReadonlySet<string>): readonly Diagnostic[];
|
|
174
222
|
/**
|
|
175
223
|
* Every concept a query leaves out, and the facet that left it out.
|
|
176
224
|
*
|
|
@@ -186,7 +234,7 @@ export declare function projectionReferenceDiagnostics(source: WorkspaceSource,
|
|
|
186
234
|
* `additionalProperties: false` and this is a question about a query rather
|
|
187
235
|
* than part of what a projection IS.
|
|
188
236
|
*/
|
|
189
|
-
export declare function explainProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext): readonly ProjectionExclusion[];
|
|
190
|
-
export declare function evaluateProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext): ProjectionResult;
|
|
237
|
+
export declare function explainProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext, memberships?: readonly FoldMembership[]): readonly ProjectionExclusion[];
|
|
238
|
+
export declare function evaluateProjection(graph: SemanticGraph, projection: ProjectionDefinition, profileContext?: ResolvedProfileContext, memberships?: readonly FoldMembership[]): ProjectionResult;
|
|
191
239
|
export declare function renderProjectionMarkdown(result: ProjectionResult, profileContext?: ResolvedProfileContext): string;
|
|
192
240
|
export declare function renderBudgetedContext(result: ProjectionResult, budgetTokens: number): string;
|
package/dist/projection.js
CHANGED
|
@@ -15,6 +15,9 @@ export { DEFAULT_NESTING } from './nesting.js';
|
|
|
15
15
|
* vocabulary above (#473).
|
|
16
16
|
*/
|
|
17
17
|
export { DEFAULT_FOLD } from './fold-tree.js';
|
|
18
|
+
import { DEFAULT_NESTING as NESTING_DEFAULT } from './nesting.js';
|
|
19
|
+
import { foldTree } from './fold-tree.js';
|
|
20
|
+
import { kindLabelOf } from './kind-label.js';
|
|
18
21
|
/**
|
|
19
22
|
* Which way a view runs, and the default. Split out for the same reason as the
|
|
20
23
|
* nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
|
|
@@ -36,6 +39,7 @@ export function canonicalProjection(projection) {
|
|
|
36
39
|
version: projection.version,
|
|
37
40
|
query: {
|
|
38
41
|
...(query.subjects === undefined ? {} : { subjects: [...query.subjects].sort() }),
|
|
42
|
+
...(query.instances === undefined ? {} : { instances: [...query.instances].sort() }),
|
|
39
43
|
...(query.documents === undefined ? {} : { documents: [...query.documents].sort() }),
|
|
40
44
|
...(query.kinds === undefined ? {} : { kinds: [...query.kinds].sort() }),
|
|
41
45
|
...(query.layers === undefined ? {} : { layers: [...query.layers].sort() }),
|
|
@@ -88,7 +92,106 @@ const claimReferences = (claims, subject, predicate) => claims.flatMap((claim) =
|
|
|
88
92
|
* why one is out. They must never be able to disagree, which is why there is
|
|
89
93
|
* one selector rather than a filter here and a reason-finder somewhere else.
|
|
90
94
|
*/
|
|
91
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Which subjects each named pattern instance holds, transitively.
|
|
97
|
+
*
|
|
98
|
+
* The SAME tree the canvas folds, computed from the same module over the same
|
|
99
|
+
* inputs, because a view that names an instance must select exactly what the
|
|
100
|
+
* box would have contained. Two implementations of "what is inside this
|
|
101
|
+
* instance" would be two answers to one question, and the one the reader sees
|
|
102
|
+
* is whichever they happened to open (rule: a test must go through the seam it
|
|
103
|
+
* is testing - so must a second caller).
|
|
104
|
+
*
|
|
105
|
+
* Nesting comes from THIS view's `presentation.nesting`, not the default, for
|
|
106
|
+
* the reason folding reads it too: containment is a property of the view, and a
|
|
107
|
+
* view that nests on composition alone holds less than one that also nests on
|
|
108
|
+
* assignment.
|
|
109
|
+
*
|
|
110
|
+
* Without `memberships` the closure is the named instances ALONE. That is a
|
|
111
|
+
* degradation, not an answer, and it is why `check` refuses rather than
|
|
112
|
+
* quietly returning a smaller view (#447, #450).
|
|
113
|
+
*/
|
|
114
|
+
export function instanceClosureOf(graph, projection, profileContext, memberships) {
|
|
115
|
+
const named = projection.query.instances;
|
|
116
|
+
const broughtInBy = new Map();
|
|
117
|
+
if (named === undefined || named.length === 0)
|
|
118
|
+
return broughtInBy;
|
|
119
|
+
// Sorted so that an instance nested inside another named instance resolves to
|
|
120
|
+
// a stable owner rather than to whichever the author typed first.
|
|
121
|
+
const roots = [...named].sort();
|
|
122
|
+
for (const root of roots)
|
|
123
|
+
broughtInBy.set(root, root);
|
|
124
|
+
if (memberships === undefined)
|
|
125
|
+
return broughtInBy;
|
|
126
|
+
const kindOf = new Map();
|
|
127
|
+
for (const claim of graph.claims) {
|
|
128
|
+
if (claim.predicate !== 'yarramate/concept/kind')
|
|
129
|
+
continue;
|
|
130
|
+
if ('value' in claim.object)
|
|
131
|
+
kindOf.set(claim.subject, claim.object.value);
|
|
132
|
+
}
|
|
133
|
+
const nodes = graph.subjects
|
|
134
|
+
.filter(({ type }) => type === 'concept')
|
|
135
|
+
.map(({ id }) => {
|
|
136
|
+
const kind = kindOf.get(id) ?? '';
|
|
137
|
+
return {
|
|
138
|
+
id,
|
|
139
|
+
kind,
|
|
140
|
+
// The same reading `projectGraphForCanvas` gives `coreKindLabel`: the
|
|
141
|
+
// nearest declared ancestor, labelled. Every rule in `foldTree` reads
|
|
142
|
+
// this and never the authored kind (#473).
|
|
143
|
+
coreKind: kindLabelOf(profileContext?.conceptKindLineages.get(kind)?.[0] ?? kind),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
const edges = graph.subjects
|
|
147
|
+
.filter(({ type }) => type === 'relationship')
|
|
148
|
+
.flatMap(({ id }) => {
|
|
149
|
+
const claim = graph.claims.find((candidate) => candidate.id === id);
|
|
150
|
+
if (claim === undefined || !('ref' in claim.object))
|
|
151
|
+
return [];
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
id,
|
|
155
|
+
kind: claim.predicate,
|
|
156
|
+
from: claim.subject,
|
|
157
|
+
to: claim.object.ref,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
});
|
|
161
|
+
const tree = foldTree({
|
|
162
|
+
nodes,
|
|
163
|
+
edges,
|
|
164
|
+
memberships,
|
|
165
|
+
nesting: projection.presentation?.nesting ?? NESTING_DEFAULT,
|
|
166
|
+
});
|
|
167
|
+
const childrenOf = new Map();
|
|
168
|
+
for (const [child, parent] of tree.parentOf) {
|
|
169
|
+
const siblings = childrenOf.get(parent);
|
|
170
|
+
if (siblings === undefined)
|
|
171
|
+
childrenOf.set(parent, [child]);
|
|
172
|
+
else
|
|
173
|
+
siblings.push(child);
|
|
174
|
+
}
|
|
175
|
+
// Transitive, because the box is: unfolding one reveals the folded rows
|
|
176
|
+
// inside it, and a view naming the outer instance wants those too.
|
|
177
|
+
for (const root of roots) {
|
|
178
|
+
const queue = [root];
|
|
179
|
+
while (queue.length > 0) {
|
|
180
|
+
const current = queue.pop();
|
|
181
|
+
for (const child of childrenOf.get(current) ?? []) {
|
|
182
|
+
if (broughtInBy.has(child))
|
|
183
|
+
continue;
|
|
184
|
+
broughtInBy.set(child, root);
|
|
185
|
+
queue.push(child);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return broughtInBy;
|
|
190
|
+
}
|
|
191
|
+
const conceptSelector = (graph, projection, profileContext, memberships) => {
|
|
192
|
+
// Computed once for the whole query rather than per subject: the fold tree is
|
|
193
|
+
// a fact about the model and the view, not about the subject being judged.
|
|
194
|
+
const instanceClosure = instanceClosureOf(graph, projection, profileContext, memberships);
|
|
92
195
|
const architectureStateIds = new Set(graph.claims
|
|
93
196
|
.filter(({ predicate }) => predicate === 'yarramate/state/type')
|
|
94
197
|
.map(({ subject }) => subject));
|
|
@@ -143,8 +246,22 @@ const conceptSelector = (graph, projection, profileContext) => {
|
|
|
143
246
|
(architectureStateIds.has(id) || !participatesInSelectedState(id))) {
|
|
144
247
|
return 'states';
|
|
145
248
|
}
|
|
146
|
-
|
|
147
|
-
|
|
249
|
+
// `subjects` and `instances` are ONE identity facet spelled two ways,
|
|
250
|
+
// and values within a facet combine with OR - the rule
|
|
251
|
+
// `docs/PROJECTIONS.md` already states for the values inside a list,
|
|
252
|
+
// applied to the two lists that name the same thing. Naming a subject and
|
|
253
|
+
// naming the instance that holds it are the same act of selection, so a
|
|
254
|
+
// query carrying both selects the union and every other facet then ANDs
|
|
255
|
+
// over it, exactly as before.
|
|
256
|
+
//
|
|
257
|
+
// The reported facet keeps `subjects` whenever the author wrote one, so
|
|
258
|
+
// that no view which exists today changes the answer it gives the editor;
|
|
259
|
+
// `instances` is reported only where it is the sole identity facet.
|
|
260
|
+
if (query.subjects !== undefined || query.instances !== undefined) {
|
|
261
|
+
if (query.subjects?.includes(id) !== true &&
|
|
262
|
+
!instanceClosure.has(id)) {
|
|
263
|
+
return query.subjects === undefined ? 'instances' : 'subjects';
|
|
264
|
+
}
|
|
148
265
|
}
|
|
149
266
|
if (query.documents !== undefined &&
|
|
150
267
|
!query.documents.includes(documentId)) {
|
|
@@ -249,6 +366,11 @@ export function unmatchedSelectors(graph, projection, profileContext) {
|
|
|
249
366
|
};
|
|
250
367
|
const subjectIds = new Set(graph.subjects.map(({ id }) => id));
|
|
251
368
|
check('subjects', query.subjects, subjectIds);
|
|
369
|
+
// Only whether the id names a subject at all. Whether that subject is a
|
|
370
|
+
// pattern INSTANCE is a second question with a different answer and its own
|
|
371
|
+
// diagnostic, because "you misspelled this" and "this is real but it is not
|
|
372
|
+
// an instance" send an author to two different places.
|
|
373
|
+
check('instances', query.instances, subjectIds);
|
|
252
374
|
check('exclude', query.exclude, subjectIds);
|
|
253
375
|
// Against the SUBJECT list rather than against owners currently in use: a
|
|
254
376
|
// team that owns nothing yet is a real concept and selecting it is a real
|
|
@@ -296,8 +418,8 @@ const locate = (source, value) => {
|
|
|
296
418
|
* caller so `check`, and anything that adopts this later, refuse in identical
|
|
297
419
|
* words with an identical code.
|
|
298
420
|
*/
|
|
299
|
-
export function projectionReferenceDiagnostics(source, projection, graph, profileContext) {
|
|
300
|
-
|
|
421
|
+
export function projectionReferenceDiagnostics(source, projection, graph, profileContext, instances) {
|
|
422
|
+
const unmatched = unmatchedSelectors(graph, projection, profileContext).map(({ facet, value, nearest }) => ({
|
|
301
423
|
severity: 'error',
|
|
302
424
|
code: 'YM921',
|
|
303
425
|
message: `Projection query \`${facet}\` names ${JSON.stringify(value)}, ` +
|
|
@@ -309,6 +431,50 @@ export function projectionReferenceDiagnostics(source, projection, graph, profil
|
|
|
309
431
|
pointer: `/query/${facet}`,
|
|
310
432
|
...locate(source.source, value),
|
|
311
433
|
}));
|
|
434
|
+
const named = projection.query.instances;
|
|
435
|
+
if (named === undefined)
|
|
436
|
+
return unmatched;
|
|
437
|
+
// The facet cannot be resolved without knowing which subjects are instances,
|
|
438
|
+
// and a caller that did not look is not the same as a workspace with none.
|
|
439
|
+
// Answering anyway would return the instances alone, which is a SMALLER view
|
|
440
|
+
// that reads exactly like a correct one - the failure #450 is about.
|
|
441
|
+
if (instances === undefined) {
|
|
442
|
+
return [
|
|
443
|
+
...unmatched,
|
|
444
|
+
{
|
|
445
|
+
severity: 'error',
|
|
446
|
+
code: 'YM923',
|
|
447
|
+
message: 'Projection query `instances` cannot be resolved here, because this ' +
|
|
448
|
+
'check was given no pattern instances to resolve it against. ' +
|
|
449
|
+
'Without them the facet would select each named instance alone and ' +
|
|
450
|
+
'silently drop everything it holds.',
|
|
451
|
+
path: source.path,
|
|
452
|
+
pointer: '/query/instances',
|
|
453
|
+
// The facet itself is what is wrong here, not any one name in it, so
|
|
454
|
+
// point at the word `instances` rather than at an arbitrary member.
|
|
455
|
+
...locate(source.source, 'instances'),
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
const knownIds = new Set(graph.subjects.map(({ id }) => id));
|
|
460
|
+
return [
|
|
461
|
+
...unmatched,
|
|
462
|
+
...named
|
|
463
|
+
// A name that resolves to nothing is already YM921's; saying it twice
|
|
464
|
+
// would send the author looking for a second fault.
|
|
465
|
+
.filter((value) => knownIds.has(value) && !instances.has(value))
|
|
466
|
+
.map((value) => ({
|
|
467
|
+
severity: 'error',
|
|
468
|
+
code: 'YM922',
|
|
469
|
+
message: `Projection query \`instances\` names ${JSON.stringify(value)}, ` +
|
|
470
|
+
'which this workspace has but which is not a pattern instance, so ' +
|
|
471
|
+
'it holds nothing for the facet to select. Name it under ' +
|
|
472
|
+
'`subjects` to select the subject itself.',
|
|
473
|
+
path: source.path,
|
|
474
|
+
pointer: '/query/instances',
|
|
475
|
+
...locate(source.source, value),
|
|
476
|
+
})),
|
|
477
|
+
];
|
|
312
478
|
}
|
|
313
479
|
/**
|
|
314
480
|
* Every concept a query leaves out, and the facet that left it out.
|
|
@@ -325,8 +491,8 @@ export function projectionReferenceDiagnostics(source, projection, graph, profil
|
|
|
325
491
|
* `additionalProperties: false` and this is a question about a query rather
|
|
326
492
|
* than part of what a projection IS.
|
|
327
493
|
*/
|
|
328
|
-
export function explainProjection(graph, projection, profileContext) {
|
|
329
|
-
const { droppedBy } = conceptSelector(graph, projection, profileContext);
|
|
494
|
+
export function explainProjection(graph, projection, profileContext, memberships) {
|
|
495
|
+
const { droppedBy } = conceptSelector(graph, projection, profileContext, memberships);
|
|
330
496
|
const exclusions = [];
|
|
331
497
|
for (const subject of graph.subjects) {
|
|
332
498
|
if (subject.type !== 'concept')
|
|
@@ -337,8 +503,8 @@ export function explainProjection(graph, projection, profileContext) {
|
|
|
337
503
|
}
|
|
338
504
|
return exclusions;
|
|
339
505
|
}
|
|
340
|
-
export function evaluateProjection(graph, projection, profileContext) {
|
|
341
|
-
const { droppedBy, architectureStateIds, participatesInSelectedState } = conceptSelector(graph, projection, profileContext);
|
|
506
|
+
export function evaluateProjection(graph, projection, profileContext, memberships) {
|
|
507
|
+
const { droppedBy, architectureStateIds, participatesInSelectedState } = conceptSelector(graph, projection, profileContext, memberships);
|
|
342
508
|
const endpointExcluded = (id) => {
|
|
343
509
|
// An exclusion is final (#267, ADR 0122). Dropping the subject from the
|
|
344
510
|
// initial selection alone would not be: `relationships: connected` adds
|