@kitschpatrol/chagent 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +5 -5
- package/dist/lib/bumpp.d.ts +1 -1
- package/dist/lib/bumpp.js +2 -2
- package/dist/lib/index.d.ts +8 -3
- package/dist/lib/index.js +2 -2
- package/dist/{lib-Byxdn2st.js → lib-BkbC8akL.js} +18 -10
- package/dist/{progress-DQtRnH1N.js → progress-BYDpiH8C.js} +1 -1
- package/dist/{review-DftIAoEl.js → review-cNqsjqhu.js} +577 -324
- package/dist/{types-B6FOOkTQ.d.ts → types-B-sACYE9.d.ts} +18 -2
- package/package.json +3 -3
- package/readme.md +14 -12
|
@@ -9,13 +9,13 @@ import semver from "semver";
|
|
|
9
9
|
import { parse } from "yaml";
|
|
10
10
|
import crossSpawn from "cross-spawn";
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
|
-
import { pathToFileURL } from "node:url";
|
|
13
12
|
import { cosmiconfig } from "cosmiconfig";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
14
|
import { isCancel, select } from "@clack/prompts";
|
|
15
15
|
|
|
16
16
|
// package.json
|
|
17
17
|
var name = "@kitschpatrol/chagent";
|
|
18
|
-
var version = "0.
|
|
18
|
+
var version = "0.6.0";
|
|
19
19
|
var engines = { "node": "^24.16.0 || >=26.3.0" };
|
|
20
20
|
|
|
21
21
|
// src/lib/defaults.ts
|
|
@@ -237,6 +237,7 @@ async function readBlobs(root, objects) {
|
|
|
237
237
|
const LOCKFILE_PATH = /(?:^|\/)(?:pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|deno\.lock|uv\.lock|poetry\.lock|pdm\.lock|Pipfile\.lock|Cargo\.lock|Gemfile\.lock|composer\.lock|go\.sum)$/v;
|
|
238
238
|
const TAG_DECORATION = "tag: refs/tags/";
|
|
239
239
|
const GITHUB_REMOTE = /github\.com[:\/]([^\/]+\/[^\/]+?)(?:\.git)?$/v;
|
|
240
|
+
const SHORT_VERSION = /^\d+(?:\.\d+)?$/v;
|
|
240
241
|
const manifestSchema$1 = z.object({
|
|
241
242
|
dependencies: z.record(z.string(), z.string()).optional(),
|
|
242
243
|
engines: z.object({ node: z.string().optional() }).optional(),
|
|
@@ -393,6 +394,29 @@ function packageInfo(manifest, directory, fallback) {
|
|
|
393
394
|
version: manifest?.version
|
|
394
395
|
};
|
|
395
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Read a version, with how many of its parts are spelled out. Short versions
|
|
399
|
+
* such as `1` or `1.2` name the first release of their series.
|
|
400
|
+
*/
|
|
401
|
+
function parseVersion(value) {
|
|
402
|
+
const version = semver.valid(value);
|
|
403
|
+
if (version !== null) return {
|
|
404
|
+
precision: 3,
|
|
405
|
+
version
|
|
406
|
+
};
|
|
407
|
+
if (!SHORT_VERSION.test(value)) return;
|
|
408
|
+
const parts = value.split(".");
|
|
409
|
+
const padded = semver.valid([
|
|
410
|
+
...parts,
|
|
411
|
+
"0",
|
|
412
|
+
"0"
|
|
413
|
+
].slice(0, 3).join("."));
|
|
414
|
+
return padded === null ? void 0 : {
|
|
415
|
+
precision: parts.length,
|
|
416
|
+
version: padded
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/** Read the version a tag names for a package, using its tag patterns. */
|
|
396
420
|
function tagVersion(tag, pkg, patterns) {
|
|
397
421
|
const templates = patterns ?? [
|
|
398
422
|
"{name}@{version}",
|
|
@@ -403,12 +427,25 @@ function tagVersion(tag, pkg, patterns) {
|
|
|
403
427
|
for (const pattern of templates) {
|
|
404
428
|
const [prefix, suffix = ""] = pattern.replaceAll("{name}", () => pkg.name).split("{version}", 2);
|
|
405
429
|
if (prefix === void 0 || !tag.startsWith(prefix) || !tag.endsWith(suffix)) continue;
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
430
|
+
const parsed = parseVersion(tag.slice(prefix.length, suffix === "" ? void 0 : -suffix.length));
|
|
431
|
+
const bareNumber = parsed?.precision === 1 && patterns === void 0 && !prefix.endsWith("v");
|
|
432
|
+
if (parsed !== void 0 && !bareNumber) return parsed;
|
|
409
433
|
}
|
|
410
434
|
}
|
|
411
435
|
/**
|
|
436
|
+
* Check that a short tag such as `v1` or `v1.2` precedes every more precise tag
|
|
437
|
+
* in its series. Floating tags that follow the latest release fail this check.
|
|
438
|
+
*/
|
|
439
|
+
async function leadsSeries(root, candidate, candidates) {
|
|
440
|
+
const major = semver.major(candidate.version);
|
|
441
|
+
const minor = semver.minor(candidate.version);
|
|
442
|
+
for (const other of candidates) {
|
|
443
|
+
if (other.precision <= candidate.precision || semver.major(other.version) !== major || candidate.precision === 2 && semver.minor(other.version) !== minor || semver.lt(other.version, candidate.version)) continue;
|
|
444
|
+
if (other.tag.commit === candidate.tag.commit || !await isAncestor(root, candidate.tag.commit, other.tag.commit)) return false;
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
412
449
|
* Predict the tag of a release that is not tagged yet, so its heading can link
|
|
413
450
|
* to it. Follow the previous tag's format, then the first configured pattern,
|
|
414
451
|
* then `v{version}`.
|
|
@@ -416,10 +453,26 @@ function tagVersion(tag, pkg, patterns) {
|
|
|
416
453
|
function pendingTag(pkg, version, previous, patterns) {
|
|
417
454
|
return (previous?.tag?.includes(previous.version) === true ? previous.tag.replace(previous.version, "{version}") : patterns?.[0] ?? "v{version}").replaceAll("{name}", () => pkg.name).replaceAll("{version}", () => version);
|
|
418
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Order releases by Git ancestry, using SemVer only for tags on the same
|
|
458
|
+
* commit.
|
|
459
|
+
*/
|
|
460
|
+
async function sortReleases(repository, releases) {
|
|
461
|
+
const history = await git(repository.root, [
|
|
462
|
+
"rev-list",
|
|
463
|
+
"--topo-order",
|
|
464
|
+
"--reverse",
|
|
465
|
+
repository.head
|
|
466
|
+
]);
|
|
467
|
+
const positions = new Map(history.split("\n").map((ref, index) => [ref, index]));
|
|
468
|
+
return releases.toSorted((a, b) => {
|
|
469
|
+
const difference = (positions.get(a.ref) ?? -1) - (positions.get(b.ref) ?? -1);
|
|
470
|
+
return difference === 0 ? semver.compare(a.version, b.version) : difference;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
419
473
|
/** Resolve release intervals from reachable tags and locally bumped manifests. */
|
|
420
474
|
async function resolveReleases(repository, pkg, config, context) {
|
|
421
|
-
const releases = await taggedReleases(repository, pkg, config);
|
|
422
|
-
releases.sort((a, b) => semver.compare(a.version, b.version));
|
|
475
|
+
const releases = await sortReleases(repository, await taggedReleases(repository, pkg, config));
|
|
423
476
|
for (let index = 1; index < releases.length; index++) {
|
|
424
477
|
const current = releases[index];
|
|
425
478
|
const previous = await precedingRelease(repository.root, releases.slice(0, index), current.ref);
|
|
@@ -428,7 +481,7 @@ async function resolveReleases(repository, pkg, config, context) {
|
|
|
428
481
|
}
|
|
429
482
|
const latest = releases.at(-1);
|
|
430
483
|
const currentVersion = pkg.version ?? (pkg.directory === "." ? context?.version : void 0);
|
|
431
|
-
if (await isPendingRelease(repository, pkg,
|
|
484
|
+
if (await isPendingRelease(repository, pkg, releases, context) && currentVersion !== void 0) releases.push({
|
|
432
485
|
date: context?.date ?? localDate(),
|
|
433
486
|
pending: true,
|
|
434
487
|
previousRef: latest?.ref,
|
|
@@ -540,25 +593,31 @@ function describedPackages(repository) {
|
|
|
540
593
|
async function taggedReleases(repository, pkg, config) {
|
|
541
594
|
const candidates = [];
|
|
542
595
|
for (const tag of repository.tags) {
|
|
543
|
-
const
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
596
|
+
const match = tagVersion(tag.name, pkg, config.tagPatterns);
|
|
597
|
+
if (match !== void 0) candidates.push({
|
|
598
|
+
...match,
|
|
599
|
+
tag
|
|
547
600
|
});
|
|
548
601
|
}
|
|
549
602
|
const isRoot = pkg.directory === ".";
|
|
550
603
|
const manifests = isRoot ? /* @__PURE__ */ new Map() : await manifestsAt(repository.root, pkg.directory, candidates.map(({ tag }) => tag.commit));
|
|
551
|
-
const
|
|
552
|
-
for (const
|
|
604
|
+
const owned = [];
|
|
605
|
+
for (const candidate of candidates) {
|
|
553
606
|
if (!isRoot) {
|
|
554
|
-
const manifest = manifests.get(tag.commit);
|
|
555
|
-
if (manifest?.version !== void 0 && semver.valid(manifest.version) !== version) continue;
|
|
607
|
+
const manifest = manifests.get(candidate.tag.commit);
|
|
608
|
+
if (manifest?.version !== void 0 && semver.valid(manifest.version) !== candidate.version) continue;
|
|
556
609
|
if (manifest === void 0 && await git(repository.root, [
|
|
557
610
|
"cat-file",
|
|
558
611
|
"-t",
|
|
559
|
-
`${tag.commit}:${pkg.directory}`
|
|
612
|
+
`${candidate.tag.commit}:${pkg.directory}`
|
|
560
613
|
], true) !== "tree") continue;
|
|
561
614
|
}
|
|
615
|
+
owned.push(candidate);
|
|
616
|
+
}
|
|
617
|
+
const releases = [];
|
|
618
|
+
for (const candidate of owned) {
|
|
619
|
+
if (candidate.precision < 3 && !await leadsSeries(repository.root, candidate, owned)) continue;
|
|
620
|
+
const { tag, version } = candidate;
|
|
562
621
|
const existing = releases.find((release) => release.version === version);
|
|
563
622
|
if (existing && existing.ref !== tag.commit) throw new Error(`Multiple reachable tags disagree about ${pkg.name} ${version}. Configure tagPatterns to select a release series.`);
|
|
564
623
|
if (existing) continue;
|
|
@@ -574,13 +633,13 @@ async function taggedReleases(repository, pkg, config) {
|
|
|
574
633
|
async function precedingRelease(root, candidates, ref) {
|
|
575
634
|
for (const candidate of candidates.toReversed()) if (await isAncestor(root, candidate.ref, ref)) return candidate;
|
|
576
635
|
}
|
|
577
|
-
async function isPendingRelease(repository, pkg,
|
|
636
|
+
async function isPendingRelease(repository, pkg, releases, context) {
|
|
578
637
|
const currentVersion = pkg.version ?? (pkg.directory === "." ? context?.version : void 0);
|
|
579
638
|
const committed = await manifestAt(repository.root, pkg.directory, repository.head);
|
|
580
639
|
const versionChanged = currentVersion !== void 0 && currentVersion !== committed?.version;
|
|
581
640
|
const parent = await manifestAt(repository.root, pkg.directory, repository.head + "^");
|
|
582
641
|
const committedBump = parent?.version !== void 0 && parent.version !== committed?.version;
|
|
583
|
-
return currentVersion !== void 0 && semver.valid(currentVersion) !== null && (
|
|
642
|
+
return currentVersion !== void 0 && semver.valid(currentVersion) !== null && releases.every((release) => release.version !== semver.valid(currentVersion)) && (versionChanged || context !== void 0 || committedBump && releases.every((release) => release.ref !== repository.head));
|
|
584
643
|
}
|
|
585
644
|
|
|
586
645
|
// src/lib/cache.ts
|
|
@@ -638,13 +697,25 @@ manager, manifest format, or directory layout. Use snapshots at those refs, not
|
|
|
638
697
|
uncommitted files or newer code at HEAD. Respect repository.directory and
|
|
639
698
|
repository.excludeDirectories when deciding which changes belong in this entry;
|
|
640
699
|
you may inspect related files for context. The supplied evidence is a starting
|
|
641
|
-
point, not a limit on your research.
|
|
700
|
+
point, not a limit on your research. Supplied commits leave out lockfiles and
|
|
701
|
+
directories named build, coverage, dist, or changelog at the top of a package,
|
|
702
|
+
which usually hold generated output. Judge those directories yourself: when one
|
|
703
|
+
holds a project's source rather than generated output, its changes belong in the
|
|
704
|
+
changelog like any others.
|
|
642
705
|
|
|
643
706
|
When mode is initialize, reconstruct this release as part of the project's full
|
|
644
|
-
historical changelog. When mode is update, cover all missing changes in the
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
707
|
+
historical changelog. When mode is update, cover all missing changes in the
|
|
708
|
+
supplied range. When mode is revise, the existing entries are your own earlier
|
|
709
|
+
draft of this release, describing it up to repository.from, and the supplied
|
|
710
|
+
range is what has happened since. Return the complete entries for the release as
|
|
711
|
+
it now stands; they replace the draft. Carry over what still holds, fold later
|
|
712
|
+
fixes and refinements into the entries they affect, combine entries that turn
|
|
713
|
+
out to describe one change, and drop an entry only when the range reverts or
|
|
714
|
+
supersedes it. Keep the wording of entries that need no change. In every mode,
|
|
715
|
+
review every commit, diff, and associated PR in the supplied range before
|
|
716
|
+
deciding what to include. A changelog records the evolution of the project,
|
|
717
|
+
including maintenance releases; it is not just a list of new application
|
|
718
|
+
features.
|
|
648
719
|
|
|
649
720
|
Cover distinct changes to behavior, public APIs and types, CLI flags and output,
|
|
650
721
|
runtime compatibility, installation and distribution, documentation and examples,
|
|
@@ -673,9 +744,16 @@ Give distinct changes separate readable bullets;
|
|
|
673
744
|
do not compress unrelated features or breaking changes into one long entry. Keep
|
|
674
745
|
each entry concise without sacrificing coverage. Consolidate related commits,
|
|
675
746
|
not unrelated changes. There is no three-entry limit for package scope, including
|
|
676
|
-
a single-package repository's root changelog.
|
|
677
|
-
|
|
678
|
-
|
|
747
|
+
a single-package repository's root changelog.
|
|
748
|
+
|
|
749
|
+
Only scope=repository asks for a summary: an entry in a monorepo's root
|
|
750
|
+
changelog, which the caller ends with links to the package changelogs that hold
|
|
751
|
+
the details. Write what a reader of the whole repository most needs to know, in
|
|
752
|
+
at most three entries, rather than repeating package entries one by one. Combine
|
|
753
|
+
related changes across packages and name the packages they affect. When
|
|
754
|
+
repository.directory is a package, summarize that package's release; otherwise
|
|
755
|
+
summarize the repository, including changes outside any package. Do not write
|
|
756
|
+
the links yourself.
|
|
679
757
|
|
|
680
758
|
Use commit messages and PRs to understand intent, then check the changed files
|
|
681
759
|
and diffs. Inspect public interfaces at both ends of the range before claiming
|
|
@@ -714,25 +792,37 @@ Put breaking public changes in the appropriate standard category and set breakin
|
|
|
714
792
|
to true; do not create a Breaking Changes category.
|
|
715
793
|
Descriptions are single-line Markdown suitable for a list item,
|
|
716
794
|
without a bullet prefix, heading, process explanation, or introductory paragraph.
|
|
717
|
-
Every change must cite at least one evidence ID supplied for its own
|
|
718
|
-
|
|
719
|
-
|
|
795
|
+
Every change must cite, in sources, at least one evidence ID supplied for its own
|
|
796
|
+
release or the full ID of a commit within that release's range. Prefer the commit
|
|
797
|
+
that made the change, including one found through your own Git research; use the
|
|
798
|
+
release's range ID when no single commit establishes it. When mode is revise,
|
|
799
|
+
cite the draft evidence ID for an entry carried over or rewritten from the
|
|
800
|
+
existing entries, along with any commit that changed it. Include a PR
|
|
720
801
|
link only when its URL and association are supplied. Do not fabricate links.
|
|
721
802
|
|
|
722
|
-
Existing entries are provided
|
|
723
|
-
|
|
724
|
-
|
|
803
|
+
Existing entries are provided for deduplication and preservation, and when mode
|
|
804
|
+
is revise, as the draft to rewrite. They are not independent evidence. Unless
|
|
805
|
+
mode is revise, omit changes already expressed by those entries, even if you
|
|
806
|
+
would phrase them differently. GitHub release-note bodies are deliberately
|
|
725
807
|
absent because this output will itself become GitHub release notes.
|
|
726
808
|
Do not read existing changelog files, generated release-note files, or GitHub
|
|
727
809
|
release bodies as source material, even when researching the repository directly.
|
|
728
810
|
|
|
811
|
+
When styleReference is present, it is a recent release section of the changelog
|
|
812
|
+
these entries will join. Match its writing conventions: voice and tense,
|
|
813
|
+
capitalization, terminal punctuation, and how code, issues, and PRs are
|
|
814
|
+
referenced. It is a style sample and nothing more. It describes another release,
|
|
815
|
+
so it is not evidence, and its length and choice of categories say nothing about
|
|
816
|
+
this one.
|
|
817
|
+
|
|
729
818
|
Before returning, check coverage across behavior/API, compatibility, dependencies,
|
|
730
819
|
documentation, build/test/CI, and distribution. Do not drop a change just because
|
|
731
820
|
its commit is called chore, docs, build, or dependency updates. Return an empty
|
|
732
821
|
changes array only when the evidence is entirely release-version bookkeeping,
|
|
733
822
|
lockfile-only resolution changes, generated artifacts, formatting noise, or
|
|
734
|
-
changes already covered by existing entries.
|
|
735
|
-
|
|
823
|
+
changes already covered by existing entries. When mode is revise, an empty array
|
|
824
|
+
removes the draft, so return one only when nothing in the draft still holds. Do
|
|
825
|
+
not pad the result with unsupported changes.`;
|
|
736
826
|
/** Invalidates cached drafts whenever the instructions change. */
|
|
737
827
|
const instructionVersion = hash(instructions);
|
|
738
828
|
|
|
@@ -762,6 +852,7 @@ const rootChangelogs = [
|
|
|
762
852
|
];
|
|
763
853
|
|
|
764
854
|
// src/lib/agents.ts
|
|
855
|
+
const COMMIT_ID = /^[\da-f]{7,64}$/iv;
|
|
765
856
|
const CONTROL_CHARACTERS = /\p{Cc}/v;
|
|
766
857
|
const HEADING = /^#{1,6}\s/v;
|
|
767
858
|
const LIST_MARKER = /^(?:[\-*]|\d+\.)(?:\s+|$)/v;
|
|
@@ -780,13 +871,10 @@ const responseJsonSchema = JSON.stringify(z.toJSONSchema(responseSchema, { targe
|
|
|
780
871
|
function normalizeDescription(description) {
|
|
781
872
|
return description.replaceAll(WHITESPACE_RUN, " ").trim().replace(LIST_MARKER, "");
|
|
782
873
|
}
|
|
783
|
-
function
|
|
784
|
-
const ids = new Set(release.evidence.map((item) => item.id));
|
|
874
|
+
function validateDescriptions(changes, id) {
|
|
785
875
|
return changes.map((change) => {
|
|
786
|
-
const unknown = change.sources.filter((source) => !ids.has(source));
|
|
787
|
-
if (unknown.length > 0) throw new Error(`Agent output for ${release.id} cites evidence that was not supplied: ${unknown.join(", ")}.`);
|
|
788
876
|
const description = normalizeDescription(change.description);
|
|
789
|
-
if (description === "" || CONTROL_CHARACTERS.test(description) || HEADING.test(description) || description.includes("<!--")) throw new Error(`Agent output for ${
|
|
877
|
+
if (description === "" || CONTROL_CHARACTERS.test(description) || HEADING.test(description) || description.includes("<!--")) throw new Error(`Agent output for ${id} has an empty description, heading, or hidden markup: ${JSON.stringify(change.description)}.`);
|
|
790
878
|
return {
|
|
791
879
|
...change,
|
|
792
880
|
description
|
|
@@ -794,8 +882,8 @@ function validateChanges(changes, release) {
|
|
|
794
882
|
});
|
|
795
883
|
}
|
|
796
884
|
/**
|
|
797
|
-
* Validate structure, completeness, release identity
|
|
798
|
-
*
|
|
885
|
+
* Validate structure, completeness, and release identity before accepting any
|
|
886
|
+
* adapter's output. Citations are checked separately, by `verifyCitations`.
|
|
799
887
|
*
|
|
800
888
|
* @returns The change sets, with descriptions normalized.
|
|
801
889
|
*/
|
|
@@ -809,10 +897,53 @@ function validateResponse(value, releases) {
|
|
|
809
897
|
const omitted = expected.keys().filter((id) => !ids.includes(id)).toArray();
|
|
810
898
|
if (omitted.length > 0) throw new Error(`Agent output omitted requested releases: ${omitted.join(", ")}.`);
|
|
811
899
|
return response.data.releases.map(({ changes, id }) => ({
|
|
812
|
-
changes:
|
|
900
|
+
changes: validateDescriptions(changes, id),
|
|
813
901
|
id
|
|
814
902
|
}));
|
|
815
903
|
}
|
|
904
|
+
/** List every commit in a release's range. */
|
|
905
|
+
async function rangeCommits(root, { repository }) {
|
|
906
|
+
const { from, to } = repository;
|
|
907
|
+
return (await git(root, ["rev-list", from === void 0 ? to : `${from}..${to}`])).split("\n");
|
|
908
|
+
}
|
|
909
|
+
async function verifyChanges(root, release, changes) {
|
|
910
|
+
const supplied = new Set(release.evidence.map((item) => item.id));
|
|
911
|
+
let commits;
|
|
912
|
+
const verified = [];
|
|
913
|
+
for (const change of changes) {
|
|
914
|
+
const sources = /* @__PURE__ */ new Set();
|
|
915
|
+
for (const source of change.sources) if (supplied.has(source)) sources.add(source);
|
|
916
|
+
else if (COMMIT_ID.test(source)) {
|
|
917
|
+
commits ??= await rangeCommits(root, release);
|
|
918
|
+
const matches = commits.filter((commit) => commit.startsWith(source.toLowerCase()));
|
|
919
|
+
if (matches.length === 1) sources.add(matches[0]);
|
|
920
|
+
}
|
|
921
|
+
if (sources.size === 0) throw new Error(`Agent output for ${release.id} cites no supplied evidence or commit in its range: ${change.sources.join(", ")}. Cite an evidence ID supplied for that release, or the ID of a commit within its range.`);
|
|
922
|
+
verified.push({
|
|
923
|
+
...change,
|
|
924
|
+
sources: [...sources]
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return verified;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Check citations against Git instead of trusting them. A change may cite
|
|
931
|
+
* evidence supplied for its release or any commit within that release's range,
|
|
932
|
+
* which lets an agent cite what its own research found. Abbreviated commit IDs
|
|
933
|
+
* are completed, and sources that hold up are kept when others do not. Pass the
|
|
934
|
+
* requested releases with their complete evidence.
|
|
935
|
+
*
|
|
936
|
+
* @throws {Error} When a change is left without a source.
|
|
937
|
+
*/
|
|
938
|
+
async function verifyCitations(root, changeSets, releases) {
|
|
939
|
+
const requested = new Map(releases.map((release) => [release.id, release]));
|
|
940
|
+
const verified = [];
|
|
941
|
+
for (const { changes, id } of changeSets) verified.push({
|
|
942
|
+
changes: await verifyChanges(root, requested.get(id), changes),
|
|
943
|
+
id
|
|
944
|
+
});
|
|
945
|
+
return verified;
|
|
946
|
+
}
|
|
816
947
|
function prompt({ releases, repair, repositoryRoot }) {
|
|
817
948
|
const correction = repair === void 0 ? "" : `\n\nYour previous response failed validation: ${repair.error}\nPrevious response (JSON):\n${JSON.stringify(repair.response)}\nReturn the complete corrected JSON object. Keep your previous findings, and repeat only the research the correction needs.`;
|
|
818
949
|
return `${instructions}${correction}\n\nInput data (JSON):\n${JSON.stringify({
|
|
@@ -936,6 +1067,83 @@ function createAgent(name) {
|
|
|
936
1067
|
};
|
|
937
1068
|
}
|
|
938
1069
|
|
|
1070
|
+
// src/lib/config.ts
|
|
1071
|
+
const configSchema = z.strictObject({
|
|
1072
|
+
agent: z.enum(agentNames).optional(),
|
|
1073
|
+
cache: z.boolean().optional(),
|
|
1074
|
+
effort: z.enum(efforts).optional(),
|
|
1075
|
+
exclude: z.array(z.string()).optional(),
|
|
1076
|
+
github: z.boolean().optional(),
|
|
1077
|
+
/** Maximum evidence characters per agent request. */
|
|
1078
|
+
maxInputChars: z.number().int().min(4e3).optional(),
|
|
1079
|
+
model: z.string().min(1).optional(),
|
|
1080
|
+
/** Workspace directory globs, overriding manifest workspace discovery. */
|
|
1081
|
+
packages: z.array(z.string()).optional(),
|
|
1082
|
+
/**
|
|
1083
|
+
* What a monorepo's root changelog records: a summary of package releases,
|
|
1084
|
+
* the root package's own changes, or nothing.
|
|
1085
|
+
*/
|
|
1086
|
+
rootChangelog: z.enum(rootChangelogs).optional(),
|
|
1087
|
+
/** Workspace package directory globs that get no changelog of their own. */
|
|
1088
|
+
skipPackage: z.array(z.string()).optional(),
|
|
1089
|
+
/** Templates containing {version} and optionally {name}. */
|
|
1090
|
+
tagPatterns: z.array(z.string().includes("{version}")).optional(),
|
|
1091
|
+
/** Minutes each agent session may run. */
|
|
1092
|
+
timeout: z.number().positive().max(maxTimeoutMinutes).optional()
|
|
1093
|
+
});
|
|
1094
|
+
const resolvedConfigSchema = configSchema.required({
|
|
1095
|
+
agent: true,
|
|
1096
|
+
cache: true,
|
|
1097
|
+
effort: true,
|
|
1098
|
+
github: true,
|
|
1099
|
+
maxInputChars: true,
|
|
1100
|
+
rootChangelog: true,
|
|
1101
|
+
timeout: true
|
|
1102
|
+
}).transform(({ timeout, ...config }) => ({
|
|
1103
|
+
...config,
|
|
1104
|
+
timeoutMs: timeout * 6e4
|
|
1105
|
+
}));
|
|
1106
|
+
function invalidConfig(source, error) {
|
|
1107
|
+
return new Error(`Invalid options in ${source}:\n${z.prettifyError(error)}`, { cause: error });
|
|
1108
|
+
}
|
|
1109
|
+
/** Define optional repository configuration with TypeScript completion. */
|
|
1110
|
+
function defineConfig(config) {
|
|
1111
|
+
return config;
|
|
1112
|
+
}
|
|
1113
|
+
/** Load configuration from the repository root or an explicitly selected file. */
|
|
1114
|
+
async function loadConfig(root, file) {
|
|
1115
|
+
const explorer = cosmiconfig("chagent", {
|
|
1116
|
+
searchPlaces: [
|
|
1117
|
+
"chagent.config.ts",
|
|
1118
|
+
"chagent.config.mjs",
|
|
1119
|
+
"chagent.config.js",
|
|
1120
|
+
"chagent.config.json",
|
|
1121
|
+
"package.json"
|
|
1122
|
+
],
|
|
1123
|
+
searchStrategy: "none"
|
|
1124
|
+
});
|
|
1125
|
+
const result = file === void 0 ? await explorer.search(root) : await explorer.load(file);
|
|
1126
|
+
if (!result) {
|
|
1127
|
+
log.debug("No configuration file found.");
|
|
1128
|
+
return {};
|
|
1129
|
+
}
|
|
1130
|
+
log.debug(`Loaded configuration from ${result.filepath}.`);
|
|
1131
|
+
const parsed = configSchema.safeParse(result.config);
|
|
1132
|
+
if (!parsed.success) throw invalidConfig(result.filepath, parsed.error);
|
|
1133
|
+
return parsed.data;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Apply configuration layers over the defaults; later layers win and undefined
|
|
1137
|
+
* values do not.
|
|
1138
|
+
*/
|
|
1139
|
+
function resolveConfig(...layers) {
|
|
1140
|
+
const merged = { ...defaults };
|
|
1141
|
+
for (const layer of layers) for (const [key, value] of Object.entries(layer)) if (value !== void 0) merged[key] = value;
|
|
1142
|
+
const parsed = resolvedConfigSchema.safeParse(merged);
|
|
1143
|
+
if (!parsed.success) throw invalidConfig("generation settings", parsed.error);
|
|
1144
|
+
return parsed.data;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
939
1147
|
// src/lib/format.ts
|
|
940
1148
|
const FORMATTER_PACKAGES = ["@kitschpatrol/shared-config", "@kitschpatrol/prettier-config"];
|
|
941
1149
|
const manifestSchema = z.object({ exports: z.object({ ".": z.object({ import: z.string() }) }) });
|
|
@@ -1038,9 +1246,11 @@ async function writeChangelog(plan) {
|
|
|
1038
1246
|
const edit = plan.files.find((file) => file.path === checkpoint.path);
|
|
1039
1247
|
try {
|
|
1040
1248
|
const written = edit === void 0 ? void 0 : await readOptional(edit.path);
|
|
1249
|
+
const edited = edit !== void 0 && changelogHash(edit.after) !== checkpoint.contentHash;
|
|
1041
1250
|
await writeCache(path.join(stateDirectory, "checkpoints"), checkpoint.path, {
|
|
1042
1251
|
...checkpoint,
|
|
1043
|
-
contentHash: written === void 0 ? checkpoint.contentHash : changelogHash(written)
|
|
1252
|
+
contentHash: written === void 0 ? checkpoint.contentHash : changelogHash(written),
|
|
1253
|
+
generated: checkpoint.generated && !edited
|
|
1044
1254
|
});
|
|
1045
1255
|
} catch {
|
|
1046
1256
|
plan.diagnostics.push("Could not save incremental metadata; the next run will recheck the full release range.");
|
|
@@ -1075,83 +1285,6 @@ async function acquireLock(file) {
|
|
|
1075
1285
|
}
|
|
1076
1286
|
}
|
|
1077
1287
|
|
|
1078
|
-
// src/lib/config.ts
|
|
1079
|
-
const configSchema = z.strictObject({
|
|
1080
|
-
agent: z.enum(agentNames).optional(),
|
|
1081
|
-
cache: z.boolean().optional(),
|
|
1082
|
-
effort: z.enum(efforts).optional(),
|
|
1083
|
-
exclude: z.array(z.string()).optional(),
|
|
1084
|
-
github: z.boolean().optional(),
|
|
1085
|
-
/** Maximum evidence characters per agent request. */
|
|
1086
|
-
maxInputChars: z.number().int().min(4e3).optional(),
|
|
1087
|
-
model: z.string().min(1).optional(),
|
|
1088
|
-
/** Workspace directory globs, overriding manifest workspace discovery. */
|
|
1089
|
-
packages: z.array(z.string()).optional(),
|
|
1090
|
-
/**
|
|
1091
|
-
* What a monorepo's root changelog records: a summary of package releases,
|
|
1092
|
-
* the root package's own changes, or nothing.
|
|
1093
|
-
*/
|
|
1094
|
-
rootChangelog: z.enum(rootChangelogs).optional(),
|
|
1095
|
-
/** Workspace package directory globs that get no changelog of their own. */
|
|
1096
|
-
skipPackage: z.array(z.string()).optional(),
|
|
1097
|
-
/** Templates containing {version} and optionally {name}. */
|
|
1098
|
-
tagPatterns: z.array(z.string().includes("{version}")).optional(),
|
|
1099
|
-
/** Minutes each agent session may run. */
|
|
1100
|
-
timeout: z.number().positive().max(maxTimeoutMinutes).optional()
|
|
1101
|
-
});
|
|
1102
|
-
const resolvedConfigSchema = configSchema.required({
|
|
1103
|
-
agent: true,
|
|
1104
|
-
cache: true,
|
|
1105
|
-
effort: true,
|
|
1106
|
-
github: true,
|
|
1107
|
-
maxInputChars: true,
|
|
1108
|
-
rootChangelog: true,
|
|
1109
|
-
timeout: true
|
|
1110
|
-
}).transform(({ timeout, ...config }) => ({
|
|
1111
|
-
...config,
|
|
1112
|
-
timeoutMs: timeout * 6e4
|
|
1113
|
-
}));
|
|
1114
|
-
function invalidConfig(source, error) {
|
|
1115
|
-
return new Error(`Invalid options in ${source}:\n${z.prettifyError(error)}`, { cause: error });
|
|
1116
|
-
}
|
|
1117
|
-
/** Define optional repository configuration with TypeScript completion. */
|
|
1118
|
-
function defineConfig(config) {
|
|
1119
|
-
return config;
|
|
1120
|
-
}
|
|
1121
|
-
/** Load configuration from the repository root or an explicitly selected file. */
|
|
1122
|
-
async function loadConfig(root, file) {
|
|
1123
|
-
const explorer = cosmiconfig("chagent", {
|
|
1124
|
-
searchPlaces: [
|
|
1125
|
-
"chagent.config.ts",
|
|
1126
|
-
"chagent.config.mjs",
|
|
1127
|
-
"chagent.config.js",
|
|
1128
|
-
"chagent.config.json",
|
|
1129
|
-
"package.json"
|
|
1130
|
-
],
|
|
1131
|
-
searchStrategy: "none"
|
|
1132
|
-
});
|
|
1133
|
-
const result = file === void 0 ? await explorer.search(root) : await explorer.load(file);
|
|
1134
|
-
if (!result) {
|
|
1135
|
-
log.debug("No configuration file found.");
|
|
1136
|
-
return {};
|
|
1137
|
-
}
|
|
1138
|
-
log.debug(`Loaded configuration from ${result.filepath}.`);
|
|
1139
|
-
const parsed = configSchema.safeParse(result.config);
|
|
1140
|
-
if (!parsed.success) throw invalidConfig(result.filepath, parsed.error);
|
|
1141
|
-
return parsed.data;
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* Apply configuration layers over the defaults; later layers win and undefined
|
|
1145
|
-
* values do not.
|
|
1146
|
-
*/
|
|
1147
|
-
function resolveConfig(...layers) {
|
|
1148
|
-
const merged = { ...defaults };
|
|
1149
|
-
for (const layer of layers) for (const [key, value] of Object.entries(layer)) if (value !== void 0) merged[key] = value;
|
|
1150
|
-
const parsed = resolvedConfigSchema.safeParse(merged);
|
|
1151
|
-
if (!parsed.success) throw invalidConfig("generation settings", parsed.error);
|
|
1152
|
-
return parsed.data;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
1288
|
// src/lib/changelog.ts
|
|
1156
1289
|
const LINE_BOUNDARY = /(?<=\n)/v;
|
|
1157
1290
|
const FENCE = /^\s{0,3}(`{3,}|~{3,})/v;
|
|
@@ -1163,9 +1296,14 @@ const WHITESPACE = /\s+/gv;
|
|
|
1163
1296
|
const TERMINAL_PUNCTUATION = /[.!]$/v;
|
|
1164
1297
|
const NEWLINE = /\r?\n/v;
|
|
1165
1298
|
const BULLET = /^\s*[\-*] /v;
|
|
1299
|
+
const ENTRY = /^\s*[\-*] /mv;
|
|
1300
|
+
const STYLE_REFERENCE_LENGTH = 2e3;
|
|
1166
1301
|
const CATEGORY_HEADING = /^### /mv;
|
|
1302
|
+
const PACKAGES_HEADING = /^### Packages\s*$/mv;
|
|
1303
|
+
const LINK_TARGET = /\]\(([^\)]*)\)/gv;
|
|
1167
1304
|
const SEMVER_LINE = /^.*\[Semantic Versioning\].*(?:\r?\n|$)/mv;
|
|
1168
1305
|
const UNRELEASED_REFERENCE = /^\[unreleased\]:[^\r\n]*(?:\r?\n)?/imv;
|
|
1306
|
+
const V_PREFIX = /^v\d/v;
|
|
1169
1307
|
const changelogHeader = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n";
|
|
1170
1308
|
/** The body of a release section without any changes to record. */
|
|
1171
1309
|
const noNotableChanges = "No notable changes.";
|
|
@@ -1234,6 +1372,21 @@ function parseChangelog(content) {
|
|
|
1234
1372
|
sections
|
|
1235
1373
|
};
|
|
1236
1374
|
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Pick the newest release section with entries, to show an agent how this
|
|
1377
|
+
* changelog is written. Long sections are cut at a line boundary.
|
|
1378
|
+
*/
|
|
1379
|
+
function styleReference({ sections }) {
|
|
1380
|
+
const body = sections.filter(({ version }) => version !== "Unreleased").map((section) => withoutPackageLinks(section.body)).find((entries) => ENTRY.test(entries));
|
|
1381
|
+
if (body === void 0) return;
|
|
1382
|
+
const lines = body.trim().split(LINE_BOUNDARY);
|
|
1383
|
+
let sample = "";
|
|
1384
|
+
for (const line of lines) {
|
|
1385
|
+
if (sample.length + line.length > STYLE_REFERENCE_LENGTH) break;
|
|
1386
|
+
sample += line;
|
|
1387
|
+
}
|
|
1388
|
+
return sample.trim() === "" ? void 0 : sample.trimEnd();
|
|
1389
|
+
}
|
|
1237
1390
|
function normalized(description) {
|
|
1238
1391
|
return description.trim().toLowerCase().replaceAll(WHITESPACE, " ").replace(TERMINAL_PUNCTUATION, "");
|
|
1239
1392
|
}
|
|
@@ -1273,6 +1426,46 @@ function laterCategoryIndex(body, category) {
|
|
|
1273
1426
|
const indexes = categories.slice(categories.indexOf(category) + 1).map((item) => categoryHeading(body, item)?.index).filter((index) => index !== void 0);
|
|
1274
1427
|
return indexes.length === 0 ? void 0 : Math.min(...indexes);
|
|
1275
1428
|
}
|
|
1429
|
+
/** Locate the package links that end a root summary's section. */
|
|
1430
|
+
function packagesBlock(body) {
|
|
1431
|
+
const heading = PACKAGES_HEADING.exec(body);
|
|
1432
|
+
if (heading === null) return;
|
|
1433
|
+
const after = heading.index + heading[0].length;
|
|
1434
|
+
const next = CATEGORY_HEADING.exec(body.slice(after));
|
|
1435
|
+
return {
|
|
1436
|
+
start: heading.index,
|
|
1437
|
+
end: next === null ? body.length : after + next.index
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
/** Remove the package links from a root summary's section, leaving its entries. */
|
|
1441
|
+
function withoutPackageLinks(body) {
|
|
1442
|
+
const block = packagesBlock(body);
|
|
1443
|
+
return block === void 0 ? body : body.slice(0, block.start) + body.slice(block.end);
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Check whether a section links to other package releases than it should.
|
|
1447
|
+
* Targets are compared, since a formatter may restyle the list around them.
|
|
1448
|
+
*/
|
|
1449
|
+
function packageLinksDiffer(body, links) {
|
|
1450
|
+
const block = body === void 0 ? void 0 : packagesBlock(body);
|
|
1451
|
+
const targets = body === void 0 || block === void 0 ? [] : body.slice(block.start, block.end).matchAll(LINK_TARGET).map((match) => match[1]).toArray();
|
|
1452
|
+
return !isDeepStrictEqual(targets, links.map(({ url }) => url));
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Merge new changes into a section's draft, or replace a revised draft with
|
|
1456
|
+
* them, and end the section with its package links.
|
|
1457
|
+
*/
|
|
1458
|
+
function sectionBody({ changes, existingBody = "", links, revise }, newline) {
|
|
1459
|
+
const draft = links === void 0 ? existingBody : withoutPackageLinks(existingBody);
|
|
1460
|
+
const body = mergeChanges(revise === true ? "" : draft, changes, newline);
|
|
1461
|
+
if (links === void 0 || links.length === 0) return body;
|
|
1462
|
+
const list = links.map(({ label, url }) => `- [${label}](${url})`);
|
|
1463
|
+
return body.trimEnd() + newline + newline + [
|
|
1464
|
+
"### Packages",
|
|
1465
|
+
"",
|
|
1466
|
+
...list
|
|
1467
|
+
].join(newline);
|
|
1468
|
+
}
|
|
1276
1469
|
/** Render a release heading with an optional comparison link. */
|
|
1277
1470
|
function releaseLink(github, release) {
|
|
1278
1471
|
if (github === void 0) return;
|
|
@@ -1282,7 +1475,7 @@ function releaseLink(github, release) {
|
|
|
1282
1475
|
return release.previousTag === void 0 ? `${base}/releases/tag/${encodeURIComponent(release.tag)}` : `${base}/compare/${encodeURIComponent(release.previousTag)}...${encodeURIComponent(release.tag)}`;
|
|
1283
1476
|
}
|
|
1284
1477
|
/** Insert new sections and update only the draft and its comparison link. */
|
|
1285
|
-
function updateMarkdown(original, entries, nodeException, github) {
|
|
1478
|
+
function updateMarkdown(original, entries, nodeException, github, releaseOrder) {
|
|
1286
1479
|
const content = original ?? "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n";
|
|
1287
1480
|
const parsed = parseChangelog(content);
|
|
1288
1481
|
const { newline } = parsed;
|
|
@@ -1291,20 +1484,30 @@ function updateMarkdown(original, entries, nodeException, github) {
|
|
|
1291
1484
|
const definitions = [];
|
|
1292
1485
|
let { footer } = parsed;
|
|
1293
1486
|
const draft = parsed.sections.find((section) => section.version === "Unreleased");
|
|
1294
|
-
const released = parsed.sections.filter((section) => section.version !== "Unreleased").map((section) =>
|
|
1487
|
+
const released = parsed.sections.filter((section) => section.version !== "Unreleased").map((section) => ({
|
|
1488
|
+
text: content.slice(section.start, section.end),
|
|
1489
|
+
version: section.version
|
|
1490
|
+
}));
|
|
1295
1491
|
let draftText = draft === void 0 ? "" : content.slice(draft.start, draft.end);
|
|
1296
|
-
for (const
|
|
1492
|
+
for (const entry of entries) {
|
|
1493
|
+
const { existingBody, release } = entry;
|
|
1297
1494
|
const link = releaseLink(github, release);
|
|
1298
1495
|
const heading = `## ${link === void 0 ? release.version : `[${release.version}]`}${release.date === void 0 ? "" : ` - ${release.date}`}`;
|
|
1299
|
-
let body =
|
|
1496
|
+
let body = sectionBody(entry, newline);
|
|
1300
1497
|
if (body.trim() === "") {
|
|
1301
|
-
if (release.version === "Unreleased")
|
|
1498
|
+
if (release.version === "Unreleased") {
|
|
1499
|
+
if (existingBody !== void 0) draftText = "";
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1302
1502
|
body = newline + noNotableChanges + newline;
|
|
1303
1503
|
}
|
|
1304
1504
|
const section = heading + newline + newline + body.trim() + newline + newline;
|
|
1305
1505
|
if (release.version === "Unreleased") draftText = section;
|
|
1306
1506
|
else {
|
|
1307
|
-
additions.push(
|
|
1507
|
+
additions.push({
|
|
1508
|
+
text: section,
|
|
1509
|
+
version: release.version
|
|
1510
|
+
});
|
|
1308
1511
|
if (existingBody !== void 0) draftText = "";
|
|
1309
1512
|
}
|
|
1310
1513
|
if (link === void 0) continue;
|
|
@@ -1316,9 +1519,13 @@ function updateMarkdown(original, entries, nodeException, github) {
|
|
|
1316
1519
|
}
|
|
1317
1520
|
if (entries.length === 0 && header === parsed.header) return content;
|
|
1318
1521
|
if (draftText === "") footer = footer.replace(UNRELEASED_REFERENCE, "");
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1522
|
+
const sections = [...additions.toReversed(), ...released];
|
|
1523
|
+
if (releaseOrder !== void 0) {
|
|
1524
|
+
const positions = new Map(releaseOrder.map((version, index) => [version, index]));
|
|
1525
|
+
sections.sort((a, b) => (positions.get(b.version) ?? -1) - (positions.get(a.version) ?? -1));
|
|
1526
|
+
}
|
|
1527
|
+
const result = sectionSpacing(sectionSpacing(header, newline) + draftText, newline) + sections.map((section) => section.text).join("") + footer;
|
|
1528
|
+
return appendDefinitions(footer === "" && released.length === 0 ? result.trimEnd() + newline : result, footer, definitions, newline);
|
|
1322
1529
|
}
|
|
1323
1530
|
/** Add link reference definitions after any existing footer. */
|
|
1324
1531
|
function appendDefinitions(content, footer, definitions, newline) {
|
|
@@ -1335,8 +1542,15 @@ function addNodeNote(header, newline, nodeException) {
|
|
|
1335
1542
|
}
|
|
1336
1543
|
return header;
|
|
1337
1544
|
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Read a heading's version the way default tag patterns read a tag, so `1.2`
|
|
1547
|
+
* and `v1` name the first release of their series and a bare `1` names none.
|
|
1548
|
+
*/
|
|
1338
1549
|
function normalizeVersion(raw) {
|
|
1339
|
-
|
|
1550
|
+
if (raw.toLowerCase() === "unreleased") return "Unreleased";
|
|
1551
|
+
const prefixed = V_PREFIX.test(raw);
|
|
1552
|
+
const parsed = parseVersion(prefixed ? raw.slice(1) : raw);
|
|
1553
|
+
return parsed === void 0 || !prefixed && parsed.precision === 1 ? raw : parsed.version;
|
|
1340
1554
|
}
|
|
1341
1555
|
function releaseHeadingVersion(line) {
|
|
1342
1556
|
const match = RELEASE_HEADING.exec(line.trimEnd());
|
|
@@ -1352,17 +1566,26 @@ function sectionSpacing(content, newline) {
|
|
|
1352
1566
|
|
|
1353
1567
|
// src/lib/evidence.ts
|
|
1354
1568
|
const COMMIT_MARKER = "";
|
|
1355
|
-
const
|
|
1356
|
-
const
|
|
1569
|
+
const OUTPUT_DIRECTORY = /^(?:build|changelog|coverage|dist)(?:\/|$)/iv;
|
|
1570
|
+
const NEVER_SOURCE = /(?:^|\/)(?:changelog(?:\.md)?$|node_modules(?:\/|$))/iv;
|
|
1571
|
+
/**
|
|
1572
|
+
* Changelogs are this tool's own output, installed dependencies are another
|
|
1573
|
+
* project's source, and lockfiles record resolved versions, not declared
|
|
1574
|
+
* dependency ranges. None of them is evidence of a change.
|
|
1575
|
+
*/
|
|
1576
|
+
function excluded(file) {
|
|
1577
|
+
return LOCKFILE_PATH.test(file) || NEVER_SOURCE.test(file);
|
|
1578
|
+
}
|
|
1357
1579
|
/**
|
|
1358
|
-
*
|
|
1359
|
-
* `src/dist` remain source.
|
|
1360
|
-
*
|
|
1580
|
+
* Recognize directories at the top of a package that usually hold generated
|
|
1581
|
+
* output, so `packages/build` and `src/dist` remain source. Supplied commits
|
|
1582
|
+
* leave them out, but they still count as changes to research: only the agent
|
|
1583
|
+
* can tell a build artifact from a project whose source lives in `coverage`.
|
|
1361
1584
|
*/
|
|
1362
|
-
function
|
|
1585
|
+
function likelyGenerated(file, packages) {
|
|
1363
1586
|
const owner = packages.findLast(({ directory }) => file.startsWith(`${directory}/`));
|
|
1364
1587
|
const relative = owner === void 0 ? file : file.slice(owner.directory.length + 1);
|
|
1365
|
-
return
|
|
1588
|
+
return OUTPUT_DIRECTORY.test(relative);
|
|
1366
1589
|
}
|
|
1367
1590
|
/**
|
|
1368
1591
|
* A package's directory and, transitively, those of the workspace packages it
|
|
@@ -1435,7 +1658,10 @@ async function commitsInRange(root, range, paths) {
|
|
|
1435
1658
|
}
|
|
1436
1659
|
return commits.values().toArray();
|
|
1437
1660
|
}
|
|
1438
|
-
/**
|
|
1661
|
+
/**
|
|
1662
|
+
* Collect a release's starting evidence, or nothing when its range holds only
|
|
1663
|
+
* changelogs, lockfiles, and version bookkeeping.
|
|
1664
|
+
*/
|
|
1439
1665
|
async function collectEvidence(repository, pkg, release, excludeDirectories = []) {
|
|
1440
1666
|
const { packages, root } = repository;
|
|
1441
1667
|
const paths = [...(pkg.directory === "." ? ["."] : await packageScope(repository, pkg, release.ref)).map((directory) => `:(literal)${directory}`), ...excludeDirectories.map((directory) => `:(exclude,literal)${directory}`)];
|
|
@@ -1457,20 +1683,25 @@ async function collectEvidence(repository, pkg, release, excludeDirectories = []
|
|
|
1457
1683
|
release.ref,
|
|
1458
1684
|
"--",
|
|
1459
1685
|
...paths
|
|
1460
|
-
])).split("\0").filter((file) => file !== "" && !
|
|
1686
|
+
])).split("\0").filter((file) => file !== "" && !excluded(file));
|
|
1461
1687
|
const rangeBlobs = await readBlobs(root, manifestObjects(candidates, from, release.ref));
|
|
1462
1688
|
const changed = new Set(candidates.filter((file) => !isManifest(file) || !isVersionOnlyEdit(rangeBlobs.get(`${from}:${file}`), rangeBlobs.get(`${release.ref}:${file}`))));
|
|
1463
1689
|
if (changed.size === 0) return [];
|
|
1464
1690
|
const sourceCommits = (await commitsInRange(root, range, paths)).map((item) => ({
|
|
1465
1691
|
...item,
|
|
1466
|
-
files: item.files.filter((file) => !
|
|
1692
|
+
files: item.files.filter((file) => !excluded(file) && !likelyGenerated(file, packages))
|
|
1467
1693
|
}));
|
|
1468
1694
|
const commitBlobs = await readBlobs(root, sourceCommits.flatMap(({ files, id }) => manifestObjects(files, `${id}^`, id)));
|
|
1469
|
-
const evidence =
|
|
1695
|
+
const evidence = [{
|
|
1696
|
+
id: `range:${from}..${release.ref}`,
|
|
1697
|
+
kind: "range",
|
|
1698
|
+
text: `Research the complete Git range ${range} using git log, git diff, git show, and git ls-tree. The supplied commits are a starting point, not a complete description of changes. Follow the scope and exclusions in repository.`
|
|
1699
|
+
}];
|
|
1700
|
+
for (const { files, id, message } of sourceCommits) if (files.some((file) => changed.has(file)) && hasSourceChanges(files, id, commitBlobs)) evidence.push({
|
|
1470
1701
|
id,
|
|
1471
1702
|
kind: "commit",
|
|
1472
1703
|
text: `${message}\nFiles: ${files.join(", ")}`
|
|
1473
|
-
})
|
|
1704
|
+
});
|
|
1474
1705
|
if (release.pending === true) {
|
|
1475
1706
|
const file = path.posix.join(pkg.directory, "package.json");
|
|
1476
1707
|
const diff = await git(root, [
|
|
@@ -1487,11 +1718,6 @@ async function collectEvidence(repository, pkg, release, excludeDirectories = []
|
|
|
1487
1718
|
text: diff
|
|
1488
1719
|
});
|
|
1489
1720
|
}
|
|
1490
|
-
if (evidence.length > 0) evidence.unshift({
|
|
1491
|
-
id: `range:${from}..${release.ref}`,
|
|
1492
|
-
kind: "range",
|
|
1493
|
-
text: `Research the complete Git range ${range} using git log, git diff, git show, and git ls-tree. The supplied commits are a starting point, not a complete description of changes. Follow the scope and exclusions in repository.`
|
|
1494
|
-
});
|
|
1495
1721
|
return evidence;
|
|
1496
1722
|
}
|
|
1497
1723
|
function githubUnavailable(error) {
|
|
@@ -1628,7 +1854,7 @@ function releaseBatches(requests, limit) {
|
|
|
1628
1854
|
for (const release of requests) {
|
|
1629
1855
|
const minimum = {
|
|
1630
1856
|
...release,
|
|
1631
|
-
evidence: release.evidence.filter((item) => item
|
|
1857
|
+
evidence: release.evidence.filter((item) => required(item))
|
|
1632
1858
|
};
|
|
1633
1859
|
const length = JSON.stringify(minimum).length + 1;
|
|
1634
1860
|
if (size + length > limit && batch.length > 0) {
|
|
@@ -1644,7 +1870,7 @@ function releaseBatches(requests, limit) {
|
|
|
1644
1870
|
const allowance = Math.max(0, Math.floor((limit - JSON.stringify(items).length) / items.length));
|
|
1645
1871
|
const seeded = items.map((item) => ({
|
|
1646
1872
|
...item,
|
|
1647
|
-
evidence: [...item.evidence, ...researchEvidence(full.get(item.id).filter((entry) => entry
|
|
1873
|
+
evidence: [...item.evidence, ...researchEvidence(full.get(item.id).filter((entry) => !required(entry)), allowance + 2)]
|
|
1648
1874
|
}));
|
|
1649
1875
|
let remaining = limit - JSON.stringify(seeded).length;
|
|
1650
1876
|
for (const item of seeded) {
|
|
@@ -1657,6 +1883,10 @@ function releaseBatches(requests, limit) {
|
|
|
1657
1883
|
return seeded;
|
|
1658
1884
|
});
|
|
1659
1885
|
}
|
|
1886
|
+
/** Keep what a request cannot do without: its range, and a draft it may cite. */
|
|
1887
|
+
function required(item) {
|
|
1888
|
+
return item.kind === "range" || item.kind === "draft";
|
|
1889
|
+
}
|
|
1660
1890
|
/** Bound the starting context; adapters can read the complete range themselves. */
|
|
1661
1891
|
function researchEvidence(evidence, limit) {
|
|
1662
1892
|
const seed = [];
|
|
@@ -1671,19 +1901,32 @@ function researchEvidence(evidence, limit) {
|
|
|
1671
1901
|
}
|
|
1672
1902
|
|
|
1673
1903
|
// src/lib/jobs.ts
|
|
1904
|
+
/** Lets a revised entry cite the draft it came from. */
|
|
1905
|
+
const draftEvidence = {
|
|
1906
|
+
id: "draft",
|
|
1907
|
+
kind: "draft",
|
|
1908
|
+
text: "The existing entries, which describe this release up to repository.from. Cite this ID for entries carried over or rewritten from them."
|
|
1909
|
+
};
|
|
1910
|
+
/** Check whether a package's changelog is a monorepo's root summary. */
|
|
1911
|
+
function summarizesPackages(context, pkg) {
|
|
1912
|
+
return pkg.directory === "." && context.repository.packages.length > 0 && context.options.rootChangelog === "summary";
|
|
1913
|
+
}
|
|
1674
1914
|
/**
|
|
1675
1915
|
* Prepare one release without launching a model, so repository research can be
|
|
1676
1916
|
* shared.
|
|
1677
1917
|
*/
|
|
1678
|
-
async function prepareRelease(context, pkg, release,
|
|
1918
|
+
async function prepareRelease(context, pkg, release, details) {
|
|
1679
1919
|
const { options, repository } = context;
|
|
1920
|
+
const { existingEntries = "", from, mode, onlyRepositoryFiles = false, styleReference } = details;
|
|
1680
1921
|
options.signal?.throwIfAborted();
|
|
1681
1922
|
const excludeDirectories = await excludedPackages(context, pkg, release.ref, onlyRepositoryFiles);
|
|
1923
|
+
const evidence = await collectEvidence(repository, pkg, from === void 0 ? release : {
|
|
1924
|
+
...release,
|
|
1925
|
+
previousRef: from
|
|
1926
|
+
}, excludeDirectories);
|
|
1927
|
+
if (mode === "revise" && evidence.length > 0) evidence.splice(1, 0, draftEvidence);
|
|
1682
1928
|
return {
|
|
1683
|
-
evidence
|
|
1684
|
-
...release,
|
|
1685
|
-
previousRef: from
|
|
1686
|
-
}, excludeDirectories),
|
|
1929
|
+
evidence,
|
|
1687
1930
|
existingEntries,
|
|
1688
1931
|
id: `${pkg.directory}@${release.version}`,
|
|
1689
1932
|
initialRelease: release.previousRef === void 0 && from === void 0,
|
|
@@ -1695,7 +1938,8 @@ async function prepareRelease(context, pkg, release, existingEntries, mode, from
|
|
|
1695
1938
|
from: from ?? release.previousRef,
|
|
1696
1939
|
to: release.ref
|
|
1697
1940
|
},
|
|
1698
|
-
scope: pkg
|
|
1941
|
+
scope: summarizesPackages(context, pkg) ? "repository" : "package",
|
|
1942
|
+
styleReference,
|
|
1699
1943
|
version: release.version
|
|
1700
1944
|
};
|
|
1701
1945
|
}
|
|
@@ -1761,9 +2005,11 @@ function progressMessage(pending, sessions) {
|
|
|
1761
2005
|
}
|
|
1762
2006
|
/**
|
|
1763
2007
|
* Run one agent session. A long session is worth saving, so invalid output gets
|
|
1764
|
-
* one correction request before the run fails.
|
|
2008
|
+
* one correction request before the run fails. Citations are checked against
|
|
2009
|
+
* the releases as they were before their evidence was trimmed to fit, since an
|
|
2010
|
+
* agent researching a range can cite what trimming left out.
|
|
1765
2011
|
*/
|
|
1766
|
-
async function runSession(context, releases) {
|
|
2012
|
+
async function runSession(context, releases, untrimmed) {
|
|
1767
2013
|
const { agent, options, repository } = context;
|
|
1768
2014
|
const request = {
|
|
1769
2015
|
releases,
|
|
@@ -1776,24 +2022,24 @@ async function runSession(context, releases) {
|
|
|
1776
2022
|
}
|
|
1777
2023
|
};
|
|
1778
2024
|
log.debug(`Starting a ${agent.name} session for ${releases.map(({ id }) => id).join(", ")} with model ${options.model ?? "default"}, effort ${options.effort}, and a ${options.timeoutMs} ms timeout.`);
|
|
2025
|
+
const accept = async (value) => verifyCitations(repository.root, validateResponse(value, releases), untrimmed);
|
|
1779
2026
|
const started = performance.now();
|
|
1780
2027
|
const response = await agent.generate(request);
|
|
1781
2028
|
log.debug(`The ${agent.name} session finished in ${Math.round(performance.now() - started)} ms.`);
|
|
1782
2029
|
try {
|
|
1783
|
-
return
|
|
2030
|
+
return await accept(response);
|
|
1784
2031
|
} catch (error) {
|
|
1785
2032
|
const message = error instanceof Error ? error.message : String(error);
|
|
1786
2033
|
log.warn(`Requesting a correction of invalid agent output. ${message}`);
|
|
1787
2034
|
options.onProgress?.("Requesting a correction of invalid agent output.");
|
|
1788
2035
|
options.signal?.throwIfAborted();
|
|
1789
|
-
|
|
2036
|
+
return accept(await agent.generate({
|
|
1790
2037
|
...request,
|
|
1791
2038
|
repair: {
|
|
1792
2039
|
error: message,
|
|
1793
2040
|
response
|
|
1794
2041
|
}
|
|
1795
|
-
});
|
|
1796
|
-
return validateResponse(corrected, releases);
|
|
2042
|
+
}));
|
|
1797
2043
|
}
|
|
1798
2044
|
}
|
|
1799
2045
|
/**
|
|
@@ -1826,7 +2072,7 @@ async function generateReleases(context, requests) {
|
|
|
1826
2072
|
options.onProgress?.(status);
|
|
1827
2073
|
for (const releases of batches) {
|
|
1828
2074
|
options.signal?.throwIfAborted();
|
|
1829
|
-
const changeSets = await runSession(context, releases);
|
|
2075
|
+
const changeSets = await runSession(context, releases, releases.map(({ id }) => unabridged.get(id)));
|
|
1830
2076
|
for (const { changes, id } of changeSets) {
|
|
1831
2077
|
results.set(id, changes);
|
|
1832
2078
|
await saveChanges(context, unabridged.get(id), changes);
|
|
@@ -1839,8 +2085,9 @@ async function generateReleases(context, requests) {
|
|
|
1839
2085
|
const checkpointSchema = z.object({
|
|
1840
2086
|
baseRef: z.string(),
|
|
1841
2087
|
contentHash: z.string(),
|
|
2088
|
+
generated: z.boolean(),
|
|
1842
2089
|
head: z.string(),
|
|
1843
|
-
instructionVersion: z.
|
|
2090
|
+
instructionVersion: z.string(),
|
|
1844
2091
|
path: z.string()
|
|
1845
2092
|
});
|
|
1846
2093
|
const HEADING_PREFIX = /^#{1,3}\s+/v;
|
|
@@ -1849,45 +2096,80 @@ const ANCHOR_PUNCTUATION = /[^\p{L}\p{N}_\s\-]/gv;
|
|
|
1849
2096
|
const ANCHOR_SPACE = /\s/gv;
|
|
1850
2097
|
/**
|
|
1851
2098
|
* Remember what this run covered, so the next one can research only newer
|
|
1852
|
-
* commits.
|
|
2099
|
+
* commits, and whether the draft it leaves is entirely generated.
|
|
1853
2100
|
*/
|
|
1854
|
-
function recordCheckpoint(context, file, content, release) {
|
|
2101
|
+
function recordCheckpoint(context, file, content, release, generated) {
|
|
1855
2102
|
if (release === void 0 || !context.options.cache) return;
|
|
1856
2103
|
context.plan.checkpoints.push({
|
|
1857
2104
|
baseRef: release.previousRef ?? "initial",
|
|
1858
2105
|
contentHash: changelogHash(content),
|
|
2106
|
+
generated,
|
|
1859
2107
|
head: context.repository.head,
|
|
1860
2108
|
instructionVersion,
|
|
1861
2109
|
path: file
|
|
1862
2110
|
});
|
|
1863
2111
|
}
|
|
1864
2112
|
/**
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
2113
|
+
* Read what the last run recorded about a changelog, when the file still reads
|
|
2114
|
+
* as that run left it.
|
|
1867
2115
|
*/
|
|
1868
|
-
async function
|
|
2116
|
+
async function readCheckpoint(context, file, content) {
|
|
1869
2117
|
if (content === void 0 || !context.options.cache) return;
|
|
1870
2118
|
const result = checkpointSchema.safeParse(await readCache(path.join(context.repository.gitDirectory, "chagent", "checkpoints"), file));
|
|
1871
|
-
|
|
1872
|
-
return await isAncestor(context.repository.root, result.data.head, release.ref) ? result.data.head : void 0;
|
|
2119
|
+
return result.success && result.data.contentHash === changelogHash(content) ? result.data : void 0;
|
|
1873
2120
|
}
|
|
1874
|
-
/**
|
|
2121
|
+
/**
|
|
2122
|
+
* Find the commit a release's entries already cover, when the checkpoint
|
|
2123
|
+
* describes the same release.
|
|
2124
|
+
*/
|
|
2125
|
+
async function incrementalBase(context, checkpoint, release) {
|
|
2126
|
+
if (checkpoint?.instructionVersion !== instructionVersion || checkpoint.baseRef !== (release.previousRef ?? "initial")) return;
|
|
2127
|
+
return await isAncestor(context.repository.root, checkpoint.head, release.ref) ? checkpoint.head : void 0;
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Decide whether the draft a run leaves is entirely generated: one that was
|
|
2131
|
+
* before stays so, and a draft written from nothing or revised becomes so.
|
|
2132
|
+
*/
|
|
2133
|
+
function leavesGeneratedDraft(entries, previously) {
|
|
2134
|
+
const draft = entries.find((entry) => entry.release.version === "Unreleased");
|
|
2135
|
+
return previously || draft !== void 0 && (draft.existingBody === void 0 || draft.revise === true);
|
|
2136
|
+
}
|
|
2137
|
+
/** Select every reachable release the changelog does not already record. */
|
|
1875
2138
|
function missingReleases(releases, content = "") {
|
|
1876
2139
|
const { sections } = parseChangelog(content);
|
|
1877
2140
|
const known = new Set(sections.map((section) => section.version));
|
|
1878
2141
|
const recorded = sections.filter((section) => section.version !== "Unreleased").map((section) => section.version);
|
|
1879
2142
|
const unrecognized = recorded.filter((version) => semver.valid(version) === null);
|
|
1880
2143
|
if (unrecognized.length > 0) throw new Error(`The existing changelog has unrecognized release headings (${unrecognized.join(", ")}). Normalize its release headings before updating it.`);
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
return releases.filter((release) => release.version === "Unreleased" || !known.has(release.version) && (newest === void 0 || semver.gt(release.version, newest)));
|
|
2144
|
+
for (const version of recorded) if (releases.every((release) => release.version !== version)) throw new Error(`Cannot locate the Git release boundary for existing changelog version ${version}. Fetch its tags or configure tagPatterns.`);
|
|
2145
|
+
return releases.filter((release) => release.version === "Unreleased" || !known.has(release.version));
|
|
1884
2146
|
}
|
|
1885
2147
|
/** Apply an explicit release context only to the package being released. */
|
|
1886
2148
|
function releaseContext(options, pkg) {
|
|
1887
2149
|
return pkg.directory === options.releaseDirectory ? options.release : void 0;
|
|
1888
2150
|
}
|
|
1889
|
-
/**
|
|
1890
|
-
async function
|
|
2151
|
+
/** Prepare the request for a release the changelog still needs. */
|
|
2152
|
+
async function prepareWork(context, pkg, release, details) {
|
|
2153
|
+
const { checkpoint, existingBody, mode } = details;
|
|
2154
|
+
const from = existingBody === void 0 ? void 0 : await incrementalBase(context, checkpoint, release);
|
|
2155
|
+
const revise = from !== void 0 && checkpoint?.generated === true;
|
|
2156
|
+
return {
|
|
2157
|
+
existingBody,
|
|
2158
|
+
release,
|
|
2159
|
+
request: await prepareRelease(context, pkg, release, {
|
|
2160
|
+
existingEntries: existingBody !== void 0 && summarizesPackages(context, pkg) ? withoutPackageLinks(existingBody) : existingBody,
|
|
2161
|
+
from,
|
|
2162
|
+
mode: revise ? "revise" : mode,
|
|
2163
|
+
styleReference: details.styleReference
|
|
2164
|
+
})
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Resolve a package's releases and prepare requests for those its changelog
|
|
2169
|
+
* still needs. Summaries cover those releases alone, or with `allSummaries`,
|
|
2170
|
+
* every release, so that a root changelog can link to any of them.
|
|
2171
|
+
*/
|
|
2172
|
+
async function preparePackage(context, pkg, allSummaries = false) {
|
|
1891
2173
|
const { repository } = context;
|
|
1892
2174
|
const file = await findChangelog(path.join(repository.root, pkg.directory));
|
|
1893
2175
|
const content = context.options.rebuild ? void 0 : file.content;
|
|
@@ -1895,15 +2177,17 @@ async function preparePackage(context, pkg, aggregateHistory = false, recordedAt
|
|
|
1895
2177
|
const releases = await resolveReleases(repository, pkg, context.options, releaseContext(context.options, pkg));
|
|
1896
2178
|
const missing = missingReleases(releases, content);
|
|
1897
2179
|
log.debug(`${pkg.name}: ${releases.length} releases, missing [${missing.map((release) => release.version).join(", ")}], changelog ${file.path}.`);
|
|
1898
|
-
const
|
|
2180
|
+
const parsed = parseChangelog(content ?? "");
|
|
2181
|
+
const draft = parsed.sections.find((section) => section.version === "Unreleased");
|
|
2182
|
+
const checkpoint = draft === void 0 ? void 0 : await readCheckpoint(context, file.path, content);
|
|
1899
2183
|
const newest = releases.findLast((release) => release.version !== "Unreleased");
|
|
1900
2184
|
const draftPromoted = draft !== void 0 && newest !== void 0 && missing.includes(newest);
|
|
1901
2185
|
const work = [];
|
|
1902
2186
|
const summaries = [];
|
|
1903
2187
|
for (const release of releases) {
|
|
1904
2188
|
const shouldWrite = missing.includes(release);
|
|
1905
|
-
if (!shouldWrite && !
|
|
1906
|
-
|
|
2189
|
+
if (!shouldWrite && !allSummaries) continue;
|
|
2190
|
+
summaries.push({
|
|
1907
2191
|
changelogPath: file.path,
|
|
1908
2192
|
changes: [],
|
|
1909
2193
|
date: release.date,
|
|
@@ -1912,50 +2196,58 @@ async function preparePackage(context, pkg, aggregateHistory = false, recordedAt
|
|
|
1912
2196
|
ref: release.ref,
|
|
1913
2197
|
tag: release.tag,
|
|
1914
2198
|
version: release.version
|
|
1915
|
-
};
|
|
1916
|
-
summaries.push(summary);
|
|
1917
|
-
if (!shouldWrite && recordedAtRoot?.(summary) === true) continue;
|
|
1918
|
-
const existingBody = (draftPromoted ? release === newest : release.version === "Unreleased") ? draft?.body : void 0;
|
|
1919
|
-
const from = existingBody === void 0 ? void 0 : await incrementalBase(context, file.path, content, release);
|
|
1920
|
-
const request = await prepareRelease(context, pkg, release, existingBody ?? "", mode, from);
|
|
1921
|
-
work.push({
|
|
1922
|
-
existingBody,
|
|
1923
|
-
release,
|
|
1924
|
-
request,
|
|
1925
|
-
shouldWrite
|
|
1926
2199
|
});
|
|
2200
|
+
if (!shouldWrite) continue;
|
|
2201
|
+
const ownsDraft = draftPromoted ? release === newest : release.version === "Unreleased";
|
|
2202
|
+
work.push(await prepareWork(context, pkg, release, {
|
|
2203
|
+
checkpoint,
|
|
2204
|
+
existingBody: ownsDraft ? draft?.body : void 0,
|
|
2205
|
+
mode,
|
|
2206
|
+
styleReference: styleReference(parsed)
|
|
2207
|
+
}));
|
|
1927
2208
|
}
|
|
2209
|
+
const exception = await hasNodeException(repository, pkg, releases);
|
|
1928
2210
|
return {
|
|
1929
2211
|
content,
|
|
1930
|
-
|
|
2212
|
+
draftGenerated: checkpoint?.generated === true,
|
|
2213
|
+
exception,
|
|
1931
2214
|
file,
|
|
2215
|
+
pkg,
|
|
1932
2216
|
releases,
|
|
1933
2217
|
summaries,
|
|
1934
2218
|
work
|
|
1935
2219
|
};
|
|
1936
2220
|
}
|
|
1937
|
-
/**
|
|
1938
|
-
|
|
2221
|
+
/**
|
|
2222
|
+
* Write a package's edit into the plan and complete its summaries. A root
|
|
2223
|
+
* summary passes `linksFor` to end each section it writes with links to the
|
|
2224
|
+
* package releases that section covers.
|
|
2225
|
+
*/
|
|
2226
|
+
function renderPackage(context, prepared, results, linksFor) {
|
|
1939
2227
|
const { plan, repository } = context;
|
|
1940
2228
|
const { content, exception, file, releases, summaries, work } = prepared;
|
|
1941
2229
|
const entries = [];
|
|
1942
2230
|
const changesByVersion = /* @__PURE__ */ new Map();
|
|
1943
|
-
for (const { existingBody, release, request
|
|
2231
|
+
for (const { existingBody, release, request } of work) {
|
|
1944
2232
|
const changes = results.get(request.id) ?? [];
|
|
2233
|
+
const links = linksFor?.(release);
|
|
2234
|
+
const revise = request.mode === "revise" && results.has(request.id);
|
|
1945
2235
|
changesByVersion.set(release.version, changes);
|
|
1946
|
-
if (
|
|
2236
|
+
if (revise || release.version !== "Unreleased" || changes.length > 0 || links !== void 0 && packageLinksDiffer(existingBody, links)) entries.push({
|
|
1947
2237
|
changes,
|
|
1948
2238
|
existingBody,
|
|
1949
|
-
|
|
2239
|
+
links,
|
|
2240
|
+
release,
|
|
2241
|
+
revise
|
|
1950
2242
|
});
|
|
1951
2243
|
}
|
|
1952
|
-
const after = updateMarkdown(content, entries, exception, repository.github);
|
|
2244
|
+
const after = updateMarkdown(content, entries, exception, repository.github, releases.map((release) => release.version));
|
|
1953
2245
|
if (after !== file.content) plan.files.push({
|
|
1954
2246
|
after,
|
|
1955
2247
|
before: file.content,
|
|
1956
2248
|
path: file.path
|
|
1957
2249
|
});
|
|
1958
|
-
recordCheckpoint(context, file.path, after, releases.at(-1));
|
|
2250
|
+
recordCheckpoint(context, file.path, after, releases.at(-1), leavesGeneratedDraft(entries, prepared.draftGenerated));
|
|
1959
2251
|
const { sections } = parseChangelog(after);
|
|
1960
2252
|
return summaries.map((summary) => ({
|
|
1961
2253
|
...summary,
|
|
@@ -1973,85 +2265,44 @@ function headingAnchor(heading) {
|
|
|
1973
2265
|
function missingPackageSummaries(summaries, content = "") {
|
|
1974
2266
|
const parsed = parseChangelog(content);
|
|
1975
2267
|
const known = new Set(parsed.sections.map((section) => section.version));
|
|
1976
|
-
return summaries.
|
|
1977
|
-
if (summary.version === "Unreleased" || known.has(`${summary.packageName}@${summary.version}`)) return false;
|
|
1978
|
-
return parsed.sections.every((section) => {
|
|
1979
|
-
const prefix = summary.packageName + "@";
|
|
1980
|
-
const version = section.version.startsWith(prefix) ? section.version.slice(prefix.length) : "";
|
|
1981
|
-
return semver.valid(version) === null || semver.lt(version, summary.version);
|
|
1982
|
-
});
|
|
1983
|
-
});
|
|
2268
|
+
return summaries.filter((summary) => summary.version !== "Unreleased" && !known.has(`${summary.packageName}@${summary.version}`));
|
|
1984
2269
|
}
|
|
1985
2270
|
|
|
1986
2271
|
// src/lib/workspace.ts
|
|
2272
|
+
/** Link to a package's release section, when its changelog has one. */
|
|
1987
2273
|
function packageLink(repository, summary) {
|
|
1988
|
-
|
|
1989
|
-
const
|
|
1990
|
-
return
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
category: "Maintenance",
|
|
2001
|
-
description: `${link}: See the package changelog.`,
|
|
2002
|
-
sources: []
|
|
2003
|
-
});
|
|
2004
|
-
else for (const change of highlights) changes.push({
|
|
2005
|
-
...change,
|
|
2006
|
-
description: `${link}: ${change.description}`
|
|
2007
|
-
});
|
|
2008
|
-
}
|
|
2009
|
-
return changes;
|
|
2010
|
-
}
|
|
2011
|
-
function promotePackageDraft(content, summary, repository) {
|
|
2012
|
-
const changes = [];
|
|
2013
|
-
const draft = parseChangelog(content ?? "").sections.find((section) => section.version === "Unreleased");
|
|
2014
|
-
if (content === void 0 || draft === void 0) return {
|
|
2015
|
-
changes,
|
|
2016
|
-
content
|
|
2017
|
-
};
|
|
2018
|
-
const source = `[${summary.packageName} Unreleased](`;
|
|
2019
|
-
const target = `[${summary.packageName} ${summary.version}](${packageLink(repository, summary)})`;
|
|
2020
|
-
const remaining = [];
|
|
2021
|
-
let category = "Changed";
|
|
2022
|
-
for (const line of draft.body.split(LINE_BOUNDARY)) {
|
|
2023
|
-
category = categories.find((item) => line.trimEnd() === `### ${item}`) ?? category;
|
|
2024
|
-
const breaking = line.startsWith("- **Breaking:** ");
|
|
2025
|
-
const prefix = breaking ? "- **Breaking:** " : "- ";
|
|
2026
|
-
const end = line.indexOf(")", prefix.length + source.length);
|
|
2027
|
-
if (end === -1 || !line.startsWith(prefix + source)) {
|
|
2028
|
-
remaining.push(line);
|
|
2029
|
-
continue;
|
|
2030
|
-
}
|
|
2031
|
-
changes.push({
|
|
2032
|
-
breaking,
|
|
2033
|
-
category,
|
|
2034
|
-
description: target + line.slice(end + 1).trimEnd(),
|
|
2035
|
-
sources: []
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
if (changes.length === 0) return {
|
|
2039
|
-
changes,
|
|
2040
|
-
content
|
|
2041
|
-
};
|
|
2042
|
-
const replacement = remaining.some((line) => line.trim() !== "" && categories.every((item) => line.trimEnd() !== `### ${item}`)) ? draft.heading + remaining.join("") : "";
|
|
2043
|
-
return {
|
|
2044
|
-
changes,
|
|
2045
|
-
content: content.slice(0, draft.start) + replacement + content.slice(draft.end)
|
|
2046
|
-
};
|
|
2047
|
-
}
|
|
2048
|
-
async function prepareIndependent(context, summaries) {
|
|
2274
|
+
if (summary.anchor === void 0) return [];
|
|
2275
|
+
const target = `${path.relative(repository.root, summary.changelogPath).split(path.sep).join("/")}#${summary.anchor}`;
|
|
2276
|
+
return [{
|
|
2277
|
+
label: `${summary.packageName} ${summary.version}`,
|
|
2278
|
+
url: repository.github === void 0 ? target : `https://github.com/${repository.github}/blob/HEAD/${target}`
|
|
2279
|
+
}];
|
|
2280
|
+
}
|
|
2281
|
+
/** Name the root's summary of a package release. */
|
|
2282
|
+
function summaryId(summary) {
|
|
2283
|
+
return `.@${summary.packageName}@${summary.version}`;
|
|
2284
|
+
}
|
|
2285
|
+
async function prepareIndependent(context, packages) {
|
|
2049
2286
|
const { repository } = context;
|
|
2050
2287
|
const file = await findChangelog(repository.root);
|
|
2051
2288
|
const content = context.options.rebuild ? void 0 : file.content;
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2289
|
+
const parsed = parseChangelog(content ?? "");
|
|
2290
|
+
const details = {
|
|
2291
|
+
mode: content === void 0 ? "initialize" : "update",
|
|
2292
|
+
styleReference: styleReference(parsed)
|
|
2293
|
+
};
|
|
2294
|
+
const releases = [];
|
|
2295
|
+
for (const item of packages) for (const summary of missingPackageSummaries(item.summaries, content)) {
|
|
2296
|
+
const release = item.releases.find((candidate) => candidate.version === summary.version);
|
|
2297
|
+
const request = await prepareRelease(context, item.pkg, release, details);
|
|
2298
|
+
releases.push({
|
|
2299
|
+
...request,
|
|
2300
|
+
evidence: request.evidence.filter((evidence) => evidence.kind === "range"),
|
|
2301
|
+
id: summaryId(summary),
|
|
2302
|
+
scope: "repository"
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
const releaseRefs = new Set(packages.flatMap((item) => item.summaries).filter((summary) => summary.version !== "Unreleased").map((summary) => summary.ref));
|
|
2055
2306
|
const release = {
|
|
2056
2307
|
previousRef: (await git(repository.root, [
|
|
2057
2308
|
"rev-list",
|
|
@@ -2061,52 +2312,63 @@ async function prepareIndependent(context, summaries) {
|
|
|
2061
2312
|
ref: repository.head,
|
|
2062
2313
|
version: "Unreleased"
|
|
2063
2314
|
};
|
|
2064
|
-
const draft =
|
|
2065
|
-
const
|
|
2315
|
+
const draft = parsed.sections.find((section) => section.version === "Unreleased")?.body;
|
|
2316
|
+
const checkpoint = await readCheckpoint(context, file.path, content);
|
|
2317
|
+
const from = await incrementalBase(context, checkpoint, release);
|
|
2318
|
+
const draftGenerated = checkpoint?.generated === true;
|
|
2066
2319
|
return {
|
|
2067
2320
|
content,
|
|
2321
|
+
draft,
|
|
2322
|
+
draftGenerated,
|
|
2068
2323
|
file,
|
|
2069
|
-
|
|
2070
|
-
|
|
2324
|
+
releases,
|
|
2325
|
+
unreleased: {
|
|
2326
|
+
release,
|
|
2327
|
+
request: await prepareRelease(context, repository.rootPackage, release, {
|
|
2328
|
+
...details,
|
|
2329
|
+
existingEntries: draft === void 0 ? void 0 : withoutPackageLinks(draft),
|
|
2330
|
+
from,
|
|
2331
|
+
mode: draft !== void 0 && from !== void 0 && draftGenerated ? "revise" : details.mode,
|
|
2332
|
+
onlyRepositoryFiles: true
|
|
2333
|
+
})
|
|
2334
|
+
}
|
|
2071
2335
|
};
|
|
2072
2336
|
}
|
|
2073
|
-
|
|
2337
|
+
/**
|
|
2338
|
+
* Write a section for each package release the root does not record yet, and
|
|
2339
|
+
* keep the draft's links to packages with unreleased changes current.
|
|
2340
|
+
*/
|
|
2341
|
+
async function renderIndependent(context, prepared, results, summaries, exception) {
|
|
2074
2342
|
const { plan, repository } = context;
|
|
2075
|
-
const { file } = prepared;
|
|
2076
|
-
|
|
2077
|
-
const entries =
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const promoted = promotePackageDraft(content, summary, repository);
|
|
2081
|
-
content = promoted.content;
|
|
2082
|
-
entries.push({
|
|
2083
|
-
changes: [...promoted.changes, ...aggregateChanges(repository, [summary])],
|
|
2084
|
-
release: {
|
|
2085
|
-
date: summary.date,
|
|
2086
|
-
ref: summary.ref,
|
|
2087
|
-
tag: summary.tag,
|
|
2088
|
-
version: label
|
|
2089
|
-
}
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
const unreleased = aggregateChanges(repository, summaries.filter((summary) => summary.version === "Unreleased"));
|
|
2093
|
-
const draft = parseChangelog(content ?? "").sections.find((section) => section.version === "Unreleased")?.body;
|
|
2094
|
-
unreleased.push(...results.get(prepared.request.id) ?? []);
|
|
2095
|
-
if (unreleased.length > 0) entries.push({
|
|
2096
|
-
changes: unreleased,
|
|
2097
|
-
existingBody: draft,
|
|
2343
|
+
const { content, draft, file, unreleased } = prepared;
|
|
2344
|
+
const ordered = await sortReleases(repository, summaries.filter((summary) => summary.version !== "Unreleased"));
|
|
2345
|
+
const entries = Array.from(missingPackageSummaries(ordered, content), (summary) => ({
|
|
2346
|
+
changes: results.get(summaryId(summary)) ?? [],
|
|
2347
|
+
links: packageLink(repository, summary),
|
|
2098
2348
|
release: {
|
|
2099
|
-
|
|
2100
|
-
|
|
2349
|
+
date: summary.date,
|
|
2350
|
+
ref: summary.ref,
|
|
2351
|
+
tag: summary.tag,
|
|
2352
|
+
version: `${summary.packageName}@${summary.version}`
|
|
2101
2353
|
}
|
|
2354
|
+
}));
|
|
2355
|
+
const changes = results.get(unreleased.request.id) ?? [];
|
|
2356
|
+
const links = summaries.filter((summary) => summary.version === "Unreleased").flatMap((summary) => packageLink(repository, summary));
|
|
2357
|
+
const revise = unreleased.request.mode === "revise" && results.has(unreleased.request.id);
|
|
2358
|
+
if (revise || changes.length > 0 || packageLinksDiffer(draft, links)) entries.push({
|
|
2359
|
+
changes,
|
|
2360
|
+
existingBody: draft,
|
|
2361
|
+
links,
|
|
2362
|
+
release: unreleased.release,
|
|
2363
|
+
revise
|
|
2102
2364
|
});
|
|
2103
|
-
const after = updateMarkdown(content, entries, exception, repository.github);
|
|
2365
|
+
const after = updateMarkdown(content, entries, exception, repository.github, ordered.map((summary) => `${summary.packageName}@${summary.version}`));
|
|
2104
2366
|
if (after !== file.content) plan.files.push({
|
|
2105
2367
|
after,
|
|
2106
2368
|
before: file.content,
|
|
2107
2369
|
path: file.path
|
|
2108
2370
|
});
|
|
2109
|
-
recordCheckpoint(context, file.path, after, prepared.
|
|
2371
|
+
recordCheckpoint(context, file.path, after, unreleased.release, leavesGeneratedDraft(entries, prepared.draftGenerated));
|
|
2110
2372
|
}
|
|
2111
2373
|
/**
|
|
2112
2374
|
* Find package releases whose own section records no notable changes, so a
|
|
@@ -2120,18 +2382,6 @@ function quietReleases(plan, packages) {
|
|
|
2120
2382
|
}
|
|
2121
2383
|
return quiet;
|
|
2122
2384
|
}
|
|
2123
|
-
function aggregateShared(context, prepared, results, exception, quiet) {
|
|
2124
|
-
const { plan, repository } = context;
|
|
2125
|
-
renderPackage(context, prepared, results);
|
|
2126
|
-
const rootEdit = plan.files.find((file) => path.dirname(file.path) === repository.root);
|
|
2127
|
-
if (rootEdit === void 0) return;
|
|
2128
|
-
const additions = prepared.summaries.map((summary) => ({
|
|
2129
|
-
changes: aggregateChanges(repository, plan.releases.filter((item) => item.version === summary.version && item.ref === summary.ref && !quiet.has(`${item.changelogPath}@${item.version}`))),
|
|
2130
|
-
existingBody: parseChangelog(rootEdit.after).sections.find((section) => section.version === summary.version)?.body,
|
|
2131
|
-
release: prepared.releases.find((item) => item.version === summary.version)
|
|
2132
|
-
}));
|
|
2133
|
-
rootEdit.after = updateMarkdown(prepared.content, additions, exception || prepared.exception, repository.github);
|
|
2134
|
-
}
|
|
2135
2385
|
/** Generate package changelogs and the root changelog that summarizes them. */
|
|
2136
2386
|
async function generateWorkspace(context) {
|
|
2137
2387
|
const { options, plan, repository } = context;
|
|
@@ -2139,21 +2389,24 @@ async function generateWorkspace(context) {
|
|
|
2139
2389
|
const shared = rootReleases.some((release) => release.version !== "Unreleased");
|
|
2140
2390
|
const rootFile = await findChangelog(repository.root);
|
|
2141
2391
|
const rootContent = options.rebuild ? void 0 : rootFile.content;
|
|
2142
|
-
const
|
|
2143
|
-
const needsPackageHistory = rootMissing === void 0 || rootMissing.some((release) => release.version !== "Unreleased");
|
|
2144
|
-
const recordedAtRoot = (summary) => rootMissing === void 0 ? rootContent !== void 0 && missingPackageSummaries([summary], rootContent).length === 0 : rootMissing.every((release) => release.version !== summary.version || release.ref !== summary.ref);
|
|
2392
|
+
const linksHistory = !shared || missingReleases(rootReleases, rootContent).some((release) => release.version !== "Unreleased");
|
|
2145
2393
|
const packages = [];
|
|
2146
|
-
for (const pkg of describedPackages(repository)) packages.push(await preparePackage(context, pkg,
|
|
2394
|
+
for (const pkg of describedPackages(repository)) packages.push(await preparePackage(context, pkg, linksHistory));
|
|
2147
2395
|
const root = shared ? await preparePackage(context, repository.rootPackage) : void 0;
|
|
2148
|
-
const independent = shared ? void 0 : await prepareIndependent(context, packages
|
|
2396
|
+
const independent = shared ? void 0 : await prepareIndependent(context, packages);
|
|
2149
2397
|
const requests = [...packages, ...root === void 0 ? [] : [root]].flatMap((pkg) => pkg.work.map((item) => item.request));
|
|
2150
|
-
if (independent !== void 0) requests.push(independent.request);
|
|
2398
|
+
if (independent !== void 0) requests.push(...independent.releases, independent.unreleased.request);
|
|
2151
2399
|
const results = await generateReleases(context, requests);
|
|
2152
2400
|
const summaries = packages.flatMap((pkg) => renderPackage(context, pkg, results));
|
|
2153
2401
|
plan.releases.push(...summaries);
|
|
2154
2402
|
const exception = packages.some((pkg) => pkg.exception);
|
|
2155
|
-
if (root !== void 0)
|
|
2156
|
-
|
|
2403
|
+
if (root !== void 0) {
|
|
2404
|
+
const quiet = quietReleases(plan, packages);
|
|
2405
|
+
renderPackage(context, {
|
|
2406
|
+
...root,
|
|
2407
|
+
exception: exception || root.exception
|
|
2408
|
+
}, results, (release) => summaries.filter((summary) => summary.version === release.version && summary.ref === release.ref && !quiet.has(`${summary.changelogPath}@${summary.version}`)).flatMap((summary) => packageLink(repository, summary)));
|
|
2409
|
+
} else if (independent !== void 0) await renderIndependent(context, independent, results, summaries, exception);
|
|
2157
2410
|
}
|
|
2158
2411
|
|
|
2159
2412
|
// src/lib/generate.ts
|
|
@@ -2379,4 +2632,4 @@ async function reviewChangelog(plan, options = {}) {
|
|
|
2379
2632
|
}
|
|
2380
2633
|
}
|
|
2381
2634
|
|
|
2382
|
-
export {
|
|
2635
|
+
export { defaultModels as C, version as E, setLogger as S, maxTimeoutMinutes as T, manifestAt as _, writeChangelog as a, isAncestor as b, resolveConfig as c, efforts as d, rootChangelogs as f, localDate as g, isVersionOnlyChange as h, generateChangelog as i, createAgent as l, inspectRepository as m, previewChangelog as n, defineConfig as o, LOCKFILE_PATH as p, reviewChangelog as r, loadConfig as s, assertReviewTerminal as t, agentNames as u, tagVersion as v, defaults as w, runCommand as x, git as y };
|