wabachi 0.1.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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/architecture/canon/authority.d.ts +35 -0
- package/dist/architecture/canon/authority.js +76 -0
- package/dist/architecture/canon/authority.js.map +1 -0
- package/dist/architecture/canon/boundaries.d.ts +31 -0
- package/dist/architecture/canon/boundaries.js +73 -0
- package/dist/architecture/canon/boundaries.js.map +1 -0
- package/dist/architecture/canon/codec.d.ts +10 -0
- package/dist/architecture/canon/codec.js +516 -0
- package/dist/architecture/canon/codec.js.map +1 -0
- package/dist/architecture/canon/constraints.d.ts +60 -0
- package/dist/architecture/canon/constraints.js +116 -0
- package/dist/architecture/canon/constraints.js.map +1 -0
- package/dist/architecture/canon/decisions.d.ts +62 -0
- package/dist/architecture/canon/decisions.js +192 -0
- package/dist/architecture/canon/decisions.js.map +1 -0
- package/dist/architecture/canon/deployment.d.ts +89 -0
- package/dist/architecture/canon/deployment.js +156 -0
- package/dist/architecture/canon/deployment.js.map +1 -0
- package/dist/architecture/canon/document.d.ts +60 -0
- package/dist/architecture/canon/document.js +106 -0
- package/dist/architecture/canon/document.js.map +1 -0
- package/dist/architecture/canon/elements.d.ts +21 -0
- package/dist/architecture/canon/elements.js +87 -0
- package/dist/architecture/canon/elements.js.map +1 -0
- package/dist/architecture/canon/flows.d.ts +37 -0
- package/dist/architecture/canon/flows.js +82 -0
- package/dist/architecture/canon/flows.js.map +1 -0
- package/dist/architecture/canon/identity.d.ts +45 -0
- package/dist/architecture/canon/identity.js +83 -0
- package/dist/architecture/canon/identity.js.map +1 -0
- package/dist/architecture/canon/relationships.d.ts +48 -0
- package/dist/architecture/canon/relationships.js +181 -0
- package/dist/architecture/canon/relationships.js.map +1 -0
- package/dist/architecture/canon/repository-mappings.d.ts +51 -0
- package/dist/architecture/canon/repository-mappings.js +134 -0
- package/dist/architecture/canon/repository-mappings.js.map +1 -0
- package/dist/architecture/canon/responsibilities.d.ts +40 -0
- package/dist/architecture/canon/responsibilities.js +89 -0
- package/dist/architecture/canon/responsibilities.js.map +1 -0
- package/dist/architecture/canon/validate.d.ts +14 -0
- package/dist/architecture/canon/validate.js +389 -0
- package/dist/architecture/canon/validate.js.map +1 -0
- package/dist/architecture/canon/views.d.ts +101 -0
- package/dist/architecture/canon/views.js +182 -0
- package/dist/architecture/canon/views.js.map +1 -0
- package/dist/architecture/cli.d.ts +2 -0
- package/dist/architecture/cli.js +158 -0
- package/dist/architecture/cli.js.map +1 -0
- package/dist/architecture/documentation/html.d.ts +3 -0
- package/dist/architecture/documentation/html.js +143 -0
- package/dist/architecture/documentation/html.js.map +1 -0
- package/dist/architecture/documentation/model.d.ts +46 -0
- package/dist/architecture/documentation/model.js +13 -0
- package/dist/architecture/documentation/model.js.map +1 -0
- package/dist/architecture/documentation/project.d.ts +4 -0
- package/dist/architecture/documentation/project.js +166 -0
- package/dist/architecture/documentation/project.js.map +1 -0
- package/dist/architecture/documentation/site.d.ts +43 -0
- package/dist/architecture/documentation/site.js +147 -0
- package/dist/architecture/documentation/site.js.map +1 -0
- package/dist/architecture/projection/structurizr-export.d.ts +65 -0
- package/dist/architecture/projection/structurizr-export.js +136 -0
- package/dist/architecture/projection/structurizr-export.js.map +1 -0
- package/dist/architecture/projection/structurizr.d.ts +34 -0
- package/dist/architecture/projection/structurizr.js +667 -0
- package/dist/architecture/projection/structurizr.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +143 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/correlation.d.ts +143 -0
- package/dist/runtime/correlation.js +919 -0
- package/dist/runtime/correlation.js.map +1 -0
- package/dist/runtime/facts.d.ts +138 -0
- package/dist/runtime/facts.js +614 -0
- package/dist/runtime/facts.js.map +1 -0
- package/dist/runtime/fixtureProvider.d.ts +10 -0
- package/dist/runtime/fixtureProvider.js +30 -0
- package/dist/runtime/fixtureProvider.js.map +1 -0
- package/dist/runtime/graftProvider.d.ts +8 -0
- package/dist/runtime/graftProvider.js +107 -0
- package/dist/runtime/graftProvider.js.map +1 -0
- package/dist/runtime/graftWiring.d.ts +30 -0
- package/dist/runtime/graftWiring.js +3 -0
- package/dist/runtime/graftWiring.js.map +1 -0
- package/dist/runtime/manifest.d.ts +17 -0
- package/dist/runtime/manifest.js +39 -0
- package/dist/runtime/manifest.js.map +1 -0
- package/dist/runtime/matrix.d.ts +337 -0
- package/dist/runtime/matrix.js +1485 -0
- package/dist/runtime/matrix.js.map +1 -0
- package/dist/runtime/observation.d.ts +36 -0
- package/dist/runtime/observation.js +3 -0
- package/dist/runtime/observation.js.map +1 -0
- package/dist/runtime/provider.d.ts +34 -0
- package/dist/runtime/provider.js +2 -0
- package/dist/runtime/provider.js.map +1 -0
- package/dist/runtime/repository.d.ts +20 -0
- package/dist/runtime/repository.js +55 -0
- package/dist/runtime/repository.js.map +1 -0
- package/dist/runtime/run.d.ts +21 -0
- package/dist/runtime/run.js +80 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/scipProvider.d.ts +11 -0
- package/dist/runtime/scipProvider.js +126 -0
- package/dist/runtime/scipProvider.js.map +1 -0
- package/dist/runtime/typescriptProvider.d.ts +10 -0
- package/dist/runtime/typescriptProvider.js +289 -0
- package/dist/runtime/typescriptProvider.js.map +1 -0
- package/dist/runtime/workflow.d.ts +30 -0
- package/dist/runtime/workflow.js +153 -0
- package/dist/runtime/workflow.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
import { mkdir, open, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FACT_SCHEMA_VERSION, factEqualityKey, } from "./facts.js";
|
|
4
|
+
/** Version of the deterministic Provider Matrix artifact. */
|
|
5
|
+
export const PROVIDER_MATRIX_SCHEMA_VERSION = 1;
|
|
6
|
+
export const MATRIX_METRIC_DEFINITIONS = [
|
|
7
|
+
{
|
|
8
|
+
id: "normalizationCoverage",
|
|
9
|
+
numerator: "normalizedFactCount",
|
|
10
|
+
denominator: "rawObservedFactCount",
|
|
11
|
+
meaning: "Fraction of retained provider observations mapped to a common normalized predicate; null when no raw observation exists.",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "matchedCoverage",
|
|
15
|
+
numerator: "facts whose entity endpoints are all matched",
|
|
16
|
+
denominator: "normalizedFactCount",
|
|
17
|
+
meaning: "Fraction of normalized facts whose entity correlations are deterministic matches.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "correlationCoverage",
|
|
21
|
+
numerator: "facts whose entity endpoints are matched or probable",
|
|
22
|
+
denominator: "normalizedFactCount",
|
|
23
|
+
meaning: "Fraction of normalized facts with a usable canonical entity identity; ambiguous and unmatched facts are excluded.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "pairwiseOverlap",
|
|
27
|
+
numerator: "distinct exact comparable fact keys present in both providers",
|
|
28
|
+
denominator: "minimum of the two providers' distinct comparable fact-key counts",
|
|
29
|
+
meaning: "Exact normalized fact overlap; conflicting values are not counted as overlap unless the exact value is also shared.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "allProviderOverlap",
|
|
33
|
+
numerator: "distinct exact comparable fact keys present in every selected provider",
|
|
34
|
+
denominator: "distinct exact comparable fact keys in the selected-provider union",
|
|
35
|
+
meaning: "Common evidence across the complete selected provider set; a provider with no fact makes the numerator zero.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "uniqueCoverage",
|
|
39
|
+
numerator: "distinct comparable logical fact keys observed by exactly one provider",
|
|
40
|
+
denominator: "that provider's distinct comparable logical fact keys",
|
|
41
|
+
meaning: "Provider-only comparable coverage; conflicting values are not called unique because both providers observed the same logical fact.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "incrementalInformationGain",
|
|
45
|
+
numerator: "new distinct exact comparable fact keys absent from the existing provider set",
|
|
46
|
+
denominator: "the added provider's distinct comparable fact keys",
|
|
47
|
+
meaning: "Deterministic gain for the explicitly recorded addition order; unsupported, ambiguous, and unmatched evidence is counted separately.",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Builds all matrix views from normalized/persisted facts. It only consumes
|
|
52
|
+
* artifacts; provider execution and observation generation are intentionally
|
|
53
|
+
* outside this function.
|
|
54
|
+
*/
|
|
55
|
+
export function buildProviderMatrix(input, options = {}) {
|
|
56
|
+
const artifact = input;
|
|
57
|
+
const entries = collectProviders(artifact, options);
|
|
58
|
+
const providers = entries.map((entry) => entry.provider);
|
|
59
|
+
const additionEntries = orderAdditions(entries, options.additionOrder);
|
|
60
|
+
const additionOrder = additionEntries.map((entry) => entry.provider);
|
|
61
|
+
const providerByKey = new Map(entries.map((entry) => [entry.key, entry.provider]));
|
|
62
|
+
const facts = artifact.facts.map(toFactDescriptor).sort(compareFactDescriptors);
|
|
63
|
+
const unsupported = artifact.unsupported.map(toUnsupportedDescriptor).sort(compareUnsupportedDescriptors);
|
|
64
|
+
const rows = buildFactRows(facts, unsupported, entries);
|
|
65
|
+
const factClasses = collectFactClasses(facts, unsupported);
|
|
66
|
+
const uniqueLogicalProviders = indexLogicalProviders(facts);
|
|
67
|
+
const coverage = providers.map((provider) => buildProviderCoverage(provider, facts, unsupported, factClasses, uniqueLogicalProviders, artifact.correlation));
|
|
68
|
+
const overlap = buildOverlap(providers, entries, facts, factClasses, rows);
|
|
69
|
+
const conflicts = rows.filter((row) => row.state === "conflict");
|
|
70
|
+
const unmatchedRecords = rows.filter((row) => row.state === "ambiguous" || row.state === "unmatched" || row.state === "unsupported");
|
|
71
|
+
const unmatched = {
|
|
72
|
+
ambiguous: unmatchedRecords.filter((row) => row.state === "ambiguous"),
|
|
73
|
+
unmatched: unmatchedRecords.filter((row) => row.state === "unmatched"),
|
|
74
|
+
unsupported: unmatchedRecords.filter((row) => row.state === "unsupported"),
|
|
75
|
+
records: unmatchedRecords,
|
|
76
|
+
};
|
|
77
|
+
const informationGain = buildInformationGain(additionEntries, facts, unsupported, coverage, factClasses, providerByKey);
|
|
78
|
+
return {
|
|
79
|
+
schemaVersion: PROVIDER_MATRIX_SCHEMA_VERSION,
|
|
80
|
+
inputFactSchemaVersion: FACT_SCHEMA_VERSION,
|
|
81
|
+
generatedBy: "deterministic-rules",
|
|
82
|
+
repositories: collectRepositories(facts, unsupported),
|
|
83
|
+
providers,
|
|
84
|
+
additionOrder,
|
|
85
|
+
metricDefinitions: MATRIX_METRIC_DEFINITIONS,
|
|
86
|
+
coverage,
|
|
87
|
+
overlap,
|
|
88
|
+
conflicts,
|
|
89
|
+
unmatched,
|
|
90
|
+
informationGain,
|
|
91
|
+
facts: rows,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Alias for callers that use the generation vocabulary. */
|
|
95
|
+
export const generateProviderMatrix = buildProviderMatrix;
|
|
96
|
+
function collectProviders(artifact, options) {
|
|
97
|
+
const byKey = new Map();
|
|
98
|
+
const add = (provider) => {
|
|
99
|
+
byKey.set(providerKey(provider), provider);
|
|
100
|
+
};
|
|
101
|
+
for (const provider of options.providers ?? [])
|
|
102
|
+
add(provider);
|
|
103
|
+
for (const fact of artifact.facts)
|
|
104
|
+
add(fact.provider);
|
|
105
|
+
for (const evidence of artifact.unsupported)
|
|
106
|
+
add(evidence.provider);
|
|
107
|
+
for (const metric of Object.values(artifact.correlation.metrics))
|
|
108
|
+
add(metric.provider);
|
|
109
|
+
return [...byKey.entries()].map(([key, provider]) => ({ key, provider })).sort(compareProviderEntries);
|
|
110
|
+
}
|
|
111
|
+
function orderAdditions(entries, requested) {
|
|
112
|
+
if (requested === undefined)
|
|
113
|
+
return [...entries];
|
|
114
|
+
const byKey = new Map(entries.map((entry) => [entry.key, entry]));
|
|
115
|
+
const result = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
for (const provider of requested) {
|
|
118
|
+
const key = providerKey(provider);
|
|
119
|
+
const entry = byKey.get(key);
|
|
120
|
+
if (entry !== undefined && !seen.has(key)) {
|
|
121
|
+
result.push(entry);
|
|
122
|
+
seen.add(key);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!seen.has(entry.key))
|
|
127
|
+
result.push(entry);
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function toFactDescriptor(fact) {
|
|
132
|
+
const correlationStatus = factCorrelationStatus(fact);
|
|
133
|
+
const comparable = correlationStatus === "matched" || correlationStatus === "probable";
|
|
134
|
+
return {
|
|
135
|
+
fact,
|
|
136
|
+
providerKey: providerKey(fact.provider),
|
|
137
|
+
factClass: fact.predicate,
|
|
138
|
+
exactKey: factEqualityKey(fact),
|
|
139
|
+
logicalKey: comparable ? logicalFactKey(fact) : undefined,
|
|
140
|
+
correlationStatus,
|
|
141
|
+
comparable,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function toUnsupportedDescriptor(evidence) {
|
|
145
|
+
const correlationStatus = factCorrelationStatus(evidence);
|
|
146
|
+
const comparable = correlationStatus === "matched" || correlationStatus === "probable";
|
|
147
|
+
return {
|
|
148
|
+
evidence,
|
|
149
|
+
providerKey: providerKey(evidence.provider),
|
|
150
|
+
factClass: evidence.predicate,
|
|
151
|
+
logicalKey: comparable ? logicalFactKey(evidence) : undefined,
|
|
152
|
+
correlationStatus,
|
|
153
|
+
comparable,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function buildFactRows(facts, unsupported, entries) {
|
|
157
|
+
const comparableGroups = new Map();
|
|
158
|
+
for (const descriptor of facts) {
|
|
159
|
+
if (descriptor.logicalKey === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
const group = comparableGroups.get(descriptor.logicalKey) ?? [];
|
|
162
|
+
group.push(descriptor);
|
|
163
|
+
comparableGroups.set(descriptor.logicalKey, group);
|
|
164
|
+
}
|
|
165
|
+
const rows = [];
|
|
166
|
+
for (const [logicalKey, group] of comparableGroups) {
|
|
167
|
+
rows.push(buildComparableRow(logicalKey, group, entries));
|
|
168
|
+
}
|
|
169
|
+
for (const descriptor of facts.filter((item) => !item.comparable)) {
|
|
170
|
+
rows.push(buildUnresolvedFactRow(descriptor, entries));
|
|
171
|
+
}
|
|
172
|
+
const unsupportedGroups = new Map();
|
|
173
|
+
for (const descriptor of unsupported) {
|
|
174
|
+
const key = descriptor.logicalKey === undefined
|
|
175
|
+
? "unsupported:" + descriptor.providerKey + ":" + descriptor.evidence.nativeEvidence.id
|
|
176
|
+
: "unsupported:" + descriptor.logicalKey;
|
|
177
|
+
const group = unsupportedGroups.get(key) ?? [];
|
|
178
|
+
group.push(descriptor);
|
|
179
|
+
unsupportedGroups.set(key, group);
|
|
180
|
+
}
|
|
181
|
+
for (const [key, group] of unsupportedGroups) {
|
|
182
|
+
rows.push(buildUnsupportedRow(key, group, entries));
|
|
183
|
+
}
|
|
184
|
+
return rows.sort(compareMatrixRows);
|
|
185
|
+
}
|
|
186
|
+
function buildComparableRow(logicalKey, descriptors, entries) {
|
|
187
|
+
const variantsByKey = new Map();
|
|
188
|
+
for (const descriptor of descriptors) {
|
|
189
|
+
const variant = variantsByKey.get(descriptor.exactKey) ?? [];
|
|
190
|
+
variant.push(descriptor);
|
|
191
|
+
variantsByKey.set(descriptor.exactKey, variant);
|
|
192
|
+
}
|
|
193
|
+
const variants = [...variantsByKey.entries()]
|
|
194
|
+
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
195
|
+
.map(([, group]) => buildFactVariant(group));
|
|
196
|
+
const observedProviders = uniqueProviders(descriptors.map((descriptor) => descriptor.fact.provider));
|
|
197
|
+
const state = variants.length > 1 ? "conflict" : observedProviders.length > 1 ? "overlap" : "provider-only";
|
|
198
|
+
const first = sortFactDescriptors(descriptors)[0];
|
|
199
|
+
if (first === undefined)
|
|
200
|
+
throw new Error("matrix comparable group cannot be empty");
|
|
201
|
+
return makeRow({
|
|
202
|
+
key: logicalKey,
|
|
203
|
+
logicalKey,
|
|
204
|
+
repository: first.fact.repository,
|
|
205
|
+
factClass: first.factClass,
|
|
206
|
+
predicate: first.fact.predicate,
|
|
207
|
+
state,
|
|
208
|
+
comparisonPossible: true,
|
|
209
|
+
correlationStatus: descriptors.some((item) => item.correlationStatus === "probable") ? "probable" : "matched",
|
|
210
|
+
variants,
|
|
211
|
+
observedProviders,
|
|
212
|
+
entries,
|
|
213
|
+
stateForObserved: "present",
|
|
214
|
+
evidenceForObserved: descriptors,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function buildUnresolvedFactRow(descriptor, entries) {
|
|
218
|
+
const fact = descriptor.fact;
|
|
219
|
+
const state = descriptor.correlationStatus === "ambiguous" ? "ambiguous" : "unmatched";
|
|
220
|
+
return makeRow({
|
|
221
|
+
key: "unresolved:" + descriptor.providerKey + ":" + fact.factId,
|
|
222
|
+
logicalKey: "unresolved:" + factEqualityKey(fact),
|
|
223
|
+
repository: fact.repository,
|
|
224
|
+
factClass: fact.predicate,
|
|
225
|
+
predicate: fact.predicate,
|
|
226
|
+
state,
|
|
227
|
+
comparisonPossible: false,
|
|
228
|
+
correlationStatus: descriptor.correlationStatus,
|
|
229
|
+
variants: [buildFactVariant([descriptor])],
|
|
230
|
+
observedProviders: [fact.provider],
|
|
231
|
+
entries,
|
|
232
|
+
stateForObserved: state,
|
|
233
|
+
evidenceForObserved: [descriptor],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function buildUnsupportedRow(key, descriptors, entries) {
|
|
237
|
+
const ordered = sortUnsupportedDescriptors(descriptors);
|
|
238
|
+
const first = ordered[0];
|
|
239
|
+
if (first === undefined)
|
|
240
|
+
throw new Error("matrix unsupported group cannot be empty");
|
|
241
|
+
const observedProviders = uniqueProviders(ordered.map((descriptor) => descriptor.evidence.provider));
|
|
242
|
+
const variants = ordered.map((descriptor) => buildUnsupportedVariant(descriptor));
|
|
243
|
+
return makeRow({
|
|
244
|
+
key,
|
|
245
|
+
logicalKey: key,
|
|
246
|
+
repository: first.evidence.repository,
|
|
247
|
+
factClass: first.factClass,
|
|
248
|
+
predicate: first.evidence.predicate,
|
|
249
|
+
state: "unsupported",
|
|
250
|
+
comparisonPossible: false,
|
|
251
|
+
correlationStatus: first.comparable ? "not-applicable" : first.correlationStatus,
|
|
252
|
+
variants,
|
|
253
|
+
observedProviders,
|
|
254
|
+
entries,
|
|
255
|
+
stateForObserved: "unsupported",
|
|
256
|
+
evidenceForObserved: ordered,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function makeRow(input) {
|
|
260
|
+
const observedKeys = new Set(input.observedProviders.map(providerKey));
|
|
261
|
+
const evidenceByProvider = new Map();
|
|
262
|
+
for (const item of input.evidenceForObserved) {
|
|
263
|
+
const provider = "fact" in item ? item.fact.provider : item.evidence.provider;
|
|
264
|
+
const key = providerKey(provider);
|
|
265
|
+
const value = evidenceByProvider.get(key) ?? { factIds: [], evidenceIds: [] };
|
|
266
|
+
if ("fact" in item) {
|
|
267
|
+
value.factIds.push(item.fact.factId);
|
|
268
|
+
value.evidenceIds.push(item.fact.nativeEvidence.id);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
value.evidenceIds.push(item.evidence.nativeEvidence.id);
|
|
272
|
+
}
|
|
273
|
+
evidenceByProvider.set(key, value);
|
|
274
|
+
}
|
|
275
|
+
const providerStates = input.entries.map((entry) => {
|
|
276
|
+
const evidence = evidenceByProvider.get(entry.key);
|
|
277
|
+
return {
|
|
278
|
+
provider: entry.provider,
|
|
279
|
+
state: observedKeys.has(entry.key) ? input.stateForObserved : "missing",
|
|
280
|
+
factIds: [...new Set(evidence?.factIds ?? [])].sort(),
|
|
281
|
+
evidenceIds: [...new Set(evidence?.evidenceIds ?? [])].sort(),
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
key: input.key,
|
|
286
|
+
logicalKey: input.logicalKey,
|
|
287
|
+
repository: input.repository,
|
|
288
|
+
factClass: input.factClass,
|
|
289
|
+
predicate: input.predicate,
|
|
290
|
+
state: input.state,
|
|
291
|
+
comparisonPossible: input.comparisonPossible,
|
|
292
|
+
correlationStatus: input.correlationStatus,
|
|
293
|
+
variants: [...input.variants].sort(compareVariants),
|
|
294
|
+
providerStates,
|
|
295
|
+
observedProviders: [...input.observedProviders].sort(compareProviders),
|
|
296
|
+
missingProviders: input.entries.filter((entry) => !observedKeys.has(entry.key)).map((entry) => entry.provider),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function buildFactVariant(descriptors) {
|
|
300
|
+
const ordered = sortFactDescriptors(descriptors);
|
|
301
|
+
const first = ordered[0];
|
|
302
|
+
if (first === undefined)
|
|
303
|
+
throw new Error("matrix fact variant cannot be empty");
|
|
304
|
+
return {
|
|
305
|
+
key: first.exactKey,
|
|
306
|
+
factIds: [...new Set(ordered.map((descriptor) => descriptor.fact.factId))].sort(),
|
|
307
|
+
evidenceIds: [...new Set(ordered.map((descriptor) => descriptor.fact.nativeEvidence.id))].sort(),
|
|
308
|
+
providers: uniqueProviders(ordered.map((descriptor) => descriptor.fact.provider)),
|
|
309
|
+
subject: toMatrixEntity(first.fact.subject),
|
|
310
|
+
predicate: first.fact.predicate,
|
|
311
|
+
object: toMatrixObject(first.fact.object),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function buildUnsupportedVariant(descriptor) {
|
|
315
|
+
const evidence = descriptor.evidence;
|
|
316
|
+
return {
|
|
317
|
+
key: "unsupported:" + evidence.nativeEvidence.id,
|
|
318
|
+
factIds: [],
|
|
319
|
+
evidenceIds: [evidence.nativeEvidence.id],
|
|
320
|
+
providers: [evidence.provider],
|
|
321
|
+
subject: toMatrixEntity(evidence.subject),
|
|
322
|
+
predicate: evidence.predicate,
|
|
323
|
+
object: toMatrixObject(evidence.object),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function toMatrixEntity(entity) {
|
|
327
|
+
return {
|
|
328
|
+
provider: entity.provider,
|
|
329
|
+
nativeId: entity.nativeId,
|
|
330
|
+
kind: entity.kind,
|
|
331
|
+
...(entity.canonicalId === undefined ? {} : { canonicalId: entity.canonicalId }),
|
|
332
|
+
candidateCanonicalIds: [...entity.candidateCanonicalIds].sort(),
|
|
333
|
+
correlationStatus: entity.correlationStatus,
|
|
334
|
+
...(entity.path === undefined ? {} : { path: entity.path }),
|
|
335
|
+
...(entity.range === undefined ? {} : { range: entity.range }),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function toMatrixObject(object) {
|
|
339
|
+
if ("nativeId" in object)
|
|
340
|
+
return { kind: "entity", entity: toMatrixEntity(object) };
|
|
341
|
+
return { kind: "value", value: object.value };
|
|
342
|
+
}
|
|
343
|
+
function buildProviderCoverage(provider, facts, unsupported, factClasses, uniqueLogicalProviders, correlation) {
|
|
344
|
+
const providerKeyValue = providerKey(provider);
|
|
345
|
+
const providerFacts = facts.filter((descriptor) => descriptor.providerKey === providerKeyValue);
|
|
346
|
+
const providerUnsupported = unsupported.filter((descriptor) => descriptor.providerKey === providerKeyValue);
|
|
347
|
+
const factCorrelation = countFactCorrelation(providerFacts);
|
|
348
|
+
const rawObservedFactCount = providerFacts.length + providerUnsupported.length;
|
|
349
|
+
const uniqueFactKeys = uniqueKeysForProvider(providerFacts, uniqueLogicalProviders);
|
|
350
|
+
const factsByClass = groupByFactClass(providerFacts);
|
|
351
|
+
const unsupportedByClass = groupByFactClass(providerUnsupported);
|
|
352
|
+
const factClassCoverage = factClasses.map((factClass) => {
|
|
353
|
+
const classFacts = factsByClass.get(factClass) ?? [];
|
|
354
|
+
const classUnsupported = unsupportedByClass.get(factClass) ?? [];
|
|
355
|
+
const classCorrelation = countFactCorrelation(classFacts);
|
|
356
|
+
const classUniqueFactKeys = uniqueKeysForProvider(classFacts, uniqueLogicalProviders);
|
|
357
|
+
return {
|
|
358
|
+
factClass,
|
|
359
|
+
rawObservedFactCount: classFacts.length + classUnsupported.length,
|
|
360
|
+
normalizedFactCount: classFacts.length,
|
|
361
|
+
unsupportedFactCount: classUnsupported.length,
|
|
362
|
+
factCorrelation: classCorrelation,
|
|
363
|
+
normalizationCoverage: ratio(classFacts.length, classFacts.length + classUnsupported.length),
|
|
364
|
+
uniqueFactCount: classUniqueFactKeys.length,
|
|
365
|
+
uniqueCoverage: ratio(classUniqueFactKeys.length, comparableLogicalFactCount(classFacts)),
|
|
366
|
+
uniqueFactKeys: classUniqueFactKeys,
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
const entity = findCorrelationMetric(provider, correlation);
|
|
370
|
+
const entityCorrelation = toEntityCorrelationCounts(entity);
|
|
371
|
+
return {
|
|
372
|
+
provider,
|
|
373
|
+
factClass: "__all__",
|
|
374
|
+
rawObservedFactCount,
|
|
375
|
+
normalizedFactCount: providerFacts.length,
|
|
376
|
+
unsupportedFactCount: providerUnsupported.length,
|
|
377
|
+
factCorrelation,
|
|
378
|
+
normalizationCoverage: ratio(providerFacts.length, rawObservedFactCount),
|
|
379
|
+
uniqueFactCount: uniqueFactKeys.length,
|
|
380
|
+
uniqueCoverage: ratio(uniqueFactKeys.length, comparableLogicalFactCount(providerFacts)),
|
|
381
|
+
uniqueFactKeys,
|
|
382
|
+
factClasses: factClassCoverage,
|
|
383
|
+
entityCorrelation,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function groupByFactClass(descriptors) {
|
|
387
|
+
const groups = new Map();
|
|
388
|
+
for (const descriptor of descriptors) {
|
|
389
|
+
const group = groups.get(descriptor.factClass) ?? [];
|
|
390
|
+
group.push(descriptor);
|
|
391
|
+
groups.set(descriptor.factClass, group);
|
|
392
|
+
}
|
|
393
|
+
return groups;
|
|
394
|
+
}
|
|
395
|
+
function countFactCorrelation(facts) {
|
|
396
|
+
const counts = { matched: 0, probable: 0, ambiguous: 0, unmatched: 0 };
|
|
397
|
+
for (const descriptor of facts)
|
|
398
|
+
counts[descriptor.correlationStatus] += 1;
|
|
399
|
+
const correlated = counts.matched + counts.probable;
|
|
400
|
+
return {
|
|
401
|
+
...counts,
|
|
402
|
+
correlated,
|
|
403
|
+
matchedCoverage: ratio(counts.matched, facts.length),
|
|
404
|
+
correlatedCoverage: ratio(correlated, facts.length),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function uniqueKeysForProvider(facts, uniqueLogicalProviders) {
|
|
408
|
+
if (facts.length === 0)
|
|
409
|
+
return [];
|
|
410
|
+
return [
|
|
411
|
+
...new Set(facts
|
|
412
|
+
.filter((descriptor) => descriptor.comparable && descriptor.logicalKey !== undefined)
|
|
413
|
+
.filter((descriptor) => uniqueLogicalProviders.get(descriptor.logicalKey ?? "")?.size === 1)
|
|
414
|
+
.map((descriptor) => descriptor.logicalKey)),
|
|
415
|
+
].sort();
|
|
416
|
+
}
|
|
417
|
+
function comparableLogicalFactCount(facts) {
|
|
418
|
+
return new Set(facts
|
|
419
|
+
.filter((descriptor) => descriptor.comparable && descriptor.logicalKey !== undefined)
|
|
420
|
+
.map((descriptor) => descriptor.logicalKey)).size;
|
|
421
|
+
}
|
|
422
|
+
function toEntityCorrelationCounts(metric) {
|
|
423
|
+
const total = metric?.total ?? 0;
|
|
424
|
+
const matched = metric?.matched ?? 0;
|
|
425
|
+
const probable = metric?.probable ?? 0;
|
|
426
|
+
const ambiguous = metric?.ambiguous ?? 0;
|
|
427
|
+
const unmatched = metric?.unmatched ?? 0;
|
|
428
|
+
return {
|
|
429
|
+
total,
|
|
430
|
+
matched,
|
|
431
|
+
probable,
|
|
432
|
+
correlated: matched + probable,
|
|
433
|
+
ambiguous,
|
|
434
|
+
unmatched,
|
|
435
|
+
matchedCoverage: ratio(matched, total),
|
|
436
|
+
correlatedCoverage: ratio(matched + probable, total),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function findCorrelationMetric(provider, correlation) {
|
|
440
|
+
const expected = providerKey(provider);
|
|
441
|
+
return Object.values(correlation.metrics).find((metric) => providerKey(metric.provider) === expected);
|
|
442
|
+
}
|
|
443
|
+
function buildOverlap(providers, entries, facts, factClasses, rows) {
|
|
444
|
+
const index = indexComparableFactSets(facts, entries, factClasses);
|
|
445
|
+
const pairwise = [];
|
|
446
|
+
for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) {
|
|
447
|
+
for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) {
|
|
448
|
+
const left = entries[leftIndex];
|
|
449
|
+
const right = entries[rightIndex];
|
|
450
|
+
for (const factClass of factClasses) {
|
|
451
|
+
pairwise.push(buildPairwiseOverlap(left, right, factClass, index));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const allProviders = factClasses.map((factClass) => buildAllProviderOverlap(providers, entries, factClass, index));
|
|
456
|
+
return {
|
|
457
|
+
pairwise: pairwise.sort(comparePairwiseOverlap),
|
|
458
|
+
allProviders: allProviders.sort((left, right) => compareCodeUnits(left.factClass, right.factClass)),
|
|
459
|
+
facts: rows.filter((row) => row.state === "overlap"),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Groups comparable facts by provider and fact class once so pairwise and
|
|
464
|
+
* all-provider overlap can look up a set instead of re-scanning every fact
|
|
465
|
+
* descriptor for every provider pair. Without this index, overlap
|
|
466
|
+
* construction is O(providers^2 x factClasses x facts), which took minutes
|
|
467
|
+
* and pegged a CPU core against the real TypeScript provider's fact volume.
|
|
468
|
+
*/
|
|
469
|
+
function indexComparableFactSets(facts, entries, factClasses) {
|
|
470
|
+
const index = new Map();
|
|
471
|
+
for (const entry of entries) {
|
|
472
|
+
for (const factClass of factClasses) {
|
|
473
|
+
index.set(indexKey(entry.key, factClass), { byLogical: new Map() });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
for (const descriptor of facts) {
|
|
477
|
+
if (!descriptor.comparable || descriptor.logicalKey === undefined)
|
|
478
|
+
continue;
|
|
479
|
+
const key = indexKey(descriptor.providerKey, descriptor.factClass);
|
|
480
|
+
const entry = index.get(key);
|
|
481
|
+
if (entry === undefined)
|
|
482
|
+
continue;
|
|
483
|
+
const values = entry.byLogical.get(descriptor.logicalKey) ?? new Set();
|
|
484
|
+
values.add(descriptor.exactKey);
|
|
485
|
+
entry.byLogical.set(descriptor.logicalKey, values);
|
|
486
|
+
}
|
|
487
|
+
return index;
|
|
488
|
+
}
|
|
489
|
+
/** Collision-free even when providerKeyValue or factClass contain the delimiter. */
|
|
490
|
+
function indexKey(providerKeyValue, factClass) {
|
|
491
|
+
return JSON.stringify([providerKeyValue, factClass]);
|
|
492
|
+
}
|
|
493
|
+
function buildPairwiseOverlap(left, right, factClass, index) {
|
|
494
|
+
const leftSets = index.get(indexKey(left.key, factClass)) ?? { byLogical: new Map() };
|
|
495
|
+
const rightSets = index.get(indexKey(right.key, factClass)) ?? { byLogical: new Map() };
|
|
496
|
+
const logicalKeys = new Set([...leftSets.byLogical.keys(), ...rightSets.byLogical.keys()]);
|
|
497
|
+
const overlapFactKeys = intersection(exactKeys(leftSets.byLogical), exactKeys(rightSets.byLogical));
|
|
498
|
+
const leftOnly = [...leftSets.byLogical.keys()].filter((key) => !rightSets.byLogical.has(key)).sort();
|
|
499
|
+
const rightOnly = [...rightSets.byLogical.keys()].filter((key) => !leftSets.byLogical.has(key)).sort();
|
|
500
|
+
const conflictLogicalKeys = [...leftSets.byLogical.keys()]
|
|
501
|
+
.filter((key) => rightSets.byLogical.has(key))
|
|
502
|
+
.filter((key) => !setsEqual(leftSets.byLogical.get(key) ?? new Set(), rightSets.byLogical.get(key) ?? new Set()))
|
|
503
|
+
.sort();
|
|
504
|
+
const leftCount = exactKeys(leftSets.byLogical).length;
|
|
505
|
+
const rightCount = exactKeys(rightSets.byLogical).length;
|
|
506
|
+
return {
|
|
507
|
+
left: left.provider,
|
|
508
|
+
right: right.provider,
|
|
509
|
+
factClass,
|
|
510
|
+
leftComparableFactCount: leftCount,
|
|
511
|
+
rightComparableFactCount: rightCount,
|
|
512
|
+
comparedLogicalFactCount: [...logicalKeys].filter((key) => leftSets.byLogical.has(key) && rightSets.byLogical.has(key)).length,
|
|
513
|
+
overlapFactCount: overlapFactKeys.length,
|
|
514
|
+
leftOnlyFactCount: leftOnly.length,
|
|
515
|
+
rightOnlyFactCount: rightOnly.length,
|
|
516
|
+
conflictFactCount: conflictLogicalKeys.length,
|
|
517
|
+
overlapCoverage: ratio(overlapFactKeys.length, Math.min(leftCount, rightCount)),
|
|
518
|
+
overlapFactKeys,
|
|
519
|
+
conflictLogicalKeys,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function buildAllProviderOverlap(providers, entries, factClass, index) {
|
|
523
|
+
const exactSets = entries.map((entry) => index.get(indexKey(entry.key, factClass)) ?? { byLogical: new Map() });
|
|
524
|
+
const exactKeySets = exactSets.map((set) => new Set(exactKeys(set.byLogical)));
|
|
525
|
+
const union = new Set();
|
|
526
|
+
for (const keys of exactKeySets)
|
|
527
|
+
for (const key of keys)
|
|
528
|
+
union.add(key);
|
|
529
|
+
const overlap = entries.length === 0 ? [] : [...union].filter((key) => exactKeySets.every((keys) => keys.has(key))).sort();
|
|
530
|
+
return {
|
|
531
|
+
factClass,
|
|
532
|
+
providers,
|
|
533
|
+
providerCount: providers.length,
|
|
534
|
+
comparableFactUnionCount: union.size,
|
|
535
|
+
allProviderOverlapFactCount: overlap.length,
|
|
536
|
+
allProviderOverlapCoverage: ratio(overlap.length, union.size),
|
|
537
|
+
overlapFactKeys: overlap,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function buildInformationGain(additionEntries, facts, unsupported, coverage, factClasses, providerByKey) {
|
|
541
|
+
const baseline = new Set();
|
|
542
|
+
const result = [];
|
|
543
|
+
for (const entry of additionEntries) {
|
|
544
|
+
const providerFacts = facts.filter((descriptor) => descriptor.providerKey === entry.key && descriptor.comparable);
|
|
545
|
+
const exactKeysForProvider = [...new Set(providerFacts.map((descriptor) => descriptor.exactKey))].sort();
|
|
546
|
+
const newComparableFactKeys = exactKeysForProvider.filter((key) => !baseline.has(key));
|
|
547
|
+
const newComparableFactKeySet = new Set(newComparableFactKeys);
|
|
548
|
+
const keysByClass = new Map();
|
|
549
|
+
for (const descriptor of providerFacts) {
|
|
550
|
+
const values = keysByClass.get(descriptor.factClass) ?? new Set();
|
|
551
|
+
values.add(descriptor.exactKey);
|
|
552
|
+
keysByClass.set(descriptor.factClass, values);
|
|
553
|
+
}
|
|
554
|
+
const classCounts = factClasses
|
|
555
|
+
.map((factClass) => {
|
|
556
|
+
const classKeys = keysByClass.get(factClass) ?? new Set();
|
|
557
|
+
const newKeys = [...classKeys].filter((key) => newComparableFactKeySet.has(key)).sort();
|
|
558
|
+
return { factClass, newComparableFactCount: newKeys.length, newComparableFactKeys: newKeys };
|
|
559
|
+
})
|
|
560
|
+
.filter((item) => item.newComparableFactCount > 0);
|
|
561
|
+
for (const key of exactKeysForProvider)
|
|
562
|
+
baseline.add(key);
|
|
563
|
+
const providerCoverage = coverage.find((item) => providerKey(item.provider) === entry.key);
|
|
564
|
+
const providerUnsupported = unsupported.filter((item) => item.providerKey === entry.key);
|
|
565
|
+
const providerAmbiguous = facts.filter((item) => item.providerKey === entry.key && item.correlationStatus === "ambiguous");
|
|
566
|
+
const providerUnmatched = facts.filter((item) => item.providerKey === entry.key && item.correlationStatus === "unmatched");
|
|
567
|
+
const entryIndex = additionEntries.findIndex((item) => item.key === entry.key);
|
|
568
|
+
const existingProviders = additionEntries.slice(0, entryIndex).map((item) => item.provider);
|
|
569
|
+
const normalizedProvider = providerByKey.get(entry.key) ?? entry.provider;
|
|
570
|
+
result.push({
|
|
571
|
+
provider: normalizedProvider,
|
|
572
|
+
existingProviders,
|
|
573
|
+
newComparableFactCount: newComparableFactKeys.length,
|
|
574
|
+
newComparableFactKeys,
|
|
575
|
+
newFactClasses: classCounts,
|
|
576
|
+
uniqueFactCount: providerCoverage?.uniqueFactCount ?? 0,
|
|
577
|
+
uniqueFactKeys: providerCoverage?.uniqueFactKeys ?? [],
|
|
578
|
+
informationGainCoverage: ratio(newComparableFactKeys.length, new Set(providerFacts.map((descriptor) => descriptor.exactKey)).size),
|
|
579
|
+
newUnsupportedFactCount: providerUnsupported.length,
|
|
580
|
+
newUnsupportedEvidenceIds: providerUnsupported.map((item) => item.evidence.nativeEvidence.id).sort(),
|
|
581
|
+
newAmbiguousFactCount: providerAmbiguous.length,
|
|
582
|
+
newAmbiguousFactIds: providerAmbiguous.map((item) => item.fact.factId).sort(),
|
|
583
|
+
newUnmatchedFactCount: providerUnmatched.length,
|
|
584
|
+
newUnmatchedFactIds: providerUnmatched.map((item) => item.fact.factId).sort(),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
function factCorrelationStatus(value) {
|
|
590
|
+
const entities = [value.subject, ...("nativeId" in value.object ? [value.object] : [])];
|
|
591
|
+
if (entities.some((entity) => entity.correlationStatus === "ambiguous"))
|
|
592
|
+
return "ambiguous";
|
|
593
|
+
if (entities.some((entity) => entity.correlationStatus === "unmatched" || entity.canonicalId === undefined)) {
|
|
594
|
+
return "unmatched";
|
|
595
|
+
}
|
|
596
|
+
if (entities.every((entity) => entity.correlationStatus === "matched"))
|
|
597
|
+
return "matched";
|
|
598
|
+
return "probable";
|
|
599
|
+
}
|
|
600
|
+
function logicalFactKey(value) {
|
|
601
|
+
const subject = value.subject.canonicalId;
|
|
602
|
+
if (subject === undefined)
|
|
603
|
+
throw new Error("logical fact key requires a canonical subject");
|
|
604
|
+
return stableSerialize({ repository: value.repository, subject, predicate: value.predicate });
|
|
605
|
+
}
|
|
606
|
+
function indexLogicalProviders(facts) {
|
|
607
|
+
const result = new Map();
|
|
608
|
+
for (const descriptor of facts) {
|
|
609
|
+
if (!descriptor.comparable || descriptor.logicalKey === undefined)
|
|
610
|
+
continue;
|
|
611
|
+
const providers = result.get(descriptor.logicalKey) ?? new Set();
|
|
612
|
+
providers.add(descriptor.providerKey);
|
|
613
|
+
result.set(descriptor.logicalKey, providers);
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
function collectFactClasses(facts, unsupported) {
|
|
618
|
+
return [...new Set([...facts.map((fact) => fact.factClass), ...unsupported.map((item) => item.factClass)])].sort();
|
|
619
|
+
}
|
|
620
|
+
function collectRepositories(facts, unsupported) {
|
|
621
|
+
const values = [...facts.map((item) => item.fact.repository), ...unsupported.map((item) => item.evidence.repository)];
|
|
622
|
+
const byKey = new Map();
|
|
623
|
+
for (const repository of values)
|
|
624
|
+
byKey.set(stableSerialize(repository), repository);
|
|
625
|
+
return [...byKey.entries()].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, value]) => value);
|
|
626
|
+
}
|
|
627
|
+
function ratio(numerator, denominator) {
|
|
628
|
+
return { numerator, denominator, value: denominator === 0 ? null : numerator / denominator };
|
|
629
|
+
}
|
|
630
|
+
function providerKey(provider) {
|
|
631
|
+
return provider.id + "\u0000" + provider.version + "\u0000" + provider.determinism;
|
|
632
|
+
}
|
|
633
|
+
function compareProviderEntries(left, right) {
|
|
634
|
+
return compareCodeUnits(left.key, right.key);
|
|
635
|
+
}
|
|
636
|
+
function compareProviders(left, right) {
|
|
637
|
+
return compareCodeUnits(providerKey(left), providerKey(right));
|
|
638
|
+
}
|
|
639
|
+
function compareFactDescriptors(left, right) {
|
|
640
|
+
const key = compareCodeUnits(left.exactKey, right.exactKey);
|
|
641
|
+
if (key !== 0)
|
|
642
|
+
return key;
|
|
643
|
+
const provider = compareCodeUnits(left.providerKey, right.providerKey);
|
|
644
|
+
if (provider !== 0)
|
|
645
|
+
return provider;
|
|
646
|
+
return compareCodeUnits(left.fact.factId, right.fact.factId);
|
|
647
|
+
}
|
|
648
|
+
function sortFactDescriptors(values) {
|
|
649
|
+
return [...values].sort(compareFactDescriptors);
|
|
650
|
+
}
|
|
651
|
+
function compareUnsupportedDescriptors(left, right) {
|
|
652
|
+
const leftKey = stableSerialize({
|
|
653
|
+
predicate: left.factClass,
|
|
654
|
+
provider: left.providerKey,
|
|
655
|
+
evidence: left.evidence.nativeEvidence.id,
|
|
656
|
+
});
|
|
657
|
+
const rightKey = stableSerialize({
|
|
658
|
+
predicate: right.factClass,
|
|
659
|
+
provider: right.providerKey,
|
|
660
|
+
evidence: right.evidence.nativeEvidence.id,
|
|
661
|
+
});
|
|
662
|
+
return compareCodeUnits(leftKey, rightKey);
|
|
663
|
+
}
|
|
664
|
+
function sortUnsupportedDescriptors(values) {
|
|
665
|
+
return [...values].sort(compareUnsupportedDescriptors);
|
|
666
|
+
}
|
|
667
|
+
function compareMatrixRows(left, right) {
|
|
668
|
+
const repository = compareCodeUnits(stableSerialize(left.repository), stableSerialize(right.repository));
|
|
669
|
+
if (repository !== 0)
|
|
670
|
+
return repository;
|
|
671
|
+
const factClass = compareCodeUnits(left.factClass, right.factClass);
|
|
672
|
+
if (factClass !== 0)
|
|
673
|
+
return factClass;
|
|
674
|
+
return compareCodeUnits(left.key, right.key);
|
|
675
|
+
}
|
|
676
|
+
function compareVariants(left, right) {
|
|
677
|
+
return compareCodeUnits(left.key, right.key);
|
|
678
|
+
}
|
|
679
|
+
function comparePairwiseOverlap(left, right) {
|
|
680
|
+
const provider = compareCodeUnits(providerKey(left.left), providerKey(right.left));
|
|
681
|
+
if (provider !== 0)
|
|
682
|
+
return provider;
|
|
683
|
+
const rightProvider = compareCodeUnits(providerKey(left.right), providerKey(right.right));
|
|
684
|
+
if (rightProvider !== 0)
|
|
685
|
+
return rightProvider;
|
|
686
|
+
return compareCodeUnits(left.factClass, right.factClass);
|
|
687
|
+
}
|
|
688
|
+
function uniqueProviders(values) {
|
|
689
|
+
const byKey = new Map();
|
|
690
|
+
for (const provider of values)
|
|
691
|
+
byKey.set(providerKey(provider), provider);
|
|
692
|
+
return [...byKey.values()].sort(compareProviders);
|
|
693
|
+
}
|
|
694
|
+
function exactKeys(byLogical) {
|
|
695
|
+
return [...new Set([...byLogical.values()].flatMap((values) => [...values]))].sort();
|
|
696
|
+
}
|
|
697
|
+
function intersection(left, right) {
|
|
698
|
+
const rightSet = new Set(right);
|
|
699
|
+
return [...new Set(left)].filter((value) => rightSet.has(value)).sort();
|
|
700
|
+
}
|
|
701
|
+
function setsEqual(left, right) {
|
|
702
|
+
if (left.size !== right.size)
|
|
703
|
+
return false;
|
|
704
|
+
for (const value of left)
|
|
705
|
+
if (!right.has(value))
|
|
706
|
+
return false;
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Deterministic ordering independent of host locale/ICU data. Artifact
|
|
711
|
+
* ordering must reproduce identically across machines.
|
|
712
|
+
*/
|
|
713
|
+
function compareCodeUnits(left, right) {
|
|
714
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
715
|
+
}
|
|
716
|
+
function stableSerialize(value) {
|
|
717
|
+
if (value === null)
|
|
718
|
+
return "null";
|
|
719
|
+
if (value === undefined)
|
|
720
|
+
return "undefined";
|
|
721
|
+
if (typeof value === "string")
|
|
722
|
+
return JSON.stringify(value);
|
|
723
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
724
|
+
return JSON.stringify(value);
|
|
725
|
+
if (typeof value === "bigint")
|
|
726
|
+
return value.toString() + "n";
|
|
727
|
+
if (Array.isArray(value))
|
|
728
|
+
return "[" + value.map((item) => stableSerialize(item)).join(",") + "]";
|
|
729
|
+
if (typeof value === "object") {
|
|
730
|
+
const record = value;
|
|
731
|
+
return ("{" +
|
|
732
|
+
Object.keys(record)
|
|
733
|
+
.sort()
|
|
734
|
+
.map((key) => JSON.stringify(key) + ":" + stableSerialize(record[key]))
|
|
735
|
+
.join(",") +
|
|
736
|
+
"}");
|
|
737
|
+
}
|
|
738
|
+
return JSON.stringify(String(value));
|
|
739
|
+
}
|
|
740
|
+
/** Renders the matrix without consulting a model or changing any matrix state. */
|
|
741
|
+
export function renderProviderMatrixReport(matrix) {
|
|
742
|
+
const codeMark = String.fromCharCode(96);
|
|
743
|
+
const lines = [
|
|
744
|
+
"# Provider Matrix",
|
|
745
|
+
"",
|
|
746
|
+
"Generated by deterministic rules from persisted normalized fact evidence.",
|
|
747
|
+
"",
|
|
748
|
+
"## Revision",
|
|
749
|
+
"",
|
|
750
|
+
matrix.repositories.length === 0
|
|
751
|
+
? "No repository revision was present in the supplied fact artifact."
|
|
752
|
+
: matrix.repositories
|
|
753
|
+
.map((repository) => "- " + repository.source + " @ " + codeMark + repository.commitSha + codeMark)
|
|
754
|
+
.join("\n"),
|
|
755
|
+
"",
|
|
756
|
+
"## Metric semantics",
|
|
757
|
+
"",
|
|
758
|
+
"| Metric | Numerator | Denominator | Meaning |",
|
|
759
|
+
"| --- | --- | --- | --- |",
|
|
760
|
+
...matrix.metricDefinitions.map((definition) => "| " +
|
|
761
|
+
escapeMarkdown(definition.id) +
|
|
762
|
+
" | " +
|
|
763
|
+
escapeMarkdown(definition.numerator) +
|
|
764
|
+
" | " +
|
|
765
|
+
escapeMarkdown(definition.denominator) +
|
|
766
|
+
" | " +
|
|
767
|
+
escapeMarkdown(definition.meaning) +
|
|
768
|
+
" |"),
|
|
769
|
+
"",
|
|
770
|
+
"## Provider coverage",
|
|
771
|
+
"",
|
|
772
|
+
"| Provider | Raw observed | Normalized | Unsupported | Normalization | Correlated | Ambiguous | Unmatched |",
|
|
773
|
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
774
|
+
...matrix.coverage.map((coverage) => "| " +
|
|
775
|
+
escapeMarkdown(providerLabel(coverage.provider)) +
|
|
776
|
+
" | " +
|
|
777
|
+
coverage.rawObservedFactCount +
|
|
778
|
+
" | " +
|
|
779
|
+
coverage.normalizedFactCount +
|
|
780
|
+
" | " +
|
|
781
|
+
coverage.unsupportedFactCount +
|
|
782
|
+
" | " +
|
|
783
|
+
formatRatio(coverage.normalizationCoverage) +
|
|
784
|
+
" | " +
|
|
785
|
+
coverage.factCorrelation.correlated +
|
|
786
|
+
" (" +
|
|
787
|
+
formatRatio(coverage.factCorrelation.correlatedCoverage) +
|
|
788
|
+
") | " +
|
|
789
|
+
coverage.factCorrelation.ambiguous +
|
|
790
|
+
" | " +
|
|
791
|
+
coverage.factCorrelation.unmatched +
|
|
792
|
+
" |"),
|
|
793
|
+
"",
|
|
794
|
+
"### Coverage by normalized fact class",
|
|
795
|
+
"",
|
|
796
|
+
"| Provider | Fact class | Raw | Normalized | Unsupported | Correlated | Unique logical facts | Unique coverage |",
|
|
797
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
798
|
+
...matrix.coverage.flatMap((coverage) => coverage.factClasses.map((item) => "| " +
|
|
799
|
+
escapeMarkdown(providerLabel(coverage.provider)) +
|
|
800
|
+
" | " +
|
|
801
|
+
codeMark +
|
|
802
|
+
escapeMarkdown(item.factClass) +
|
|
803
|
+
codeMark +
|
|
804
|
+
" | " +
|
|
805
|
+
item.rawObservedFactCount +
|
|
806
|
+
" | " +
|
|
807
|
+
item.normalizedFactCount +
|
|
808
|
+
" | " +
|
|
809
|
+
item.unsupportedFactCount +
|
|
810
|
+
" | " +
|
|
811
|
+
item.factCorrelation.correlated +
|
|
812
|
+
" (" +
|
|
813
|
+
formatRatio(item.factCorrelation.correlatedCoverage) +
|
|
814
|
+
") | " +
|
|
815
|
+
item.uniqueFactCount +
|
|
816
|
+
" | " +
|
|
817
|
+
formatRatio(item.uniqueCoverage) +
|
|
818
|
+
" |")),
|
|
819
|
+
"",
|
|
820
|
+
"## Unique information and incremental gain",
|
|
821
|
+
"",
|
|
822
|
+
"Unique means a comparable logical fact key observed by exactly one provider; it is not a quality ranking.",
|
|
823
|
+
"",
|
|
824
|
+
"| Added provider | Existing set | New comparable facts | Gain coverage | Unique comparable facts | Ambiguous | Unmatched | Unsupported |",
|
|
825
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
826
|
+
...matrix.informationGain.map((gain) => "| " +
|
|
827
|
+
escapeMarkdown(providerLabel(gain.provider)) +
|
|
828
|
+
" | " +
|
|
829
|
+
(gain.existingProviders.length === 0
|
|
830
|
+
? "∅"
|
|
831
|
+
: gain.existingProviders.map(providerLabel).map(escapeMarkdown).join(", ")) +
|
|
832
|
+
" | " +
|
|
833
|
+
gain.newComparableFactCount +
|
|
834
|
+
" | " +
|
|
835
|
+
formatRatio(gain.informationGainCoverage) +
|
|
836
|
+
" | " +
|
|
837
|
+
gain.uniqueFactCount +
|
|
838
|
+
" | " +
|
|
839
|
+
gain.newAmbiguousFactCount +
|
|
840
|
+
" | " +
|
|
841
|
+
gain.newUnmatchedFactCount +
|
|
842
|
+
" | " +
|
|
843
|
+
gain.newUnsupportedFactCount +
|
|
844
|
+
" |"),
|
|
845
|
+
"",
|
|
846
|
+
"## Overlap and conflicts",
|
|
847
|
+
"",
|
|
848
|
+
...renderOverlapSummary(matrix),
|
|
849
|
+
"",
|
|
850
|
+
"## Incomparable evidence",
|
|
851
|
+
"",
|
|
852
|
+
"- Ambiguous correlation rows: " + matrix.unmatched.ambiguous.length,
|
|
853
|
+
"- Unmatched correlation rows: " + matrix.unmatched.unmatched.length,
|
|
854
|
+
"- Unsupported predicate rows: " + matrix.unmatched.unsupported.length,
|
|
855
|
+
"",
|
|
856
|
+
];
|
|
857
|
+
return lines.join("\n") + "\n";
|
|
858
|
+
}
|
|
859
|
+
function renderOverlapSummary(matrix) {
|
|
860
|
+
const codeMark = String.fromCharCode(96);
|
|
861
|
+
const lines = [
|
|
862
|
+
"Exact overlap rows: " + matrix.overlap.facts.length,
|
|
863
|
+
"Conflict rows: " + matrix.conflicts.length,
|
|
864
|
+
"",
|
|
865
|
+
"| Fact class | Provider pair | Exact overlap | Conflicts | Left only | Right only |",
|
|
866
|
+
"| --- | --- | ---: | ---: | ---: | ---: |",
|
|
867
|
+
];
|
|
868
|
+
for (const pair of matrix.overlap.pairwise) {
|
|
869
|
+
lines.push("| " +
|
|
870
|
+
codeMark +
|
|
871
|
+
escapeMarkdown(pair.factClass) +
|
|
872
|
+
codeMark +
|
|
873
|
+
" | " +
|
|
874
|
+
escapeMarkdown(providerLabel(pair.left) + " / " + providerLabel(pair.right)) +
|
|
875
|
+
" | " +
|
|
876
|
+
pair.overlapFactCount +
|
|
877
|
+
" | " +
|
|
878
|
+
pair.conflictFactCount +
|
|
879
|
+
" | " +
|
|
880
|
+
pair.leftOnlyFactCount +
|
|
881
|
+
" | " +
|
|
882
|
+
pair.rightOnlyFactCount +
|
|
883
|
+
" |");
|
|
884
|
+
}
|
|
885
|
+
lines.push("", "### Unique comparable evidence", "");
|
|
886
|
+
const uniqueCoverage = matrix.coverage.flatMap((coverage) => coverage.factClasses
|
|
887
|
+
.filter((item) => item.uniqueFactCount > 0)
|
|
888
|
+
.map((item) => "- " +
|
|
889
|
+
escapeMarkdown(providerLabel(coverage.provider)) +
|
|
890
|
+
" / " +
|
|
891
|
+
codeMark +
|
|
892
|
+
escapeMarkdown(item.factClass) +
|
|
893
|
+
codeMark +
|
|
894
|
+
": " +
|
|
895
|
+
item.uniqueFactCount +
|
|
896
|
+
" logical fact(s)"));
|
|
897
|
+
lines.push(...(uniqueCoverage.length === 0 ? ["None."] : uniqueCoverage));
|
|
898
|
+
lines.push("", "### Disagreement details", "");
|
|
899
|
+
if (matrix.conflicts.length === 0) {
|
|
900
|
+
lines.push("None.");
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
for (const row of matrix.conflicts) {
|
|
904
|
+
const subject = row.variants[0]?.subject;
|
|
905
|
+
const subjectLabel = subject?.canonicalId ?? subject?.nativeId ?? "unknown";
|
|
906
|
+
const variants = row.variants.map((variant) => {
|
|
907
|
+
const providers = variant.providers.map(providerLabel).map(escapeMarkdown).join(", ");
|
|
908
|
+
return providers + " => " + formatMatrixObject(variant.object);
|
|
909
|
+
});
|
|
910
|
+
lines.push("- " +
|
|
911
|
+
codeMark +
|
|
912
|
+
escapeMarkdown(row.factClass) +
|
|
913
|
+
codeMark +
|
|
914
|
+
" subject " +
|
|
915
|
+
codeMark +
|
|
916
|
+
escapeMarkdown(subjectLabel) +
|
|
917
|
+
codeMark +
|
|
918
|
+
": " +
|
|
919
|
+
variants.join("; "));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return lines;
|
|
923
|
+
}
|
|
924
|
+
function formatMatrixObject(object) {
|
|
925
|
+
if (object.kind === "value")
|
|
926
|
+
return JSON.stringify(object.value);
|
|
927
|
+
return "entity " + (object.entity.canonicalId ?? object.entity.nativeId);
|
|
928
|
+
}
|
|
929
|
+
function providerLabel(provider) {
|
|
930
|
+
return provider.id + "@" + provider.version;
|
|
931
|
+
}
|
|
932
|
+
function formatRatio(value) {
|
|
933
|
+
return value.value === null ? value.numerator + "/" + value.denominator + " (n/a)" : value.value.toFixed(3);
|
|
934
|
+
}
|
|
935
|
+
function escapeMarkdown(value) {
|
|
936
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
937
|
+
}
|
|
938
|
+
function projectMatrixEntity(entity) {
|
|
939
|
+
return {
|
|
940
|
+
provider: entity.provider,
|
|
941
|
+
nativeId: entity.nativeId,
|
|
942
|
+
...(entity.canonicalId === undefined ? {} : { canonicalId: entity.canonicalId }),
|
|
943
|
+
candidateCanonicalIds: entity.candidateCanonicalIds,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function projectMatrixObject(object) {
|
|
947
|
+
if (object.kind === "value")
|
|
948
|
+
return object;
|
|
949
|
+
return { kind: "entity", entity: projectMatrixEntity(object.entity) };
|
|
950
|
+
}
|
|
951
|
+
function projectMatrixVariant(variant) {
|
|
952
|
+
return {
|
|
953
|
+
key: variant.key,
|
|
954
|
+
factIds: variant.factIds,
|
|
955
|
+
evidenceIds: variant.evidenceIds,
|
|
956
|
+
providers: variant.providers,
|
|
957
|
+
subject: projectMatrixEntity(variant.subject),
|
|
958
|
+
predicate: variant.predicate,
|
|
959
|
+
object: projectMatrixObject(variant.object),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Projects a row for persistence: `variants[].subject`/`object` are reduced
|
|
964
|
+
* from full entity copies (kind/path/range) to identity references, since
|
|
965
|
+
* the full entity is already persisted once per fact in
|
|
966
|
+
* normalized/facts.json and reachable through `factIds`/`evidenceIds`.
|
|
967
|
+
*/
|
|
968
|
+
function projectMatrixRow(row) {
|
|
969
|
+
return {
|
|
970
|
+
key: row.key,
|
|
971
|
+
logicalKey: row.logicalKey,
|
|
972
|
+
repository: row.repository,
|
|
973
|
+
factClass: row.factClass,
|
|
974
|
+
predicate: row.predicate,
|
|
975
|
+
state: row.state,
|
|
976
|
+
comparisonPossible: row.comparisonPossible,
|
|
977
|
+
correlationStatus: row.correlationStatus,
|
|
978
|
+
variants: row.variants.map(projectMatrixVariant),
|
|
979
|
+
providerStates: row.providerStates,
|
|
980
|
+
observedProviders: row.observedProviders,
|
|
981
|
+
missingProviders: row.missingProviders,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Writes aggregate and sliced machine-readable artifacts plus the report.
|
|
986
|
+
* The writer performs no provider execution and no normalization.
|
|
987
|
+
*
|
|
988
|
+
* `normalized/facts.json` is the source of truth for full fact/evidence
|
|
989
|
+
* detail. Every persisted row here carries only aggregate counts plus
|
|
990
|
+
* `factIds`/`evidenceIds`/`canonicalId` references back to it -- no fact
|
|
991
|
+
* envelope (path/range/provider-native payload) is re-embedded. `matrix.json`
|
|
992
|
+
* itself omits the full row set and per-state row slices, since those are
|
|
993
|
+
* exactly what `overlap.json`/`conflicts.json`/`unmatched.json` persist.
|
|
994
|
+
*/
|
|
995
|
+
export async function writeProviderMatrixArtifacts(runRoot, matrix) {
|
|
996
|
+
const matrixRoot = path.join(runRoot, "matrix");
|
|
997
|
+
await mkdir(matrixRoot, { recursive: true });
|
|
998
|
+
const matrixPath = path.join(matrixRoot, "matrix.json");
|
|
999
|
+
const coveragePath = path.join(matrixRoot, "coverage.json");
|
|
1000
|
+
const overlapPath = path.join(matrixRoot, "overlap.json");
|
|
1001
|
+
const conflictsPath = path.join(matrixRoot, "conflicts.json");
|
|
1002
|
+
const unmatchedPath = path.join(matrixRoot, "unmatched.json");
|
|
1003
|
+
const informationGainPath = path.join(matrixRoot, "information-gain.json");
|
|
1004
|
+
const reportPath = path.join(runRoot, "report.md");
|
|
1005
|
+
const conflictRows = matrix.conflicts.map(projectMatrixRow);
|
|
1006
|
+
const overlapRows = matrix.overlap.facts.map(projectMatrixRow);
|
|
1007
|
+
const unmatchedAmbiguous = matrix.unmatched.ambiguous.map(projectMatrixRow);
|
|
1008
|
+
const unmatchedUnmatched = matrix.unmatched.unmatched.map(projectMatrixRow);
|
|
1009
|
+
const unmatchedUnsupported = matrix.unmatched.unsupported.map(projectMatrixRow);
|
|
1010
|
+
await writeJson(matrixPath, {
|
|
1011
|
+
schemaVersion: matrix.schemaVersion,
|
|
1012
|
+
inputFactSchemaVersion: matrix.inputFactSchemaVersion,
|
|
1013
|
+
generatedBy: matrix.generatedBy,
|
|
1014
|
+
repositories: matrix.repositories,
|
|
1015
|
+
providers: matrix.providers,
|
|
1016
|
+
additionOrder: matrix.additionOrder,
|
|
1017
|
+
metricDefinitions: matrix.metricDefinitions,
|
|
1018
|
+
rowCounts: {
|
|
1019
|
+
total: matrix.facts.length,
|
|
1020
|
+
coverageProviders: matrix.coverage.length,
|
|
1021
|
+
overlapPairwise: matrix.overlap.pairwise.length,
|
|
1022
|
+
overlapAllProviders: matrix.overlap.allProviders.length,
|
|
1023
|
+
overlapFacts: overlapRows.length,
|
|
1024
|
+
conflicts: conflictRows.length,
|
|
1025
|
+
unmatchedAmbiguous: unmatchedAmbiguous.length,
|
|
1026
|
+
unmatchedUnmatched: unmatchedUnmatched.length,
|
|
1027
|
+
unmatchedUnsupported: unmatchedUnsupported.length,
|
|
1028
|
+
informationGain: matrix.informationGain.length,
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
await writeJson(coveragePath, {
|
|
1032
|
+
schemaVersion: matrix.schemaVersion,
|
|
1033
|
+
metricDefinitions: matrix.metricDefinitions,
|
|
1034
|
+
repositories: matrix.repositories,
|
|
1035
|
+
providers: matrix.providers,
|
|
1036
|
+
coverage: matrix.coverage,
|
|
1037
|
+
});
|
|
1038
|
+
await writeJson(overlapPath, {
|
|
1039
|
+
schemaVersion: matrix.schemaVersion,
|
|
1040
|
+
metricDefinitions: matrix.metricDefinitions,
|
|
1041
|
+
repositories: matrix.repositories,
|
|
1042
|
+
overlap: {
|
|
1043
|
+
pairwise: matrix.overlap.pairwise,
|
|
1044
|
+
allProviders: matrix.overlap.allProviders,
|
|
1045
|
+
facts: overlapRows,
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
await writeJson(conflictsPath, {
|
|
1049
|
+
schemaVersion: matrix.schemaVersion,
|
|
1050
|
+
metricDefinitions: matrix.metricDefinitions,
|
|
1051
|
+
repositories: matrix.repositories,
|
|
1052
|
+
conflicts: conflictRows,
|
|
1053
|
+
});
|
|
1054
|
+
await writeJson(unmatchedPath, {
|
|
1055
|
+
schemaVersion: matrix.schemaVersion,
|
|
1056
|
+
metricDefinitions: matrix.metricDefinitions,
|
|
1057
|
+
repositories: matrix.repositories,
|
|
1058
|
+
// `records` (ambiguous+unmatched+unsupported combined) is omitted: it
|
|
1059
|
+
// duplicated the three arrays above in full. A reader that needs the
|
|
1060
|
+
// combined set concatenates them; each row already carries `state`.
|
|
1061
|
+
unmatched: {
|
|
1062
|
+
ambiguous: unmatchedAmbiguous,
|
|
1063
|
+
unmatched: unmatchedUnmatched,
|
|
1064
|
+
unsupported: unmatchedUnsupported,
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
await writeJson(informationGainPath, {
|
|
1068
|
+
schemaVersion: matrix.schemaVersion,
|
|
1069
|
+
metricDefinitions: matrix.metricDefinitions,
|
|
1070
|
+
repositories: matrix.repositories,
|
|
1071
|
+
additionOrder: matrix.additionOrder,
|
|
1072
|
+
informationGain: matrix.informationGain,
|
|
1073
|
+
});
|
|
1074
|
+
await writeFile(reportPath, renderProviderMatrixReport(matrix), "utf8");
|
|
1075
|
+
return {
|
|
1076
|
+
matrixPath,
|
|
1077
|
+
coveragePath,
|
|
1078
|
+
overlapPath,
|
|
1079
|
+
conflictsPath,
|
|
1080
|
+
unmatchedPath,
|
|
1081
|
+
informationGainPath,
|
|
1082
|
+
reportPath,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
/** Builds then persists a matrix, accepting either normalized facts or a matrix. */
|
|
1086
|
+
export async function writeProviderMatrix(runRoot, input, options = {}) {
|
|
1087
|
+
const matrix = isProviderMatrix(input) ? input : buildProviderMatrix(input, options);
|
|
1088
|
+
return writeProviderMatrixArtifacts(runRoot, matrix);
|
|
1089
|
+
}
|
|
1090
|
+
/** Persists the exact normalized artifact consumed by the matrix builder. */
|
|
1091
|
+
export async function writeNormalizedFactsArtifact(filePath, input) {
|
|
1092
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1093
|
+
const handle = await open(filePath, "w");
|
|
1094
|
+
const writer = new JsonArtifactWriter(handle);
|
|
1095
|
+
try {
|
|
1096
|
+
await writer.write(`{"schemaVersion":${FACT_SCHEMA_VERSION},"facts":`);
|
|
1097
|
+
await writer.writeArray(input.facts, persistFactEnvelope);
|
|
1098
|
+
await writer.write(`,"unsupported":`);
|
|
1099
|
+
await writer.writeArray(input.unsupported, persistUnsupportedEvidence);
|
|
1100
|
+
await writer.write(`,"correlation":`);
|
|
1101
|
+
await writer.writeValue(input.correlation);
|
|
1102
|
+
await writer.write("}\n");
|
|
1103
|
+
await writer.flush();
|
|
1104
|
+
}
|
|
1105
|
+
finally {
|
|
1106
|
+
await handle.close();
|
|
1107
|
+
}
|
|
1108
|
+
return filePath;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* The in-memory envelope carries the provider-native payload in three
|
|
1112
|
+
* places (`providerNative`, `nativeEvidence.providerNative`, and
|
|
1113
|
+
* `nativeEvidence.observation.providerNative`). Persisting all three
|
|
1114
|
+
* verbatim would triple the artifact's size for no benefit, so the payload
|
|
1115
|
+
* is retained once under `nativeEvidence.providerNative` and the other two
|
|
1116
|
+
* locations become `$ref` pointers that `resolveNativeEvidenceRefs` (used by
|
|
1117
|
+
* `readNormalizedFactsArtifact`) expands back on read.
|
|
1118
|
+
*/
|
|
1119
|
+
function persistFactEnvelope(fact) {
|
|
1120
|
+
const reference = nativeEvidenceRef(fact.nativeEvidence.id);
|
|
1121
|
+
return {
|
|
1122
|
+
...fact,
|
|
1123
|
+
providerNative: reference,
|
|
1124
|
+
nativeEvidence: {
|
|
1125
|
+
...fact.nativeEvidence,
|
|
1126
|
+
observation: { ...fact.nativeEvidence.observation, providerNative: reference },
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function persistUnsupportedEvidence(evidence) {
|
|
1131
|
+
const reference = nativeEvidenceRef(evidence.nativeEvidence.id);
|
|
1132
|
+
return {
|
|
1133
|
+
...evidence,
|
|
1134
|
+
providerNative: reference,
|
|
1135
|
+
nativeEvidence: {
|
|
1136
|
+
...evidence.nativeEvidence,
|
|
1137
|
+
observation: { ...evidence.nativeEvidence.observation, providerNative: reference },
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function nativeEvidenceRef(nativeEvidenceId) {
|
|
1142
|
+
return { $ref: `nativeEvidence:${nativeEvidenceId}/providerNative` };
|
|
1143
|
+
}
|
|
1144
|
+
const NATIVE_EVIDENCE_REF_PATTERN = /^nativeEvidence:(.+)\/providerNative$/u;
|
|
1145
|
+
/**
|
|
1146
|
+
* Resolves every `$ref` produced by `persistFactEnvelope`/`persistUnsupportedEvidence`
|
|
1147
|
+
* back to the retained `nativeEvidence.providerNative` payload, so a reader
|
|
1148
|
+
* of the persisted artifact recovers the exact in-memory envelope shape.
|
|
1149
|
+
*/
|
|
1150
|
+
function resolveNativeEvidenceRefs(record) {
|
|
1151
|
+
const byId = new Map();
|
|
1152
|
+
for (const item of [...record.facts, ...record.unsupported]) {
|
|
1153
|
+
const entry = asRecord(item);
|
|
1154
|
+
const nativeEvidence = entry === undefined ? undefined : asRecord(entry.nativeEvidence);
|
|
1155
|
+
const id = nativeEvidence?.id;
|
|
1156
|
+
if (typeof id === "string" && nativeEvidence !== undefined)
|
|
1157
|
+
byId.set(id, nativeEvidence.providerNative);
|
|
1158
|
+
}
|
|
1159
|
+
const resolve = (value) => {
|
|
1160
|
+
const record = asRecord(value);
|
|
1161
|
+
const ref = typeof record?.$ref === "string" ? NATIVE_EVIDENCE_REF_PATTERN.exec(record.$ref) : null;
|
|
1162
|
+
if (ref === null)
|
|
1163
|
+
return value;
|
|
1164
|
+
const target = byId.get(ref[1]);
|
|
1165
|
+
return target === undefined ? value : target;
|
|
1166
|
+
};
|
|
1167
|
+
for (const item of [...record.facts, ...record.unsupported]) {
|
|
1168
|
+
const entry = asRecord(item);
|
|
1169
|
+
if (entry === undefined)
|
|
1170
|
+
continue;
|
|
1171
|
+
entry.providerNative = resolve(entry.providerNative);
|
|
1172
|
+
const nativeEvidence = asRecord(entry.nativeEvidence);
|
|
1173
|
+
if (nativeEvidence === undefined)
|
|
1174
|
+
continue;
|
|
1175
|
+
const observation = asRecord(nativeEvidence.observation);
|
|
1176
|
+
if (observation !== undefined)
|
|
1177
|
+
observation.providerNative = resolve(observation.providerNative);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Reads a persisted #11 normalized-facts artifact without running providers.
|
|
1182
|
+
*
|
|
1183
|
+
* Mottainai-scale artifacts exceed V8's ~1GB single-string limit, so this
|
|
1184
|
+
* never materializes the whole file as one string (unlike `JSON.parse(await
|
|
1185
|
+
* readFile(filePath, "utf8"))`). It streams the file in chunks and parses
|
|
1186
|
+
* each top-level array element (`facts[i]`, `unsupported[i]`) as its own
|
|
1187
|
+
* JSON string instead, relying on the fixed top-level key order
|
|
1188
|
+
* (`schemaVersion`, `facts`, `unsupported`, `correlation`) that
|
|
1189
|
+
* `writeNormalizedFactsArtifact` always writes.
|
|
1190
|
+
*/
|
|
1191
|
+
export async function readNormalizedFactsArtifact(filePath) {
|
|
1192
|
+
const reader = new StreamingNormalizedFactsReader(filePath);
|
|
1193
|
+
const result = await reader.read();
|
|
1194
|
+
resolveNativeEvidenceRefs({ facts: result.facts, unsupported: result.unsupported });
|
|
1195
|
+
return result;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Minimal streaming reader for the fixed shape `writeNormalizedFactsArtifact`
|
|
1199
|
+
* produces. It reads the file in fixed-size chunks and only ever holds one
|
|
1200
|
+
* top-level array element's text in memory at a time, so total file size is
|
|
1201
|
+
* not bounded by V8's single-string length limit.
|
|
1202
|
+
*/
|
|
1203
|
+
class StreamingNormalizedFactsReader {
|
|
1204
|
+
filePath;
|
|
1205
|
+
buffer = "";
|
|
1206
|
+
position = 0;
|
|
1207
|
+
handle;
|
|
1208
|
+
eof = false;
|
|
1209
|
+
constructor(filePath) {
|
|
1210
|
+
this.filePath = filePath;
|
|
1211
|
+
}
|
|
1212
|
+
async read() {
|
|
1213
|
+
this.handle = await open(this.filePath, "r");
|
|
1214
|
+
try {
|
|
1215
|
+
await this.expectLiteral("{");
|
|
1216
|
+
await this.expectKey("schemaVersion");
|
|
1217
|
+
const schemaVersion = await this.readJsonScalarUntil(new Set([",", "}"]));
|
|
1218
|
+
if (schemaVersion !== FACT_SCHEMA_VERSION) {
|
|
1219
|
+
throw new Error("normalized fact artifact schemaVersion must be " + FACT_SCHEMA_VERSION);
|
|
1220
|
+
}
|
|
1221
|
+
await this.expectSeparatorThenKey("facts");
|
|
1222
|
+
const facts = await this.readJsonArray();
|
|
1223
|
+
await this.expectSeparatorThenKey("unsupported");
|
|
1224
|
+
const unsupported = await this.readJsonArray();
|
|
1225
|
+
await this.expectSeparatorThenKey("correlation");
|
|
1226
|
+
const correlation = await this.readJsonValueUntil(new Set([",", "}"]));
|
|
1227
|
+
if (!isRecord(correlation))
|
|
1228
|
+
throw new Error("normalized fact artifact correlation must be an object");
|
|
1229
|
+
return {
|
|
1230
|
+
schemaVersion,
|
|
1231
|
+
facts: facts,
|
|
1232
|
+
unsupported: unsupported,
|
|
1233
|
+
correlation: correlation,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
finally {
|
|
1237
|
+
await this.handle.close();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
async fill() {
|
|
1241
|
+
if (this.eof || this.handle === undefined)
|
|
1242
|
+
return;
|
|
1243
|
+
const chunkSize = 1024 * 1024;
|
|
1244
|
+
const chunk = Buffer.alloc(chunkSize);
|
|
1245
|
+
const { bytesRead } = await this.handle.read(chunk, 0, chunkSize, null);
|
|
1246
|
+
if (bytesRead === 0) {
|
|
1247
|
+
this.eof = true;
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
this.buffer += chunk.toString("utf8", 0, bytesRead);
|
|
1251
|
+
}
|
|
1252
|
+
async ensure(count) {
|
|
1253
|
+
while (this.buffer.length - this.position < count && !this.eof)
|
|
1254
|
+
await this.fill();
|
|
1255
|
+
}
|
|
1256
|
+
async peek() {
|
|
1257
|
+
await this.ensure(1);
|
|
1258
|
+
return this.buffer[this.position];
|
|
1259
|
+
}
|
|
1260
|
+
compact() {
|
|
1261
|
+
if (this.position > 4 * 1024 * 1024) {
|
|
1262
|
+
const trimmed = this.position;
|
|
1263
|
+
this.buffer = this.buffer.slice(this.position);
|
|
1264
|
+
this.position = 0;
|
|
1265
|
+
return trimmed;
|
|
1266
|
+
}
|
|
1267
|
+
return 0;
|
|
1268
|
+
}
|
|
1269
|
+
async skipWhitespace() {
|
|
1270
|
+
for (;;) {
|
|
1271
|
+
const char = await this.peek();
|
|
1272
|
+
if (char === undefined || !/\s/u.test(char))
|
|
1273
|
+
return;
|
|
1274
|
+
this.position += 1;
|
|
1275
|
+
this.compact();
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async expectLiteral(literal) {
|
|
1279
|
+
await this.skipWhitespace();
|
|
1280
|
+
await this.ensure(literal.length);
|
|
1281
|
+
if (this.buffer.slice(this.position, this.position + literal.length) !== literal) {
|
|
1282
|
+
throw new Error("normalized fact artifact malformed near byte " + this.position);
|
|
1283
|
+
}
|
|
1284
|
+
this.position += literal.length;
|
|
1285
|
+
this.compact();
|
|
1286
|
+
}
|
|
1287
|
+
async expectKey(key) {
|
|
1288
|
+
await this.expectLiteral(JSON.stringify(key));
|
|
1289
|
+
await this.expectLiteral(":");
|
|
1290
|
+
}
|
|
1291
|
+
async expectSeparatorThenKey(key) {
|
|
1292
|
+
await this.expectLiteral(",");
|
|
1293
|
+
await this.expectKey(key);
|
|
1294
|
+
}
|
|
1295
|
+
/** Reads raw JSON text up to (not including) the first unnested delimiter, then parses it. */
|
|
1296
|
+
async readJsonValueUntil(delimiters) {
|
|
1297
|
+
const text = await this.readRawJsonUntil(delimiters);
|
|
1298
|
+
return JSON.parse(text);
|
|
1299
|
+
}
|
|
1300
|
+
async readJsonScalarUntil(delimiters) {
|
|
1301
|
+
const value = await this.readJsonValueUntil(delimiters);
|
|
1302
|
+
if (typeof value !== "number")
|
|
1303
|
+
throw new Error("normalized fact artifact expected a numeric field");
|
|
1304
|
+
return value;
|
|
1305
|
+
}
|
|
1306
|
+
/** Reads raw top-level JSON text (object/array/scalar) up to an unnested delimiter, without consuming it. */
|
|
1307
|
+
async readRawJsonUntil(delimiters) {
|
|
1308
|
+
await this.skipWhitespace();
|
|
1309
|
+
let depth = 0;
|
|
1310
|
+
let inString = false;
|
|
1311
|
+
let escaped = false;
|
|
1312
|
+
let start = this.position;
|
|
1313
|
+
let text = "";
|
|
1314
|
+
for (;;) {
|
|
1315
|
+
const char = await this.peek();
|
|
1316
|
+
if (char === undefined)
|
|
1317
|
+
throw new Error("normalized fact artifact ended unexpectedly");
|
|
1318
|
+
if (inString) {
|
|
1319
|
+
if (escaped) {
|
|
1320
|
+
escaped = false;
|
|
1321
|
+
}
|
|
1322
|
+
else if (char === "\\") {
|
|
1323
|
+
escaped = true;
|
|
1324
|
+
}
|
|
1325
|
+
else if (char === '"') {
|
|
1326
|
+
inString = false;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
else if (char === '"') {
|
|
1330
|
+
inString = true;
|
|
1331
|
+
}
|
|
1332
|
+
else if (depth === 0 && delimiters.has(char)) {
|
|
1333
|
+
text += this.buffer.slice(start, this.position);
|
|
1334
|
+
return text;
|
|
1335
|
+
}
|
|
1336
|
+
else if (char === "{" || char === "[") {
|
|
1337
|
+
depth += 1;
|
|
1338
|
+
}
|
|
1339
|
+
else if (char === "}" || char === "]") {
|
|
1340
|
+
depth -= 1;
|
|
1341
|
+
}
|
|
1342
|
+
this.position += 1;
|
|
1343
|
+
if (this.buffer.length - this.position < 2 && !this.eof) {
|
|
1344
|
+
text += this.buffer.slice(start, this.position);
|
|
1345
|
+
await this.fill();
|
|
1346
|
+
start = this.position;
|
|
1347
|
+
}
|
|
1348
|
+
const trimmed = this.compact();
|
|
1349
|
+
start -= trimmed;
|
|
1350
|
+
if (this.position < start)
|
|
1351
|
+
start = this.position;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
/** Streams a top-level JSON array, parsing one element at a time. */
|
|
1355
|
+
async readJsonArray() {
|
|
1356
|
+
await this.expectLiteral("[");
|
|
1357
|
+
const values = [];
|
|
1358
|
+
await this.skipWhitespace();
|
|
1359
|
+
if ((await this.peek()) === "]") {
|
|
1360
|
+
this.position += 1;
|
|
1361
|
+
this.compact();
|
|
1362
|
+
return values;
|
|
1363
|
+
}
|
|
1364
|
+
for (;;) {
|
|
1365
|
+
const text = await this.readRawJsonUntil(new Set([",", "]"]));
|
|
1366
|
+
values.push(JSON.parse(text));
|
|
1367
|
+
await this.skipWhitespace();
|
|
1368
|
+
const delimiter = await this.peek();
|
|
1369
|
+
this.position += 1;
|
|
1370
|
+
this.compact();
|
|
1371
|
+
if (delimiter === "]")
|
|
1372
|
+
return values;
|
|
1373
|
+
if (delimiter !== ",")
|
|
1374
|
+
throw new Error("normalized fact artifact malformed array near byte " + this.position);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/** Reads a persisted normalized artifact and generates its deterministic matrix. */
|
|
1379
|
+
export async function buildProviderMatrixFromArtifact(filePath, options = {}) {
|
|
1380
|
+
return buildProviderMatrix(await readNormalizedFactsArtifact(filePath), options);
|
|
1381
|
+
}
|
|
1382
|
+
function isProviderMatrix(value) {
|
|
1383
|
+
return isRecord(value) && value.schemaVersion === PROVIDER_MATRIX_SCHEMA_VERSION && "metricDefinitions" in value;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Streams a value to disk through `JsonArtifactWriter` instead of building
|
|
1387
|
+
* one `JSON.stringify(value)` string first. Exported so every large
|
|
1388
|
+
* generated artifact (not just the ones matrix.ts itself writes) can avoid
|
|
1389
|
+
* the same V8 string-length limit.
|
|
1390
|
+
*/
|
|
1391
|
+
export async function writeStreamingJson(filePath, value) {
|
|
1392
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1393
|
+
const handle = await open(filePath, "w");
|
|
1394
|
+
const writer = new JsonArtifactWriter(handle);
|
|
1395
|
+
try {
|
|
1396
|
+
await writer.writeValue(value);
|
|
1397
|
+
await writer.write("\n");
|
|
1398
|
+
await writer.flush();
|
|
1399
|
+
}
|
|
1400
|
+
finally {
|
|
1401
|
+
await handle.close();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function writeJson(filePath, value) {
|
|
1405
|
+
await writeStreamingJson(filePath, value);
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Serializes large generated artifacts without first constructing one giant
|
|
1409
|
+
* JSON string. Mottainai's TypeScript observation set is large enough for a
|
|
1410
|
+
* single JSON.stringify(matrix) call to exceed V8's string limit.
|
|
1411
|
+
*/
|
|
1412
|
+
export class JsonArtifactWriter {
|
|
1413
|
+
handle;
|
|
1414
|
+
buffer = "";
|
|
1415
|
+
constructor(handle) {
|
|
1416
|
+
this.handle = handle;
|
|
1417
|
+
}
|
|
1418
|
+
async write(value) {
|
|
1419
|
+
this.buffer += value;
|
|
1420
|
+
if (this.buffer.length >= 1024 * 1024)
|
|
1421
|
+
await this.flush();
|
|
1422
|
+
}
|
|
1423
|
+
async flush() {
|
|
1424
|
+
if (this.buffer.length === 0)
|
|
1425
|
+
return;
|
|
1426
|
+
await this.handle.write(this.buffer);
|
|
1427
|
+
this.buffer = "";
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Streams a possibly-large array by transforming and serializing one
|
|
1431
|
+
* element at a time, so the whole transformed array is never held in
|
|
1432
|
+
* memory at once (unlike `writeValue(values.map(transform))`).
|
|
1433
|
+
*/
|
|
1434
|
+
async writeArray(values, transform) {
|
|
1435
|
+
await this.write("[");
|
|
1436
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1437
|
+
if (index > 0)
|
|
1438
|
+
await this.write(",");
|
|
1439
|
+
await this.writeValue(transform(values[index]));
|
|
1440
|
+
}
|
|
1441
|
+
await this.write("]");
|
|
1442
|
+
}
|
|
1443
|
+
async writeValue(value) {
|
|
1444
|
+
if (value === null) {
|
|
1445
|
+
await this.write("null");
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (Array.isArray(value)) {
|
|
1449
|
+
await this.write("[");
|
|
1450
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1451
|
+
if (index > 0)
|
|
1452
|
+
await this.write(",");
|
|
1453
|
+
await this.writeValue(value[index]);
|
|
1454
|
+
}
|
|
1455
|
+
await this.write("]");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (typeof value === "object") {
|
|
1459
|
+
const record = value;
|
|
1460
|
+
await this.write("{");
|
|
1461
|
+
let first = true;
|
|
1462
|
+
for (const key of Object.keys(record)) {
|
|
1463
|
+
if (record[key] === undefined)
|
|
1464
|
+
continue;
|
|
1465
|
+
if (!first)
|
|
1466
|
+
await this.write(",");
|
|
1467
|
+
first = false;
|
|
1468
|
+
await this.write(JSON.stringify(key));
|
|
1469
|
+
await this.write(":");
|
|
1470
|
+
await this.writeValue(record[key]);
|
|
1471
|
+
}
|
|
1472
|
+
await this.write("}");
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const serialized = JSON.stringify(value);
|
|
1476
|
+
await this.write(serialized === undefined ? "null" : serialized);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function asRecord(value) {
|
|
1480
|
+
return isRecord(value) ? value : undefined;
|
|
1481
|
+
}
|
|
1482
|
+
function isRecord(value) {
|
|
1483
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1484
|
+
}
|
|
1485
|
+
//# sourceMappingURL=matrix.js.map
|