@aburi/diff 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/index.d.mts +88 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +248 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/components.ts +9 -3
- package/src/delta.ts +4 -1
- package/src/diff.ts +14 -1
- package/src/errors.ts +9 -0
- package/src/index.ts +9 -0
- package/src/match.ts +26 -13
- package/src/slice.ts +321 -0
- package/src/status.ts +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kage1020
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Symbols are paired across base / head via cascading stages:
|
|
|
21
21
|
|
|
22
22
|
Unmatched base Symbols become `removed`, unmatched head Symbols become `added`.
|
|
23
23
|
Matched pairs are classified into one of five statuses per
|
|
24
|
-
[`design/
|
|
24
|
+
[`docs/design/diff-algorithm.md`](../../docs/design/diff-algorithm.md):
|
|
25
25
|
`unchanged` / `moved` / `changed` / `moved+changed` / `dropped-toggled`
|
|
26
26
|
(with `to-dropped` / `to-kept` direction).
|
|
27
27
|
|
|
@@ -59,4 +59,4 @@ await writeFile("out/diff.json", writeCanonicalDiff(diff, { format: "compact" })
|
|
|
59
59
|
## See also
|
|
60
60
|
|
|
61
61
|
- [`schema/aburi.diff.v1.json`](../../schema/aburi.diff.v1.json)
|
|
62
|
-
- [`design/
|
|
62
|
+
- [`docs/design/diff-algorithm.md`](../../docs/design/diff-algorithm.md)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SerializeOptions } from "@aburi/core";
|
|
2
|
-
import { Component, ComponentDiff, Dependency, DependencyDiff, DiffResult, IR, IRRef, MatchRationale, Signature, Symbol, SymbolDelta } from "@aburi/types";
|
|
1
|
+
import { CallEdge, SerializeOptions } from "@aburi/core";
|
|
2
|
+
import { Component, ComponentDiff, Dependency, DependencyDiff, DiffResult, IR, IRRef, MatchRationale, Signature, SliceRecord, Symbol, SymbolChange, SymbolDelta, SymbolId } from "@aburi/types";
|
|
3
3
|
|
|
4
4
|
//#region src/components.d.ts
|
|
5
5
|
/**
|
|
@@ -172,7 +172,16 @@ type DiffErrorCode = /** `base.$schema` and `head.$schema` disagree; a diff acro
|
|
|
172
172
|
* `dependencies`, or an absent `$schema` — anything that would otherwise crash with a
|
|
173
173
|
* cryptic `TypeError: undefined is not iterable` deep in a matching stage.
|
|
174
174
|
*/
|
|
175
|
-
| "ir-shape-invalid"
|
|
175
|
+
| "ir-shape-invalid"
|
|
176
|
+
/**
|
|
177
|
+
* A `SliceRecord` broke the derivation invariant of slice-view.md §7.1 / §8.2:
|
|
178
|
+
* `members[]` empty, `members[]` not in strictly ascending order, or `id` not
|
|
179
|
+
* equal to `"slice:" + members[0]`. The last two compare one property against
|
|
180
|
+
* another and so have no JSON Schema equivalent; the pass checks them itself
|
|
181
|
+
* (§7.4). Raised only by an Aburi bug, never by user input — see
|
|
182
|
+
* `SliceRecordViolation.kind` for which clause broke.
|
|
183
|
+
*/
|
|
184
|
+
| "slice-invariant-violated";
|
|
176
185
|
interface DiffErrorDetail {
|
|
177
186
|
code: DiffErrorCode;
|
|
178
187
|
/** Offending value (schema id, fuzz value, etc.) when applicable. */
|
|
@@ -254,6 +263,79 @@ declare function ownerSimilarity(baseName: string, headName: string): number;
|
|
|
254
263
|
/** §3.4.1 tail note — used by drop-4.5 weak matcher and threshold lookup. */
|
|
255
264
|
declare function lastSegment(qname: string): string;
|
|
256
265
|
//#endregion
|
|
266
|
+
//#region src/slice.d.ts
|
|
267
|
+
/** The three inputs of the Slice View pass — docs/design/slice-view.md §3. */
|
|
268
|
+
interface SliceInput {
|
|
269
|
+
/** SymbolChange records produced by `buildDiff` (pre-sort is not required). */
|
|
270
|
+
changes: readonly SymbolChange[];
|
|
271
|
+
/**
|
|
272
|
+
* Resolved call edges from the base IR. Typically produced by
|
|
273
|
+
* `reconstructCallEdgesFromIR(baseIR)` inside `buildDiff` — Slice View
|
|
274
|
+
* consumes only resolved edges (§5.4), never `Symbol.calls[]` directly.
|
|
275
|
+
*/
|
|
276
|
+
baseCallEdges: readonly CallEdge[];
|
|
277
|
+
/** Resolved call edges from the head IR (same source rule as base). */
|
|
278
|
+
headCallEdges: readonly CallEdge[];
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Slice View clustering pass — docs/design/slice-view.md §2, §4–§8.
|
|
282
|
+
*
|
|
283
|
+
* Groups the changed-Symbol set of a diff into weakly-connected components
|
|
284
|
+
* over the union of base and head call edges. Emits a `SliceRecord[]` whose
|
|
285
|
+
* cluster ids are `"slice:" + <smallest-member Symbol id>`, both `slices[]`
|
|
286
|
+
* and each `members[]` sorted ascending.
|
|
287
|
+
*
|
|
288
|
+
* Pure function: `(changes, baseCallEdges, headCallEdges)` → `SliceRecord[]`.
|
|
289
|
+
* Determinism, idempotence, input-order insensitivity, and locality are all
|
|
290
|
+
* guaranteed — see `computeWeaklyConnectedComponents` in `@aburi/core`.
|
|
291
|
+
*
|
|
292
|
+
* Returns `[]` when no SymbolChange is Node-eligible per §4.1; callers should
|
|
293
|
+
* still serialise the empty array — the Markdown projection omits the section
|
|
294
|
+
* (§9.4 / §12.5) but the JSON always emits the key so consumers never
|
|
295
|
+
* distinguish "field absent" from "no slices" (§11.2).
|
|
296
|
+
*
|
|
297
|
+
* Every returned record has passed `assertSliceRecordInvariant` (§7.4).
|
|
298
|
+
*/
|
|
299
|
+
declare function computeSlices(input: SliceInput): SliceRecord[];
|
|
300
|
+
/**
|
|
301
|
+
* §7.1 — the anchor of a Slice: its lexicographically smallest member.
|
|
302
|
+
*
|
|
303
|
+
* Answers from `members[0]`. `id` is derived from the anchor, so
|
|
304
|
+
* reconstructing the anchor by stripping the `"slice:"` prefix is circular at
|
|
305
|
+
* best and, for a record that broke the derivation, silently wrong: it would
|
|
306
|
+
* name a Symbol that is not in the Slice. `id` is read here only to name the
|
|
307
|
+
* offending record in the error message. Consumers that need the anchor call
|
|
308
|
+
* this; consumers that need a label keep using `id` directly.
|
|
309
|
+
*/
|
|
310
|
+
declare function sliceAnchor(record: SliceRecord): SymbolId;
|
|
311
|
+
/** Which clause of the §7.1 / §8.2 invariant a `SliceRecord` broke. */
|
|
312
|
+
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 (§11.1). */ | "members-empty" /** `members[]` is not in strictly ascending order, so `members[0]` need not be the smallest (§8.2, §11.1). */ | "members-unordered" /** `id` is not `"slice:" + members[0]` (§7.1). */ | "id-not-derived" /** The anchor is itself in a reserved id namespace, so `id` would read as a doubled prefix (§7.5). */ | "anchor-in-reserved-namespace";
|
|
313
|
+
interface SliceRecordViolation {
|
|
314
|
+
kind: SliceViolationKind;
|
|
315
|
+
/** The offending record's `id` when it has a usable one, else a short stand-in. */
|
|
316
|
+
subject: string;
|
|
317
|
+
/** Human-facing explanation, suitable for an error message or a validator error. */
|
|
318
|
+
message: string;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Report which clause of the §7.1 / §8.2 invariant a value breaks, or `null`
|
|
322
|
+
* when it is a well-formed `SliceRecord`. Non-throwing counterpart of
|
|
323
|
+
* `assertSliceRecordInvariant`: the pass wants an exception, a schema
|
|
324
|
+
* validator wants a verdict it can turn into its own error.
|
|
325
|
+
*
|
|
326
|
+
* Takes `unknown` rather than `SliceRecord` deliberately. §7.4 layer 2 points
|
|
327
|
+
* this at documents written by third-party or older producers — data that has
|
|
328
|
+
* not been type-checked by definition — so anything that assumed a well-typed
|
|
329
|
+
* argument would crash on exactly the input it exists to reject.
|
|
330
|
+
*/
|
|
331
|
+
declare function sliceRecordViolation(value: unknown): SliceRecordViolation | null;
|
|
332
|
+
/**
|
|
333
|
+
* Throwing form of `sliceRecordViolation`. Raised as a coded `DiffError` so
|
|
334
|
+
* callers branch on `code` rather than parsing the message; which clause broke
|
|
335
|
+
* is `SliceRecordViolation.kind`, for callers that need to tell them apart.
|
|
336
|
+
*/
|
|
337
|
+
declare function assertSliceRecordInvariant(record: SliceRecord): void;
|
|
338
|
+
//#endregion
|
|
257
339
|
//#region src/status.d.ts
|
|
258
340
|
type SymbolStatus = "unchanged" | "moved" | "changed" | "moved+changed" | "dropped-toggled";
|
|
259
341
|
/**
|
|
@@ -263,8 +345,8 @@ type SymbolStatus = "unchanged" | "moved" | "changed" | "moved+changed" | "dropp
|
|
|
263
345
|
* drop-rule flip, not a genuine code edit (§4.1 rationale).
|
|
264
346
|
*
|
|
265
347
|
* "pathChanged" catches both physical file relocation AND in-file rename: any Symbol id
|
|
266
|
-
* mismatch between base and head means the identifier moved, and DF9 ("method
|
|
267
|
-
* (
|
|
348
|
+
* mismatch between base and head means the identifier moved, and DF9 ("method rename
|
|
349
|
+
* (same file, same logic) → moved") relies on this. Stage-1 pairs cannot trigger the id branch
|
|
268
350
|
* (they match by exact id), so this only affects stage-2+ pairings where the pair was
|
|
269
351
|
* discovered via fingerprint or name/signature similarity.
|
|
270
352
|
*/
|
|
@@ -273,5 +355,5 @@ declare function classifyStatus(base: Symbol, head: Symbol): SymbolStatus;
|
|
|
273
355
|
type DropDirection = "to-dropped" | "to-kept";
|
|
274
356
|
declare function dropDirection(head: Symbol): DropDirection;
|
|
275
357
|
//#endregion
|
|
276
|
-
export { DEFAULT_LINE_FUZZ, type DeltaOptions, DiffError, type DiffErrorCode, type DiffErrorDetail, type DiffInput, type DropDirection, type GitRenameMap, MAX_LINE_FUZZ, MIN_LINE_FUZZ, type SymbolPair, type SymbolStatus, buildDiff, classifyStatus, computeSymbolDelta, diffComponents, diffDependencies, dropDirection, jaccard, jaccardTokens, lastSegment, matchStageDroppedWeak, matchStageGitRename, matchStageId, matchStageLogicFingerprint, matchStageNameSignature, nameSimilarity, ownerSimilarity, signatureSimilarity, tokenizeName, writeCanonicalDiff };
|
|
358
|
+
export { DEFAULT_LINE_FUZZ, type DeltaOptions, 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, diffComponents, diffDependencies, dropDirection, jaccard, jaccardTokens, lastSegment, matchStageDroppedWeak, matchStageGitRename, matchStageId, matchStageLogicFingerprint, matchStageNameSignature, nameSimilarity, ownerSimilarity, signatureSimilarity, sliceAnchor, sliceRecordViolation, tokenizeName, writeCanonicalDiff };
|
|
277
359
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/components.ts","../src/delta.ts","../src/match.ts","../src/diff.ts","../src/errors.ts","../src/signature.ts","../src/similarity.ts","../src/status.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/components.ts","../src/delta.ts","../src/match.ts","../src/diff.ts","../src/errors.ts","../src/signature.ts","../src/similarity.ts","../src/slice.ts","../src/status.ts"],"mappings":";;;;;;;AAgBA;;;;;iBAAgB,cAAA,CACd,IAAA,WAAe,SAAA,IACf,IAAA,WAAe,SAAA,KACd,aAAA;;;;;;iBA4Ca,gBAAA,CACd,IAAA,WAAe,UAAA,IACf,IAAA,WAAe,UAAA,KACd,cAAA;;;cCrDU,iBAAA;AAAA,cACA,aAAA;AAAA,cACA,aAAA;AAAA,UAEI,YAAA;EDDa;;;;;;ECQ5B,QAAQ;AAAA;;;;;;ADLM;iBCcA,kBAAA,CACd,IAAA,EAAM,MAAA,EACN,IAAA,EAAM,MAAA,EACN,OAAA,GAAS,YAAA,GACR,WAAA;;;;;;ADrBH;;UENiB,UAAA;EACf,IAAA,EAAM,MAAA;EACN,IAAA,EAAM,MAAA;EACN,SAAA,EAAW,cAAA;AAAA;;KAID,YAAA,GAAe,WAAW;;;;;iBAStB,YAAA,CACd,IAAA,WAAe,MAAA,IACf,IAAA,WAAe,MAAA;EAEf,OAAA,EAAS,UAAA;EACT,aAAA,EAAe,MAAA;EACf,aAAA,EAAe,MAAA;AAAA;;;;;;;;;;iBA6BD,mBAAA,CACd,aAAA,WAAwB,MAAA,IACxB,aAAA,WAAwB,MAAA,IACxB,SAAA,EAAW,YAAA;EAEX,OAAA,EAAS,UAAA;EACT,aAAA,EAAe,MAAA;EACf,aAAA,EAAe,MAAA;AAAA;;ADvDjB;;;;AAA8B;AAC9B;;;;AAA0B;iBCqHV,0BAAA,CACd,aAAA,WAAwB,MAAA,IACxB,aAAA,WAAwB,MAAA;EAExB,OAAA,EAAS,UAAA;EACT,aAAA,EAAe,MAAA;EACf,aAAA,EAAe,MAAA;AAAA;ADxHjB;;;;AAOU;AASV;;;;;;;;;;;;AAhBA,iBCuMgB,uBAAA,CACd,aAAA,WAAwB,MAAA,IACxB,aAAA,WAAwB,MAAA;EAExB,OAAA,EAAS,UAAA;EACT,aAAA,EAAe,MAAA;EACf,aAAA,EAAe,MAAA;AAAA;ADzLH;;;;AC3Bd;;;;;AD2Bc,iBC2QE,qBAAA,CACd,aAAA,WAAwB,MAAA,IACxB,aAAA,WAAwB,MAAA;EAExB,OAAA,EAAS,UAAA;EACT,aAAA,EAAe,MAAA;EACf,aAAA,EAAe,MAAA;AAAA;;;UCnSA,SAAA;EACf,MAAA,EAAQ,EAAA;EACR,MAAA,EAAQ,EAAA;EHJO;EGMf,IAAA,EAAM,KAAA;EACN,IAAA,EAAM,KAAA;EHLQ;EGOd,SAAA;IAAc,IAAA;IAAc,OAAA;EAAA;EHR5B;EGUA,UAAA,GAAa,YAAA;EHTC;EGWd,KAAA,GAAQ,YAAA;AAAA;;;;;;;;iBAYM,SAAA,CAAU,KAAA,EAAO,SAAA,GAAY,UAAU;;;;;;iBA6MvC,kBAAA,CAAmB,IAAA,EAAM,UAAA,EAAY,OAAA,GAAS,gBAAqB;;;;;;;KClPvE,aAAA;;;;;;;;;;AJcI;AA4ChB;;;;;;UIpCiB,eAAA;EACf,IAAA,EAAM,aAAa;EJoCJ;EIlCf,KAAA;AAAA;AAAA,cAGW,SAAA,SAAkB,KAAA;EAAA,SACpB,IAAA,EAAM,aAAA;EAAA,SACN,KAAA;cAEG,OAAA,UAAiB,MAAA,EAAQ,eAAA,EAAiB,OAAA;IAAY,KAAA;EAAA;AAAA;;;;;;AJrBpE;;;;;;;;;;;;iBKCgB,mBAAA,CACd,IAAA,EAAM,SAAA,qBACN,IAAA,EAAM,SAAS;;;;;;;ALHjB;;;;;;;;;;;;AAAA,iBMCgB,YAAA,CAAa,KAAa;;ANE1B;AA4ChB;;;iBMQgB,OAAA,CAAQ,CAAA,qBAAsB,CAAoB;;iBAYlD,aAAA,CAAc,CAAA,UAAW,CAAS;;;;;;iBASlC,cAAA,CAAe,QAAA,UAAkB,QAAgB;;;AN1BhD;;;;ACrDjB;;;;AAA8B;AAC9B;;;iBKgGgB,eAAA,CAAgB,QAAA,UAAkB,QAAgB;ALhGxC;AAAA,iBKiHV,WAAA,CAAY,KAAa;;;;UC1HxB,UAAA;EPWD;EOTd,OAAA,WAAkB,YAAA;;;;;;EAMlB,aAAA,WAAwB,QAAA;EPIT;EOFf,aAAA,WAAwB,QAAA;AAAA;;;;APIV;AA4ChB;;;;;;;;;;;;;;AAGiB;iBO7BD,aAAA,CAAc,KAAA,EAAO,UAAA,GAAa,WAAW;;;ANxB7D;;;;AAA8B;AAC9B;;;iBMyFgB,WAAA,CAAY,MAAA,EAAQ,WAAA,GAAc,QAAQ;ANzFhC;AAAA,KMsGd,kBAAA,sMNnGZ,2MAOU;AAAA,UMwGO,oBAAA;EACf,IAAA,EAAM,kBAAkB;EN/FlB;EMiGN,OAAA;EN/FS;EMiGT,OAAA;AAAA;;;;;;;;;;ANhGY;;iBM8GE,oBAAA,CAAqB,KAAA,YAAiB,oBAAoB;;ALzI1E;;;;iBKyOgB,0BAAA,CAA2B,MAAmB,EAAX,WAAW;;;KCjPlD,YAAA;;;ARcZ;;;;;;;;;;iBQAgB,cAAA,CAAe,IAAA,EAAM,MAAA,EAAU,IAAA,EAAM,MAAA,GAAW,YAAA;;KAepD,aAAA;AAAA,iBAEI,aAAA,CAAc,IAAA,EAAM,MAAA,GAAW,aAAa"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { serializeCanonical } from "@aburi/core";
|
|
1
|
+
import { RESERVED_LANGUAGE_IDS, computeWeaklyConnectedComponents, reconstructCallEdgesFromIR, serializeCanonical, trySymbolId } from "@aburi/core";
|
|
2
2
|
|
|
3
3
|
//#region src/components.ts
|
|
4
4
|
/**
|
|
@@ -208,7 +208,7 @@ function diffEffects(base, head) {
|
|
|
208
208
|
const mapper = (e) => ({
|
|
209
209
|
item: e,
|
|
210
210
|
key: `${e.id}::${e.target}`,
|
|
211
|
-
line: e.line
|
|
211
|
+
line: e.line ?? 0
|
|
212
212
|
});
|
|
213
213
|
return differentiate(base.map(mapper), head.map(mapper), effectsEqual, Number.POSITIVE_INFINITY);
|
|
214
214
|
}
|
|
@@ -582,7 +582,7 @@ function matchStageGitRename(remainingBase, remainingHead, renameMap) {
|
|
|
582
582
|
continue;
|
|
583
583
|
}
|
|
584
584
|
const expectedId = rewriteIdFile(b.id, b.source.file, newPath);
|
|
585
|
-
const h = headById.get(expectedId);
|
|
585
|
+
const h = expectedId === null ? void 0 : headById.get(expectedId);
|
|
586
586
|
if (h === void 0 || usedHead.has(h.id)) {
|
|
587
587
|
remaining.push(b);
|
|
588
588
|
continue;
|
|
@@ -600,13 +600,26 @@ function matchStageGitRename(remainingBase, remainingHead, renameMap) {
|
|
|
600
600
|
remainingHead: remainingHead.filter((h) => !usedHead.has(h.id))
|
|
601
601
|
};
|
|
602
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Predict the id a base Symbol would carry after git moved its file. The second Symbol-id
|
|
605
|
+
* derivation in the codebase after `makeSymbolId` itself, so it goes back through the same
|
|
606
|
+
* constructor rather than re-concatenating the parts: `trySymbolId` rejects a rename target
|
|
607
|
+
* the id grammar cannot express (a backslash path, say) instead of minting an id no head
|
|
608
|
+
* Symbol can ever equal.
|
|
609
|
+
*
|
|
610
|
+
* Returns `null` for that case, and the unchanged id when no rename applies — the caller
|
|
611
|
+
* treats a null the same way it treats a lookup miss, leaving the pair for stage 3.
|
|
612
|
+
*/
|
|
603
613
|
function rewriteIdFile(id, oldPath, newPath) {
|
|
604
614
|
const colon = id.indexOf(":");
|
|
605
615
|
const hash = id.indexOf("#");
|
|
606
616
|
if (colon < 0 || hash < 0 || hash < colon) return id;
|
|
607
|
-
const language = id.slice(0, colon);
|
|
608
617
|
if (id.slice(colon + 1, hash) !== oldPath) return id;
|
|
609
|
-
return
|
|
618
|
+
return trySymbolId({
|
|
619
|
+
language: id.slice(0, colon),
|
|
620
|
+
file: newPath,
|
|
621
|
+
qualifiedName: id.slice(hash + 1)
|
|
622
|
+
});
|
|
610
623
|
}
|
|
611
624
|
/**
|
|
612
625
|
* §3.3 — group base Symbols by `fingerprint.logic` and match head Symbols whose logic
|
|
@@ -821,6 +834,226 @@ function basename(path) {
|
|
|
821
834
|
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
822
835
|
}
|
|
823
836
|
|
|
837
|
+
//#endregion
|
|
838
|
+
//#region src/slice.ts
|
|
839
|
+
/**
|
|
840
|
+
* Slice View clustering pass — docs/design/slice-view.md §2, §4–§8.
|
|
841
|
+
*
|
|
842
|
+
* Groups the changed-Symbol set of a diff into weakly-connected components
|
|
843
|
+
* over the union of base and head call edges. Emits a `SliceRecord[]` whose
|
|
844
|
+
* cluster ids are `"slice:" + <smallest-member Symbol id>`, both `slices[]`
|
|
845
|
+
* and each `members[]` sorted ascending.
|
|
846
|
+
*
|
|
847
|
+
* Pure function: `(changes, baseCallEdges, headCallEdges)` → `SliceRecord[]`.
|
|
848
|
+
* Determinism, idempotence, input-order insensitivity, and locality are all
|
|
849
|
+
* guaranteed — see `computeWeaklyConnectedComponents` in `@aburi/core`.
|
|
850
|
+
*
|
|
851
|
+
* Returns `[]` when no SymbolChange is Node-eligible per §4.1; callers should
|
|
852
|
+
* still serialise the empty array — the Markdown projection omits the section
|
|
853
|
+
* (§9.4 / §12.5) but the JSON always emits the key so consumers never
|
|
854
|
+
* distinguish "field absent" from "no slices" (§11.2).
|
|
855
|
+
*
|
|
856
|
+
* Every returned record has passed `assertSliceRecordInvariant` (§7.4).
|
|
857
|
+
*/
|
|
858
|
+
function computeSlices(input) {
|
|
859
|
+
const nodeIds = collectNodeIds(input.changes);
|
|
860
|
+
if (nodeIds.length === 0) return [];
|
|
861
|
+
const nodeIdSet = new Set(nodeIds);
|
|
862
|
+
return computeWeaklyConnectedComponents(nodeIds, collectEdges(input.baseCallEdges, input.headCallEdges, nodeIdSet), (id) => id).map(makeSliceRecord);
|
|
863
|
+
}
|
|
864
|
+
const SLICE_ID_PREFIX = "slice:";
|
|
865
|
+
/**
|
|
866
|
+
* §7.1 — the single place production code derives a Slice id. Everything else
|
|
867
|
+
* in `src/` either receives an id or renders one. (Tests spell the prefix out
|
|
868
|
+
* literally on purpose: an expectation written in terms of the function under
|
|
869
|
+
* test would agree with it no matter what it produced.)
|
|
870
|
+
*
|
|
871
|
+
* The cast is the one place a `SliceId` comes into existence. `SliceId` and
|
|
872
|
+
* `SymbolId` are separate brands precisely so this concatenation cannot be
|
|
873
|
+
* open-coded anywhere else: a bare `"slice:" + x` evaluates to `string`, which
|
|
874
|
+
* `SliceRecord.id` no longer accepts.
|
|
875
|
+
*/
|
|
876
|
+
function sliceIdFor(anchor) {
|
|
877
|
+
return `${SLICE_ID_PREFIX}${anchor}`;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Build one SliceRecord from an ascending-sorted component and check its own
|
|
881
|
+
* post-condition before letting it out (§7.4 layer 1).
|
|
882
|
+
*
|
|
883
|
+
* The check is not defending against untrusted input — this function builds
|
|
884
|
+
* the id itself, so the derivation clause is true by construction here and
|
|
885
|
+
* only the two clauses about `members` can actually fire. Those are the point:
|
|
886
|
+
* `members[0]` is the anchor only because `computeWeaklyConnectedComponents`
|
|
887
|
+
* returns each component sorted ascending, a guarantee that lives one layer
|
|
888
|
+
* down and is invisible from this file. If it ever stops holding, the diff
|
|
889
|
+
* would otherwise keep emitting well-formed-looking SliceRecords naming the
|
|
890
|
+
* wrong anchor. Here it fails instead.
|
|
891
|
+
*/
|
|
892
|
+
function makeSliceRecord(members) {
|
|
893
|
+
const anchor = members[0];
|
|
894
|
+
if (anchor === void 0) throw new DiffError("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" });
|
|
895
|
+
const record = {
|
|
896
|
+
id: sliceIdFor(anchor),
|
|
897
|
+
members: [...members]
|
|
898
|
+
};
|
|
899
|
+
assertSliceRecordInvariant(record);
|
|
900
|
+
return record;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* §7.1 — the anchor of a Slice: its lexicographically smallest member.
|
|
904
|
+
*
|
|
905
|
+
* Answers from `members[0]`. `id` is derived from the anchor, so
|
|
906
|
+
* reconstructing the anchor by stripping the `"slice:"` prefix is circular at
|
|
907
|
+
* best and, for a record that broke the derivation, silently wrong: it would
|
|
908
|
+
* name a Symbol that is not in the Slice. `id` is read here only to name the
|
|
909
|
+
* offending record in the error message. Consumers that need the anchor call
|
|
910
|
+
* this; consumers that need a label keep using `id` directly.
|
|
911
|
+
*/
|
|
912
|
+
function sliceAnchor(record) {
|
|
913
|
+
const anchor = record.members[0];
|
|
914
|
+
if (anchor === void 0) {
|
|
915
|
+
const violation = emptyMembersViolation(record.id);
|
|
916
|
+
throw new DiffError(violation.message, {
|
|
917
|
+
code: "slice-invariant-violated",
|
|
918
|
+
value: violation.subject
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return anchor;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Report which clause of the §7.1 / §8.2 invariant a value breaks, or `null`
|
|
925
|
+
* when it is a well-formed `SliceRecord`. Non-throwing counterpart of
|
|
926
|
+
* `assertSliceRecordInvariant`: the pass wants an exception, a schema
|
|
927
|
+
* validator wants a verdict it can turn into its own error.
|
|
928
|
+
*
|
|
929
|
+
* Takes `unknown` rather than `SliceRecord` deliberately. §7.4 layer 2 points
|
|
930
|
+
* this at documents written by third-party or older producers — data that has
|
|
931
|
+
* not been type-checked by definition — so anything that assumed a well-typed
|
|
932
|
+
* argument would crash on exactly the input it exists to reject.
|
|
933
|
+
*/
|
|
934
|
+
function sliceRecordViolation(value) {
|
|
935
|
+
if (typeof value !== "object" || value === null) return {
|
|
936
|
+
kind: "malformed-shape",
|
|
937
|
+
subject: "<not an object>",
|
|
938
|
+
message: `Expected a SliceRecord object; got ${value === null ? "null" : typeof value}.`
|
|
939
|
+
};
|
|
940
|
+
const { id, members } = value;
|
|
941
|
+
const subject = typeof id === "string" ? id : "<missing id>";
|
|
942
|
+
if (typeof id !== "string") return {
|
|
943
|
+
kind: "malformed-shape",
|
|
944
|
+
subject,
|
|
945
|
+
message: `SliceRecord ${subject}: id must be a string; got ${typeof id}.`
|
|
946
|
+
};
|
|
947
|
+
if (!Array.isArray(members) || members.some((member) => typeof member !== "string")) return {
|
|
948
|
+
kind: "malformed-shape",
|
|
949
|
+
subject,
|
|
950
|
+
message: `SliceRecord ${subject}: members must be an array of strings.`
|
|
951
|
+
};
|
|
952
|
+
const anchor = members[0];
|
|
953
|
+
if (anchor === void 0) return emptyMembersViolation(subject);
|
|
954
|
+
const reservedAnchor = reservedNamespaceOf(anchor);
|
|
955
|
+
if (reservedAnchor !== null) return {
|
|
956
|
+
kind: "anchor-in-reserved-namespace",
|
|
957
|
+
subject,
|
|
958
|
+
message: `SliceRecord anchor "${anchor}" uses the reserved language token "${reservedAnchor}", so its Slice id would repeat the prefix (ir-schema.md §3.5, slice-view.md §7.5).`
|
|
959
|
+
};
|
|
960
|
+
for (let i = 1; i < members.length; i++) {
|
|
961
|
+
const previous = members[i - 1];
|
|
962
|
+
const current = members[i];
|
|
963
|
+
if (previous < current) continue;
|
|
964
|
+
return {
|
|
965
|
+
kind: "members-unordered",
|
|
966
|
+
subject,
|
|
967
|
+
message: `SliceRecord ${subject}: members[] is not in strictly ascending order at index ${i} ("${current}" follows "${previous}"), so members[0] is not necessarily the anchor (slice-view.md §8.2 for the order, §11.1 for uniqueness).`
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const expected = sliceIdFor(anchor);
|
|
971
|
+
if (id !== expected) return {
|
|
972
|
+
kind: "id-not-derived",
|
|
973
|
+
subject,
|
|
974
|
+
message: `SliceRecord id "${id}" is not derived from the anchor "${anchor}"; expected "${expected}" (slice-view.md §7.1).`
|
|
975
|
+
};
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
/** The reserved token an id opens with, or `null`. Shares the list `@aburi/core` enforces. */
|
|
979
|
+
function reservedNamespaceOf(id) {
|
|
980
|
+
const colon = id.indexOf(":");
|
|
981
|
+
if (colon < 0) return null;
|
|
982
|
+
const token = id.slice(0, colon);
|
|
983
|
+
return RESERVED_LANGUAGE_IDS.has(token) ? token : null;
|
|
984
|
+
}
|
|
985
|
+
/** Single source of the empty-`members[]` wording, shared with `sliceAnchor`. */
|
|
986
|
+
function emptyMembersViolation(subject) {
|
|
987
|
+
return {
|
|
988
|
+
kind: "members-empty",
|
|
989
|
+
subject,
|
|
990
|
+
message: `SliceRecord ${subject}: members[] is empty, so the Slice has no anchor (slice-view.md §11.1 requires at least one member).`
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Throwing form of `sliceRecordViolation`. Raised as a coded `DiffError` so
|
|
995
|
+
* callers branch on `code` rather than parsing the message; which clause broke
|
|
996
|
+
* is `SliceRecordViolation.kind`, for callers that need to tell them apart.
|
|
997
|
+
*/
|
|
998
|
+
function assertSliceRecordInvariant(record) {
|
|
999
|
+
const violation = sliceRecordViolation(record);
|
|
1000
|
+
if (violation === null) return;
|
|
1001
|
+
throw new DiffError(violation.message, {
|
|
1002
|
+
code: "slice-invariant-violated",
|
|
1003
|
+
value: violation.subject
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* §4.1 Node selection: keep every SymbolChange whose status is
|
|
1008
|
+
* added / removed / changed / moved+changed / dropped-toggled. The identity
|
|
1009
|
+
* is the head-side Symbol id where present (added / changed / moved+changed /
|
|
1010
|
+
* dropped-toggled), the base-side id for removed. Pure moved and unchanged
|
|
1011
|
+
* (already stripped upstream) are excluded — §4.1 / §4.3.
|
|
1012
|
+
*
|
|
1013
|
+
* The returned array is deliberately not deduplicated: a well-formed
|
|
1014
|
+
* SymbolChange list from `buildDiff` never mentions the same Symbol id
|
|
1015
|
+
* twice, and Union-Find would coalesce any accidental duplicate anyway.
|
|
1016
|
+
*/
|
|
1017
|
+
function collectNodeIds(changes) {
|
|
1018
|
+
const ids = [];
|
|
1019
|
+
for (const change of changes) {
|
|
1020
|
+
const id = nodeIdOf(change);
|
|
1021
|
+
if (id === null) continue;
|
|
1022
|
+
ids.push(id);
|
|
1023
|
+
}
|
|
1024
|
+
return ids;
|
|
1025
|
+
}
|
|
1026
|
+
function nodeIdOf(change) {
|
|
1027
|
+
switch (change.status) {
|
|
1028
|
+
case "added": return change.symbol.id;
|
|
1029
|
+
case "removed": return change.symbol.id;
|
|
1030
|
+
case "changed":
|
|
1031
|
+
case "moved+changed":
|
|
1032
|
+
case "dropped-toggled": return change.after.id;
|
|
1033
|
+
case "moved": return null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* §5.1 Edge selection: union of base and head, restricted to edges whose
|
|
1038
|
+
* BOTH endpoints are Nodes, canonicalised to `(u, v)` with `u < v` (self-
|
|
1039
|
+
* loops implicitly dropped). Multi-edges collapse naturally inside the WCC
|
|
1040
|
+
* primitive — no explicit dedup needed.
|
|
1041
|
+
*
|
|
1042
|
+
* The union is the load-bearing rule of §5.3: a controller that called an
|
|
1043
|
+
* old service in base and a new service in head needs both edges to land
|
|
1044
|
+
* all three Symbols in a single Slice.
|
|
1045
|
+
*/
|
|
1046
|
+
function collectEdges(baseEdges, headEdges, nodeIds) {
|
|
1047
|
+
const pairs = [];
|
|
1048
|
+
for (const edge of baseEdges) appendEdgeIfBothNodes(edge, nodeIds, pairs);
|
|
1049
|
+
for (const edge of headEdges) appendEdgeIfBothNodes(edge, nodeIds, pairs);
|
|
1050
|
+
return pairs;
|
|
1051
|
+
}
|
|
1052
|
+
function appendEdgeIfBothNodes(edge, nodeIds, out) {
|
|
1053
|
+
if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) return;
|
|
1054
|
+
out.push([edge.from, edge.to]);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
824
1057
|
//#endregion
|
|
825
1058
|
//#region src/status.ts
|
|
826
1059
|
/**
|
|
@@ -830,8 +1063,8 @@ function basename(path) {
|
|
|
830
1063
|
* drop-rule flip, not a genuine code edit (§4.1 rationale).
|
|
831
1064
|
*
|
|
832
1065
|
* "pathChanged" catches both physical file relocation AND in-file rename: any Symbol id
|
|
833
|
-
* mismatch between base and head means the identifier moved, and DF9 ("method
|
|
834
|
-
* (
|
|
1066
|
+
* mismatch between base and head means the identifier moved, and DF9 ("method rename
|
|
1067
|
+
* (same file, same logic) → moved") relies on this. Stage-1 pairs cannot trigger the id branch
|
|
835
1068
|
* (they match by exact id), so this only affects stage-2+ pairings where the pair was
|
|
836
1069
|
* discovered via fingerprint or name/signature similarity.
|
|
837
1070
|
*/
|
|
@@ -966,6 +1199,11 @@ function buildDiff(input) {
|
|
|
966
1199
|
summary.depsAdded = dependencies.added.length;
|
|
967
1200
|
summary.depsRemoved = dependencies.removed.length;
|
|
968
1201
|
symbols.sort(compareSymbolChange);
|
|
1202
|
+
const slices = computeSlices({
|
|
1203
|
+
changes: symbols,
|
|
1204
|
+
baseCallEdges: reconstructCallEdgesFromIR(input.baseIR),
|
|
1205
|
+
headCallEdges: reconstructCallEdgesFromIR(input.headIR)
|
|
1206
|
+
});
|
|
969
1207
|
return {
|
|
970
1208
|
$schema: DIFF_SCHEMA,
|
|
971
1209
|
generator: input.generator ?? DEFAULT_GENERATOR,
|
|
@@ -974,7 +1212,8 @@ function buildDiff(input) {
|
|
|
974
1212
|
summary,
|
|
975
1213
|
symbols,
|
|
976
1214
|
components,
|
|
977
|
-
dependencies
|
|
1215
|
+
dependencies,
|
|
1216
|
+
slices
|
|
978
1217
|
};
|
|
979
1218
|
}
|
|
980
1219
|
/** §9.1 — refuse to diff across schema versions. */
|
|
@@ -1037,5 +1276,5 @@ function writeCanonicalDiff(diff, options = {}) {
|
|
|
1037
1276
|
}
|
|
1038
1277
|
|
|
1039
1278
|
//#endregion
|
|
1040
|
-
export { DEFAULT_LINE_FUZZ, DiffError, MAX_LINE_FUZZ, MIN_LINE_FUZZ, buildDiff, classifyStatus, computeSymbolDelta, diffComponents, diffDependencies, dropDirection, jaccard, jaccardTokens, lastSegment, matchStageDroppedWeak, matchStageGitRename, matchStageId, matchStageLogicFingerprint, matchStageNameSignature, nameSimilarity, ownerSimilarity, signatureSimilarity, tokenizeName, writeCanonicalDiff };
|
|
1279
|
+
export { DEFAULT_LINE_FUZZ, DiffError, MAX_LINE_FUZZ, MIN_LINE_FUZZ, assertSliceRecordInvariant, buildDiff, classifyStatus, computeSlices, computeSymbolDelta, diffComponents, diffDependencies, dropDirection, jaccard, jaccardTokens, lastSegment, matchStageDroppedWeak, matchStageGitRename, matchStageId, matchStageLogicFingerprint, matchStageNameSignature, nameSimilarity, ownerSimilarity, signatureSimilarity, sliceAnchor, sliceRecordViolation, tokenizeName, writeCanonicalDiff };
|
|
1041
1280
|
//# sourceMappingURL=index.mjs.map
|