@intentset/core 0.1.0 → 0.3.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/export.d.ts +11 -2
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +156 -15
- package/dist/export.js.map +1 -1
- package/dist/impact.d.ts +9 -1
- package/dist/impact.d.ts.map +1 -1
- package/dist/impact.js +29 -0
- package/dist/impact.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/read-export.d.ts +32 -0
- package/dist/read-export.d.ts.map +1 -0
- package/dist/read-export.js +775 -0
- package/dist/read-export.js.map +1 -0
- package/dist/types.d.ts +179 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +35 -2
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/export.ts +182 -14
- package/src/impact.ts +42 -1
- package/src/index.ts +1 -0
- package/src/read-export.ts +892 -0
- package/src/types.ts +198 -8
package/src/export.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The export envelope (
|
|
2
|
+
* The export envelope (spec/export.md; spec/export.schema.json; Core §12).
|
|
3
3
|
* Artifacts are sorted by ID and every list inside is sorted, so the same
|
|
4
4
|
* validation result with the same `generatedAt` is the same bytes. Bodies
|
|
5
5
|
* are included only when asked for.
|
|
6
|
+
*
|
|
7
|
+
* Restricted artifacts are withheld unless asked for, as they are from agent
|
|
8
|
+
* context: absent from `artifacts`, `products` and every report, their IDs
|
|
9
|
+
* removed from the links of what is exported and redacted from diagnostic
|
|
10
|
+
* text, and every omission counted where it happened, so a short export never
|
|
11
|
+
* reads as a complete one. The graph hash still covers them: it names the
|
|
12
|
+
* snapshot, which evidence is bound to, not the projection.
|
|
6
13
|
*/
|
|
7
14
|
import { graphHash, sortKeysDeep } from "./hash.ts";
|
|
8
15
|
import { compareStrings } from "./report.ts";
|
|
@@ -11,6 +18,8 @@ import {
|
|
|
11
18
|
EXPORT_CONTRACT,
|
|
12
19
|
type ExportArtifact,
|
|
13
20
|
type ExportEnvelope,
|
|
21
|
+
type ExportImpactHit,
|
|
22
|
+
type ExportReports,
|
|
14
23
|
LINK_KINDS,
|
|
15
24
|
type Level,
|
|
16
25
|
type LinkKind,
|
|
@@ -18,7 +27,9 @@ import {
|
|
|
18
27
|
SPEC_VERSION,
|
|
19
28
|
type SliceMeta,
|
|
20
29
|
type ValidationResult,
|
|
30
|
+
type Visibility,
|
|
21
31
|
} from "./types.ts";
|
|
32
|
+
import type { Diagnostic } from "./diagnostics.ts";
|
|
22
33
|
|
|
23
34
|
/** What the caller knows and the validation result does not: identity, commit, scope, level, release. */
|
|
24
35
|
export interface ExportMeta {
|
|
@@ -27,6 +38,8 @@ export interface ExportMeta {
|
|
|
27
38
|
/** The commit the export describes, or null with `commitUnavailable` saying why (a default reason is written when none is given). */
|
|
28
39
|
commit: string | null;
|
|
29
40
|
commitUnavailable?: string;
|
|
41
|
+
/** True when tracked files differed from the commit. Ignored when the commit is null. */
|
|
42
|
+
uncommitted?: boolean;
|
|
30
43
|
/** The scope globs validated; defaults to every Markdown file. */
|
|
31
44
|
scope?: string[];
|
|
32
45
|
/** The level the validation ran at; defaults to L1. */
|
|
@@ -35,17 +48,29 @@ export interface ExportMeta {
|
|
|
35
48
|
/** ISO 8601; defaults to now. Fix it to make two exports comparable byte for byte. */
|
|
36
49
|
generatedAt?: string;
|
|
37
50
|
includeBodies?: boolean;
|
|
51
|
+
/** Export restricted artifacts too. Default false: they are withheld and counted (spec/export.md §3). */
|
|
52
|
+
includeRestricted?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Report sections, each computed over the whole graph by the package that
|
|
55
|
+
* owns it. The export withholds inside them exactly as it does for artifacts.
|
|
56
|
+
*/
|
|
57
|
+
reports?: ExportReports;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
|
-
/**
|
|
60
|
+
/** spec/export.md: the `intentset/export/0.2` envelope over a validation result. */
|
|
41
61
|
export function exportGraph(result: ValidationResult, registries: Registries, meta: ExportMeta): ExportEnvelope {
|
|
42
62
|
const { graph } = result;
|
|
43
|
-
const
|
|
63
|
+
const withheld = withheldIds(result, meta.includeRestricted === true);
|
|
64
|
+
const ids = [...graph.artifacts.keys()].filter((id) => !withheld.ids.has(id)).sort(compareStrings);
|
|
44
65
|
const errors = result.diagnostics.filter((d) => d.severity === "error").length;
|
|
45
66
|
const warnings = result.diagnostics.length - errors;
|
|
46
|
-
const
|
|
47
|
-
|
|
67
|
+
const diagnostics = withholdDiagnostics(result.diagnostics, withheld);
|
|
68
|
+
const source: ExportEnvelope["source"] = { commit: meta.commit, uncommitted: false };
|
|
69
|
+
if (meta.commit === null) {
|
|
48
70
|
source.commitUnavailable = meta.commitUnavailable ?? "No commit was supplied to the export.";
|
|
71
|
+
} else {
|
|
72
|
+
source.uncommitted = meta.uncommitted === true;
|
|
73
|
+
}
|
|
49
74
|
|
|
50
75
|
return {
|
|
51
76
|
contract: EXPORT_CONTRACT,
|
|
@@ -62,24 +87,92 @@ export function exportGraph(result: ValidationResult, registries: Registries, me
|
|
|
62
87
|
status: result.ok ? "pass" : "fail",
|
|
63
88
|
errors,
|
|
64
89
|
warnings,
|
|
65
|
-
diagnostics
|
|
90
|
+
diagnostics,
|
|
91
|
+
},
|
|
92
|
+
withholding: {
|
|
93
|
+
visibilities: meta.includeRestricted === true ? [] : ["restricted"],
|
|
94
|
+
artifacts: withheld.ids.size,
|
|
95
|
+
diagnostics: result.diagnostics.length - diagnostics.length,
|
|
66
96
|
},
|
|
67
|
-
registries: exportRegistries(registries),
|
|
68
|
-
artifacts: ids.map((id) => exportArtifact(result, id, meta.includeBodies === true)),
|
|
97
|
+
registries: exportRegistries(registries, withheld.ids),
|
|
98
|
+
artifacts: ids.map((id) => exportArtifact(result, id, meta.includeBodies === true, withheld.ids)),
|
|
99
|
+
reports: withholdReports(meta.reports ?? {}, withheld.ids),
|
|
69
100
|
};
|
|
70
101
|
}
|
|
71
102
|
|
|
72
|
-
|
|
103
|
+
interface Withheld {
|
|
104
|
+
ids: Set<string>;
|
|
105
|
+
/** Source paths of the withheld artifacts, for diagnostics that name a file but no artifact. */
|
|
106
|
+
paths: Set<string>;
|
|
107
|
+
/** Matches any withheld ID as a whole token, or null when nothing is withheld. */
|
|
108
|
+
pattern: RegExp | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const WITHHELD_VISIBILITIES: readonly Visibility[] = ["restricted"];
|
|
112
|
+
|
|
113
|
+
function withheldIds(result: ValidationResult, includeRestricted: boolean): Withheld {
|
|
114
|
+
const ids = new Set<string>();
|
|
115
|
+
const paths = new Set<string>();
|
|
116
|
+
if (!includeRestricted) {
|
|
117
|
+
for (const [id, artifact] of result.graph.artifacts) {
|
|
118
|
+
if (WITHHELD_VISIBILITIES.includes(artifact.meta.visibility)) {
|
|
119
|
+
ids.add(id);
|
|
120
|
+
paths.add(artifact.path);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const alternatives = [...ids].sort((a, b) => b.length - a.length || compareStrings(a, b)).map(escapeRegExp);
|
|
125
|
+
const pattern =
|
|
126
|
+
alternatives.length === 0 ? null : new RegExp(`(?<![A-Z0-9-])(?:${alternatives.join("|")})(?![A-Z0-9-])`, "g");
|
|
127
|
+
return { ids, paths, pattern };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function escapeRegExp(text: string): string {
|
|
131
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Diagnostics about a withheld artifact are left out; any other that names one has the ID redacted. */
|
|
135
|
+
function withholdDiagnostics(diagnostics: readonly Diagnostic[], withheld: Withheld): Diagnostic[] {
|
|
136
|
+
const out: Diagnostic[] = [];
|
|
137
|
+
for (const d of diagnostics) {
|
|
138
|
+
if (d.artifact !== null && withheld.ids.has(d.artifact)) continue;
|
|
139
|
+
if (d.path !== null && withheld.paths.has(d.path)) continue;
|
|
140
|
+
const pattern = withheld.pattern;
|
|
141
|
+
if (pattern === null) {
|
|
142
|
+
out.push(d);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const redact = (text: string) => text.replace(pattern, "a withheld artifact");
|
|
146
|
+
out.push({ ...d, message: redact(d.message), remediation: redact(d.remediation) });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function exportArtifact(
|
|
152
|
+
result: ValidationResult,
|
|
153
|
+
id: string,
|
|
154
|
+
includeBody: boolean,
|
|
155
|
+
withheld: ReadonlySet<string>,
|
|
156
|
+
): ExportArtifact {
|
|
73
157
|
const { graph } = result;
|
|
74
158
|
const artifact = graph.artifacts.get(id);
|
|
75
159
|
if (artifact === undefined) throw new Error(`graph has no artifact ${id}`);
|
|
76
160
|
const { meta } = artifact;
|
|
161
|
+
let withheldLinks = 0;
|
|
162
|
+
const keep = (list: Iterable<string>): string[] => {
|
|
163
|
+
const out: string[] = [];
|
|
164
|
+
for (const target of list) {
|
|
165
|
+
if (withheld.has(target)) withheldLinks++;
|
|
166
|
+
else out.push(target);
|
|
167
|
+
}
|
|
168
|
+
return out.sort(compareStrings);
|
|
169
|
+
};
|
|
77
170
|
|
|
78
171
|
const links: ExportArtifact["links"] = {};
|
|
79
172
|
for (const kind of LINK_KINDS) {
|
|
80
173
|
if (kind === "parent") continue;
|
|
81
174
|
const list = meta.links[kind];
|
|
82
|
-
if (list !== undefined) links[kind] =
|
|
175
|
+
if (list !== undefined) links[kind] = keep(list);
|
|
83
176
|
}
|
|
84
177
|
const derived: ExportArtifact["derived"] = {};
|
|
85
178
|
const byKind = new Map<LinkKind, string[]>();
|
|
@@ -90,7 +183,15 @@ function exportArtifact(result: ValidationResult, id: string, includeBody: boole
|
|
|
90
183
|
}
|
|
91
184
|
for (const kind of LINK_KINDS) {
|
|
92
185
|
const list = byKind.get(kind);
|
|
93
|
-
if (list !== undefined)
|
|
186
|
+
if (list !== undefined) {
|
|
187
|
+
const kept = keep(new Set(list));
|
|
188
|
+
if (kept.length > 0) derived[kind] = kept;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let parent = meta.parent ?? null;
|
|
192
|
+
if (parent !== null && withheld.has(parent)) {
|
|
193
|
+
parent = null;
|
|
194
|
+
withheldLinks++;
|
|
94
195
|
}
|
|
95
196
|
|
|
96
197
|
const exported: ExportArtifact = {
|
|
@@ -105,9 +206,10 @@ function exportArtifact(result: ValidationResult, id: string, includeBody: boole
|
|
|
105
206
|
revision: meta.revision ?? null,
|
|
106
207
|
path: artifact.path,
|
|
107
208
|
sourceHash: artifact.sourceHash,
|
|
108
|
-
parent
|
|
209
|
+
parent,
|
|
109
210
|
links,
|
|
110
211
|
derived,
|
|
212
|
+
withheldLinks,
|
|
111
213
|
availability: meta.availability === undefined ? null : sortedAvailability(meta.availability),
|
|
112
214
|
slice: meta.slice === undefined ? null : sortedSlice(meta.slice),
|
|
113
215
|
verification:
|
|
@@ -124,6 +226,72 @@ function exportArtifact(result: ValidationResult, id: string, includeBody: boole
|
|
|
124
226
|
return exported;
|
|
125
227
|
}
|
|
126
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Each report as its producer computed it, less what is withheld: entries
|
|
231
|
+
* about a withheld artifact go, lists inside an entry lose withheld IDs, and
|
|
232
|
+
* an impact hit whose path passes through a withheld artifact goes too, each
|
|
233
|
+
* counted in the `withheld` beside it.
|
|
234
|
+
*/
|
|
235
|
+
function withholdReports(reports: ExportReports, withheld: ReadonlySet<string>): ExportReports {
|
|
236
|
+
const out: ExportReports = {};
|
|
237
|
+
const split = (list: readonly string[]): [string[], number] => {
|
|
238
|
+
const kept = list.filter((id) => !withheld.has(id));
|
|
239
|
+
return [kept, list.length - kept.length];
|
|
240
|
+
};
|
|
241
|
+
if (reports.evidence !== undefined) {
|
|
242
|
+
const evidence = reports.evidence;
|
|
243
|
+
out.evidence = {
|
|
244
|
+
...evidence,
|
|
245
|
+
verifications: evidence.verifications.filter((v) => !withheld.has(v.id)),
|
|
246
|
+
claims: evidence.claims
|
|
247
|
+
.filter((claim) => !withheld.has(claim.id))
|
|
248
|
+
.map((claim) => {
|
|
249
|
+
const [verifications, removed] = split(claim.verifications);
|
|
250
|
+
return { ...claim, verifications, withheld: claim.withheld + removed };
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (reports.knowledge !== undefined) {
|
|
255
|
+
out.knowledge = {
|
|
256
|
+
...reports.knowledge,
|
|
257
|
+
artifacts: reports.knowledge.artifacts
|
|
258
|
+
.filter((entry) => !withheld.has(entry.id))
|
|
259
|
+
.map((entry) => {
|
|
260
|
+
const [sources, a] = split(entry.sources);
|
|
261
|
+
const [changed, b] = split(entry.changed);
|
|
262
|
+
const [missing, c] = split(entry.missing);
|
|
263
|
+
return { ...entry, sources, changed, missing, withheld: entry.withheld + a + b + c };
|
|
264
|
+
}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (reports.impact !== undefined) {
|
|
268
|
+
const visible = (hit: ExportImpactHit) => !withheld.has(hit.id) && hit.path.every((step) => !withheld.has(step.id));
|
|
269
|
+
out.impact = {
|
|
270
|
+
...reports.impact,
|
|
271
|
+
starts: reports.impact.starts
|
|
272
|
+
.filter((entry) => !withheld.has(entry.start))
|
|
273
|
+
.map((entry) => {
|
|
274
|
+
const direct = entry.direct.filter(visible);
|
|
275
|
+
const candidates = entry.candidates.filter(visible);
|
|
276
|
+
const context = entry.context.filter(visible);
|
|
277
|
+
const [ancestors, removed] = split(entry.ancestors);
|
|
278
|
+
const hits = entry.direct.length + entry.candidates.length + entry.context.length;
|
|
279
|
+
const kept = direct.length + candidates.length + context.length;
|
|
280
|
+
return { ...entry, direct, candidates, context, ancestors, withheld: entry.withheld + hits - kept + removed };
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (reports.ownership !== undefined) {
|
|
285
|
+
const files = reports.ownership.files.filter((file) => file.owner === null || !withheld.has(file.owner));
|
|
286
|
+
out.ownership = {
|
|
287
|
+
...reports.ownership,
|
|
288
|
+
files,
|
|
289
|
+
withheld: reports.ownership.withheld + reports.ownership.files.length - files.length,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
127
295
|
function sortedAvailability(availability: Availability): Availability {
|
|
128
296
|
return {
|
|
129
297
|
products: [...availability.products].sort(compareStrings),
|
|
@@ -153,7 +321,7 @@ function sortedSlice(slice: SliceMeta): SliceMeta {
|
|
|
153
321
|
return out;
|
|
154
322
|
}
|
|
155
323
|
|
|
156
|
-
function exportRegistries(registries: Registries): Registries {
|
|
324
|
+
function exportRegistries(registries: Registries, withheld: ReadonlySet<string>): Registries {
|
|
157
325
|
return {
|
|
158
326
|
owners: [...registries.owners].sort(compareStrings),
|
|
159
327
|
audiences: [...registries.audiences].sort(compareStrings),
|
|
@@ -166,7 +334,7 @@ function exportRegistries(registries: Registries): Registries {
|
|
|
166
334
|
id: resource.id,
|
|
167
335
|
path: resource.path,
|
|
168
336
|
owner: resource.owner,
|
|
169
|
-
consumers:
|
|
337
|
+
consumers: resource.consumers.filter((id) => !withheld.has(id)).sort(compareStrings),
|
|
170
338
|
}))
|
|
171
339
|
.sort((a, b) => compareStrings(a.id, b.id)),
|
|
172
340
|
};
|
package/src/impact.ts
CHANGED
|
@@ -7,8 +7,22 @@
|
|
|
7
7
|
* reason sentence per step. Reachability is recorded, never asserted as
|
|
8
8
|
* proof that runtime behavior changed.
|
|
9
9
|
*/
|
|
10
|
+
import { graphHash } from "./hash.ts";
|
|
10
11
|
import { compareStrings } from "./report.ts";
|
|
11
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
Edge,
|
|
14
|
+
ExportImpactHit,
|
|
15
|
+
Graph,
|
|
16
|
+
ImpactHit,
|
|
17
|
+
ImpactReport,
|
|
18
|
+
ImpactReportSection,
|
|
19
|
+
ImpactStep,
|
|
20
|
+
LinkKind,
|
|
21
|
+
} from "./types.ts";
|
|
22
|
+
|
|
23
|
+
/** Said beside every impact report, because reachability is easy to read as more than it is (Core §10). */
|
|
24
|
+
export const IMPACT_NOTE =
|
|
25
|
+
"Reachability through authored links marks an artifact for review; it is not proof that runtime behavior changed (Core §10).";
|
|
12
26
|
|
|
13
27
|
const PHRASES: Record<LinkKind, string> = {
|
|
14
28
|
parent: "has {to} as its navigation parent",
|
|
@@ -88,3 +102,30 @@ function toHit(graph: Graph, id: string, path: ImpactStep[]): ImpactHit | null {
|
|
|
88
102
|
if (artifact === undefined) return null;
|
|
89
103
|
return { id, type: artifact.meta.type, title: artifact.meta.title, path };
|
|
90
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `reports.impact` for the export (spec/export.md §4.3): the impact report from
|
|
108
|
+
* every artifact in the graph, or from `starts` when given, with each hit
|
|
109
|
+
* reduced to its ID and path. A consumer joins hits to `artifacts` by ID.
|
|
110
|
+
*/
|
|
111
|
+
export function impactReport(graph: Graph, starts?: Iterable<string>): ImpactReportSection {
|
|
112
|
+
const ids = [...new Set(starts ?? graph.artifacts.keys())]
|
|
113
|
+
.filter((id) => graph.artifacts.has(id))
|
|
114
|
+
.sort(compareStrings);
|
|
115
|
+
const hit = (h: ImpactHit): ExportImpactHit => ({ id: h.id, path: h.path });
|
|
116
|
+
return {
|
|
117
|
+
graphHash: graphHash(graph),
|
|
118
|
+
note: IMPACT_NOTE,
|
|
119
|
+
starts: ids.map((id) => {
|
|
120
|
+
const report = impact(graph, id);
|
|
121
|
+
return {
|
|
122
|
+
start: id,
|
|
123
|
+
direct: report.direct.map(hit),
|
|
124
|
+
candidates: report.candidates.map(hit),
|
|
125
|
+
context: report.context.map(hit),
|
|
126
|
+
ancestors: report.ancestors,
|
|
127
|
+
withheld: 0,
|
|
128
|
+
};
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
}
|
package/src/index.ts
CHANGED