@aburi/diff 0.4.0 → 0.5.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/README.md +5 -2
- package/dist/index.d.mts +182 -276
- package/dist/index.mjs +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -12,8 +12,11 @@ Symbols are paired across base / head via cascading stages:
|
|
|
12
12
|
1. **id-match** — `<language>:<path>#<qname>` equal → same Symbol.
|
|
13
13
|
2. **git-rename** — optional table (from `git diff --find-renames`) rescues moved
|
|
14
14
|
files whose ids diverged only in the path segment.
|
|
15
|
-
3. **logic-fingerprint** — remaining Symbols with the same
|
|
16
|
-
hash pair up (rename-invariant, whitespace-invariant).
|
|
15
|
+
3. **logic-fingerprint** — remaining Symbols of the same kind with the same
|
|
16
|
+
`fingerprint.logic` hash pair up (rename-invariant, whitespace-invariant). Where
|
|
17
|
+
that logic names nothing (a class, a body that only calls something), unrelated
|
|
18
|
+
bodies share the hash, so a pair there also needs names of two words or more
|
|
19
|
+
with a similarity of 0.85.
|
|
17
20
|
4. **name+signature** — same simple name + compatible `Signature` (input/output
|
|
18
21
|
set + throws) — the fuzzy layer.
|
|
19
22
|
5. **dropped-weak** — last-resort match for pairs where both sides are dropped
|
package/dist/index.d.mts
CHANGED
|
@@ -3,23 +3,15 @@ import { Component, ComponentDiff, Dependency, DependencyDiff, DependencyEndpoin
|
|
|
3
3
|
|
|
4
4
|
//#region src/components.d.ts
|
|
5
5
|
/**
|
|
6
|
-
* `docs/design/diff-algorithm.md`
|
|
7
|
-
* #2) and does not check it: `buildDiff` establishes that
|
|
8
|
-
* reaching this export directly owns the obligation, because the lookup map here is
|
|
9
|
-
* last-write-wins
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* so the projection layer had no before/after pair to render. The delta booleans stay exactly
|
|
16
|
-
* what they were — a summary of the three axes a reviewer scans for architectural movement — and
|
|
17
|
-
* a `changed[]` entry with all three `false` is a well-formed answer meaning "something else
|
|
18
|
-
* about this component moved", not a bug.
|
|
19
|
-
*
|
|
20
|
-
* `modified` deltas are intentionally absent from this diff (diff-algorithm.md §5.2.3): fields
|
|
21
|
-
* are reported as before/after pairs via the `changed[]` entries so consumers can render them
|
|
22
|
-
* without stringifying arrays.
|
|
6
|
+
* `docs/design/diff-algorithm.md` — Component diff. Assumes `components[].id` is unique
|
|
7
|
+
* on each side (ir-schema.md #2) and does not check it: `buildDiff` establishes that, and
|
|
8
|
+
* a caller reaching this export directly owns the obligation, because the lookup map here is
|
|
9
|
+
* last-write-wins.
|
|
10
|
+
*
|
|
11
|
+
* Any field that differs makes a Component `changed` — the whole object is compared, not the
|
|
12
|
+
* three axes the delta names, so a `changed[]` entry with all three booleans `false` is a
|
|
13
|
+
* well-formed answer meaning "something else about this component moved". `modified` deltas
|
|
14
|
+
* are intentionally absent: fields are reported as before/after pairs.
|
|
23
15
|
*/
|
|
24
16
|
declare function diffComponents(base: readonly Component[], head: readonly Component[]): ComponentDiff;
|
|
25
17
|
/**
|
|
@@ -29,58 +21,40 @@ declare function diffComponents(base: readonly Component[], head: readonly Compo
|
|
|
29
21
|
* `symbolFiles` is keyed on the endpoint id exactly as `dependencies[]` spells it, and its
|
|
30
22
|
* values come from `symbols[].source.file` — the same space `stats.skippedFiles[].path` is in,
|
|
31
23
|
* and the same space `buildDiff` classifies Symbols by. Reading the file out of the id's path
|
|
32
|
-
* segment instead would
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* A Component endpoint is absent from the map, which is how it stays out of the reclassification
|
|
37
|
-
* without a special case: a Component is an aggregate over roots and has no file to lose.
|
|
24
|
+
* segment instead would be a second answer to "which file is this endpoint in" that nothing
|
|
25
|
+
* forces to agree with the first. A Component endpoint is absent from the map, which keeps it
|
|
26
|
+
* out of the reclassification without a special case: an aggregate over roots has no file to
|
|
27
|
+
* lose.
|
|
38
28
|
*/
|
|
39
29
|
interface DependencySideView {
|
|
40
30
|
/**
|
|
41
|
-
* `source.file` of every Symbol this document holds, by
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* is the answer for a Component id. Demanding the kind up front would move that decision to
|
|
45
|
-
* the caller, where it would need a second Symbol-id silhouette test to make.
|
|
31
|
+
* `source.file` of every Symbol this document holds, keyed by `DependencyEndpoint` rather
|
|
32
|
+
* than `SymbolId` because the lookup happens with an endpoint whose kind is not yet known,
|
|
33
|
+
* and "absent" is the answer for a Component id.
|
|
46
34
|
*/
|
|
47
35
|
symbolFiles: ReadonlyMap<DependencyEndpoint, RelativePath>;
|
|
48
|
-
/**
|
|
49
|
-
* Files this document never analysed, by path, with the reason it gave. `RelativePath` on
|
|
50
|
-
* both sides of the file space is what states the bridge these two maps exist to make — one
|
|
51
|
-
* map's values are the other's keys — though the alias is unbranded, so it is intent rather
|
|
52
|
-
* than enforcement.
|
|
53
|
-
*/
|
|
36
|
+
/** Files this document never analysed, by path, with the reason it gave. */
|
|
54
37
|
lostFiles: ReadonlyMap<RelativePath, SkipReason>;
|
|
55
38
|
}
|
|
56
39
|
/**
|
|
57
|
-
* Build a side view from a document.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* so "a Symbol reported unknown and the edges it took with it cannot disagree about which file
|
|
61
|
-
* went missing" is a property of the wiring rather than a claim in a comment.
|
|
62
|
-
*
|
|
63
|
-
* Exported because `DependencySideView` is public and `diffDependencies` requires one: without
|
|
64
|
-
* a factory a caller would have to reproduce the `symbols[].source.file` keying and the
|
|
65
|
-
* `stats.skippedFiles` read, and the likeliest outcome of that is a caller who gets it wrong.
|
|
40
|
+
* Build a side view from a document. Exported because `DependencySideView` is public and
|
|
41
|
+
* `diffDependencies` requires one; `buildDiff` reads `lostFiles` for its own Symbol
|
|
42
|
+
* classification from this same object.
|
|
66
43
|
*/
|
|
67
44
|
declare function dependencySideView(ir: IR): DependencySideView;
|
|
68
45
|
/**
|
|
69
|
-
* `docs/design/diff-algorithm.md`
|
|
70
|
-
* `(from, to, via)` triple; direction and effect changes
|
|
71
|
-
*
|
|
72
|
-
* terms as `diffComponents
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* The return type declares `unknown` present, where the schema leaves it optional for documents
|
|
83
|
-
* that predate the field. A reader may find it missing; this function always writes it.
|
|
46
|
+
* `docs/design/diff-algorithm.md` — Dependency diff. Identity is the composite
|
|
47
|
+
* `(from, to, via)` triple; direction and effect changes surface as an added + removed pair so
|
|
48
|
+
* `modified` is not part of the schema. Uniqueness of the triple is the caller's
|
|
49
|
+
* obligation on the same terms as `diffComponents`.
|
|
50
|
+
*
|
|
51
|
+
* `sides` separates a deletion from a loss and is required rather than optional:
|
|
52
|
+
* omitting it would silently classify every edge into a lost file as a deletion while still
|
|
53
|
+
* writing `unknown: []`, which the schema defines as "nothing was unknown". A caller with no
|
|
54
|
+
* skip list passes a side view whose `lostFiles` is empty.
|
|
55
|
+
*
|
|
56
|
+
* The return type declares `unknown` present, where the schema leaves it optional for
|
|
57
|
+
* documents that predate the field.
|
|
84
58
|
*/
|
|
85
59
|
declare function diffDependencies(base: readonly Dependency[], head: readonly Dependency[], sides: {
|
|
86
60
|
base: DependencySideView;
|
|
@@ -95,18 +69,21 @@ declare const MAX_LINE_FUZZ = 10;
|
|
|
95
69
|
declare const MIN_LINE_FUZZ = 0;
|
|
96
70
|
interface DeltaOptions {
|
|
97
71
|
/**
|
|
98
|
-
*
|
|
72
|
+
* Line fuzz for pairing an edited rule/call/decorator with its predecessor
|
|
73
|
+
* (diff-algorithm.md); an unchanged one pairs however far it moved. Must be an integer in
|
|
99
74
|
* `[MIN_LINE_FUZZ, MAX_LINE_FUZZ]` (`0..10`); anything outside — or a non-finite value —
|
|
100
|
-
* throws `DiffError({ code: "invalid-line-fuzz" })`. Setting `0`
|
|
101
|
-
* the
|
|
75
|
+
* throws `DiffError({ code: "invalid-line-fuzz" })`. Setting `0` pairs an edit only with an
|
|
76
|
+
* element on the same line, and does not stop an unchanged element from pairing; omitting the
|
|
77
|
+
* field falls back to `DEFAULT_LINE_FUZZ` (2).
|
|
102
78
|
*/
|
|
103
79
|
lineFuzz?: number;
|
|
104
80
|
}
|
|
105
81
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
82
|
+
* The full per-Symbol delta between two paired Symbols (diff-algorithm.md). The axis booleans
|
|
83
|
+
* come from fingerprint comparison; the array deltas from identity-preserving pairing, with line
|
|
84
|
+
* fuzz deciding only how far an edited element may sit from the one it replaced.
|
|
85
|
+
* `confidenceChanged` is always written, `false` included, so a reader can tell it from a diff
|
|
86
|
+
* that predates the field.
|
|
110
87
|
*/
|
|
111
88
|
declare function computeSymbolDelta(base: Symbol, head: Symbol, options?: DeltaOptions): SymbolDelta;
|
|
112
89
|
//#endregion
|
|
@@ -124,10 +101,10 @@ interface SymbolPair {
|
|
|
124
101
|
/** Optional map returned by git for stage 2 (`old path` → `new path`). */
|
|
125
102
|
type GitRenameMap = ReadonlyMap<string, string>;
|
|
126
103
|
/**
|
|
127
|
-
*
|
|
128
|
-
* is the highest-confidence signal (no heuristics, just hash lookup).
|
|
104
|
+
* Stage 1 (diff-algorithm.md) — segments Symbols with identical `id` into paired matches.
|
|
105
|
+
* Runs first because it is the highest-confidence signal (no heuristics, just hash lookup).
|
|
129
106
|
*
|
|
130
|
-
* Assumes `id` is unique on each side (ir-schema.md
|
|
107
|
+
* Assumes `id` is unique on each side (ir-schema.md #1) and does not check it:
|
|
131
108
|
* `buildDiff` establishes that before calling, and a caller reaching this export directly
|
|
132
109
|
* owns the obligation. A repeat on the head side loses all but the last entry to the lookup
|
|
133
110
|
* map and then removes every one of them from `remainingHead`; a repeat on the base side
|
|
@@ -140,10 +117,10 @@ declare function matchStageId(base: readonly Symbol[], head: readonly Symbol[]):
|
|
|
140
117
|
remainingHead: Symbol[];
|
|
141
118
|
};
|
|
142
119
|
/**
|
|
143
|
-
*
|
|
144
|
-
* scans), rewrite the base id with the head-side file path and look
|
|
145
|
-
* files renamed onto one target predict the same head id, so the
|
|
146
|
-
* before one is chosen.
|
|
120
|
+
* Stage 2 (diff-algorithm.md) — git rename detection. When a rename map is available
|
|
121
|
+
* (typical of ref-driven scans), rewrite the base id with the head-side file path and look
|
|
122
|
+
* for a hit. Two base files renamed onto one target predict the same head id, so the
|
|
123
|
+
* claimants are collected before one is chosen.
|
|
147
124
|
*
|
|
148
125
|
* The rewriter only touches the `file` portion of the id — the `<language>:` prefix and
|
|
149
126
|
* the trailing `#<qualified-name>` segment stay unchanged so a moved file with the same
|
|
@@ -156,14 +133,19 @@ declare function matchStageGitRename(remainingBase: readonly Symbol[], remaining
|
|
|
156
133
|
remainingHead: Symbol[];
|
|
157
134
|
};
|
|
158
135
|
/**
|
|
159
|
-
*
|
|
136
|
+
* Stage 3 (diff-algorithm.md) — group both sides by kind and `fingerprint.logic` and pair
|
|
137
|
+
* within each group. Two branches:
|
|
160
138
|
* - single base candidate → paired with `logic-fingerprint`, no similarity test
|
|
161
139
|
* - several → `nameSimilarity` disambiguates at ≥ 0.85; a group that cannot reach it is left
|
|
162
140
|
* whole for stage 4 to re-evaluate
|
|
163
141
|
*
|
|
164
|
-
* Dropped symbols are excluded
|
|
142
|
+
* Dropped symbols are excluded — their logic fingerprint is the sentinel
|
|
165
143
|
* `"000000000000"` and would collide with every other dropped Symbol in the workspace.
|
|
166
144
|
* They flow to the stage-4.5 weak matcher instead.
|
|
145
|
+
*
|
|
146
|
+
* A logic value that names nothing (`logicNamesNothing`) is shared the same way, but by
|
|
147
|
+
* Symbols that are kept: its group forms and pairs, on names alone and without the first
|
|
148
|
+
* branch. See `pairWithinLogicGroup`.
|
|
167
149
|
*/
|
|
168
150
|
declare function matchStageLogicFingerprint(remainingBase: readonly Symbol[], remainingHead: readonly Symbol[]): {
|
|
169
151
|
matched: SymbolPair[];
|
|
@@ -171,39 +153,15 @@ declare function matchStageLogicFingerprint(remainingBase: readonly Symbol[], re
|
|
|
171
153
|
remainingHead: Symbol[];
|
|
172
154
|
};
|
|
173
155
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* §3.4.6's owner gate runs before the score: a pair whose owners are not the same class, or
|
|
179
|
-
* one a rename of the other, is not the same Symbol however well the rest agrees, and no
|
|
180
|
-
* weight small enough to leave the name axis meaning something can outvote a matching member
|
|
181
|
-
* name and signature.
|
|
182
|
-
*
|
|
183
|
-
* The composite score, over the pairs that clear it:
|
|
156
|
+
* Stage 4 (diff-algorithm.md) — name + signature similarity with a `(kind, signatureNullness)`
|
|
157
|
+
* bucket pre-filter, so a class body is never paired against a function. The owner gate runs
|
|
158
|
+
* before the score; over the pairs that clear it:
|
|
184
159
|
* 0.5 * memberSimilarity + 0.3 * signatureSimilarity + 0.2 * OWNER_AXIS_SATISFIED
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* §3.4.3 threshold table, applied per head by the token count of its last name segment:
|
|
191
|
-
* - 1 token → 1.0 (an exact match on both remaining axes)
|
|
192
|
-
* - 2 tokens → 0.95 (strict, avoids `getUser` vs `getUsers`)
|
|
193
|
-
* - default → 0.85
|
|
194
|
-
*
|
|
195
|
-
* §3.4.3 also names two Symbols the stage does not read at all, because for them the score is
|
|
196
|
-
* high on evidence it does not have rather than on a resemblance: one whose signature is
|
|
197
|
-
* null, since `signatureSimilarity(null, null)` is 1.0 and the bucket key restricts it to
|
|
198
|
-
* candidates that are equally signature-less; and one whose qualified name carries a single
|
|
199
|
-
* distinct token, since that is the whole of what its identity amounts to. Neither can be
|
|
200
|
-
* expressed as a threshold — the second wants a bar above the top of the scale.
|
|
201
|
-
*
|
|
202
|
-
* Every pairing in a bucket that clears its head’s threshold becomes a candidate, and the
|
|
203
|
-
* candidates are settled in score order rather than one head at a time — see
|
|
204
|
-
* `acceptInScoreOrder`. That doubles the pairings scored, from a bucket that shrank as heads
|
|
205
|
-
* consumed it to the full bucket every time; `createNameScorer` more than pays for it by
|
|
206
|
-
* tokenising each distinct name once per pass instead of once per comparison.
|
|
160
|
+
* `memberSimilarity` reads the last segment only, since the gate has settled the owner and
|
|
161
|
+
* reading it again would charge twice. Thresholds are per head, see `thresholdFor`; the two
|
|
162
|
+
* Symbols the stage does not read at all are explained at `saysEnoughToPair`. Candidates are
|
|
163
|
+
* settled in score order (`acceptInScoreOrder`), and `createNameScorer` pays for the full
|
|
164
|
+
* bucket being scored by tokenising each name once per pass.
|
|
207
165
|
*/
|
|
208
166
|
declare function matchStageNameSignature(remainingBase: readonly Symbol[], remainingHead: readonly Symbol[]): {
|
|
209
167
|
matched: SymbolPair[];
|
|
@@ -211,32 +169,18 @@ declare function matchStageNameSignature(remainingBase: readonly Symbol[], remai
|
|
|
211
169
|
remainingHead: Symbol[];
|
|
212
170
|
};
|
|
213
171
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* false-positive risk because dropped Symbols sit outside the IR's main review surface.
|
|
172
|
+
* Stage 4.5 (diff-algorithm.md) — dropped-only weak matcher. With the fingerprint zeroed, the only
|
|
173
|
+
* signals left are the last segment of the qualified name and the file basename, and either alone
|
|
174
|
+
* pairs — deliberately lax, since dropped Symbols sit outside the main review surface.
|
|
218
175
|
*
|
|
219
|
-
* What it does not accept is a signal that identifies nothing
|
|
220
|
-
*
|
|
221
|
-
* kind under one with every other, at a score they all tied on, and the results landed in
|
|
176
|
+
* What it does not accept is a signal that identifies nothing: a basename hit on `index.ts`
|
|
177
|
+
* paired every dropped Symbol of one kind with every other and landed them in
|
|
222
178
|
* `summary.moved`, which `--fail-on moved` gates on. So a half counts only when exactly one
|
|
223
|
-
* dropped base and one dropped head carry that key
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* basename used to produce.
|
|
229
|
-
*
|
|
230
|
-
* "Exactly one" is counted over the Symbols this stage is handed, not over every dropped
|
|
231
|
-
* Symbol in the Document — stages 1 and 2 have already taken theirs, and a key they emptied
|
|
232
|
-
* out identifies again. That is the intended reading, since the question is which leftover a
|
|
233
|
-
* key picks out, but it is also how a shared `index.ts` comes back: three dropped classes
|
|
234
|
-
* under one, two of them matched by id, and the basename identifies the two that are left.
|
|
235
|
-
*
|
|
236
|
-
* It also leaves nothing for §3.4.5's 0.5-per-half scale to rank, so §3.8's sweep is the
|
|
237
|
-
* wrong instrument: it settles conflicts by score, and with none to settle by it would drop
|
|
238
|
-
* one identified pairing for another over nothing but the id it sorts under. `bestPairing`
|
|
239
|
-
* takes a maximum matching instead.
|
|
179
|
+
* dropped base and one dropped head carry that key — counted over the Symbols this stage is
|
|
180
|
+
* handed, since a key the earlier stages emptied out identifies again. The candidates are
|
|
181
|
+
* then the identifying keys themselves, at most `2 × min(base, head)` of them, all worth the
|
|
182
|
+
* same; the score-ordered sweep would settle a conflict on nothing but id order, so
|
|
183
|
+
* `bestPairing` takes a maximum matching instead.
|
|
240
184
|
*/
|
|
241
185
|
declare function matchStageDroppedWeak(remainingBase: readonly Symbol[], remainingHead: readonly Symbol[]): {
|
|
242
186
|
matched: SymbolPair[];
|
|
@@ -246,12 +190,9 @@ declare function matchStageDroppedWeak(remainingBase: readonly Symbol[], remaini
|
|
|
246
190
|
//#endregion
|
|
247
191
|
//#region src/diff.d.ts
|
|
248
192
|
/**
|
|
249
|
-
* The two counters `buildDiff` always writes
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* optionality is about *documents*, not about this function — a caller holding a value it just
|
|
253
|
-
* built should not have to re-decide what "absent" would have meant. Same reason
|
|
254
|
-
* `diffDependencies` declares its `unknown` array present.
|
|
193
|
+
* The two counters `buildDiff` always writes. They are optional on `Summary` only so a diff
|
|
194
|
+
* written before they existed stays valid; a caller holding a freshly built value should not
|
|
195
|
+
* have to re-decide what "absent" means.
|
|
255
196
|
*/
|
|
256
197
|
interface UnknownCounters {
|
|
257
198
|
unknown: number;
|
|
@@ -270,70 +211,48 @@ interface DiffInput {
|
|
|
270
211
|
};
|
|
271
212
|
/** Optional stage-2 rename table. When null/undefined stage 2 is skipped. */
|
|
272
213
|
gitRenames?: GitRenameMap | null;
|
|
273
|
-
/** Passed through to computeSymbolDelta (
|
|
214
|
+
/** Passed through to computeSymbolDelta (line fuzz, diff-algorithm.md). */
|
|
274
215
|
delta?: DeltaOptions;
|
|
275
216
|
}
|
|
276
217
|
/**
|
|
277
218
|
* Top-level entry: run the 5-stage matcher, classify each pair, produce array deltas,
|
|
278
219
|
* fold in Component / Dependency diffs, and assemble the `aburi.diff.v1` JSON projection.
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* serialisation.
|
|
220
|
+
* Pure; `writeCanonicalDiff` serialises, so callers can run the Markdown projection or the
|
|
221
|
+
* `--fail-on` gate over the result first.
|
|
282
222
|
*/
|
|
283
223
|
declare function buildDiff(input: DiffInput): DiffResult & {
|
|
284
224
|
notCompared: NotComparedFile[];
|
|
285
225
|
summary: Summary & UnknownCounters;
|
|
286
226
|
};
|
|
287
227
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* `stats.skippedFiles` is Class B: absent means the writer predates the field, not that
|
|
291
|
-
* nothing was lost. An empty map is the honest answer either way — with no enumeration
|
|
292
|
-
* there is no absence this function can explain, and guessing from
|
|
293
|
-
* `totalFiles > parsedFiles` would attach a reason to whichever Symbols happened to be
|
|
294
|
-
* missing. The CLI says so on stderr instead; a diff cannot invent the list.
|
|
295
|
-
*/
|
|
296
|
-
/**
|
|
297
|
-
* Byte-deterministic serialiser for a DiffResult. Delegates to `@aburi/core`
|
|
298
|
-
* `serializeCanonical` so the sort order, NFC normalisation, and codepoint key sort are
|
|
299
|
-
* shared with the IR side.
|
|
228
|
+
* Byte-deterministic serialiser for a DiffResult, sharing `@aburi/core`'s canonical sort
|
|
229
|
+
* order, NFC normalisation and key sort with the IR side.
|
|
300
230
|
*/
|
|
301
231
|
declare function writeCanonicalDiff(diff: DiffResult, options?: SerializeOptions): string;
|
|
302
232
|
//#endregion
|
|
303
233
|
//#region src/errors.d.ts
|
|
304
|
-
type DiffErrorCode = /** `base.$schema` and `head.$schema` disagree
|
|
234
|
+
type DiffErrorCode = /** `base.$schema` and `head.$schema` disagree (diff-algorithm.md). */"schema-mismatch"
|
|
305
235
|
/**
|
|
306
|
-
* `
|
|
307
|
-
*
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* A non-empty `$schema` is raised under this code too. That one is `buildDiff`'s own
|
|
315
|
-
* requirement rather than the schema's — two Documents that both say `""` agree with each
|
|
316
|
-
* other, so `schema-mismatch` would never fire on the pair.
|
|
317
|
-
*
|
|
318
|
-
* Not the semantic invariants: those are statements about a Document whose answer the diff
|
|
319
|
-
* does not depend on, and refusing an unsorted `symbols[]` would withhold a diff the
|
|
320
|
-
* matcher can produce.
|
|
236
|
+
* `DeltaOptions.lineFuzz` was not an integer in the documented [0, 10] range
|
|
237
|
+
* (diff-algorithm.md).
|
|
238
|
+
*/
|
|
239
|
+
| "invalid-line-fuzz"
|
|
240
|
+
/**
|
|
241
|
+
* `baseIR` or `headIR` is not a Document of the shape `aburi.ir.v1` requires (invariant #20,
|
|
242
|
+
* `checkDocumentShape`), or its `$schema` is empty. Not the semantic invariants — see
|
|
243
|
+
* `assertDiffable`.
|
|
321
244
|
*/
|
|
322
245
|
| "ir-shape-invalid"
|
|
323
246
|
/**
|
|
324
247
|
* `baseIR` or `headIR` repeats an identity the diff keys on: a `symbols[].id`, a
|
|
325
248
|
* `components[].id`, or a `dependencies[]` `(from, to, via)` triple. Distinct from
|
|
326
249
|
* `ir-shape-invalid` because the Document is well-formed — the collision changes the
|
|
327
|
-
* answer rather than preventing one
|
|
250
|
+
* answer rather than preventing one (ir-schema.md #1 / #2 / #13).
|
|
328
251
|
*/
|
|
329
252
|
| "ir-identity-collision"
|
|
330
253
|
/**
|
|
331
|
-
* A `SliceRecord` broke the derivation invariant of slice-view.md
|
|
332
|
-
*
|
|
333
|
-
* equal to `"slice:" + members[0]`. The last two compare one property against
|
|
334
|
-
* another and so have no JSON Schema equivalent; the pass checks them itself
|
|
335
|
-
* (§7.4). Raised only by an Aburi bug, never by user input — see
|
|
336
|
-
* `SliceRecordViolation.kind` for which clause broke.
|
|
254
|
+
* A `SliceRecord` broke the derivation invariant of slice-view.md. Raised only by
|
|
255
|
+
* an Aburi bug, never by user input — see `SliceRecordViolation.kind` for which clause broke.
|
|
337
256
|
*/
|
|
338
257
|
| "slice-invariant-violated";
|
|
339
258
|
interface DiffErrorDetail {
|
|
@@ -341,12 +260,8 @@ interface DiffErrorDetail {
|
|
|
341
260
|
/** Offending value (schema id, fuzz value, etc.) when applicable. */
|
|
342
261
|
value?: string;
|
|
343
262
|
/**
|
|
344
|
-
* Populated only for `ir-shape-invalid`, one entry per breach of invariant #20
|
|
345
|
-
*
|
|
346
|
-
* The message quotes the first and counts the rest, which is enough to start on and not
|
|
347
|
-
* enough to finish: a caller fixing a hand-assembled Document needs the others without
|
|
348
|
-
* running the diff again per field. `CoreErrorDetail.violations` carries the same array
|
|
349
|
-
* for the same reason, and the two are the same `IntegrityViolation` type.
|
|
263
|
+
* Populated only for `ir-shape-invalid`, one entry per breach of invariant #20, so a caller
|
|
264
|
+
* repairing a hand-assembled Document sees every breach rather than the first.
|
|
350
265
|
*/
|
|
351
266
|
violations?: readonly IntegrityViolation[];
|
|
352
267
|
}
|
|
@@ -361,117 +276,107 @@ declare class DiffError extends Error {
|
|
|
361
276
|
//#endregion
|
|
362
277
|
//#region src/signature.d.ts
|
|
363
278
|
/**
|
|
364
|
-
*
|
|
365
|
-
* `throws
|
|
366
|
-
*
|
|
367
|
-
* - null + non-null (either side) → 0.0
|
|
368
|
-
* - both non-null: mean of three subscores
|
|
369
|
-
*
|
|
370
|
-
* Subscores:
|
|
371
|
-
* - inputs: ordered type-string equality rate (`i-th type equal` counts as 1, else 0)
|
|
372
|
-
* - outputs: same shape, but outputs is an array of type strings
|
|
373
|
-
* - throws: unordered set intersection over union
|
|
374
|
-
*
|
|
375
|
-
* Empty subarrays on both sides count as 1.0 (they are trivially equal); on one side only
|
|
376
|
-
* they count as 0.0 (asymmetric information).
|
|
279
|
+
* signatureSimilarity (diff-algorithm.md) — mean of three subscores over `inputs` (ordered
|
|
280
|
+
* type equality rate), `outputs` (same) and `throws` (Jaccard). Two null signatures score
|
|
281
|
+
* 1.0, one null scores 0.0. Empty on both sides counts as 1.0, empty on one side as 0.0.
|
|
377
282
|
*/
|
|
378
283
|
declare function signatureSimilarity(base: Signature | null | undefined, head: Signature | null | undefined): number;
|
|
379
284
|
//#endregion
|
|
380
285
|
//#region src/similarity.d.ts
|
|
381
286
|
/**
|
|
382
287
|
* String similarity utilities dedicated to the diff engine. Kept in one file so the
|
|
383
|
-
* name/signature/owner formulas are auditable side by side against diff-algorithm.md
|
|
288
|
+
* name/signature/owner formulas are auditable side by side against diff-algorithm.md.
|
|
384
289
|
*/
|
|
385
290
|
/**
|
|
386
|
-
* Tokenise a qualified name into
|
|
387
|
-
*
|
|
388
|
-
* -
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* and so is `получитьПользователя` — its camel hump does not register. A separator still
|
|
401
|
-
* splits (`ユーザー.取得` gives two), and a mixed name splits on its ASCII half.
|
|
402
|
-
*
|
|
403
|
-
* That makes the token count a poor measure of how much such a name says, which matters
|
|
404
|
-
* wherever a count is read as a proxy for that — see §3.4.3's admissibility rule.
|
|
291
|
+
* Tokenise a qualified name into de-duplicated lowercase word segments, splitting on camel /
|
|
292
|
+
* Pascal boundaries, `_`, `.`, `::` and digit runs: `InvoiceService.createInvoice` →
|
|
293
|
+
* `["invoice", "service", "create"]`. De-duplicated because Jaccard relies on set semantics.
|
|
294
|
+
*
|
|
295
|
+
* The input is normalised to NFC first, so a name spelled with combining marks tokenises as
|
|
296
|
+
* the name it is: `ガ` written U+30AB U+3099 is the same one token as U+30AC, and a Hangul
|
|
297
|
+
* syllable written as conjoining jamo is the same one token as its precomposed block.
|
|
298
|
+
*
|
|
299
|
+
* The camel boundary is Unicode case, not the ASCII ranges, so `получитьПользователя` splits
|
|
300
|
+
* at its hump the way `getUser` does. What no case rule can split is a script that has no
|
|
301
|
+
* case at all: `ユーザー情報を取得する` and `获取用户信息` come back whole however much they
|
|
302
|
+
* say. That is a fact about the writing system rather than about the name, which is why the
|
|
303
|
+
* token count is not what diff-algorithm.md §3.4.3 asks how much a name says — see
|
|
304
|
+
* `nameEvidence`.
|
|
405
305
|
*/
|
|
406
306
|
declare function tokenizeName(input: string): string[];
|
|
407
307
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
308
|
+
* How much a qualified name says, counted in words.
|
|
309
|
+
*
|
|
310
|
+
* The distinct-token count is this number for every name the tokeniser can segment, and every
|
|
311
|
+
* caller wanting *granularity* — how coarse a Jaccard over these tokens will be — should keep
|
|
312
|
+
* reading that count instead. This answers the different question diff-algorithm.md §3.4.3
|
|
313
|
+
* asks: could two unrelated Symbols carry this name by coincidence? One word could be carried
|
|
314
|
+
* twice; a phrase could not.
|
|
315
|
+
*
|
|
316
|
+
* A token of a cased or alphabetic script is one word, so it is worth 1 — `main`, `handler`
|
|
317
|
+
* and `initialize` alike, however long they run, because length is not what makes a word
|
|
318
|
+
* coincidental. A run of a script that writes no word boundary cannot be segmented, so it is
|
|
319
|
+
* counted instead: its characters divided by the longest a single word of that script runs,
|
|
320
|
+
* which is a floor on the number of words in it. `获取用户信息` is six Han characters, so it
|
|
321
|
+
* says at least two words and is admitted; `ハンドラー` is five kana, so it says at least
|
|
322
|
+
* one, which is what `handler` says and not enough to pair on.
|
|
323
|
+
*
|
|
324
|
+
* Characters are counted once each, the way tokens are — `取得取得` says what `取得` says,
|
|
325
|
+
* as `getGet` says what `get` says. Combining marks are not counted: after NFC they are the
|
|
326
|
+
* remainder of a character that did not compose, not a word.
|
|
327
|
+
*
|
|
328
|
+
* The number is a floor rather than a count, so it is fractional where a run is: `取得` is
|
|
329
|
+
* two thirds of a word, `getUser` is exactly 2.
|
|
411
330
|
*/
|
|
412
|
-
declare function
|
|
331
|
+
declare function nameEvidence(qname: string): number;
|
|
413
332
|
/**
|
|
414
|
-
* Jaccard
|
|
415
|
-
*
|
|
416
|
-
* matching pass, so this has no caller inside this file.
|
|
333
|
+
* Jaccard similarity |A ∩ B| / |A ∪ B| over two token lists. Empty on both sides is 1.0
|
|
334
|
+
* (both are "no tokens", equivalent for nameSimilarity); empty on one side only is 0.0.
|
|
417
335
|
*/
|
|
336
|
+
declare function jaccard(a: readonly string[], b: readonly string[]): number;
|
|
337
|
+
/** Jaccard over the tokens of two strings. Public surface only; nothing in this file calls it. */
|
|
418
338
|
declare function jaccardTokens(a: string, b: string): number;
|
|
419
|
-
/**
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
* what the score should reward.
|
|
423
|
-
*/
|
|
339
|
+
/** The member name; read by the weak matcher and the threshold lookup. */
|
|
340
|
+
declare function lastSegment(qname: string): string;
|
|
341
|
+
/** Jaccard over the tokens of the full qualified name, every segment weighted alike. */
|
|
424
342
|
declare function nameSimilarity(baseName: string, headName: string): number;
|
|
425
343
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* reading it on both axes charges for it twice.
|
|
344
|
+
* Jaccard over the tokens of the **last segment** only. What stage 4's composite reads, because the
|
|
345
|
+
* owner gate decides the owner separately and reading it on both axes charges twice.
|
|
429
346
|
*/
|
|
430
347
|
declare function memberSimilarity(baseName: string, headName: string): number;
|
|
431
348
|
/**
|
|
432
|
-
*
|
|
433
|
-
* same owner, or one whose owner was renamed. A gate rather than a score,
|
|
434
|
-
* owner cannot do what R-8 asks. `UserRepo.findById` and `AdminRepo.findById`
|
|
435
|
-
* member name and their signature, so a shared `Repo` token at weight 0.2 carries
|
|
436
|
-
* 0.8667 against a 0.85 threshold — while `UsersRepository.findById`, which *is* the
|
|
437
|
-
* shares no owner token and scores 0.8000. The collision outscores the rename, and
|
|
438
|
-
* weight only moves both: at 0.3 two three-token class names sharing two tokens land
|
|
439
|
-
* exactly 0.85.
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* - `topLevel` → `` (empty)
|
|
445
|
-
*
|
|
446
|
-
* Two empty owners are compatible: top-level Symbols share the one outer scope. One empty and
|
|
447
|
-
* one not are never compatible — the two live at different depths.
|
|
448
|
-
*
|
|
449
|
-
* Otherwise the owners must correspond segment for segment, and within a segment every token
|
|
450
|
-
* on each side must find a distinct partner on the other under `sameWord`. Both sides must be
|
|
451
|
-
* covered, so `UserRepo` and `UserRepoV2` are two classes rather than one renamed — an added
|
|
452
|
-
* token is as much evidence of a sibling as of a rename, and R-8's business is refusing the
|
|
453
|
-
* collision.
|
|
349
|
+
* The owner gate (diff-algorithm.md, R-8) — whether two Symbols are close enough in *scope* to be
|
|
350
|
+
* the same Symbol: the same owner, or one whose owner was renamed. A gate rather than a score,
|
|
351
|
+
* because grading the owner cannot do what R-8 asks. `UserRepo.findById` and `AdminRepo.findById`
|
|
352
|
+
* agree on their member name and their signature, so a shared `Repo` token at weight 0.2 carries
|
|
353
|
+
* them to 0.8667 against a 0.85 threshold — while `UsersRepository.findById`, which *is* the
|
|
354
|
+
* rename, shares no owner token and scores 0.8000. The collision outscores the rename, and
|
|
355
|
+
* raising the weight only moves both: at 0.3 two three-token class names sharing two tokens land
|
|
356
|
+
* on exactly 0.85.
|
|
357
|
+
*
|
|
358
|
+
* Two empty owners are compatible (top-level Symbols share the outer scope); one empty and one
|
|
359
|
+
* not never are. Otherwise the owners must correspond segment for segment, every token on each
|
|
360
|
+
* side finding a distinct partner under `sameWord`.
|
|
454
361
|
*/
|
|
455
362
|
declare function ownersAreCompatible(baseName: string, headName: string): boolean;
|
|
456
|
-
/** §3.4.1 tail note — used by drop-4.5 weak matcher and threshold lookup. */
|
|
457
|
-
declare function lastSegment(qname: string): string;
|
|
458
363
|
//#endregion
|
|
459
364
|
//#region src/slice.d.ts
|
|
460
|
-
/** The three inputs of the Slice View pass — docs/design/slice-view.md
|
|
365
|
+
/** The three inputs of the Slice View pass — docs/design/slice-view.md. */
|
|
461
366
|
interface SliceInput {
|
|
462
367
|
/** SymbolChange records produced by `buildDiff` (pre-sort is not required). */
|
|
463
368
|
changes: readonly SymbolChange[];
|
|
464
369
|
/**
|
|
465
370
|
* Resolved call edges from the base IR. Typically produced by
|
|
466
371
|
* `reconstructCallEdgesFromIR(baseIR)` inside `buildDiff` — Slice View
|
|
467
|
-
* consumes only resolved edges
|
|
372
|
+
* consumes only resolved edges, never `Symbol.calls[]` directly.
|
|
468
373
|
*/
|
|
469
374
|
baseCallEdges: readonly CallEdge[];
|
|
470
375
|
/** Resolved call edges from the head IR (same source rule as base). */
|
|
471
376
|
headCallEdges: readonly CallEdge[];
|
|
472
377
|
}
|
|
473
378
|
/**
|
|
474
|
-
* Slice View clustering pass — docs/design/slice-view.md
|
|
379
|
+
* Slice View clustering pass — docs/design/slice-view.md.
|
|
475
380
|
*
|
|
476
381
|
* Groups the changed-Symbol set of a diff into weakly-connected components
|
|
477
382
|
* over the union of base and head call edges. Emits a `SliceRecord[]` whose
|
|
@@ -482,16 +387,16 @@ interface SliceInput {
|
|
|
482
387
|
* Determinism, idempotence, input-order insensitivity, and locality are all
|
|
483
388
|
* guaranteed — see `computeWeaklyConnectedComponents` in `@aburi/core`.
|
|
484
389
|
*
|
|
485
|
-
* Returns `[]` when no SymbolChange is Node-eligible
|
|
390
|
+
* Returns `[]` when no SymbolChange is Node-eligible; callers should
|
|
486
391
|
* still serialise the empty array — the Markdown projection omits the section
|
|
487
|
-
*
|
|
488
|
-
* distinguish "field absent" from "no slices"
|
|
392
|
+
* but the JSON always emits the key so consumers never
|
|
393
|
+
* distinguish "field absent" from "no slices".
|
|
489
394
|
*
|
|
490
|
-
* Every returned record has passed `assertSliceRecordInvariant
|
|
395
|
+
* Every returned record has passed `assertSliceRecordInvariant`.
|
|
491
396
|
*/
|
|
492
397
|
declare function computeSlices(input: SliceInput): SliceRecord[];
|
|
493
398
|
/**
|
|
494
|
-
*
|
|
399
|
+
* The anchor of a Slice: its lexicographically smallest member.
|
|
495
400
|
*
|
|
496
401
|
* Answers from `members[0]`. `id` is derived from the anchor, so
|
|
497
402
|
* reconstructing the anchor by stripping the `"slice:"` prefix is circular at
|
|
@@ -501,8 +406,8 @@ declare function computeSlices(input: SliceInput): SliceRecord[];
|
|
|
501
406
|
* this; consumers that need a label keep using `id` directly.
|
|
502
407
|
*/
|
|
503
408
|
declare function sliceAnchor(record: SliceRecord): SymbolId;
|
|
504
|
-
/** Which clause of the
|
|
505
|
-
type SliceViolationKind = /** Not a `SliceRecord` at all: not an object, or `id` / `members` of the wrong type. */"malformed-shape" /** `members[]` has no entries, so the Slice has no anchor
|
|
409
|
+
/** Which clause of the slice-view.md derivation invariant a `SliceRecord` broke. */
|
|
410
|
+
type SliceViolationKind = /** Not a `SliceRecord` at all: not an object, or `id` / `members` of the wrong type. */"malformed-shape" /** `members[]` has no entries, so the Slice has no anchor. */ | "members-empty" /** `members[]` is not in strictly ascending order, so `members[0]` need not be the smallest. */ | "members-unordered" /** `id` is not `"slice:" + members[0]`. */ | "id-not-derived" /** The anchor is itself in a reserved id namespace, so `id` would read as a doubled prefix. */ | "anchor-in-reserved-namespace";
|
|
506
411
|
interface SliceRecordViolation {
|
|
507
412
|
kind: SliceViolationKind;
|
|
508
413
|
/** The offending record's `id` when it has a usable one, else a short stand-in. */
|
|
@@ -511,12 +416,12 @@ interface SliceRecordViolation {
|
|
|
511
416
|
message: string;
|
|
512
417
|
}
|
|
513
418
|
/**
|
|
514
|
-
* Report which clause of the
|
|
419
|
+
* Report which clause of the slice-view.md derivation invariant a value breaks, or `null`
|
|
515
420
|
* when it is a well-formed `SliceRecord`. Non-throwing counterpart of
|
|
516
421
|
* `assertSliceRecordInvariant`: the pass wants an exception, a schema
|
|
517
422
|
* validator wants a verdict it can turn into its own error.
|
|
518
423
|
*
|
|
519
|
-
* Takes `unknown` rather than `SliceRecord` deliberately.
|
|
424
|
+
* Takes `unknown` rather than `SliceRecord` deliberately. Enforcement layer 2 points
|
|
520
425
|
* this at documents written by third-party or older producers — data that has
|
|
521
426
|
* not been type-checked by definition — so anything that assumed a well-typed
|
|
522
427
|
* argument would crash on exactly the input it exists to reject.
|
|
@@ -532,20 +437,21 @@ declare function assertSliceRecordInvariant(record: SliceRecord): void;
|
|
|
532
437
|
//#region src/status.d.ts
|
|
533
438
|
type SymbolStatus = "unchanged" | "moved" | "changed" | "moved+changed" | "dropped-toggled";
|
|
534
439
|
/**
|
|
535
|
-
*
|
|
536
|
-
* has absolute priority:
|
|
537
|
-
*
|
|
538
|
-
*
|
|
440
|
+
* Which of the five statuses a matched pair falls into (diff-algorithm.md). `dropped-toggled`
|
|
441
|
+
* has absolute priority: a fingerprint change under a drop-rule flip is a consequence of the
|
|
442
|
+
* flip, not a code edit.
|
|
443
|
+
*
|
|
444
|
+
* `pathChanged` covers both file relocation and in-file rename: any id mismatch means the
|
|
445
|
+
* identifier moved, which DF9 ("method rename (same file, same logic) → moved") relies on.
|
|
446
|
+
* Stage-1 pairs match by exact id, so only stage-2+ pairings can take the id branch.
|
|
539
447
|
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
* (they match by exact id), so this only affects stage-2+ pairings where the pair was
|
|
544
|
-
* discovered via fingerprint or name/signature similarity.
|
|
448
|
+
* A confidence change is a change although no fingerprint reads it: the same code classified
|
|
449
|
+
* less surely is something a reviewer has to see, and `unchanged` is never reported. A dropped
|
|
450
|
+
* pair keeps its `unchanged`: a dropped Symbol is outside what the diff asks a reviewer to read.
|
|
545
451
|
*/
|
|
546
452
|
declare function classifyStatus(base: Symbol, head: Symbol): SymbolStatus;
|
|
547
|
-
/** Direction of a `dropped-toggled` transition (
|
|
453
|
+
/** Direction of a `dropped-toggled` transition (SymbolDroppedToggled, diff-algorithm.md). */
|
|
548
454
|
type DropDirection = "to-dropped" | "to-kept";
|
|
549
455
|
declare function dropDirection(head: Symbol): DropDirection;
|
|
550
456
|
//#endregion
|
|
551
|
-
export { DEFAULT_LINE_FUZZ, type DeltaOptions, type DependencySideView, DiffError, type DiffErrorCode, type DiffErrorDetail, type DiffInput, type DropDirection, type GitRenameMap, MAX_LINE_FUZZ, MIN_LINE_FUZZ, type SliceInput, type SliceRecordViolation, type SliceViolationKind, type SymbolPair, type SymbolStatus, assertSliceRecordInvariant, buildDiff, classifyStatus, computeSlices, computeSymbolDelta, dependencySideView, diffComponents, diffDependencies, dropDirection, jaccard, jaccardTokens, lastSegment, matchStageDroppedWeak, matchStageGitRename, matchStageId, matchStageLogicFingerprint, matchStageNameSignature, memberSimilarity, nameSimilarity, ownersAreCompatible, signatureSimilarity, sliceAnchor, sliceRecordViolation, tokenizeName, writeCanonicalDiff };
|
|
457
|
+
export { DEFAULT_LINE_FUZZ, type DeltaOptions, type DependencySideView, DiffError, type DiffErrorCode, type DiffErrorDetail, type DiffInput, type DropDirection, type GitRenameMap, MAX_LINE_FUZZ, MIN_LINE_FUZZ, type SliceInput, type SliceRecordViolation, type SliceViolationKind, type SymbolPair, type SymbolStatus, assertSliceRecordInvariant, buildDiff, classifyStatus, computeSlices, computeSymbolDelta, dependencySideView, diffComponents, diffDependencies, dropDirection, jaccard, jaccardTokens, lastSegment, matchStageDroppedWeak, matchStageGitRename, matchStageId, matchStageLogicFingerprint, matchStageNameSignature, memberSimilarity, nameEvidence, nameSimilarity, ownersAreCompatible, signatureSimilarity, sliceAnchor, sliceRecordViolation, tokenizeName, writeCanonicalDiff };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DOCUMENT_SUBJECT as e,RESERVED_LANGUAGE_IDS as t,checkDocumentShape as n,computeWeaklyConnectedComponents as r,reconstructCallEdgesFromIR as i,serializeCanonical as a,trySymbolId as o}from"@aburi/core";var s=class extends Error{code;value;violations;constructor(e,t,n){super(e,n),this.name=`DiffError`,this.code=t.code,this.value=t.value,this.violations=t.violations}};function c(e,t){let n=new Map;for(let t of e)n.set(t.id,t);let r=new Map;for(let e of t)r.set(e.id,e);let i=[],a=[],o=[];for(let[e,t]of r){let r=n.get(e);if(r===void 0){i.push(t);continue}g(r,t)||o.push({before:r,after:t,delta:{rootsChanged:!b(r.roots,t.roots),publicApiChanged:!b(r.publicApi??[],t.publicApi??[]),frameworksChanged:!b(r.frameworks??[],t.frameworks??[])}})}for(let[e,t]of n)r.has(e)||a.push(t);return x(i),x(a),o.sort((e,t)=>e.after.id<t.after.id?-1:+(e.after.id>t.after.id)),{added:i,removed:a,changed:o}}function l(e){let t=new Map;for(let n of e.symbols)t.set(n.id,n.source.file);let n=new Map;for(let t of e.stats.skippedFiles??[])n.set(t.path,t.reason);return{symbolFiles:t,lostFiles:n}}function u(e,t,n){let r=new Map;for(let t of e)r.set(m(t),t);let i=new Map;for(let e of t)i.set(m(e),e);let a=[],o=[],s=[];for(let[e,t]of i){let i=r.get(e);if(i===void 0){let e=d(t,n.head,n.base);e.length>0?s.push({dependency:t,absentFrom:`base`,lostFiles:e}):a.push(t);continue}(i.direction!==t.direction||(i.effect??null)!==(t.effect??null))&&(o.push(i),a.push(t))}for(let[e,t]of r){if(i.has(e))continue;let r=d(t,n.base,n.head);r.length>0?s.push({dependency:t,absentFrom:`head`,lostFiles:r}):o.push(t)}return a.sort(h),o.sort(h),s.sort((e,t)=>h(e.dependency,t.dependency)),{added:a,removed:o,unknown:s}}function d(e,t,n){let r=new Map;for(let i of[e.from,e.to]){let e=t.symbolFiles.get(i);if(e===void 0)continue;let a=n.lostFiles.get(e);a!==void 0&&r.set(e,a)}return[...r.entries()].map(([e,t])=>({path:e,reason:t})).sort((e,t)=>e.path<t.path?-1:+(e.path>t.path))}const f=[`from`,`to`,`via`];function p(e){return e.join(`::`)}function m(e){return p(f.map(t=>e[t]))}function h(e,t){let n=m(e),r=m(t);return n<r?-1:+(n>r)}function g(e,t){return _(e,`base`)===_(t,`head`)}function _(e,t){try{return a(v(e),{format:`compact`})}catch(n){throw new s(`${t} components[id=${e.id}] cannot be compared: ${n instanceof Error?n.message:String(n)}`,{code:`ir-shape-invalid`,value:`components[id=${e.id}]`},{cause:n})}}function v(e){let t={...e};(t.description===null||t.description===void 0)&&delete t.description;for(let e of y){let n=t[e];(n===void 0||Array.isArray(n)&&n.length===0)&&delete t[e]}return t}const y=[`publicApi`,`frameworks`];function b(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function x(e){e.sort((e,t)=>e.id<t.id?-1:+(e.id>t.id))}const S=2,ee=10,te=0;function C(e,t,n={}){let r=ne(n.lineFuzz??2),i={apiChanged:e.fingerprint.api!==t.fingerprint.api,logicChanged:e.fingerprint.logic!==t.fingerprint.logic,syntaxChanged:e.fingerprint.syntax!==t.fingerprint.syntax,componentChanged:(e.component??null)!==(t.component??null),visibilityChanged:e.visibility!==t.visibility};return i.rules=ae(e.rules,t.rules,r),i.effects=se(e.effects,t.effects),i.calls=le(e.calls,t.calls,r),i.decorators=de(e.decorators,t.decorators,r),i.signature=pe(e.signature??null,t.signature??null),i}function ne(e){if(!Number.isFinite(e)||!Number.isInteger(e))throw new s(`config.diff.lineFuzz must be an integer in [0, 10]; got ${String(e)}.`,{code:`invalid-line-fuzz`,value:String(e)});if(e<0||e>10)throw new s(`config.diff.lineFuzz must be within [0, 10]; got ${e}.`,{code:`invalid-line-fuzz`,value:String(e)});return e}function w(e,t,n,r){let i=new Set(e.map((e,t)=>t)),a=new Set(t.map((e,t)=>t)),o=new Map;for(let s of[!0,!1]){let c=(e,t)=>e.key===t.key&&Math.abs(e.line-t.line)<=r&&(!s||n(e.item,t.item));for(let[n,r]of ie(e,t,i,a,c)){i.delete(n),a.delete(r);let t=e[n];t!==void 0&&o.set(r,t)}}let s=[],c=[];for(let[e,r]of t.entries()){let t=o.get(e);t===void 0?s.push(r.item):n(t.item,r.item)||c.push(r.item)}return{added:s,removed:e.filter((e,t)=>i.has(t)).map(e=>e.item),modified:c}}const T={pairs:0,distance:0};function re(e,t){return e.pairs===t.pairs?e.distance<t.distance:e.pairs>t.pairs}function ie(e,t,n,r,i){let a=Array.from({length:e.length+1},()=>Array.from({length:t.length+1},()=>T)),o=(o,s)=>{let c=e[o],l=t[s];if(c===void 0||l===void 0||!n.has(o)||!r.has(s)||!i(c,l))return null;let u=a[o+1]?.[s+1]??T;return{pairs:u.pairs+1,distance:u.distance+Math.abs(c.line-l.line)}};for(let n=e.length-1;n>=0;n--)for(let e=t.length-1;e>=0;e--){let t=a[n+1]?.[e]??T,r=a[n]?.[e+1]??T,i=re(t,r)?t:r,s=o(n,e);s!==null&&re(s,i)&&(i=s);let c=a[n];c!==void 0&&(c[e]=i)}let s=[],c=0,l=0;for(;c<e.length&&l<t.length;){let e=a[c]?.[l]??T,t=o(c,l);if(t!==null&&t.pairs===e.pairs&&t.distance===e.distance){s.push([c,l]),c++,l++;continue}let n=a[c+1]?.[l]??T;n.pairs===e.pairs&&n.distance===e.distance?c++:l++}return s}function ae(e,t,n){let r=e=>({item:e,key:e.type,line:e.line});return w(e.map(r),t.map(r),oe,n)}function oe(e,t){return e.type===t.type&&(e.condition??null)===(t.condition??null)&&(e.what??null)===(t.what??null)&&(e.expr??null)===(t.expr??null)&&(e.loopKind??null)===(t.loopKind??null)}function se(e,t){let n=e=>({item:e,key:`${e.id}::${e.target}`,line:e.line??0});return w(e.map(n),t.map(n),ce,1/0)}function ce(e,t){return e.id===t.id&&e.target===t.target&&e.plugin===t.plugin&&e.confidence===t.confidence}function le(e,t,n){let r=e=>({item:e,key:e.target,line:e.line});return w(e.map(r),t.map(r),ue,n)}function ue(e,t){return e.target===t.target&&(e.resolved??null)===(t.resolved??null)}function de(e,t,n){let r=e=>({item:e,key:e.name,line:e.line});return w(e.map(r),t.map(r),fe,n)}function fe(e,t){if(e.name!==t.name||e.arguments.length!==t.arguments.length)return!1;for(let n=0;n<e.arguments.length;n++)if(e.arguments[n]!==t.arguments[n])return!1;return!0}function pe(e,t){if(e===null&&t===null)return null;if(e===null||t===null){let n={added:[],removed:[],modified:[]},r=e??t;if(r===null)return null;let i=e===null;return{inputs:i?{added:[...r.inputs],removed:[],modified:[]}:{added:[],removed:[...r.inputs],modified:[]},outputs:i?{added:[...r.outputs],removed:[],modified:[]}:{added:[],removed:[...r.outputs],modified:[]},throws:i?{added:[...r.throws],removed:[],modified:[]}:{added:[],removed:[...r.throws],modified:[]},asyncChanged:(e?.async??!1)!==(t?.async??!1),generatorChanged:(e?.generator??!1)!==(t?.generator??!1),typeParametersChanged:!ge(e?.typeParameters??n.added.map(String),t?.typeParameters??n.added.map(String))}}let n=(e,t)=>({item:e,key:`${t}:${e.name}`,line:t});return{inputs:w(e.inputs.map(n),t.inputs.map(n),(e,t)=>e.name===t.name&&e.type===t.type,0),outputs:me(e.outputs,t.outputs),throws:he(e.throws,t.throws),asyncChanged:e.async!==t.async,generatorChanged:e.generator!==t.generator,typeParametersChanged:!ge(e.typeParameters,t.typeParameters)}}function me(e,t){let n=[],r=[],i=Math.max(e.length,t.length);for(let a=0;a<i;a++){let i=a<e.length?e[a]:void 0,o=a<t.length?t[a]:void 0;i===void 0&&o!==void 0?n.push(o):o===void 0&&i!==void 0?r.push(i):i!==o&&(i!==void 0&&r.push(i),o!==void 0&&n.push(o))}return{added:n,removed:r,modified:[]}}function he(e,t){let n=new Set(e),r=new Set(t),i=[],a=[];for(let e of r)n.has(e)||i.push(e);for(let e of n)r.has(e)||a.push(e);return{added:i,removed:a,modified:[]}}function ge(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function E(e,t){let n=e??null,r=t??null;if(n===null&&r===null)return 1;if(n===null||r===null)return 0;let i=D(n.inputs.map(e=>e.type),r.inputs.map(e=>e.type)),a=D(n.outputs,r.outputs),o=_e(n.throws,r.throws);return(i+a+o)/3}function D(e,t){if(e.length===0&&t.length===0)return 1;if(e.length!==t.length)return 0;let n=0;for(let r=0;r<e.length;r++)e[r]===t[r]&&n++;return n/e.length}function _e(e,t){if(e.length===0&&t.length===0)return 1;let n=new Set(e),r=new Set(t),i=0;for(let e of n)r.has(e)&&i++;let a=n.size+r.size-i;return a===0?1:i/a}function O(e){let t=new Set,n=[];for(let r of e.split(/[.:_\-\s/]+/))if(r.length!==0)for(let e of ve(r)){let r=e.toLowerCase();r.length===0||t.has(r)||(t.add(r),n.push(r))}return n}function ve(e){let t=[],n=``;for(let r of e){if(n===``){n=r;continue}ye(n.slice(-1),r)?(t.push(n),n=r):n+=r}return n.length>0&&t.push(n),t}function ye(e,t){let n=e>=`a`&&e<=`z`,r=e>=`A`&&e<=`Z`,i=e>=`0`&&e<=`9`,a=t>=`a`&&t<=`z`,o=t>=`A`&&t<=`Z`;return!!(n&&o||(n||r)&&t>=`0`&&t<=`9`||i&&(a||o))}function be(e,t){return k(new Set(e),new Set(t))}function k(e,t){if(e.size===0&&t.size===0)return 1;if(e.size===0||t.size===0)return 0;let[n,r]=e.size<=t.size?[e,t]:[t,e],i=0;for(let e of n)r.has(e)&&i++;return i/(e.size+t.size-i)}function xe(e,t){return be(O(e),O(t))}const A=e=>new Set(O(e));function Se(e,t){return j(A,e,t)}function Ce(e,t){return M(A,e,t)}function we(e,t){return De(A,e,t)}function Te(e,t){if(e===t)return!0;let[n,r]=e.length<=t.length?[e,t]:[t,e];return Ee(n,r)}function Ee(e,t){return t===`${e}s`||t===`${e}es`?!0:e.endsWith(`y`)&&t===`${e.slice(0,-1)}ies`}function j(e,t,n){return k(e(t),e(n))}function M(e,t,n){return k(e(R(t)),e(R(n)))}function De(e,t,n){let r=L(t),i=L(n);return r===``&&i===``?!0:r===``||i===``?!1:r===i?!0:N(r,i,e)}function N(e,t,n){let r=e.split(`.`),i=t.split(`.`);return r.length===i.length?r.every((e,t)=>{let r=i[t];return r===void 0?!1:e===r?!0:Oe(n(e),n(r))}):!1}function Oe(e,t){if(e.size!==t.size||e.size>32)return!1;let n=[...t],r=new Map;for(let t of e)if(!P(t,n,r,new Set))return!1;return!0}function P(e,t,n,r){for(let i of t){if(r.has(i)||!Te(e,i))continue;r.add(i);let a=n.get(i);if(a===void 0||P(a,t,n,r))return n.set(i,e),!0}return!1}function ke(e,t,n){let r=e.slice(0,F(e)),i=t.slice(0,F(t));if(r===i)return!0;let a=n(r),o=n(i);if(a.size!==o.size||a.size>32)return!1;for(let e of a){let t=!1;for(let n of o)if(Te(e,n)){t=!0;break}if(!t)return!1}return!0}function F(e){let t=e.indexOf(`.`);return t<0?e.length:t}function I(){let e=new Map,t=t=>{let n=e.get(t);if(n!==void 0)return n;let r=new Set(O(t));return e.set(t,r),r},n=new Map,r=e=>{let t=n.get(e);if(t!==void 0)return t;let r=L(e);return n.set(e,r),r};return{name:(e,n)=>j(t,e,n),member:(e,n)=>M(t,e,n),ownersCompatible:(e,n)=>{let i=r(e),a=r(n);return i===a?!0:i===``||a===``||!ke(i,a,t)?!1:N(i,a,t)}}}function L(e){let t=e.indexOf(`::`);if(t>=0)return e.slice(0,t);let n=e.lastIndexOf(`.`);return n>=0?e.slice(0,n):``}function R(e){let t=e.indexOf(`::`);if(t>=0)return e.slice(t+2);let n=e.lastIndexOf(`.`);return n>=0?e.slice(n+1):e}function z(e){e.sort(Ae);let t=new Set,n=new Set,r=[];for(let i of e)t.has(i.base.id)||n.has(i.head.id)||(t.add(i.base.id),n.add(i.head.id),r.push(i));return{pairs:r,baseIds:t,headIds:n}}function Ae(e,t){return e.score===t.score?e.base.id===t.base.id?e.head.id<t.head.id?-1:+(e.head.id>t.head.id):e.base.id<t.base.id?-1:1:t.score-e.score}function B(e,t){return e.filter(e=>!t.has(e.id))}function V(e,t){let n=new Map;for(let e of t)n.set(e.id,e);let r=[],i=[],a=new Set;for(let t of e){let e=n.get(t.id);e===void 0?i.push(t):(r.push({base:t,head:e,rationale:`id-match`}),a.add(t.id))}return{matched:r,remainingBase:i,remainingHead:t.filter(e=>!a.has(e.id))}}function H(e,t,n){if(n===null||n.size===0)return{matched:[],remainingBase:[...e],remainingHead:[...t]};let r=new Map;for(let e of t)r.set(e.id,e);let i=new Map;for(let t of e){let e=n.get(t.source.file);if(e===void 0)continue;let a=Me(t.id,t.source.file,e);if(a===null)continue;let o=r.get(a);if(o===void 0)continue;let s=i.get(a);s===void 0?i.set(a,{head:o,bases:[t]}):s.bases.push(t)}let a=[],o=new Set,s=new Set;for(let{head:e,bases:t}of i.values()){let n=je(t);a.push({base:n,head:e,rationale:`git-rename`}),o.add(n.id),s.add(e.id)}return{matched:a,remainingBase:B(e,o),remainingHead:B(t,s)}}function je(e){let t=e[0];for(let n of e)n.id<t.id&&(t=n);return t}function Me(e,t,n){let r=e.indexOf(`:`),i=e.indexOf(`#`);return r<0||i<0||i<r||e.slice(r+1,i)!==t?e:o({language:e.slice(0,r),file:n,qualifiedName:e.slice(i+1)})}function U(e,t){let n=W(e),r=W(t),i=I(),a=[];for(let[e,t]of r){let r=n.get(e);r!==void 0&&a.push(...Ne(r,t,i))}let o=new Set(a.map(e=>e.base.id)),s=new Set(a.map(e=>e.head.id));return{matched:a,remainingBase:B(e,o),remainingHead:B(t,s)}}function W(e){let t=new Map;for(let n of e){if(n.dropped||n.fingerprint.logic===`000000000000`)continue;let e=t.get(n.fingerprint.logic)??[];e.push(n),t.set(n.fingerprint.logic,e)}return t}function Ne(e,t,n){let r=[...e],i=[...t],a=[];for(;r.length>0&&i.length>0;){let e=r.length===1?r[0]:void 0;if(e!==void 0){let t=Pe(e,i,n);if(t===void 0)break;a.push({base:e,head:t,rationale:`logic-fingerprint`});break}let t=[];for(let e of r)for(let r of i){let i=n.name(e.name,r.name);i>=.85&&t.push({base:e,head:r,score:i})}let o=z(t);if(o.pairs.length===0)break;for(let{base:e,head:t}of o.pairs)a.push({base:e,head:t,rationale:`logic-fingerprint+name-disambiguation`});r=B(r,o.baseIds),i=B(i,o.headIds)}return a}function Pe(e,t,n){let r;for(let i of t){let t=n.name(e.name,i.name);if(r===void 0||t>r.score){r={head:i,score:t};continue}t===r.score&&i.id<r.head.id&&(r={head:i,score:t})}return r?.head}function G(e,t){let n=new Map;for(let t of e){if(t.dropped||!J(t.name))continue;let e=Fe(t),r=n.get(e)??{members:[],byToken:new Map},i=r.members.length;r.members.push(t);for(let e of q(t.name)){let t=r.byToken.get(e)??[];t.push(i),r.byToken.set(e,t)}n.set(e,r)}let r=new Map;for(let e of n.values())r.set(e,new Int32Array(e.members.length));let i=I(),a=[],o=0;for(let e of t){if(e.dropped||e.signature===null||e.signature===void 0||!J(e.name))continue;let t=n.get(Fe(e));if(t===void 0)continue;let s=Re(e.name),c=Ie(s),l=q(e.name),u=0;for(let e of l)u+=t.byToken.get(e)?.length??0;if(u>=t.members.length){for(let n of t.members)K(n,e,i,c,s,a);continue}let d=r.get(t);if(d!==void 0){o++;for(let n of l)for(let r of t.byToken.get(n)??[]){if(d[r]===o)continue;d[r]=o;let n=t.members[r];n!==void 0&&K(n,e,i,c,s,a)}}}let s=z(a);return{matched:s.pairs.map(({base:e,head:t})=>({base:e,head:t,rationale:`name-signature`})),remainingBase:B(e,s.baseIds),remainingHead:B(t,s.headIds)}}function K(e,t,n,r,i,a){let o=n.member(e.name,t.name);if(o<r||!n.ownersCompatible(e.name,t.name))return;let s=.5*o+.3*E(e.signature??null,t.signature??null)+.2*Le;s>=i&&a.push({base:e,head:t,score:s})}function Fe(e){let t=e.signature===null||e.signature===void 0?`no-sig`:`has-sig`;return`${e.kind}::${t}`}function Ie(e){return 2*(e-.5)}function q(e){let t=O(R(e));return t.length===0?[` none`]:t}const Le=1;function Re(e){let t=O(R(e)).length;return t<=1?1:t===2?.95:.85}function J(e){return O(e).length>1}function Y(e,t){let n=e.filter(e=>e.dropped),r=t.filter(e=>e.dropped),i=[];for(let e of[Ue,We])i.push(...Ve(n,r,e));let a=ze(i);return{matched:a.map(({base:e,head:t})=>({base:e,head:t,rationale:`dropped-weak-match`})),remainingBase:B(e,new Set(a.map(e=>e.base.id))),remainingHead:B(t,new Set(a.map(e=>e.head.id)))}}function ze(e){let t=[...e].sort(Be),n=new Map,r=new Map;for(let[e,i]of t.entries())X(n,i.base.id,e),X(r,i.head.id,e);let i=e=>{let i=t[e];return i===void 0?[]:[...n.get(i.base.id)??[],...r.get(i.head.id)??[]].filter(t=>t!==e)},a=t.map(()=>!1),o=[],s=e=>{let n=e,r=!0;for(;!a[n];){a[n]=!0;let e=t[n];r&&e!==void 0&&o.push(e),r=!r;let s=i(n).find(e=>a[e]===!1);if(s===void 0)return;n=s}};for(let[e]of t.entries())!a[e]&&i(e).length<=1&&s(e);for(let[e]of t.entries())a[e]||s(e);return o}function Be(e,t){return e.base.id===t.base.id?e.head.id<t.head.id?-1:+(e.head.id>t.head.id):e.base.id<t.base.id?-1:1}function X(e,t,n){let r=e.get(t);r===void 0?e.set(t,[n]):r.push(n)}function Ve(e,t,n){let r=He(e,n),i=He(t,n),a=[];for(let[e,t]of r){let n=i.get(e);n!==void 0&&a.push({base:t,head:n})}return a}function He(e,t){let n=new Map,r=new Set;for(let i of e){let e=t(i);if(!r.has(e)){if(n.has(e)){n.delete(e),r.add(e);continue}n.set(e,i)}}return n}function Ue(e){return`${e.kind}/${R(e.name)}`}function We(e){return`${e.kind}/${Ge(e.source.file)}`}function Ge(e){let t=e.lastIndexOf(`/`);return t>=0?e.slice(t+1):e}function Ke(e){let t=$e(e.changes);if(t.length===0)return[];let n=new Set(t);return r(t,tt(e.baseCallEdges,e.headCallEdges,n),e=>e).map(Je)}function qe(e){return`slice:${e}`}function Je(e){let t=e[0];if(t===void 0)throw new s(`computeSlices: the clustering utility returned an empty component; every weakly-connected component contains at least the node that seeded it (slice-view.md §6).`,{code:`slice-invariant-violated`});let n={id:qe(t),members:[...e]};return Z(n),n}function Ye(e){let t=e.members[0];if(t===void 0){let t=Qe(e.id);throw new s(t.message,{code:`slice-invariant-violated`,value:t.subject})}return t}function Xe(e){if(typeof e!=`object`||!e)return{kind:`malformed-shape`,subject:`<not an object>`,message:`Expected a SliceRecord object; got ${e===null?`null`:typeof e}.`};let{id:t,members:n}=e,r=typeof t==`string`?t:`<missing id>`;if(typeof t!=`string`)return{kind:`malformed-shape`,subject:r,message:`SliceRecord ${r}: id must be a string; got ${typeof t}.`};if(!Array.isArray(n)||n.some(e=>typeof e!=`string`))return{kind:`malformed-shape`,subject:r,message:`SliceRecord ${r}: members must be an array of strings.`};let i=n[0];if(i===void 0)return Qe(r);let a=Ze(i);if(a!==null)return{kind:`anchor-in-reserved-namespace`,subject:r,message:`SliceRecord anchor "${i}" uses the reserved language token "${a}", so its Slice id would repeat the prefix (ir-schema.md §3.5, slice-view.md §7.5).`};for(let e=1;e<n.length;e++){let t=n[e-1],i=n[e];if(!(t<i))return{kind:`members-unordered`,subject:r,message:`SliceRecord ${r}: members[] is not in strictly ascending order at index ${e} ("${i}" follows "${t}"), so members[0] is not necessarily the anchor (slice-view.md §8.2 for the order, §11.1 for uniqueness).`}}let o=qe(i);return t===o?null:{kind:`id-not-derived`,subject:r,message:`SliceRecord id "${t}" is not derived from the anchor "${i}"; expected "${o}" (slice-view.md §7.1).`}}function Ze(e){let n=e.indexOf(`:`);if(n<0)return null;let r=e.slice(0,n);return t.has(r)?r:null}function Qe(e){return{kind:`members-empty`,subject:e,message:`SliceRecord ${e}: members[] is empty, so the Slice has no anchor (slice-view.md §11.1 requires at least one member).`}}function Z(e){let t=Xe(e);if(t!==null)throw new s(t.message,{code:`slice-invariant-violated`,value:t.subject})}function $e(e){let t=[];for(let n of e){let e=et(n);e!==null&&t.push(e)}return t}function et(e){switch(e.status){case`added`:return e.symbol.id;case`removed`:return e.symbol.id;case`unknown`:return e.symbol.id;case`changed`:case`moved+changed`:case`dropped-toggled`:return e.after.id;case`moved`:return null}}function tt(e,t,n){let r=[];for(let t of e)nt(t,n,r);for(let e of t)nt(e,n,r);return r}function nt(e,t,n){!t.has(e.from)||!t.has(e.to)||n.push([e.from,e.to])}function rt(e,t){if(e.dropped!==t.dropped)return`dropped-toggled`;let n=e.source.file!==t.source.file||e.id!==t.id,r=e.fingerprint.api!==t.fingerprint.api||e.fingerprint.logic!==t.fingerprint.logic||e.fingerprint.syntax!==t.fingerprint.syntax;return n&&r?`moved+changed`:n?`moved`:r?`changed`:`unchanged`}function it(e){return e.dropped?`to-dropped`:`to-kept`}const at={name:`aburi`,version:`0.0.0`};function ot(e){ut(e.baseIR,`baseIR`),ut(e.headIR,`headIR`),ct(e.baseIR,e.headIR);let t=V(e.baseIR.symbols,e.headIR.symbols),n=H(t.remainingBase,t.remainingHead,e.gitRenames??null),r=U(n.remainingBase,n.remainingHead),a=G(r.remainingBase,r.remainingHead),o=Y(a.remainingBase,a.remainingHead),s=[...t.matched,...n.matched,...r.matched,...a.matched,...o.matched],d={unknown:0,depsUnknown:0,added:0,removed:0,moved:0,movedChanged:0,changed:0,droppedToggled:0,unchanged:0,droppedAdded:0,droppedRemoved:0,componentsAdded:0,componentsRemoved:0,componentsChanged:0,depsAdded:0,depsRemoved:0},f=0,p=[];for(let t of s){let n=rt(t.base,t.head);if(n===`unchanged`){d.unchanged++;continue}if(n===`dropped-toggled`){d.droppedToggled++,p.push({status:`dropped-toggled`,before:t.base,after:t.head,direction:it(t.head)});continue}if(n===`moved`){d.moved++,p.push({status:`moved`,before:t.base,after:t.head,rationale:t.rationale});continue}if(n===`changed`){d.changed++,p.push({status:`changed`,before:t.base,after:t.head,delta:C(t.base,t.head,e.delta??{})});continue}d.movedChanged++,p.push({status:`moved+changed`,before:t.base,after:t.head,rationale:t.rationale,delta:C(t.base,t.head,e.delta??{})})}let m=o.remainingHead,h=o.remainingBase,g=l(e.baseIR),_=l(e.headIR),v=_.lostFiles,y=g.lostFiles;for(let e of m){if(e.dropped){d.droppedAdded++;continue}let t=y.get(e.source.file);if(t!==void 0){f++,p.push({status:`unknown`,symbol:e,absentFrom:`base`,reason:t});continue}d.added++,p.push({status:`added`,symbol:e})}for(let e of h){if(e.dropped){d.droppedRemoved++;continue}let t=v.get(e.source.file);if(t!==void 0){f++,p.push({status:`unknown`,symbol:e,absentFrom:`head`,reason:t});continue}d.removed++,p.push({status:`removed`,symbol:e})}let b=c(e.baseIR.components,e.headIR.components);d.componentsAdded=b.added.length,d.componentsRemoved=b.removed.length,d.componentsChanged=b.changed.length;let x=u(e.baseIR.dependencies,e.headIR.dependencies,{base:g,head:_});d.depsAdded=x.added.length,d.depsRemoved=x.removed.length,d.depsUnknown=x.unknown.length,d.unknown=f,p.sort(ht);let S=Ke({changes:p,baseCallEdges:i(e.baseIR),headCallEdges:i(e.headIR)});return{$schema:`https://aburi.kage1020.com/schema/aburi.diff.v1.json`,generator:e.generator??at,base:e.base,head:e.head,summary:d,symbols:p,components:b,dependencies:x,slices:S,notCompared:st(y,v)}}function st(e,t){let n=[];for(let[r,i]of e){let e=t.get(r);e!==void 0&&n.push({path:r,baseReason:i,headReason:e})}return n.sort((e,t)=>e.path<t.path?-1:+(e.path>t.path))}function ct(e,t){if(e.$schema!==t.$schema)throw new s(`Base IR schema "${e.$schema}" does not match head IR schema "${t.$schema}"; a diff across schema versions is not supported.`,{code:`schema-mismatch`,value:e.$schema})}const Q=e=>e.join(``),lt=[{field:`symbols`,identityFields:[`id`],keyOf:Q,noun:`id`,show:Q,consequence:`stage 1 pairs Symbols by id and every later stage tracks the base Symbols it has consumed by id, so a repeat leaves one entry out of the diff entirely or classifies its counterpart twice (ir-schema.md §14 #1)`},{field:`components`,identityFields:[`id`],keyOf:Q,noun:`id`,show:Q,consequence:`Component identity is the id, so a repeat hides one entry and can report a change the two revisions do not contain (ir-schema.md §14 #2)`},{field:`dependencies`,identityFields:f,keyOf:p,noun:`(from, to, via) triple`,show:e=>`(${e.join(`, `)})`,consequence:`direction and effect are deliberately outside Dependency identity (§6.2), so a repeat surfaces as an added + removed pair no reader can tell from a real flip (ir-schema.md §14 #13)`}];function ut(e,t){let r=n(e),i=r[0];if(i!==void 0){let e=dt(t,i.subject),n=r.length-1,a=n>0?` (and ${n} more)`:``;throw new s(`${e}: ${i.message}${a}.`,{code:`ir-shape-invalid`,value:e,violations:r.map(e=>({...e,subject:dt(t,e.subject)}))})}if(e.$schema.length===0)throw $(t,`${t}: "$schema" is empty, not a schema URL.`);for(let n of lt)ft(e[n.field],`${t}.${n.field}`,n)}function dt(t,n){return n===e?t:`${t}.${n}`}function ft(e,t,n){if(!Array.isArray(e))throw $(t,`${t} must be an array.`);let r=new Map;for(let[i,a]of e.entries()){let e=`${t}[${i}]`,o=pt(a,e,n),c=n.keyOf(o),l=r.get(c);if(l===void 0){r.set(c,i);continue}let u=n.show(o);throw new s(`${e} repeats the ${n.noun} "${u}" first seen at index ${l}; ${n.consequence}.`,{code:`ir-identity-collision`,value:u})}}function pt(e,t,n){if(typeof e!=`object`||!e||Array.isArray(e))throw $(t,`${t} must be an object; got ${mt(e)}.`);let r=[];for(let i of n.identityFields){let n=e[i];if(typeof n!=`string`)throw $(`${t}.${i}`,`${t}.${i} must be a string; got ${mt(n)}.`);r.push(n)}return r}function $(e,t){return new s(t,{code:`ir-shape-invalid`,value:e})}function mt(e){return e===void 0?`undefined`:e===null?`null`:Array.isArray(e)?`an array`:`a ${typeof e}`}function ht(e,t){if(e.status!==t.status)return e.status<t.status?-1:1;let n=gt(e),r=gt(t);return n<r?-1:+(n>r)}function gt(e){return e.status===`added`||e.status===`removed`||e.status===`unknown`?e.symbol.id:e.after.id}function _t(e,t={}){return a(e,t)}export{S as DEFAULT_LINE_FUZZ,s as DiffError,ee as MAX_LINE_FUZZ,te as MIN_LINE_FUZZ,Z as assertSliceRecordInvariant,ot as buildDiff,rt as classifyStatus,Ke as computeSlices,C as computeSymbolDelta,l as dependencySideView,c as diffComponents,u as diffDependencies,it as dropDirection,be as jaccard,xe as jaccardTokens,R as lastSegment,Y as matchStageDroppedWeak,H as matchStageGitRename,V as matchStageId,U as matchStageLogicFingerprint,G as matchStageNameSignature,Ce as memberSimilarity,Se as nameSimilarity,we as ownersAreCompatible,E as signatureSimilarity,Ye as sliceAnchor,Xe as sliceRecordViolation,O as tokenizeName,_t as writeCanonicalDiff};
|
|
1
|
+
import{DOCUMENT_SUBJECT as e,RESERVED_LANGUAGE_IDS as t,ZERO_FINGERPRINT as n,checkDocumentShape as r,compareBy as i,compareCodeUnit as a,computeWeaklyConnectedComponents as o,logicNamesNothing as s,reconstructCallEdgesFromIR as c,serializeCanonical as l,stringArraysEqual as u,trySymbolId as d}from"@aburi/core";var f=class extends Error{code;value;violations;constructor(e,t,n){super(e,n),this.name=`DiffError`,this.code=t.code,this.value=t.value,this.violations=t.violations}};const p=i(e=>e.id);function m(e,t){let n=new Map;for(let t of e)n.set(t.id,t);let r=new Map;for(let e of t)r.set(e.id,e);let a=[],o=[],s=[];for(let[e,t]of r){let r=n.get(e);if(r===void 0){a.push(t);continue}te(r,t)||s.push({before:r,after:t,delta:{rootsChanged:!u(r.roots,t.roots),publicApiChanged:!u(r.publicApi??[],t.publicApi??[]),frameworksChanged:!u(r.frameworks??[],t.frameworks??[])}})}for(let[e,t]of n)r.has(e)||o.push(t);return a.sort(p),o.sort(p),s.sort(i(e=>e.after.id)),{added:a,removed:o,changed:s}}function h(e){let t=new Map;for(let n of e.symbols)t.set(n.id,n.source.file);let n=new Map;for(let t of e.stats.skippedFiles??[])n.set(t.path,t.reason);return{symbolFiles:t,lostFiles:n}}function ee(e,t,n){let r=new Map;for(let t of e)r.set(y(t),t);let i=new Map;for(let e of t)i.set(y(e),e);let a=[],o=[],s=[];for(let[e,t]of i){let i=r.get(e);if(i===void 0){let e=g(t,n.head,n.base);e.length>0?s.push({dependency:t,absentFrom:`base`,lostFiles:e}):a.push(t);continue}(i.direction!==t.direction||(i.effect??null)!==(t.effect??null))&&(o.push(i),a.push(t))}for(let[e,t]of r){if(i.has(e))continue;let r=g(t,n.base,n.head);r.length>0?s.push({dependency:t,absentFrom:`head`,lostFiles:r}):o.push(t)}return a.sort(b),o.sort(b),s.sort((e,t)=>b(e.dependency,t.dependency)),{added:a,removed:o,unknown:s}}function g(e,t,n){let r=new Map;for(let i of[e.from,e.to]){let e=t.symbolFiles.get(i);if(e===void 0)continue;let a=n.lostFiles.get(e);a!==void 0&&r.set(e,a)}return[...r.entries()].map(([e,t])=>({path:e,reason:t})).sort(i(e=>e.path))}const _=[`from`,`to`,`via`];function v(e){return e.join(`::`)}function y(e){return v(_.map(t=>e[t]))}const b=i(y);function te(e,t){return x(e,`base`)===x(t,`head`)}function x(e,t){try{return l(ne(e),{format:`compact`})}catch(n){throw new f(`${t} components[id=${e.id}] cannot be compared: ${n instanceof Error?n.message:String(n)}`,{code:`ir-shape-invalid`,value:`components[id=${e.id}]`},{cause:n})}}function ne(e){let t={...e};(t.description===null||t.description===void 0)&&delete t.description;for(let e of re){let n=t[e];(n===void 0||Array.isArray(n)&&n.length===0)&&delete t[e]}return t}const re=[`publicApi`,`frameworks`],ie=2,ae=10,oe=0;function S(e,t,n={}){let r=se(n.lineFuzz??2);return{apiChanged:e.fingerprint.api!==t.fingerprint.api,logicChanged:e.fingerprint.logic!==t.fingerprint.logic,syntaxChanged:e.fingerprint.syntax!==t.fingerprint.syntax,componentChanged:(e.component??null)!==(t.component??null),visibilityChanged:e.visibility!==t.visibility,confidenceChanged:e.confidence!==t.confidence,rules:de(e.rules,t.rules,r),effects:pe(e.effects,t.effects),calls:he(e.calls,t.calls,r),decorators:_e(e.decorators,t.decorators,r),signature:ye(e.signature??null,t.signature??null)}}function se(e){if(!Number.isFinite(e)||!Number.isInteger(e))throw new f(`lineFuzz must be an integer in [0, 10]; got ${String(e)}.`,{code:`invalid-line-fuzz`,value:String(e)});if(e<0||e>10)throw new f(`lineFuzz must be within [0, 10]; got ${e}.`,{code:`invalid-line-fuzz`,value:String(e)});return e}function C(e,t,n,r){let i=new Set(e.map((e,t)=>t)),a=new Set(t.map((e,t)=>t)),o=new Map,s=[(e,t)=>e.key===t.key&&n(e.item,t.item),(e,t)=>e.key===t.key&&Math.abs(e.line-t.line)<=r],c=ce(e,t);for(let e of s)for(let t of c){let n=ue(t.base.filter(e=>i.has(e.index)),t.head.filter(e=>a.has(e.index)),e);for(let[e,t]of n)i.delete(e.index),a.delete(t.index),o.set(t.index,e.element)}let l=[],u=[];for(let[e,r]of t.entries()){let t=o.get(e);t===void 0?l.push(r.item):n(t.item,r.item)||u.push(r.item)}return{added:l,removed:e.filter((e,t)=>i.has(t)).map(e=>e.item),modified:u}}function ce(e,t){let n=new Map,r=e=>{let t=n.get(e);return t===void 0&&(t={base:[],head:[]},n.set(e,t)),t};for(let[t,n]of e.entries())r(n.key).base.push({index:t,element:n});for(let[e,n]of t.entries())r(n.key).head.push({index:e,element:n});return[...n.values()].filter(e=>e.base.length>0&&e.head.length>0)}const w={pairs:0,distance:0};function le(e,t){return e.pairs===t.pairs?e.distance<t.distance:e.pairs>t.pairs}function ue(e,t,n){let r=Array.from({length:e.length+1},()=>Array.from({length:t.length+1},()=>w)),i=(i,a)=>{let o=e[i],s=t[a];if(o===void 0||s===void 0||!n(o.element,s.element))return null;let c=r[i+1]?.[a+1]??w,l=c.distance+Math.abs(o.element.line-s.element.line);return{score:{pairs:c.pairs+1,distance:l},pair:[o,s]}};for(let n=e.length-1;n>=0;n--)for(let e=t.length-1;e>=0;e--){let t=r[n+1]?.[e]??w,a=r[n]?.[e+1]??w,o=le(t,a)?t:a,s=i(n,e)?.score;s!==void 0&&le(s,o)&&(o=s);let c=r[n];c!==void 0&&(c[e]=o)}let a=[],o=0,s=0;for(;o<e.length&&s<t.length;){let e=r[o]?.[s]??w,t=i(o,s);if(t!==null&&t.score.pairs===e.pairs&&t.score.distance===e.distance){a.push(t.pair),o++,s++;continue}let n=r[o+1]?.[s]??w;n.pairs===e.pairs&&n.distance===e.distance?o++:s++}return a}function de(e,t,n){let r=e=>({item:e,key:e.type,line:e.line});return C(e.map(r),t.map(r),fe,n)}function fe(e,t){return e.type===t.type&&(e.condition??null)===(t.condition??null)&&(e.what??null)===(t.what??null)&&(e.expr??null)===(t.expr??null)&&(e.loopKind??null)===(t.loopKind??null)}function pe(e,t){let n=e=>({item:e,key:`${e.id}::${e.target}`,line:e.line??0});return C(e.map(n),t.map(n),me,1/0)}function me(e,t){return e.id===t.id&&e.target===t.target&&e.plugin===t.plugin&&e.confidence===t.confidence&&(e.propagated??!1)===(t.propagated??!1)&&u([...e.derivedFrom??[]].sort(a),[...t.derivedFrom??[]].sort(a))}function he(e,t,n){let r=e=>({item:e,key:e.target,line:e.line});return C(e.map(r),t.map(r),ge,n)}function ge(e,t){return e.target===t.target&&(e.resolved??null)===(t.resolved??null)}function _e(e,t,n){let r=e=>({item:e,key:e.name,line:e.line});return C(e.map(r),t.map(r),ve,n)}function ve(e,t){return e.name===t.name&&(e.qualifier??null)===(t.qualifier??null)&&u(e.arguments,t.arguments)}function ye(e,t){if(e===null)return t===null?null:T(t,`added`);if(t===null)return T(e,`removed`);let n=(e,t)=>({item:e,key:`${t}:${e.name}`,line:t});return{inputs:C(e.inputs.map(n),t.inputs.map(n),(e,t)=>e.name===t.name&&e.type===t.type,0),outputs:be(e.outputs,t.outputs),throws:xe(e.throws,t.throws),asyncChanged:e.async!==t.async,generatorChanged:e.generator!==t.generator,typeParametersChanged:!u(e.typeParameters,t.typeParameters)}}function T(e,t){let n=e=>({added:t===`added`?[...e]:[],removed:t===`removed`?[...e]:[],modified:[]});return{inputs:n(e.inputs),outputs:n(e.outputs),throws:n(e.throws),asyncChanged:e.async,generatorChanged:e.generator,typeParametersChanged:e.typeParameters.length>0}}function be(e,t){let n=[],r=[],i=Math.max(e.length,t.length);for(let a=0;a<i;a++){let i=a<e.length?e[a]:void 0,o=a<t.length?t[a]:void 0;i===void 0&&o!==void 0?n.push(o):o===void 0&&i!==void 0?r.push(i):i!==o&&(i!==void 0&&r.push(i),o!==void 0&&n.push(o))}return{added:n,removed:r,modified:[]}}function xe(e,t){let n=new Set(e),r=new Set(t),i=[],a=[];for(let e of r)n.has(e)||i.push(e);for(let e of n)r.has(e)||a.push(e);return{added:i,removed:a,modified:[]}}function E(e){let t=new Set,n=[];for(let r of e.normalize(`NFC`).split(/[.:_\-\s/]+/))if(r.length!==0)for(let e of Se(r)){let r=e.toLowerCase();r.length===0||t.has(r)||(t.add(r),n.push(r))}return n}function Se(e){let t=[],n=``,r=``;for(let i of e){if(n===``){n=i,r=i;continue}Te(r,i)?(t.push(n),n=i):n+=i,r=i}return n.length>0&&t.push(n),t}const D=/\p{Ll}/u,Ce=/\p{Lu}|\p{Lt}/u,we=/\p{Nd}/u;function Te(e,t){let n=D.test(e),r=Ce.test(e),i=we.test(e),a=D.test(t),o=Ce.test(t),s=we.test(t);return!!(n&&o||(n||r)&&s||i&&(a||o))}const Ee=/\p{scx=Han}/u,De=/[\p{scx=Hiragana}\p{scx=Katakana}\p{scx=Hangul}]/u,Oe=/\p{M}/u;function O(e){let t=new Set,n=0,r=0,i=0;for(let a of E(e)){let e=!1;for(let i of a){if(Oe.test(i))continue;let a=Ee.test(i);if(!a&&!De.test(i)){e=!0;continue}t.has(i)||(t.add(i),a?n++:r++)}e&&i++}return i+n/3+r/6}function k(e,t){return A(new Set(e),new Set(t))}function A(e,t){if(e.size===0&&t.size===0)return 1;if(e.size===0||t.size===0)return 0;let[n,r]=e.size<=t.size?[e,t]:[t,e],i=0;for(let e of n)r.has(e)&&i++;return i/(e.size+t.size-i)}function ke(e,t){return k(E(e),E(t))}function j(e){let t=e.lastIndexOf(`::`),n=e.lastIndexOf(`.`);return t>n?{owner:e.slice(0,t),member:e.slice(t+2)}:n>=0?{owner:e.slice(0,n),member:e.slice(n+1)}:{owner:``,member:e}}function M(e){return j(e).member}const N=e=>new Set(E(e));function Ae(e,t){return F(N,e,t)}function je(e,t){return I(N,e,t)}function Me(e,t){return L(j(e).owner,j(t).owner,N)}function P(e,t){if(e===t)return!0;let[n,r]=e.length<=t.length?[e,t]:[t,e];return Ne(n,r)}function Ne(e,t){return t===`${e}s`||t===`${e}es`?!0:e.endsWith(`y`)&&t===`${e.slice(0,-1)}ies`}function F(e,t,n){return A(e(t),e(n))}function I(e,t,n){return A(e(M(t)),e(M(n)))}function L(e,t,n){return e===t?!0:e===``||t===``||!Ie(e,t,n)?!1:Pe(e,t,n)}function Pe(e,t,n){let r=e.split(`.`),i=t.split(`.`);return r.length===i.length?r.every((e,t)=>{let r=i[t];return r===void 0?!1:e===r?!0:Fe(n(e),n(r))}):!1}function Fe(e,t){if(e.size!==t.size||e.size>32)return!1;let n=[...t],r=new Map;for(let t of e)if(!R(t,n,r,new Set))return!1;return!0}function R(e,t,n,r){for(let i of t){if(r.has(i)||!P(e,i))continue;r.add(i);let a=n.get(i);if(a===void 0||R(a,t,n,r))return n.set(i,e),!0}return!1}function Ie(e,t,n){let r=e.slice(0,z(e)),i=t.slice(0,z(t));if(r===i)return!0;let a=n(r),o=n(i);if(a.size!==o.size||a.size>32)return!1;for(let e of a){let t=!1;for(let n of o)if(P(e,n)){t=!0;break}if(!t)return!1}return!0}function z(e){let t=e.indexOf(`.`);return t<0?e.length:t}function B(){let e=new Map,t=t=>{let n=e.get(t);if(n!==void 0)return n;let r=new Set(E(t));return e.set(t,r),r},n=new Map,r=e=>{let t=n.get(e);if(t!==void 0)return t;let r=j(e).owner;return n.set(e,r),r};return{name:(e,n)=>F(t,e,n),member:(e,n)=>I(t,e,n),ownersCompatible:(e,n)=>L(r(e),r(n),t)}}function V(e,t){let n=e??null,r=t??null;if(n===null&&r===null)return 1;if(n===null||r===null)return 0;let i=H(n.inputs.map(e=>e.type),r.inputs.map(e=>e.type)),a=H(n.outputs,r.outputs),o=k(n.throws,r.throws);return(i+a+o)/3}function H(e,t){if(e.length===0&&t.length===0)return 1;if(e.length!==t.length)return 0;let n=0;for(let r=0;r<e.length;r++)e[r]===t[r]&&n++;return n/e.length}function U(e){e.sort(Le);let t=new Set,n=new Set,r=[];for(let i of e)t.has(i.base.id)||n.has(i.head.id)||(t.add(i.base.id),n.add(i.head.id),r.push(i));return{pairs:r,baseIds:t,headIds:n}}function Le(e,t){return t.score-e.score||W(e,t)}function W(e,t){return a(e.base.id,t.base.id)||a(e.head.id,t.head.id)}function G(e,t){return e.filter(e=>!t.has(e.id))}function K(e,t){let n=new Map;for(let e of t)n.set(e.id,e);let r=[],i=[],a=new Set;for(let t of e){let e=n.get(t.id);e===void 0?i.push(t):(r.push({base:t,head:e,rationale:`id-match`}),a.add(t.id))}return{matched:r,remainingBase:i,remainingHead:G(t,a)}}function q(e,t,n){if(n===null||n.size===0)return{matched:[],remainingBase:[...e],remainingHead:[...t]};let r=new Map;for(let e of t)r.set(e.id,e);let i=new Map;for(let t of e){let e=n.get(t.source.file);if(e===void 0)continue;let a=ze(t.id,t.source.file,e);if(a===null)continue;let o=r.get(a);if(o===void 0)continue;let s=i.get(a);s===void 0?i.set(a,{head:o,bases:[t]}):s.bases.push(t)}let a=[],o=new Set,s=new Set;for(let{head:e,bases:t}of i.values()){let n=Re(t);a.push({base:n,head:e,rationale:`git-rename`}),o.add(n.id),s.add(e.id)}return{matched:a,remainingBase:G(e,o),remainingHead:G(t,s)}}function Re(e){let t=e[0];for(let n of e)n.id<t.id&&(t=n);return t}function ze(e,t,n){let r=e.indexOf(`:`),i=e.indexOf(`#`);return r<0||i<0||i<r||e.slice(r+1,i)!==t?e:d({language:e.slice(0,r),file:n,qualifiedName:e.slice(i+1)})}function J(e,t){let n=Y(e),r=Y(t),i=B(),a=[];for(let[e,t]of r){let r=n.get(e);r!==void 0&&a.push(...Be(r.symbols,t.symbols,i,t.evidenceless))}let o=new Set(a.map(e=>e.base.id)),s=new Set(a.map(e=>e.head.id));return{matched:a,remainingBase:G(e,o),remainingHead:G(t,s)}}function Y(e){let t=new Map;for(let r of e){if(r.dropped||r.fingerprint.logic===n)continue;let e=s(r),i=JSON.stringify([r.kind,r.fingerprint.logic,e]),a=t.get(i);a===void 0?t.set(i,{evidenceless:e,symbols:[r]}):a.symbols.push(r)}return t}function Be(e,t,n,r){let i=e=>!r||Z(e.name),a=e.filter(i),o=t.filter(i),s=[];for(;a.length>0&&o.length>0;){let e=!r&&a.length===1?a[0]:void 0;if(e!==void 0){let t=Ve(e,o,n);if(t===void 0)break;s.push({base:e,head:t,rationale:`logic-fingerprint`});break}let t=[];for(let e of a)for(let r of o){let i=n.name(e.name,r.name);i>=.85&&t.push({base:e,head:r,score:i})}let i=U(t);if(i.pairs.length===0)break;for(let{base:e,head:t}of i.pairs)s.push({base:e,head:t,rationale:`logic-fingerprint+name-disambiguation`});a=G(a,i.baseIds),o=G(o,i.headIds)}return s}function Ve(e,t,n){let r;for(let i of t){let t=n.name(e.name,i.name);if(r===void 0||t>r.score){r={head:i,score:t};continue}t===r.score&&i.id<r.head.id&&(r={head:i,score:t})}return r?.head}function X(e,t){let n=new Map;for(let t of e){if(t.dropped||!Z(t.name))continue;let e=Ue(t),r=n.get(e)??{members:[],byToken:new Map},i=r.members.length;r.members.push(t);for(let e of Ge(t.name)){let t=r.byToken.get(e)??[];t.push(i),r.byToken.set(e,t)}n.set(e,r)}let r=new Map;for(let e of n.values())r.set(e,new Int32Array(e.members.length));let i=B(),a=[],o=0;for(let e of t){if(e.dropped||e.signature===null||e.signature===void 0||!Z(e.name))continue;let t=n.get(Ue(e));if(t===void 0)continue;let s=qe(e.name),c=We(s),l=Ge(e.name),u=0;for(let e of l)u+=t.byToken.get(e)?.length??0;if(u>=t.members.length){for(let n of t.members)He(n,e,i,c,s,a);continue}let d=r.get(t);if(d!==void 0){o++;for(let n of l)for(let r of t.byToken.get(n)??[]){if(d[r]===o)continue;d[r]=o;let n=t.members[r];n!==void 0&&He(n,e,i,c,s,a)}}}let s=U(a);return{matched:s.pairs.map(({base:e,head:t})=>({base:e,head:t,rationale:`name-signature`})),remainingBase:G(e,s.baseIds),remainingHead:G(t,s.headIds)}}function He(e,t,n,r,i,a){let o=n.member(e.name,t.name);if(o<r||!n.ownersCompatible(e.name,t.name))return;let s=.5*o+.3*V(e.signature??null,t.signature??null)+.2*Ke;s>=i&&a.push({base:e,head:t,score:s})}function Ue(e){let t=e.signature===null||e.signature===void 0?`no-sig`:`has-sig`;return`${e.kind}::${t}`}function We(e){return 2*(e-.5)}function Ge(e){let t=E(M(e));return t.length===0?[` none`]:t}const Ke=1;function qe(e){let t=E(M(e)).length;return t<=1?1:t===2?.95:.85}function Z(e){return O(e)>1}function Je(e,t){let n=e.filter(e=>e.dropped),r=t.filter(e=>e.dropped),i=[];for(let e of[$e,et])i.push(...Ze(n,r,e));let a=Ye(i);return{matched:a.map(({base:e,head:t})=>({base:e,head:t,rationale:`dropped-weak-match`})),remainingBase:G(e,new Set(a.map(e=>e.base.id))),remainingHead:G(t,new Set(a.map(e=>e.head.id)))}}function Ye(e){let t=[...e].sort(W),n=new Map,r=new Map;for(let[e,i]of t.entries())Xe(n,i.base.id,e),Xe(r,i.head.id,e);let i=e=>{let i=t[e];return i===void 0?[]:[...n.get(i.base.id)??[],...r.get(i.head.id)??[]].filter(t=>t!==e)},a=t.map(()=>!1),o=[],s=e=>{let n=e,r=!0;for(;!a[n];){a[n]=!0;let e=t[n];r&&e!==void 0&&o.push(e),r=!r;let s=i(n).find(e=>a[e]===!1);if(s===void 0)return;n=s}};for(let[e]of t.entries())!a[e]&&i(e).length<=1&&s(e);for(let[e]of t.entries())a[e]||s(e);return o}function Xe(e,t,n){let r=e.get(t);r===void 0?e.set(t,[n]):r.push(n)}function Ze(e,t,n){let r=Qe(e,n),i=Qe(t,n),a=[];for(let[e,t]of r){let n=i.get(e);n!==void 0&&a.push({base:t,head:n})}return a}function Qe(e,t){let n=new Map,r=new Set;for(let i of e){let e=t(i);if(!r.has(e)){if(n.has(e)){n.delete(e),r.add(e);continue}n.set(e,i)}}return n}function $e(e){return`${e.kind}/${M(e.name)}`}function et(e){return`${e.kind}/${tt(e.source.file)}`}function tt(e){let t=e.lastIndexOf(`/`);return t>=0?e.slice(t+1):e}function nt(e,t){if(e.dropped!==t.dropped)return`dropped-toggled`;let n=e.source.file!==t.source.file||e.id!==t.id,r=e.fingerprint.api!==t.fingerprint.api||e.fingerprint.logic!==t.fingerprint.logic||e.fingerprint.syntax!==t.fingerprint.syntax||!t.dropped&&e.confidence!==t.confidence;return n&&r?`moved+changed`:n?`moved`:r?`changed`:`unchanged`}function rt(e){return e.dropped?`to-dropped`:`to-kept`}function Q(e){return e.status===`added`||e.status===`removed`||e.status===`unknown`?e.symbol:e.after}function it(e){let t=ft(e.changes);if(t.length===0)return[];let n=new Set(t);return o(t,mt(e.baseCallEdges,e.headCallEdges,n),e=>e).map(ot)}function at(e){return`slice:${e}`}function ot(e){let t=e[0];if(t===void 0)throw new f(`computeSlices: the clustering utility returned an empty component; every weakly-connected component contains at least the node that seeded it (slice-view.md).`,{code:`slice-invariant-violated`});let n={id:at(t),members:[...e]};return dt(n),n}function st(e){let t=e.members[0];if(t===void 0){let t=ut(e.id);throw new f(t.message,{code:`slice-invariant-violated`,value:t.subject})}return t}function ct(e){if(typeof e!=`object`||!e)return{kind:`malformed-shape`,subject:`<not an object>`,message:`Expected a SliceRecord object; got ${e===null?`null`:typeof e}.`};let{id:t,members:n}=e,r=typeof t==`string`?t:`<missing id>`;if(typeof t!=`string`)return{kind:`malformed-shape`,subject:r,message:`SliceRecord ${r}: id must be a string; got ${typeof t}.`};if(!Array.isArray(n)||n.some(e=>typeof e!=`string`))return{kind:`malformed-shape`,subject:r,message:`SliceRecord ${r}: members must be an array of strings.`};let i=n[0];if(i===void 0)return ut(r);let a=lt(i);if(a!==null)return{kind:`anchor-in-reserved-namespace`,subject:r,message:`SliceRecord anchor "${i}" uses the reserved language token "${a}", so its Slice id would repeat the prefix (ir-schema.md, slice-view.md).`};for(let e=1;e<n.length;e++){let t=n[e-1],i=n[e];if(!(t<i))return{kind:`members-unordered`,subject:r,message:`SliceRecord ${r}: members[] is not in strictly ascending order at index ${e} ("${i}" follows "${t}"), so members[0] is not necessarily the anchor (slice-view.md for the order and uniqueness).`}}let o=at(i);return t===o?null:{kind:`id-not-derived`,subject:r,message:`SliceRecord id "${t}" is not derived from the anchor "${i}"; expected "${o}" (slice-view.md).`}}function lt(e){let n=e.indexOf(`:`);if(n<0)return null;let r=e.slice(0,n);return t.has(r)?r:null}function ut(e){return{kind:`members-empty`,subject:e,message:`SliceRecord ${e}: members[] is empty, so the Slice has no anchor (slice-view.md requires at least one member).`}}function dt(e){let t=ct(e);if(t!==null)throw new f(t.message,{code:`slice-invariant-violated`,value:t.subject})}function ft(e){let t=[];for(let n of e)switch(n.status){case`added`:case`removed`:case`unknown`:case`changed`:case`moved+changed`:case`dropped-toggled`:t.push(Q(n).id);break;case`moved`:break;default:return pt(n)}return t}function pt(e){throw Error(`computeSlices: unhandled SymbolChange status ${JSON.stringify(e)}; every status has to declare whether it is a Slice Node (slice-view.md).`)}function mt(e,t,n){let r=[];for(let t of e)ht(t,n,r);for(let e of t)ht(e,n,r);return r}function ht(e,t,n){!t.has(e.from)||!t.has(e.to)||n.push([e.from,e.to])}const gt={name:`aburi`,version:`0.0.0`};function _t(e){xt(e.baseIR,`baseIR`),xt(e.headIR,`headIR`),yt(e.baseIR,e.headIR);let t=K(e.baseIR.symbols,e.headIR.symbols),n=q(t.remainingBase,t.remainingHead,e.gitRenames??null),r=J(n.remainingBase,n.remainingHead),i=X(r.remainingBase,r.remainingHead),a=Je(i.remainingBase,i.remainingHead),o=[...t.matched,...n.matched,...r.matched,...i.matched,...a.matched],s={unknown:0,depsUnknown:0,added:0,removed:0,moved:0,movedChanged:0,changed:0,droppedToggled:0,unchanged:0,droppedAdded:0,droppedRemoved:0,componentsAdded:0,componentsRemoved:0,componentsChanged:0,depsAdded:0,depsRemoved:0},l=0,u=[];for(let t of o){let n=nt(t.base,t.head);if(n===`unchanged`){s.unchanged++;continue}if(n===`dropped-toggled`){s.droppedToggled++,u.push({status:`dropped-toggled`,before:t.base,after:t.head,direction:rt(t.head)});continue}if(n===`moved`){s.moved++,u.push({status:`moved`,before:t.base,after:t.head,rationale:t.rationale});continue}if(n===`changed`){s.changed++,u.push({status:`changed`,before:t.base,after:t.head,delta:S(t.base,t.head,e.delta??{})});continue}s.movedChanged++,u.push({status:`moved+changed`,before:t.base,after:t.head,rationale:t.rationale,delta:S(t.base,t.head,e.delta??{})})}let d=h(e.baseIR),f=h(e.headIR),p=f.lostFiles,g=d.lostFiles;for(let e of a.remainingHead){if(e.dropped){s.droppedAdded++;continue}let t=g.get(e.source.file);if(t!==void 0){l++,u.push({status:`unknown`,symbol:e,absentFrom:`base`,reason:t});continue}s.added++,u.push({status:`added`,symbol:e})}for(let e of a.remainingBase){if(e.dropped){s.droppedRemoved++;continue}let t=p.get(e.source.file);if(t!==void 0){l++,u.push({status:`unknown`,symbol:e,absentFrom:`head`,reason:t});continue}s.removed++,u.push({status:`removed`,symbol:e})}let _=m(e.baseIR.components,e.headIR.components);s.componentsAdded=_.added.length,s.componentsRemoved=_.removed.length,s.componentsChanged=_.changed.length;let v=ee(e.baseIR.dependencies,e.headIR.dependencies,{base:d,head:f});s.depsAdded=v.added.length,s.depsRemoved=v.removed.length,s.depsUnknown=v.unknown.length,s.unknown=l,u.sort(wt);let y=it({changes:u,baseCallEdges:c(e.baseIR),headCallEdges:c(e.headIR)});return{$schema:`https://aburi.kage1020.com/schema/aburi.diff.v1.json`,generator:e.generator??gt,base:e.base,head:e.head,summary:s,symbols:u,components:_,dependencies:v,slices:y,notCompared:vt(g,p)}}function vt(e,t){let n=[];for(let[r,i]of e){let e=t.get(r);e!==void 0&&n.push({path:r,baseReason:i,headReason:e})}return n.sort(i(e=>e.path))}function yt(e,t){if(e.$schema!==t.$schema)throw new f(`Base IR schema "${e.$schema}" does not match head IR schema "${t.$schema}"; a diff across schema versions is not supported.`,{code:`schema-mismatch`,value:e.$schema})}const $=e=>e.join(``),bt=[{field:`symbols`,identities:e=>e.symbols.map(e=>[e.id]),keyOf:$,noun:`id`,show:$,consequence:`stage 1 pairs Symbols by id and every later stage tracks the base Symbols it has consumed by id, so a repeat leaves one entry out of the diff entirely or classifies its counterpart twice (ir-schema.md #1)`},{field:`components`,identities:e=>e.components.map(e=>[e.id]),keyOf:$,noun:`id`,show:$,consequence:`Component identity is the id, so a repeat hides one entry and can report a change the two revisions do not contain (ir-schema.md #2)`},{field:`dependencies`,identities:e=>e.dependencies.map(e=>_.map(t=>e[t])),keyOf:v,noun:`(from, to, via) triple`,show:e=>`(${e.join(`, `)})`,consequence:`direction and effect are deliberately outside Dependency identity (diff-algorithm.md), so a repeat surfaces as an added + removed pair no reader can tell from a real flip (ir-schema.md #13)`}];function xt(e,t){let n=r(e),i=n[0];if(i!==void 0){let e=St(t,i.subject),r=n.length-1,a=r>0?` (and ${r} more)`:``;throw new f(`${e}: ${i.message}${a}.`,{code:`ir-shape-invalid`,value:e,violations:n.map(e=>({...e,subject:St(t,e.subject)}))})}if(e.$schema.length===0)throw new f(`${t}: "$schema" is empty, not a schema URL.`,{code:`ir-shape-invalid`,value:t});for(let n of bt)Ct(n.identities(e),`${t}.${n.field}`,n)}function St(t,n){return n===e?t:`${t}.${n}`}function Ct(e,t,n){let r=new Map;for(let[i,a]of e.entries()){let e=n.keyOf(a),o=r.get(e);if(o===void 0){r.set(e,i);continue}let s=n.show(a);throw new f(`${t}[${i}] repeats the ${n.noun} "${s}" first seen at index ${o}; ${n.consequence}.`,{code:`ir-identity-collision`,value:s})}}function wt(e,t){return a(e.status,t.status)||a(Q(e).id,Q(t).id)}function Tt(e,t={}){return l(e,t)}export{ie as DEFAULT_LINE_FUZZ,f as DiffError,ae as MAX_LINE_FUZZ,oe as MIN_LINE_FUZZ,dt as assertSliceRecordInvariant,_t as buildDiff,nt as classifyStatus,it as computeSlices,S as computeSymbolDelta,h as dependencySideView,m as diffComponents,ee as diffDependencies,rt as dropDirection,k as jaccard,ke as jaccardTokens,M as lastSegment,Je as matchStageDroppedWeak,q as matchStageGitRename,K as matchStageId,J as matchStageLogicFingerprint,X as matchStageNameSignature,je as memberSimilarity,O as nameEvidence,Ae as nameSimilarity,Me as ownersAreCompatible,V as signatureSimilarity,st as sliceAnchor,ct as sliceRecordViolation,E as tokenizeName,Tt as writeCanonicalDiff};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aburi/diff",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Aburi diff engine: 5-stage semantic matcher (id / git-rename / logic fingerprint / name+signature / dropped weak) followed by status classification and per-Symbol delta computation, emitting aburi.diff.v1-conformant output for PR-review workflows",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "kage1020",
|
|
@@ -32,15 +32,16 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@aburi/core": "0.
|
|
36
|
-
"@aburi/types": "0.
|
|
35
|
+
"@aburi/core": "0.5.0",
|
|
36
|
+
"@aburi/types": "0.5.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^26.0.1",
|
|
40
40
|
"ajv": "^8.20.0",
|
|
41
41
|
"tsdown": "^0.22.3",
|
|
42
42
|
"typescript": "^6.0.3",
|
|
43
|
-
"vitest": "^4.1.11"
|
|
43
|
+
"vitest": "^4.1.11",
|
|
44
|
+
"@aburi/test-support": "0.0.1"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "tsdown",
|