yarramate 1.9.0 → 1.11.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/visual/session-server.js +4 -3
- package/dist/adapters/visual/workspace-model.js +2 -2
- package/dist/apply-command.js +7 -6
- package/dist/import-command.js +2 -2
- package/dist/interrogate-command.d.ts +10 -0
- package/dist/interrogate-command.js +164 -1
- package/dist/relationship-matrix.d.ts +2 -0
- package/dist/relationship-matrix.js +24 -0
- package/dist/visual-app/assets/index-Cuj0wIhg.js +394 -0
- package/dist/visual-app/index.html +1 -1
- package/dist/visual-app-lib/editor.js +26526 -26460
- package/dist/visual-app-lib/types/interrogate-command.d.ts +10 -0
- package/dist/visual-app-lib/types/relationship-matrix.d.ts +2 -0
- package/dist/visual-app-lib/types/yaml-emission.d.ts +34 -0
- package/dist/workbook.js +12 -10
- package/dist/yaml-emission.d.ts +34 -0
- package/dist/yaml-emission.js +50 -0
- package/package.json +1 -1
- package/schema/yarramate-interrogation-report.schema.json +4 -0
- package/dist/visual-app/assets/index-DsH9ve0w.js +0 -394
|
@@ -6,7 +6,7 @@ import { basename, extname, join, relative, resolve, sep } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import Ajv2020Module from "ajv/dist/2020.js";
|
|
8
8
|
import { WebSocketServer } from "ws";
|
|
9
|
-
import { parse
|
|
9
|
+
import { parse } from "yaml";
|
|
10
10
|
import { VISUAL_LIMITS, VISUAL_PROTOCOL_VERSION, digestOf, parseVisualBrowserInput, parseVisualResponse, parseVisualSessionStarted, parseVisualStatus, toWireFileUri, visualBrowserInputType, } from "./protocol.js";
|
|
11
11
|
import { appendTerminalEvent, appendVisualEvent, appendVisualResponse, createVisualSession, isActionableVisualEvent, recoverVisualSession, removeVisualSession, writeVisualSessionDescriptor, } from "./session-store.js";
|
|
12
12
|
import { loadProjection, } from "../../projection.js";
|
|
@@ -15,6 +15,7 @@ import { adoptLandedViews, exclusionsOf, matchedIdsOf, planViewWrites, published
|
|
|
15
15
|
import { compileWorkspaceWithProfileContext, } from "../../compiler.js";
|
|
16
16
|
import { planOperations, posixDirectoryOf, writeConflictDiagnostics, } from "../../apply-command.js";
|
|
17
17
|
import { createFileSystemStore } from "../../source-store.js";
|
|
18
|
+
import { emitYaml } from "../../yaml-emission.js";
|
|
18
19
|
import { loadWorkspaceManifest, } from "../../workspace.js";
|
|
19
20
|
import visualLayoutSchema from "../../../schema/yarramate-visual-layout.schema.json" with { type: "json" };
|
|
20
21
|
import visualProjectionSchema from "../../../schema/yarramate-projection.schema.json" with { type: "json" };
|
|
@@ -1306,7 +1307,7 @@ export const startVisualServer = async (options) => {
|
|
|
1306
1307
|
}
|
|
1307
1308
|
return;
|
|
1308
1309
|
}
|
|
1309
|
-
const operationsSource =
|
|
1310
|
+
const operationsSource = emitYaml({
|
|
1310
1311
|
format: "yarramate/operations/v1",
|
|
1311
1312
|
operations: event.payload.operations,
|
|
1312
1313
|
});
|
|
@@ -1417,7 +1418,7 @@ export const startVisualServer = async (options) => {
|
|
|
1417
1418
|
}
|
|
1418
1419
|
const path = `.yarramate/visual-layout/${projectionId}.yaml`;
|
|
1419
1420
|
mkdirSync(layoutDir, { recursive: true });
|
|
1420
|
-
writeFileSync(resolve(options.cwd, path),
|
|
1421
|
+
writeFileSync(resolve(options.cwd, path), emitYaml({
|
|
1421
1422
|
format: "yarramate/visual-layout/v1",
|
|
1422
1423
|
projectionId,
|
|
1423
1424
|
positions,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { stringify } from "yaml";
|
|
2
1
|
import { loadProjection } from "../../projection.js";
|
|
2
|
+
import { emitYaml } from "../../yaml-emission.js";
|
|
3
3
|
import { withDiagnosticSubjects } from "../../compiler.js";
|
|
4
4
|
import { posixDirectoryOf } from "../../apply-command.js";
|
|
5
5
|
import { projectGraphForCanvas } from "../../graph-projection.js";
|
|
@@ -259,7 +259,7 @@ export const planViewWrites = (operations, workspace, store) => {
|
|
|
259
259
|
diagnostics.push(serverDiagnostic("YMVS315", `The workspace manifest covers no projection in "${posixDirectoryOf(operation.path)}", so a view saved there would never load`));
|
|
260
260
|
continue;
|
|
261
261
|
}
|
|
262
|
-
const source =
|
|
262
|
+
const source = emitYaml(operation.projection);
|
|
263
263
|
const loaded = loadProjection({ path: operation.path, source });
|
|
264
264
|
if (!loaded.ok) {
|
|
265
265
|
// The composed document is the only source these are about, so they are
|
package/dist/apply-command.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { isMap, isScalar, isSeq, parseDocument,
|
|
1
|
+
import { isMap, isScalar, isSeq, parseDocument, } from 'yaml';
|
|
2
2
|
import Ajv2020Module from 'ajv/dist/2020.js';
|
|
3
3
|
import { loadAdapterMapping } from './adapter-mapping.js';
|
|
4
4
|
import { compileWorkspace, withDiagnosticSubjects, } from './compiler.js';
|
|
5
5
|
import { evaluateEvidence, loadEvidence } from './evidence.js';
|
|
6
|
+
import { emitYaml } from './yaml-emission.js';
|
|
6
7
|
import { loadProjection } from './projection.js';
|
|
7
8
|
import { loadSourceDocument, locateSourcePath, } from './source-document.js';
|
|
8
9
|
import { declaredStateIds, rewriteSubjectReferences, scanSubjectReferences, } from './subject-references.js';
|
|
@@ -61,7 +62,7 @@ const reindent = (text, indent) => text.split('\n').join(`\n${' '.repeat(indent)
|
|
|
61
62
|
// A plain value as YAML source. lineWidth 0 keeps strings we author on one
|
|
62
63
|
// line; genuinely multi-line strings become block scalars and are re-indented
|
|
63
64
|
// by the caller.
|
|
64
|
-
const valueText = (value) =>
|
|
65
|
+
const valueText = (value) => emitYaml(value, { lineWidth: 0 }).trimEnd();
|
|
65
66
|
const pairFor = (map, key) => map.items.find((pair) => isScalar(pair.key) && pair.key.value === key);
|
|
66
67
|
const nodeRange = (node) => {
|
|
67
68
|
const range = node.range;
|
|
@@ -72,7 +73,7 @@ const nodeRange = (node) => {
|
|
|
72
73
|
};
|
|
73
74
|
// Renders `items` as block sequence entries at the given marker indent.
|
|
74
75
|
const sequenceEntries = (items, markerIndent) => {
|
|
75
|
-
const rendered =
|
|
76
|
+
const rendered = emitYaml(items, { lineWidth: 0 }).trimEnd();
|
|
76
77
|
return `${' '.repeat(markerIndent)}${reindent(rendered, markerIndent)}`;
|
|
77
78
|
};
|
|
78
79
|
const splice = (source, start, end, text) => source.slice(0, start) + text + source.slice(end);
|
|
@@ -186,7 +187,7 @@ const rewriteFlowItem = (source, map, mutate) => {
|
|
|
186
187
|
const mutated = mutate(map.toJSON());
|
|
187
188
|
const [start, valueEnd] = nodeRange(map);
|
|
188
189
|
const fieldIndent = indentAt(source, start);
|
|
189
|
-
const rendered = reindent(
|
|
190
|
+
const rendered = reindent(emitYaml(mutated, { lineWidth: 0 }).trimEnd(), fieldIndent);
|
|
190
191
|
return splice(source, start, valueEnd, rendered);
|
|
191
192
|
};
|
|
192
193
|
const setScalarField = (source, map, key, value) => {
|
|
@@ -235,7 +236,7 @@ const appendListField = (source, map, key, additions) => {
|
|
|
235
236
|
? nodeRange(sequence)
|
|
236
237
|
: nodeRange(pair.value);
|
|
237
238
|
if (isSeq(sequence) && sequence.flow) {
|
|
238
|
-
const flow =
|
|
239
|
+
const flow = emitYaml(merged, {
|
|
239
240
|
collectionStyle: 'flow',
|
|
240
241
|
lineWidth: 0,
|
|
241
242
|
}).trimEnd();
|
|
@@ -278,7 +279,7 @@ const removeCollectionItem = (source, collection, matches) => {
|
|
|
278
279
|
const [start, valueEnd] = nodeRange(sequence);
|
|
279
280
|
return splice(source, start, valueEnd, remaining.length === 0
|
|
280
281
|
? '[]'
|
|
281
|
-
:
|
|
282
|
+
: emitYaml(remaining, {
|
|
282
283
|
collectionStyle: 'flow',
|
|
283
284
|
lineWidth: 0,
|
|
284
285
|
}).trimEnd());
|
package/dist/import-command.js
CHANGED
|
@@ -9,10 +9,10 @@ import { buildWorkbookSheets } from './workbook.js';
|
|
|
9
9
|
import { baselineSheets, mergeWorkbook } from './workbook-merge.js';
|
|
10
10
|
import { operationsFrom, operationsDocument } from './workbook-operations.js';
|
|
11
11
|
import { readWorkbook } from './workbook-read.js';
|
|
12
|
+
import { emitYaml } from './yaml-emission.js';
|
|
12
13
|
import { writeFileSync, mkdtempSync } from 'node:fs';
|
|
13
14
|
import { tmpdir } from 'node:os';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
|
-
import { stringify } from 'yaml';
|
|
16
16
|
/**
|
|
17
17
|
* `yarramate import xlsx <file> <workspace.yaml>` (#355, ADR 0127).
|
|
18
18
|
*
|
|
@@ -137,7 +137,7 @@ export async function runImportCommand(options, cwd) {
|
|
|
137
137
|
// atomic gate, and the surgical YAML editing that keeps comments.
|
|
138
138
|
const scratch = mkdtempSync(join(tmpdir(), 'yarramate-import-'));
|
|
139
139
|
const operationsPath = join(scratch, 'operations.yaml');
|
|
140
|
-
writeFileSync(operationsPath,
|
|
140
|
+
writeFileSync(operationsPath, emitYaml(operationsDocument(planned.operations)), 'utf8');
|
|
141
141
|
const applied = runApplyCommand([operationsPath, workspacePath], cwd);
|
|
142
142
|
if (applied.exitCode !== 0)
|
|
143
143
|
return applied;
|
|
@@ -178,6 +178,16 @@ export interface ReportQuestion {
|
|
|
178
178
|
readonly scope: 'workspace' | 'subject';
|
|
179
179
|
readonly authority: 'human' | 'agent' | 'either';
|
|
180
180
|
readonly open: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Whether the question was asked at all (#375, ADR 0132). Absent means
|
|
183
|
+
* true; only a subject-scoped question whose selector matched NO subject
|
|
184
|
+
* carries `asked: false`. Without it, never-asked and answered were
|
|
185
|
+
* byte-identical (`open: false`), and a host summing closed questions
|
|
186
|
+
* read an empty model as a satisfied interview — completion inferred
|
|
187
|
+
* from an empty set, the exact reading ADR 0125 refuses one level up by
|
|
188
|
+
* not evaluating a closed wave's questions at all.
|
|
189
|
+
*/
|
|
190
|
+
readonly asked?: boolean;
|
|
181
191
|
readonly question: string;
|
|
182
192
|
readonly materiality: string;
|
|
183
193
|
readonly resolution: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Ajv2020Module from 'ajv/dist/2020.js';
|
|
2
2
|
import { loadSourceDocument, locateSourcePath, } from './source-document.js';
|
|
3
3
|
import { nearDuplicateIndex } from './subject-identity.js';
|
|
4
|
+
import { sourceKindsPermitting, tableKnowsConceptKind, tableKnowsRelationshipKind, targetKindsPermitting, } from './relationship-matrix.js';
|
|
4
5
|
import catalogueSchema from '../schema/yarramate-question-catalogue.schema.json' with { type: 'json'
|
|
5
6
|
};
|
|
6
7
|
// `.default ?? module`, not a bare `.default`: NodeNext sees the raw CJS
|
|
@@ -517,7 +518,16 @@ patternMemberships) {
|
|
|
517
518
|
}
|
|
518
519
|
return { ...base, open: isOpen };
|
|
519
520
|
}
|
|
520
|
-
|
|
521
|
+
// Selection and trigger filtering are separate reads on purpose
|
|
522
|
+
// (#375, ADR 0132): a selector matching nobody means the question
|
|
523
|
+
// was never asked, and reporting that as a closed question said it
|
|
524
|
+
// had been asked and answered — completion inferred from an empty
|
|
525
|
+
// set, the reading ADR 0125 refuses one level up.
|
|
526
|
+
const selected = selectSubjects(index, question.subjects, profileContext);
|
|
527
|
+
if (selected.length === 0) {
|
|
528
|
+
return { ...base, open: false, asked: false };
|
|
529
|
+
}
|
|
530
|
+
const matches = selected.filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext, evidence, patternMemberships)));
|
|
521
531
|
if (matches.length === 0) {
|
|
522
532
|
return { ...base, open: false };
|
|
523
533
|
}
|
|
@@ -648,6 +658,128 @@ const unresolvableKinds = (catalogue, profileContext) => {
|
|
|
648
658
|
return hash > 0 && loadedProfiles.has(kind.slice(0, hash));
|
|
649
659
|
});
|
|
650
660
|
};
|
|
661
|
+
// The core kind an authored kind resolves to, or undefined when this workspace
|
|
662
|
+
// cannot resolve it at all. Lineage is ancestor-first, so `lineage[0]` is the
|
|
663
|
+
// core identity and an extension inherits its parent's row in the table
|
|
664
|
+
// (ADR 0097); a core kind is its own lineage head. This is also why
|
|
665
|
+
// `kindMatching: descendants` needs no special handling: a descendant shares
|
|
666
|
+
// its ancestor's row and column, so checking the named kind covers them all.
|
|
667
|
+
// A kind the table has no row for answers every query with an empty set,
|
|
668
|
+
// which reads exactly like "forbidden". The guards keep the two apart, so a
|
|
669
|
+
// vocabulary the table cannot judge is passed over in silence rather than
|
|
670
|
+
// accused - the direction every ambiguity here resolves in. They are the
|
|
671
|
+
// reason nothing below casts: the typed queries are reached through the
|
|
672
|
+
// check rather than around it.
|
|
673
|
+
const localKindOf = (kind, lineages) => {
|
|
674
|
+
const lineage = lineages.get(kind);
|
|
675
|
+
if (lineage === undefined)
|
|
676
|
+
return undefined;
|
|
677
|
+
const identity = lineage[0] ?? kind;
|
|
678
|
+
return identity.slice(identity.indexOf('#') + 1);
|
|
679
|
+
};
|
|
680
|
+
const coreConceptKindOf = (kind, lineages) => {
|
|
681
|
+
const local = localKindOf(kind, lineages);
|
|
682
|
+
return local !== undefined && tableKnowsConceptKind(local) ? local : undefined;
|
|
683
|
+
};
|
|
684
|
+
const coreRelationshipKindOf = (kind, lineages) => {
|
|
685
|
+
const local = localKindOf(kind, lineages);
|
|
686
|
+
return local !== undefined && tableKnowsRelationshipKind(local)
|
|
687
|
+
? local
|
|
688
|
+
: undefined;
|
|
689
|
+
};
|
|
690
|
+
/**
|
|
691
|
+
* Remedies a trigger offers that no model could author.
|
|
692
|
+
*
|
|
693
|
+
* A trigger names the ways its question can be satisfied, and each one is an
|
|
694
|
+
* OFFER: add this relationship, in this direction, from one of these kinds.
|
|
695
|
+
* The ArchiMate table the compiler admits relationships against decides which
|
|
696
|
+
* of those are authorable. An offer it forbids is a lie the catalogue tells,
|
|
697
|
+
* and the unit is the offer rather than the question, because a question with
|
|
698
|
+
* three offers and one dead one still reads as answerable: a reader takes the
|
|
699
|
+
* dead option, authors it, and the compiler refuses the write (ADR 0133).
|
|
700
|
+
*
|
|
701
|
+
* Both conditions naming a relationship are checked, and `missing-linkage` is
|
|
702
|
+
* the MORE checkable of the two. `missing-relationship` asks whether any of
|
|
703
|
+
* the 62 core kinds may stand opposite, which only the seven kinds nothing may
|
|
704
|
+
* realize can fail. `missing-linkage` names its own counterpart kinds, so the
|
|
705
|
+
* question narrows to those, a dead offer is far likelier, and the diagnostic
|
|
706
|
+
* can point at the exact list that admits nothing. Both of the defects that
|
|
707
|
+
* prompted this check in the reporting consumer's catalogue were linkage
|
|
708
|
+
* offers, not relationship ones.
|
|
709
|
+
*
|
|
710
|
+
* `missing-attestation` and `missing-claim` are not checked: they name no
|
|
711
|
+
* relationship, so there is no table to consult, and inventing one for them
|
|
712
|
+
* would be the second encoding of ArchiMate's rules this check exists to
|
|
713
|
+
* prevent.
|
|
714
|
+
*
|
|
715
|
+
* Narrow in the same two ways `unresolvableKinds` is narrow. Without a profile
|
|
716
|
+
* context there is no lineage to resolve an extension kind through, so nothing
|
|
717
|
+
* is reported rather than guessed; and a kind that resolves nowhere is
|
|
718
|
+
* `YM914`'s business, not this one. A trigger with any unresolvable counterpart
|
|
719
|
+
* is skipped whole, because a partial reading could accuse a question that a
|
|
720
|
+
* dormant cross-profile kind would have answered.
|
|
721
|
+
*/
|
|
722
|
+
const unauthorableOffers = (catalogue, profileContext) => {
|
|
723
|
+
const found = [];
|
|
724
|
+
const conceptCore = (kind) => coreConceptKindOf(kind, profileContext.conceptKindLineages);
|
|
725
|
+
for (const [questionIndex, question] of catalogue.questions.entries()) {
|
|
726
|
+
const subjectKinds = question.subjects?.kinds ?? [];
|
|
727
|
+
if (subjectKinds.length === 0)
|
|
728
|
+
continue;
|
|
729
|
+
for (const [conditionIndex, condition] of question.trigger.entries()) {
|
|
730
|
+
const trigger = condition;
|
|
731
|
+
if (trigger.condition !== 'missing-relationship' &&
|
|
732
|
+
trigger.condition !== 'missing-linkage') {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
// `any` and `either` are satisfied by a relationship in either
|
|
736
|
+
// direction, so an offer is dead only when both directions are.
|
|
737
|
+
const directions = trigger.direction === 'any' || trigger.direction === 'either'
|
|
738
|
+
? ['incoming', 'outgoing']
|
|
739
|
+
: [trigger.direction ?? 'any'];
|
|
740
|
+
const named = trigger.counterpartKinds;
|
|
741
|
+
const counterparts = named?.map(conceptCore);
|
|
742
|
+
if (counterparts?.some((kind) => kind === undefined) === true)
|
|
743
|
+
continue;
|
|
744
|
+
for (const subjectKind of subjectKinds) {
|
|
745
|
+
const subject = conceptCore(subjectKind);
|
|
746
|
+
if (subject === undefined)
|
|
747
|
+
continue;
|
|
748
|
+
for (const [kindIndex, relationshipKind] of (trigger.kinds ?? []).entries()) {
|
|
749
|
+
const relationship = coreRelationshipKindOf(relationshipKind, profileContext.relationshipKindLineages);
|
|
750
|
+
if (relationship === undefined)
|
|
751
|
+
continue;
|
|
752
|
+
const authorable = directions.some((direction) => {
|
|
753
|
+
const opposite = direction === 'incoming'
|
|
754
|
+
? sourceKindsPermitting(relationship, subject)
|
|
755
|
+
: targetKindsPermitting(relationship, subject);
|
|
756
|
+
return counterparts === undefined
|
|
757
|
+
? opposite.size > 0
|
|
758
|
+
: counterparts.some((kind) => kind !== undefined && opposite.has(kind));
|
|
759
|
+
});
|
|
760
|
+
if (authorable)
|
|
761
|
+
continue;
|
|
762
|
+
found.push({
|
|
763
|
+
questionId: question.id,
|
|
764
|
+
subjectKind,
|
|
765
|
+
relationshipKind,
|
|
766
|
+
direction: trigger.direction ?? 'any',
|
|
767
|
+
counterpartKinds: named,
|
|
768
|
+
path: [
|
|
769
|
+
'questions',
|
|
770
|
+
questionIndex,
|
|
771
|
+
'trigger',
|
|
772
|
+
conditionIndex,
|
|
773
|
+
'kinds',
|
|
774
|
+
kindIndex,
|
|
775
|
+
],
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return found;
|
|
782
|
+
};
|
|
651
783
|
const loadCatalogueDocument = (catalogueSource) => {
|
|
652
784
|
const loaded = loadSourceDocument(catalogueSource, validateCatalogue, 'Question catalogue');
|
|
653
785
|
if (!loaded.ok)
|
|
@@ -693,6 +825,25 @@ profileContext === undefined
|
|
|
693
825
|
message: `Kind "${reference.kind}" is not declared by profile "${reference.kind.slice(0, reference.kind.indexOf('#'))}", which this workspace loads, so the question can never fire`,
|
|
694
826
|
...locate(reference.path),
|
|
695
827
|
}));
|
|
828
|
+
// YM916, the sibling of YM914. YM914 refuses a question that can never FIRE;
|
|
829
|
+
// this refuses one that offers a remedy nobody could author. Both failures are
|
|
830
|
+
// invisible, and this one twice over: the question reads as ordinary
|
|
831
|
+
// unfinished work, and a reader who takes the dead offer learns only when the
|
|
832
|
+
// compiler refuses the write it led them to (ADR 0133).
|
|
833
|
+
const unauthorableOfferDiagnostics = ({ catalogue, locate }, profileContext) => profileContext === undefined
|
|
834
|
+
? []
|
|
835
|
+
: unauthorableOffers(catalogue, profileContext).map((offer) => ({
|
|
836
|
+
severity: 'error',
|
|
837
|
+
code: 'YM916',
|
|
838
|
+
message: `Question "${offer.questionId}" offers "${offer.relationshipKind}" ` +
|
|
839
|
+
`${offer.direction} on "${offer.subjectKind}", which the ` +
|
|
840
|
+
'ArchiMate relationship table permits from ' +
|
|
841
|
+
(offer.counterpartKinds === undefined
|
|
842
|
+
? 'no kind at all'
|
|
843
|
+
: `none of the counterpart kinds it names (${offer.counterpartKinds.join(', ')})`) +
|
|
844
|
+
', so no model could author it',
|
|
845
|
+
...locate(offer.path),
|
|
846
|
+
}));
|
|
696
847
|
/**
|
|
697
848
|
* A catalogue is qualified on the way OUT, never in what an author writes.
|
|
698
849
|
*
|
|
@@ -801,6 +952,7 @@ export function composeCatalogues(sources, profileContext) {
|
|
|
801
952
|
const crossDiagnostics = documents.flatMap((document) => [
|
|
802
953
|
...undeclaredWaveDiagnostics(document, declaredWaves),
|
|
803
954
|
...unresolvableKindDiagnostics(document, profileContext),
|
|
955
|
+
...unauthorableOfferDiagnostics(document, profileContext),
|
|
804
956
|
]);
|
|
805
957
|
if (crossDiagnostics.length > 0) {
|
|
806
958
|
return { ok: false, diagnostics: crossDiagnostics };
|
|
@@ -837,6 +989,10 @@ export function loadQuestionCatalogue(catalogueSource, profileContext) {
|
|
|
837
989
|
if (kindDiagnostics.length > 0) {
|
|
838
990
|
return { ok: false, diagnostics: kindDiagnostics };
|
|
839
991
|
}
|
|
992
|
+
const offerDiagnostics = unauthorableOfferDiagnostics(loaded.document, profileContext);
|
|
993
|
+
if (offerDiagnostics.length > 0) {
|
|
994
|
+
return { ok: false, diagnostics: offerDiagnostics };
|
|
995
|
+
}
|
|
840
996
|
return { ok: true, catalogue: loaded.document.catalogue };
|
|
841
997
|
}
|
|
842
998
|
// Shared by interrogate and `ask --open`: the wave-by-wave human report.
|
|
@@ -859,6 +1015,13 @@ export function renderInterrogationReport(report) {
|
|
|
859
1015
|
continue;
|
|
860
1016
|
}
|
|
861
1017
|
for (const question of wave.questions) {
|
|
1018
|
+
if (question.asked === false) {
|
|
1019
|
+
// Never asked is not closed (#375): `closed` says answered, and a
|
|
1020
|
+
// selector that matched nobody asked nothing — the question-level
|
|
1021
|
+
// twin of the wave's own "not yet" line above.
|
|
1022
|
+
lines.push(` unasked ${question.id} — nothing it selects exists yet`);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
862
1025
|
if (!question.open) {
|
|
863
1026
|
lines.push(` closed ${question.id}`);
|
|
864
1027
|
continue;
|
|
@@ -6,6 +6,8 @@ export declare const isCoreConceptKindId: (id: string) => id is CoreConceptKindI
|
|
|
6
6
|
export declare const permittedRelationshipKinds: (from: CoreConceptKindId, to: CoreConceptKindId) => ReadonlySet<RelationshipKind>;
|
|
7
7
|
export declare const relationshipPermitted: (from: CoreConceptKindId, kind: RelationshipKind, to: CoreConceptKindId) => boolean;
|
|
8
8
|
export declare const matrixEndpointAspects: (kind: RelationshipKind, endpoint: 'source' | 'target') => ReadonlySet<Aspect>;
|
|
9
|
+
export declare const tableKnowsConceptKind: (kind: string) => kind is CoreConceptKindId;
|
|
10
|
+
export declare const tableKnowsRelationshipKind: (kind: string) => kind is RelationshipKind;
|
|
9
11
|
/** Kinds that may stand as the source of `kind` into `to`. */
|
|
10
12
|
export declare const sourceKindsPermitting: (kind: RelationshipKind, to: CoreConceptKindId) => ReadonlySet<CoreConceptKindId>;
|
|
11
13
|
/** Kinds that may stand as the target of `kind` from `from`. */
|
|
@@ -66,6 +66,30 @@ export const matrixEndpointAspects = (kind, endpoint) => {
|
|
|
66
66
|
endpointAspectCache.set(key, aspects);
|
|
67
67
|
return aspects;
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Whether the table has a row and column for this kind at all.
|
|
71
|
+
*
|
|
72
|
+
* Every query below answers an ABSENT kind with an empty set, which reads
|
|
73
|
+
* identically to "the table forbids this" - the empty-set conflation, in the
|
|
74
|
+
* one place where mistaking it turns a gate into a false accuser. A caller
|
|
75
|
+
* deciding whether something is forbidden must therefore ask this first, so
|
|
76
|
+
* that "not in the table" resolves toward silence rather than toward blame.
|
|
77
|
+
*
|
|
78
|
+
* Nothing reachable through a profile should fail it: `parent` is required on
|
|
79
|
+
* every declared kind and resolves to a core ancestor, so a lineage head is
|
|
80
|
+
* always a table kind. The predicates exist because that guarantee is another
|
|
81
|
+
* module's to keep, and a gate should not rest on one silently.
|
|
82
|
+
*
|
|
83
|
+
* They are type guards rather than booleans so that a caller holding a bare
|
|
84
|
+
* string reaches the typed queries through the check instead of around it by
|
|
85
|
+
* a cast. The cast is where the hole actually opens: the signatures already
|
|
86
|
+
* refuse an unknown kind, and asserting past them is what lets an absent kind
|
|
87
|
+
* arrive and be read as a forbidden one.
|
|
88
|
+
*/
|
|
89
|
+
const CORE_CONCEPT_KINDS = new Set(CORE_CONCEPT_KIND_ORDER);
|
|
90
|
+
const CORE_RELATIONSHIP_KIND_SET = new Set(relationshipKinds);
|
|
91
|
+
export const tableKnowsConceptKind = (kind) => CORE_CONCEPT_KINDS.has(kind);
|
|
92
|
+
export const tableKnowsRelationshipKind = (kind) => CORE_RELATIONSHIP_KIND_SET.has(kind);
|
|
69
93
|
/** Kinds that may stand as the source of `kind` into `to`. */
|
|
70
94
|
export const sourceKindsPermitting = (kind, to) => new Set(CORE_CONCEPT_KIND_ORDER.filter((from) => relationshipPermitted(from, kind, to)));
|
|
71
95
|
/** Kinds that may stand as the target of `kind` from `from`. */
|