@nimiplatform/nimi-coding 0.3.1 → 0.4.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/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +285 -41
- package/README.zh-CN.md +294 -31
- package/bin/nimicoding.mjs +1 -1
- package/cli/commands/authority.mjs +849 -0
- package/cli/commands/clear.mjs +27 -7
- package/cli/commands/doctor.mjs +10 -1
- package/cli/commands/start.mjs +23 -27
- package/cli/commands/sync.mjs +21 -101
- package/cli/constants.mjs +7 -31
- package/cli/dev/check-authority-surface.mjs +173 -0
- package/cli/dev/check-product-conformance.mjs +917 -0
- package/cli/help.mjs +10 -17
- package/cli/index.mjs +6 -47
- package/cli/lib/authority/anchors.mjs +491 -0
- package/cli/lib/authority/audit.mjs +490 -0
- package/cli/lib/authority/closed-sets.mjs +646 -0
- package/cli/lib/authority/compile.mjs +110 -0
- package/cli/lib/authority/consumers.mjs +470 -0
- package/cli/lib/authority/diagnostics.mjs +87 -0
- package/cli/lib/authority/diff.mjs +287 -0
- package/cli/lib/authority/discover.mjs +447 -0
- package/cli/lib/authority/evidence-snapshot.mjs +1 -0
- package/cli/lib/authority/evidence.mjs +1432 -0
- package/cli/lib/authority/format.mjs +197 -0
- package/cli/lib/authority/git-snapshot.mjs +1065 -0
- package/cli/lib/authority/graph.mjs +351 -0
- package/cli/lib/authority/impact.mjs +252 -0
- package/cli/lib/authority/lexicon.mjs +55 -0
- package/cli/lib/authority/query.mjs +162 -0
- package/cli/lib/authority/repository-corpus.mjs +37 -0
- package/cli/lib/authority/repository-inventory.mjs +34 -0
- package/cli/lib/authority/review.mjs +222 -0
- package/cli/lib/authority/sarif.mjs +181 -0
- package/cli/lib/authority/scope-bindings.mjs +225 -0
- package/cli/lib/authority/source-map.mjs +105 -0
- package/cli/lib/authority/source-markdown.mjs +167 -0
- package/cli/lib/authority/source-yaml.mjs +312 -0
- package/cli/lib/authority/surface.mjs +79 -0
- package/cli/lib/authority/terms.mjs +421 -0
- package/cli/lib/authority/tracked-text.mjs +70 -0
- package/cli/lib/authority/validate.mjs +308 -0
- package/cli/lib/bootstrap.mjs +55 -71
- package/cli/lib/doctor.mjs +40 -123
- package/cli/lib/entrypoints.mjs +103 -105
- package/cli/lib/fs-helpers.mjs +139 -15
- package/cli/lib/internal/governance/config.mjs +0 -36
- package/cli/lib/shared.mjs +8 -21
- package/cli/lib/sync.mjs +81 -92
- package/cli/lib/value-helpers.mjs +0 -26
- package/cli/lib/yaml-helpers.mjs +1 -124
- package/cli/seeds/bootstrap.mjs +31 -87
- package/cli/seeds/seed-policy.yaml +9 -63
- package/contracts/authority-evidence-bindings.schema.yaml +53 -0
- package/contracts/authority-evidence-probe-results.schema.yaml +38 -0
- package/contracts/authority-impact-dispositions.schema.yaml +13 -0
- package/contracts/authority-scope-bindings.schema.yaml +15 -0
- package/contracts/authority-source.schema.yaml +64 -0
- package/contracts/authority-verifier-bindings.schema.yaml +20 -0
- package/methodology/authority-authoring.yaml +144 -0
- package/methodology/core.yaml +31 -26
- package/package.json +10 -6
- package/spec/authority-anchors.yaml +99 -0
- package/spec/authority-audit.yaml +85 -0
- package/spec/authority-authoring.yaml +150 -0
- package/spec/authority-closed-sets.yaml +65 -0
- package/spec/authority-consumers.yaml +70 -0
- package/spec/authority-context.yaml +50 -0
- package/spec/authority-discovery.yaml +119 -0
- package/spec/authority-evidence.yaml +200 -0
- package/spec/authority-graph.yaml +59 -0
- package/spec/authority-impact.yaml +84 -0
- package/spec/authority-review.yaml +89 -0
- package/spec/authority-terms.yaml +59 -0
- package/spec/product-scope.yaml +36 -23
- package/cli/commands/blueprint-audit.mjs +0 -91
- package/cli/commands/classify-spec-tree.mjs +0 -5
- package/cli/commands/generate-spec-derived-docs.mjs +0 -151
- package/cli/commands/generate-spec-migration-plan.mjs +0 -30
- package/cli/commands/surface-validator-command.mjs +0 -49
- package/cli/commands/validate-domain-admission.mjs +0 -5
- package/cli/commands/validate-guidance-bodies.mjs +0 -5
- package/cli/commands/validate-placement.mjs +0 -5
- package/cli/commands/validate-projection-edges.mjs +0 -5
- package/cli/commands/validate-spec-audit.mjs +0 -27
- package/cli/commands/validate-spec-governance.mjs +0 -144
- package/cli/commands/validate-spec-tree.mjs +0 -27
- package/cli/commands/validate-table-family.mjs +0 -5
- package/cli/commands/validate-tracked-output-admission.mjs +0 -5
- package/cli/lib/blueprint-audit.mjs +0 -363
- package/cli/lib/contracts.mjs +0 -21
- package/cli/lib/internal/contracts-loaders.mjs +0 -47
- package/cli/lib/internal/contracts-parse.mjs +0 -192
- package/cli/lib/internal/governance/runner.mjs +0 -35
- package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
- package/cli/lib/internal/validators-shared.mjs +0 -12
- package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
- package/cli/lib/internal/validators-spec.mjs +0 -380
- package/cli/lib/validators.mjs +0 -30
- package/config/bootstrap.yaml +0 -6
- package/config/spec-generation-inputs.yaml +0 -41
- package/contracts/domain-admission.schema.yaml +0 -56
- package/contracts/migration-inventory.schema.yaml +0 -15
- package/contracts/negative-fixtures.yaml +0 -26
- package/contracts/placement-contract.schema.yaml +0 -127
- package/contracts/projection-edge.schema.yaml +0 -46
- package/contracts/shared-enums.yaml +0 -63
- package/contracts/spec-generation-audit.schema.yaml +0 -46
- package/contracts/spec-generation-inputs.schema.yaml +0 -43
- package/contracts/spec-layout.schema.yaml +0 -32
- package/contracts/surface-taxonomy.schema.yaml +0 -139
- package/contracts/table-family.schema.yaml +0 -133
- package/contracts/tracked-output-admission.schema.yaml +0 -17
- package/methodology/four-closure-policy.yaml +0 -20
- package/methodology/role-separation-policy.yaml +0 -22
- package/methodology/spec-reconstruction.yaml +0 -69
- package/spec/_meta/spec-tree-model.yaml +0 -44
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { compileAuthorityPath } from "./compile.mjs";
|
|
6
|
+
import { compareText, makeDiagnostic, portablePath, sortDiagnostics } from "./diagnostics.mjs";
|
|
7
|
+
|
|
8
|
+
const DIFF_FORMAT = "nimicoding.authority-diff/v1";
|
|
9
|
+
|
|
10
|
+
async function sourceRoot(inputPath) {
|
|
11
|
+
const absolute = path.resolve(inputPath);
|
|
12
|
+
try {
|
|
13
|
+
const info = await stat(absolute);
|
|
14
|
+
return info.isDirectory() ? absolute : path.dirname(absolute);
|
|
15
|
+
} catch {
|
|
16
|
+
return path.dirname(absolute);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sidePath(file, basePath, side) {
|
|
21
|
+
const relative = portablePath(file, basePath);
|
|
22
|
+
const safe = path.isAbsolute(relative) ? path.basename(relative) : relative;
|
|
23
|
+
return path.posix.join(side, safe || ".");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compareRelated(left, right) {
|
|
27
|
+
return compareText(left.path, right.path)
|
|
28
|
+
|| left.range.start.line - right.range.start.line
|
|
29
|
+
|| left.range.start.column - right.range.start.column
|
|
30
|
+
|| compareText(left.pointer, right.pointer)
|
|
31
|
+
|| compareText(left.role, right.role);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sideDiagnostics(diagnostics, basePath, side) {
|
|
35
|
+
return diagnostics.map((diagnostic) => ({
|
|
36
|
+
...diagnostic,
|
|
37
|
+
path: sidePath(diagnostic.path, basePath, side),
|
|
38
|
+
related: diagnostic.related
|
|
39
|
+
.map((related) => ({ ...related, path: sidePath(related.path, basePath, side) }))
|
|
40
|
+
.sort(compareRelated),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function portableMapping(mapping, basePath) {
|
|
45
|
+
if (!mapping) return null;
|
|
46
|
+
return { ...mapping, file: portablePath(mapping.file, basePath) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mappedAt(compiled, unitIndex, pointer, basePath) {
|
|
50
|
+
const exact = compiled.sourceMap.fields[`/units/${unitIndex}${pointer}`];
|
|
51
|
+
if (exact) return portableMapping(exact, basePath);
|
|
52
|
+
const prefix = `/units/${unitIndex}${pointer}/`;
|
|
53
|
+
const nested = Object.entries(compiled.sourceMap.fields)
|
|
54
|
+
.filter(([candidate]) => candidate.startsWith(prefix))
|
|
55
|
+
.sort(([left], [right]) => compareText(left, right))[0]?.[1];
|
|
56
|
+
return portableMapping(nested, basePath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function addChange(changes, { unitId, operation = "modified", pointer, before, after, beforeSource = null, afterSource = null }) {
|
|
60
|
+
changes.push({
|
|
61
|
+
unitId,
|
|
62
|
+
operation,
|
|
63
|
+
pointer,
|
|
64
|
+
before,
|
|
65
|
+
after,
|
|
66
|
+
...(beforeSource ? { beforeSource } : {}),
|
|
67
|
+
...(afterSource ? { afterSource } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compareScalar(changes, beforeEntry, afterEntry, pointer, beforeBase, afterBase) {
|
|
72
|
+
const before = beforeEntry.unit;
|
|
73
|
+
const after = afterEntry.unit;
|
|
74
|
+
const read = (value, parts) => parts.reduce((current, part) => current?.[part], value);
|
|
75
|
+
const parts = pointer.slice(1).split("/");
|
|
76
|
+
const left = read(before, parts) ?? null;
|
|
77
|
+
const right = read(after, parts) ?? null;
|
|
78
|
+
if (JSON.stringify(left) === JSON.stringify(right)) return;
|
|
79
|
+
addChange(changes, {
|
|
80
|
+
unitId: after.id,
|
|
81
|
+
pointer,
|
|
82
|
+
before: left,
|
|
83
|
+
after: right,
|
|
84
|
+
beforeSource: mappedAt(beforeEntry.compiled, beforeEntry.index, pointer, beforeBase),
|
|
85
|
+
afterSource: mappedAt(afterEntry.compiled, afterEntry.index, pointer, afterBase),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function compareValueSet(changes, beforeEntry, afterEntry, pointer, beforeBase, afterBase) {
|
|
90
|
+
const parts = pointer.slice(1).split("/");
|
|
91
|
+
const read = (unit) => parts.reduce((current, part) => current?.[part], unit) ?? [];
|
|
92
|
+
const beforeValues = read(beforeEntry.unit);
|
|
93
|
+
const afterValues = read(afterEntry.unit);
|
|
94
|
+
const beforeSet = new Set(beforeValues.map((value) => JSON.stringify(value)));
|
|
95
|
+
const afterSet = new Set(afterValues.map((value) => JSON.stringify(value)));
|
|
96
|
+
for (const [operation, values, otherSet, entry, basePath, sourceSide] of [
|
|
97
|
+
["removed", beforeValues, afterSet, beforeEntry, beforeBase, "beforeSource"],
|
|
98
|
+
["added", afterValues, beforeSet, afterEntry, afterBase, "afterSource"],
|
|
99
|
+
]) {
|
|
100
|
+
values.forEach((value, index) => {
|
|
101
|
+
if (otherSet.has(JSON.stringify(value))) return;
|
|
102
|
+
addChange(changes, {
|
|
103
|
+
unitId: afterEntry.unit.id,
|
|
104
|
+
operation,
|
|
105
|
+
pointer,
|
|
106
|
+
before: operation === "removed" ? value : null,
|
|
107
|
+
after: operation === "added" ? value : null,
|
|
108
|
+
[sourceSide]: mappedAt(entry.compiled, entry.index, `${pointer}/${index}`, basePath),
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compareRelations(changes, beforeEntry, afterEntry, beforeBase, afterBase) {
|
|
115
|
+
const beforeRelations = beforeEntry.unit.relations;
|
|
116
|
+
const afterRelations = afterEntry.unit.relations;
|
|
117
|
+
const beforeSet = new Set(beforeRelations.map((edge) => `${edge.type}\0${edge.target}`));
|
|
118
|
+
const afterSet = new Set(afterRelations.map((edge) => `${edge.type}\0${edge.target}`));
|
|
119
|
+
for (const [operation, edges, otherSet, entry, basePath, sourceSide] of [
|
|
120
|
+
["removed", beforeRelations, afterSet, beforeEntry, beforeBase, "beforeSource"],
|
|
121
|
+
["added", afterRelations, beforeSet, afterEntry, afterBase, "afterSource"],
|
|
122
|
+
]) {
|
|
123
|
+
edges.forEach((edge, index) => {
|
|
124
|
+
if (otherSet.has(`${edge.type}\0${edge.target}`)) return;
|
|
125
|
+
addChange(changes, {
|
|
126
|
+
unitId: afterEntry.unit.id,
|
|
127
|
+
operation,
|
|
128
|
+
pointer: "/relations",
|
|
129
|
+
before: operation === "removed" ? edge : null,
|
|
130
|
+
after: operation === "added" ? edge : null,
|
|
131
|
+
[sourceSide]: mappedAt(entry.compiled, entry.index, `/relations/${index}`, basePath),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sideMappedAt(compiled, unitIndex, pointer, basePath, side) {
|
|
138
|
+
const mapped = mappedAt(compiled, unitIndex, pointer, basePath);
|
|
139
|
+
return { ...mapped, file: path.posix.join(side, mapped.file) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function transitionDiagnostics(before, after, beforeBase, afterBase) {
|
|
143
|
+
const diagnostics = [];
|
|
144
|
+
const afterById = new Map(after.ir.units.map((unit) => [unit.id, unit]));
|
|
145
|
+
before.ir.units.forEach((unit, index) => {
|
|
146
|
+
if (afterById.has(unit.id)) return;
|
|
147
|
+
const mapped = sideMappedAt(before, index, "/id", beforeBase, "before");
|
|
148
|
+
diagnostics.push(makeDiagnostic({
|
|
149
|
+
code: "AUTH_DIFF_TRANSITION_INVALID",
|
|
150
|
+
file: mapped.file,
|
|
151
|
+
range: mapped.range,
|
|
152
|
+
pointer: mapped.sourcePointer,
|
|
153
|
+
reason: `authority identity cannot physically disappear: ${unit.id}`,
|
|
154
|
+
repair: unit.lifecycle === "removed"
|
|
155
|
+
? "retain the removed tombstone; permanent reserved-ID storage is not admitted"
|
|
156
|
+
: "replace the active unit with a valid removed tombstone carrying an explicit reason",
|
|
157
|
+
}));
|
|
158
|
+
});
|
|
159
|
+
const beforeById = new Map(before.ir.units.map((unit) => [unit.id, unit]));
|
|
160
|
+
after.ir.units.forEach((unit, index) => {
|
|
161
|
+
const previous = beforeById.get(unit.id);
|
|
162
|
+
if (!previous) return;
|
|
163
|
+
const mapped = sideMappedAt(after, index, "/id", afterBase, "after");
|
|
164
|
+
if (previous.lifecycle === "removed" && unit.lifecycle !== "removed") diagnostics.push(makeDiagnostic({
|
|
165
|
+
code: "AUTH_DIFF_TRANSITION_INVALID",
|
|
166
|
+
file: mapped.file,
|
|
167
|
+
range: mapped.range,
|
|
168
|
+
pointer: mapped.sourcePointer,
|
|
169
|
+
reason: `removed identity cannot return to ${unit.lifecycle}: ${unit.id}`,
|
|
170
|
+
repair: "retain the removed identity and declare a new product-authorized ID",
|
|
171
|
+
}));
|
|
172
|
+
if (previous.kind !== unit.kind) diagnostics.push(makeDiagnostic({
|
|
173
|
+
code: "AUTH_DIFF_TRANSITION_INVALID",
|
|
174
|
+
file: mapped.file,
|
|
175
|
+
range: mapped.range,
|
|
176
|
+
pointer: mapped.sourcePointer,
|
|
177
|
+
reason: `authority identity cannot change kind from ${previous.kind} to ${unit.kind}: ${unit.id}`,
|
|
178
|
+
repair: "retain the original identity and declare a distinct ID for the new kind",
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
181
|
+
return sortDiagnostics(diagnostics);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function compareUnits(before, after, beforeBase, afterBase) {
|
|
185
|
+
const changes = [];
|
|
186
|
+
const beforeById = new Map(before.ir.units.map((unit, index) => [unit.id, { unit, index, compiled: before }]));
|
|
187
|
+
const afterById = new Map(after.ir.units.map((unit, index) => [unit.id, { unit, index, compiled: after }]));
|
|
188
|
+
const ids = [...new Set([...beforeById.keys(), ...afterById.keys()])].sort(compareText);
|
|
189
|
+
for (const id of ids) {
|
|
190
|
+
const left = beforeById.get(id);
|
|
191
|
+
const right = afterById.get(id);
|
|
192
|
+
if (!left) {
|
|
193
|
+
addChange(changes, {
|
|
194
|
+
unitId: id,
|
|
195
|
+
operation: "unit_added",
|
|
196
|
+
pointer: "",
|
|
197
|
+
before: null,
|
|
198
|
+
after: right.unit,
|
|
199
|
+
afterSource: mappedAt(after, right.index, "/id", afterBase),
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!right) continue;
|
|
204
|
+
for (const pointer of ["/kind", "/owner", "/lifecycle", "/metadata/title", "/metadata/reason", "/semantic/meaning", "/semantic/modality", "/semantic/statement", "/semantic/condition", "/semantic/failure"]) {
|
|
205
|
+
compareScalar(changes, left, right, pointer, beforeBase, afterBase);
|
|
206
|
+
}
|
|
207
|
+
compareValueSet(changes, left, right, "/semantic/scope", beforeBase, afterBase);
|
|
208
|
+
compareRelations(changes, left, right, beforeBase, afterBase);
|
|
209
|
+
}
|
|
210
|
+
return changes.sort((left, right) => (
|
|
211
|
+
compareText(left.unitId, right.unitId)
|
|
212
|
+
|| compareText(left.pointer, right.pointer)
|
|
213
|
+
|| compareText(left.operation, right.operation)
|
|
214
|
+
|| compareText(JSON.stringify(left.before), JSON.stringify(right.before))
|
|
215
|
+
|| compareText(JSON.stringify(left.after), JSON.stringify(right.after))
|
|
216
|
+
));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function canonicalDiffBytes(diff) {
|
|
220
|
+
return Buffer.byteLength(JSON.stringify(diff), "utf8");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function budgetDiagnostic(requiredBytes, maxBytes) {
|
|
224
|
+
return makeDiagnostic({
|
|
225
|
+
code: "AUTH_DIFF_BUDGET",
|
|
226
|
+
file: ".",
|
|
227
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
228
|
+
pointer: "",
|
|
229
|
+
reason: `complete semantic diff requires ${requiredBytes} UTF-8 bytes but max-bytes is ${maxBytes}`,
|
|
230
|
+
repair: "increase the explicit byte budget; partial semantic diff is forbidden",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function diffAuthorityPaths(beforePath, afterPath, { maxBytes = null } = {}) {
|
|
235
|
+
const [before, after, beforeBase, afterBase] = await Promise.all([
|
|
236
|
+
compileAuthorityPath(beforePath),
|
|
237
|
+
compileAuthorityPath(afterPath),
|
|
238
|
+
sourceRoot(beforePath),
|
|
239
|
+
sourceRoot(afterPath),
|
|
240
|
+
]);
|
|
241
|
+
if (!before.ok || !after.ok) return {
|
|
242
|
+
ok: false,
|
|
243
|
+
diagnostics: sortDiagnostics([
|
|
244
|
+
...sideDiagnostics(before.diagnostics, beforeBase, "before"),
|
|
245
|
+
...sideDiagnostics(after.diagnostics, afterBase, "after"),
|
|
246
|
+
]),
|
|
247
|
+
fileCount: before.fileCount + after.fileCount,
|
|
248
|
+
unitCount: 0,
|
|
249
|
+
diff: null,
|
|
250
|
+
};
|
|
251
|
+
const invalidTransitions = transitionDiagnostics(before, after, beforeBase, afterBase);
|
|
252
|
+
if (invalidTransitions.length > 0) return {
|
|
253
|
+
ok: false,
|
|
254
|
+
diagnostics: invalidTransitions,
|
|
255
|
+
fileCount: before.fileCount + after.fileCount,
|
|
256
|
+
unitCount: 0,
|
|
257
|
+
diff: null,
|
|
258
|
+
};
|
|
259
|
+
const changes = compareUnits(before, after, beforeBase, afterBase);
|
|
260
|
+
const diff = {
|
|
261
|
+
format: DIFF_FORMAT,
|
|
262
|
+
changes,
|
|
263
|
+
summary: {
|
|
264
|
+
units: new Set(changes.map((change) => change.unitId)).size,
|
|
265
|
+
changes: changes.length,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
const payloadBytes = canonicalDiffBytes(diff);
|
|
269
|
+
if (maxBytes !== null && payloadBytes > maxBytes) return {
|
|
270
|
+
ok: false,
|
|
271
|
+
diagnostics: [budgetDiagnostic(payloadBytes, maxBytes)],
|
|
272
|
+
fileCount: before.fileCount + after.fileCount,
|
|
273
|
+
unitCount: 0,
|
|
274
|
+
payloadBytes,
|
|
275
|
+
diff: null,
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
diagnostics: [],
|
|
280
|
+
fileCount: before.fileCount + after.fileCount,
|
|
281
|
+
unitCount: new Set([...before.ir.units, ...after.ir.units].map((unit) => unit.id)).size,
|
|
282
|
+
payloadBytes,
|
|
283
|
+
diff,
|
|
284
|
+
before,
|
|
285
|
+
after,
|
|
286
|
+
};
|
|
287
|
+
}
|