@tallyrelease/core 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/dist/explain.d.ts +2 -0
- package/dist/explain.js +1 -0
- package/dist/facts.d.ts +10 -0
- package/dist/facts.js +28 -7
- package/dist/plugin.d.ts +6 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +52 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1](https://github.com/abinnovision/tallyrelease/compare/core-v0.3.0...core-v0.3.1) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **core:** count prereleases from a unit's introduction without a line base ([#48](https://github.com/abinnovision/tallyrelease/issues/48)) ([06a591b](https://github.com/abinnovision/tallyrelease/commit/06a591b3522cf60ce5bde56e393f464617ffa101))
|
|
9
|
+
|
|
3
10
|
## [0.3.0](https://github.com/abinnovision/tally-release/compare/core-v0.2.0...core-v0.3.0) (2026-10-16)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @tallyrelease/core
|
|
2
2
|
|
|
3
|
-
Pure planning core and plugin contract for [tallyrelease](https://github.com/abinnovision/
|
|
3
|
+
Pure planning core and plugin contract for [tallyrelease](https://github.com/abinnovision/tallyrelease). It does no I/O: files and git are read through the injected `ReadonlyFs`, `Ctx` and `GitReader`.
|
|
4
4
|
|
|
5
5
|
Most users want [`@tallyrelease/cli`](https://www.npmjs.com/package/@tallyrelease/cli) instead. Install this package to write an ecosystem plugin or to embed the planner.
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ export const example = definePlugin({
|
|
|
22
22
|
});
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
See `Plugin` in [`src/plugin.ts`](https://github.com/abinnovision/
|
|
25
|
+
See `Plugin` in [`src/plugin.ts`](https://github.com/abinnovision/tallyrelease/blob/main/packages/core/src/plugin.ts) for the available hooks (`discover`, `edges`, `inputs`, `versionFiles`, `schemes`, `intent`).
|
|
26
26
|
|
|
27
27
|
## Testing
|
|
28
28
|
|
package/dist/explain.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface UnitExplanation {
|
|
|
25
25
|
name: string;
|
|
26
26
|
members: string[];
|
|
27
27
|
} | null;
|
|
28
|
+
/** Origin of the prerelease counter without a line base; see `UnitFacts`. */
|
|
29
|
+
introduced: string | null;
|
|
28
30
|
/** What set `unit.next`: the bump, the initial version or a Release-As value. */
|
|
29
31
|
nextSource: "bump" | "initial" | "release-as" | null;
|
|
30
32
|
/** Changed unit files (reason `direct`) or changed global inputs (`global`). */
|
package/dist/explain.js
CHANGED
|
@@ -52,6 +52,7 @@ export const explainUnit = async (workspace, git, { unit: id, ...opts }) => {
|
|
|
52
52
|
plan,
|
|
53
53
|
unit,
|
|
54
54
|
group: group ? { name: group.name, members: [...group.members] } : null,
|
|
55
|
+
introduced: unitFacts?.introduced ?? null,
|
|
55
56
|
nextSource: releases.get(id)?.nextSource ?? null,
|
|
56
57
|
files,
|
|
57
58
|
changes,
|
package/dist/facts.d.ts
CHANGED
|
@@ -40,6 +40,15 @@ export interface UnitFacts {
|
|
|
40
40
|
readonly fromTag: UnitTag | null;
|
|
41
41
|
/** Highest stable tag on head's first-parent chain. */
|
|
42
42
|
readonly lineBaseTag: UnitTag | null;
|
|
43
|
+
/**
|
|
44
|
+
* Without a line base and with a root other than `.`: the oldest commit on
|
|
45
|
+
* head's first-parent chain that touches the unit root.
|
|
46
|
+
*/
|
|
47
|
+
readonly introduced: string | null;
|
|
48
|
+
/**
|
|
49
|
+
* First-parent commits since the line base; without one, since and
|
|
50
|
+
* including `introduced`, else `total`.
|
|
51
|
+
*/
|
|
43
52
|
readonly sinceLineBase: number;
|
|
44
53
|
readonly total: number;
|
|
45
54
|
/** Changes since the base tag attributed to this unit, core commit order. */
|
|
@@ -61,6 +70,7 @@ export interface GroupFacts {
|
|
|
61
70
|
readonly baseTag: UnitTag | null;
|
|
62
71
|
/** Highest stable member tag on head's first-parent chain. */
|
|
63
72
|
readonly lineBaseTag: UnitTag | null;
|
|
73
|
+
/** Without a line base, the max over the members. */
|
|
64
74
|
readonly sinceLineBase: number;
|
|
65
75
|
}
|
|
66
76
|
/** Git and intent data of every released unit, collected before release math. */
|
package/dist/facts.js
CHANGED
|
@@ -156,6 +156,20 @@ export const collectPlanFacts = async (workspace, git, opts) => {
|
|
|
156
156
|
}
|
|
157
157
|
return a.sha !== b.sha && (await isAncestor(a.sha, b.sha)) ? b : a;
|
|
158
158
|
};
|
|
159
|
+
/** Counter since the line base; without one, since the unit's introduction. */
|
|
160
|
+
const lineCount = async (root, lineBaseTag) => {
|
|
161
|
+
if (lineBaseTag) {
|
|
162
|
+
return {
|
|
163
|
+
introduced: null,
|
|
164
|
+
sinceLineBase: await countSince(lineBaseTag.sha),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const introduced = root === "." ? null : await git.introduced(root, head);
|
|
168
|
+
return {
|
|
169
|
+
introduced,
|
|
170
|
+
sinceLineBase: introduced === null ? total : (await countSince(introduced)) + 1,
|
|
171
|
+
};
|
|
172
|
+
};
|
|
159
173
|
const bases = new Map();
|
|
160
174
|
for (const unit of workspace.units) {
|
|
161
175
|
const matched = tags.get(unit.id);
|
|
@@ -173,9 +187,14 @@ export const collectPlanFacts = async (workspace, git, opts) => {
|
|
|
173
187
|
break;
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
|
-
bases.set(unit.id, {
|
|
190
|
+
bases.set(unit.id, {
|
|
191
|
+
scheme,
|
|
192
|
+
baseTag: stable.at(-1) ?? null,
|
|
193
|
+
lineBaseTag,
|
|
194
|
+
// eslint-disable-next-line no-await-in-loop -- one git call per unit
|
|
195
|
+
...(await lineCount(unit.root, lineBaseTag)),
|
|
196
|
+
});
|
|
177
197
|
}
|
|
178
|
-
const sinceLine = async (tag) => tag ? await countSince(tag.sha) : total;
|
|
179
198
|
const groups = [];
|
|
180
199
|
for (const { name, members } of workspace.groups) {
|
|
181
200
|
let baseTag = null;
|
|
@@ -194,8 +213,10 @@ export const collectPlanFacts = async (workspace, git, opts) => {
|
|
|
194
213
|
members,
|
|
195
214
|
baseTag,
|
|
196
215
|
lineBaseTag,
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
sinceLineBase: lineBaseTag
|
|
217
|
+
? // eslint-disable-next-line no-await-in-loop -- memoized per line base
|
|
218
|
+
await countSince(lineBaseTag.sha)
|
|
219
|
+
: Math.max(0, ...members.map((id) => bases.get(id)?.sinceLineBase ?? 0)),
|
|
199
220
|
});
|
|
200
221
|
}
|
|
201
222
|
const groupOf = new Map(groups.flatMap((g) => g.members.map((id) => [id, g])));
|
|
@@ -206,7 +227,7 @@ export const collectPlanFacts = async (workspace, git, opts) => {
|
|
|
206
227
|
if (!matched || !own) {
|
|
207
228
|
continue;
|
|
208
229
|
}
|
|
209
|
-
const { baseTag, lineBaseTag } = own;
|
|
230
|
+
const { baseTag, lineBaseTag, introduced, sinceLineBase } = own;
|
|
210
231
|
const group = groupOf.get(unit.id);
|
|
211
232
|
const fromTag = group ? group.baseTag : baseTag;
|
|
212
233
|
const rangeBase = group &&
|
|
@@ -227,8 +248,8 @@ export const collectPlanFacts = async (workspace, git, opts) => {
|
|
|
227
248
|
baseTag,
|
|
228
249
|
fromTag,
|
|
229
250
|
lineBaseTag,
|
|
230
|
-
|
|
231
|
-
sinceLineBase
|
|
251
|
+
introduced,
|
|
252
|
+
sinceLineBase,
|
|
232
253
|
total,
|
|
233
254
|
changes: changes.filter((c) => c.units.includes(unit.id)),
|
|
234
255
|
releaseAs: changes.flatMap(({ change }) => (change.releaseAs ?? [])
|
package/dist/plugin.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface GitReader {
|
|
|
55
55
|
countFirstParent: (from: string | null, to: string) => MaybePromise<number>;
|
|
56
56
|
/** Tree-to-tree changed paths without rename detection, sorted. */
|
|
57
57
|
diffFiles: (from: string, to: string) => MaybePromise<readonly string[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Oldest commit on the first-parent chain of `to` whose diff to its first
|
|
60
|
+
* parent (the empty tree for a root commit) touches a file under directory
|
|
61
|
+
* `path`; null if none.
|
|
62
|
+
*/
|
|
63
|
+
introduced: (path: string, to: string) => MaybePromise<string | null>;
|
|
58
64
|
}
|
|
59
65
|
export interface TagRef {
|
|
60
66
|
name: string;
|
package/dist/testing.d.ts
CHANGED
|
@@ -52,5 +52,6 @@ export type ReaderFactory = (dag: Dag) => MaybePromise<{
|
|
|
52
52
|
* chain, c branches from b and merges into m, d renames pkg/x.ts to pkg/y.ts,
|
|
53
53
|
* f adds a path with a quote, a tab and a non-ASCII character, g is empty and
|
|
54
54
|
* h carries footers. `a` has a later committer date than b, c and d.
|
|
55
|
+
* `introduced` runs on its own fixture.
|
|
55
56
|
*/
|
|
56
57
|
export declare const gitReaderConformance: (makeReader: ReaderFactory) => void;
|
package/dist/testing.js
CHANGED
|
@@ -43,6 +43,26 @@ const CONFORMANCE_DAG = {
|
|
|
43
43
|
{ name: "side", commit: "c" },
|
|
44
44
|
],
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* First-parent chain r - s - t - u - m - v; k branches from r and merges into
|
|
48
|
+
* m. s adds pkg/ab, t adds pkg/a, u deletes it and v re-adds it. k adds side
|
|
49
|
+
* and gone, m drops gone and v adds it again.
|
|
50
|
+
*/
|
|
51
|
+
const INTRODUCED_DAG = {
|
|
52
|
+
commits: [
|
|
53
|
+
{ id: "r", files: { "lib/r.ts": "r\n" } },
|
|
54
|
+
{ id: "s", files: { "pkg/ab/x.ts": "s\n" } },
|
|
55
|
+
{
|
|
56
|
+
id: "k",
|
|
57
|
+
parents: ["r"],
|
|
58
|
+
files: { "side/k.ts": "k\n", "gone/x.ts": "k\n" },
|
|
59
|
+
},
|
|
60
|
+
{ id: "t", parents: ["s"], files: { "pkg/a/x.ts": "t\n" } },
|
|
61
|
+
{ id: "u", files: { "pkg/a/x.ts": null } },
|
|
62
|
+
{ id: "m", parents: ["u", "k"], files: { "gone/x.ts": null } },
|
|
63
|
+
{ id: "v", files: { "pkg/a/x.ts": "v\n", "gone/x.ts": "v\n" } },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
46
66
|
/** In-memory `ReadonlyFs` over a path-to-content map. */
|
|
47
67
|
export const memoryFs = (files) => {
|
|
48
68
|
const paths = Object.keys(files).sort(compare);
|
|
@@ -171,6 +191,13 @@ export const memoryGit = (dag) => {
|
|
|
171
191
|
return firstParentChain(to).filter((n) => !skip.has(n.id)).length;
|
|
172
192
|
},
|
|
173
193
|
diffFiles: (from, to) => changed(node(from).tree, node(to).tree),
|
|
194
|
+
introduced: (path, to) => {
|
|
195
|
+
const under = (p) => p === path || p.startsWith(`${path}/`);
|
|
196
|
+
const intro = firstParentChain(to)
|
|
197
|
+
.reverse()
|
|
198
|
+
.find((n) => changed(n.parents[0] === undefined ? new Map() : node(n.parents[0]).tree, n.tree).some(under));
|
|
199
|
+
return intro?.id ?? null;
|
|
200
|
+
},
|
|
174
201
|
tree: (ref) => Object.fromEntries(node(ref).tree),
|
|
175
202
|
parents: (ref) => [...node(ref).parents],
|
|
176
203
|
};
|
|
@@ -180,6 +207,7 @@ export const memoryGit = (dag) => {
|
|
|
180
207
|
* chain, c branches from b and merges into m, d renames pkg/x.ts to pkg/y.ts,
|
|
181
208
|
* f adds a path with a quote, a tab and a non-ASCII character, g is empty and
|
|
182
209
|
* h carries footers. `a` has a later committer date than b, c and d.
|
|
210
|
+
* `introduced` runs on its own fixture.
|
|
183
211
|
*/
|
|
184
212
|
export const gitReaderConformance = (makeReader) => {
|
|
185
213
|
describe("GitReader conformance", () => {
|
|
@@ -290,5 +318,29 @@ export const gitReaderConformance = (makeReader) => {
|
|
|
290
318
|
[ODD_PATH],
|
|
291
319
|
]);
|
|
292
320
|
});
|
|
321
|
+
describe("introduced", () => {
|
|
322
|
+
let intro;
|
|
323
|
+
let introSha;
|
|
324
|
+
const introduced = async (path) => await intro.introduced(path, introSha("v"));
|
|
325
|
+
beforeAll(async () => {
|
|
326
|
+
({ git: intro, sha: introSha } = await makeReader(INTRODUCED_DAG));
|
|
327
|
+
});
|
|
328
|
+
it("finds the commit that adds a path on the chain", async () => {
|
|
329
|
+
expect(await introduced("lib")).toBe(introSha("r"));
|
|
330
|
+
expect(await introduced("pkg/ab")).toBe(introSha("s"));
|
|
331
|
+
});
|
|
332
|
+
it("attributes a path added on a side branch to the merge", async () => {
|
|
333
|
+
expect(await introduced("side")).toBe(introSha("m"));
|
|
334
|
+
});
|
|
335
|
+
it("matches whole directories and keeps the oldest commit", async () => {
|
|
336
|
+
expect(await introduced("pkg/a")).toBe(introSha("t"));
|
|
337
|
+
});
|
|
338
|
+
it("skips a merge identical to its first parent for the path", async () => {
|
|
339
|
+
expect(await introduced("gone")).toBe(introSha("v"));
|
|
340
|
+
});
|
|
341
|
+
it("returns null for a path no commit touches", async () => {
|
|
342
|
+
expect(await introduced("missing")).toBeNull();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
293
345
|
});
|
|
294
346
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyrelease/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Pure planning core of tallyrelease",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/abinnovision/
|
|
7
|
+
"url": "git+https://github.com/abinnovision/tallyrelease.git",
|
|
8
8
|
"directory": "packages/core"
|
|
9
9
|
},
|
|
10
10
|
"license": "Apache-2.0",
|