@kitschpatrol/chagent 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +4 -4
- package/dist/lib/bumpp.d.ts +1 -1
- package/dist/lib/bumpp.js +1 -1
- package/dist/lib/index.d.ts +8 -3
- package/dist/lib/index.js +2 -2
- package/dist/{lib-Byxdn2st.js → lib-DVZXk4YE.js} +18 -10
- package/dist/{review-DftIAoEl.js → review-Bl10DPO7.js} +533 -299
- package/dist/{types-B6FOOkTQ.d.ts → types-B-sACYE9.d.ts} +18 -2
- package/package.json +1 -1
- package/readme.md +12 -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.5.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,10 +427,23 @@ 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;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
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;
|
|
409
445
|
}
|
|
446
|
+
return true;
|
|
410
447
|
}
|
|
411
448
|
/**
|
|
412
449
|
* Predict the tag of a release that is not tagged yet, so its heading can link
|
|
@@ -540,25 +577,31 @@ function describedPackages(repository) {
|
|
|
540
577
|
async function taggedReleases(repository, pkg, config) {
|
|
541
578
|
const candidates = [];
|
|
542
579
|
for (const tag of repository.tags) {
|
|
543
|
-
const
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
580
|
+
const match = tagVersion(tag.name, pkg, config.tagPatterns);
|
|
581
|
+
if (match !== void 0) candidates.push({
|
|
582
|
+
...match,
|
|
583
|
+
tag
|
|
547
584
|
});
|
|
548
585
|
}
|
|
549
586
|
const isRoot = pkg.directory === ".";
|
|
550
587
|
const manifests = isRoot ? /* @__PURE__ */ new Map() : await manifestsAt(repository.root, pkg.directory, candidates.map(({ tag }) => tag.commit));
|
|
551
|
-
const
|
|
552
|
-
for (const
|
|
588
|
+
const owned = [];
|
|
589
|
+
for (const candidate of candidates) {
|
|
553
590
|
if (!isRoot) {
|
|
554
|
-
const manifest = manifests.get(tag.commit);
|
|
555
|
-
if (manifest?.version !== void 0 && semver.valid(manifest.version) !== version) continue;
|
|
591
|
+
const manifest = manifests.get(candidate.tag.commit);
|
|
592
|
+
if (manifest?.version !== void 0 && semver.valid(manifest.version) !== candidate.version) continue;
|
|
556
593
|
if (manifest === void 0 && await git(repository.root, [
|
|
557
594
|
"cat-file",
|
|
558
595
|
"-t",
|
|
559
|
-
`${tag.commit}:${pkg.directory}`
|
|
596
|
+
`${candidate.tag.commit}:${pkg.directory}`
|
|
560
597
|
], true) !== "tree") continue;
|
|
561
598
|
}
|
|
599
|
+
owned.push(candidate);
|
|
600
|
+
}
|
|
601
|
+
const releases = [];
|
|
602
|
+
for (const candidate of owned) {
|
|
603
|
+
if (candidate.precision < 3 && !await leadsSeries(repository.root, candidate, owned)) continue;
|
|
604
|
+
const { tag, version } = candidate;
|
|
562
605
|
const existing = releases.find((release) => release.version === version);
|
|
563
606
|
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
607
|
if (existing) continue;
|
|
@@ -638,13 +681,25 @@ manager, manifest format, or directory layout. Use snapshots at those refs, not
|
|
|
638
681
|
uncommitted files or newer code at HEAD. Respect repository.directory and
|
|
639
682
|
repository.excludeDirectories when deciding which changes belong in this entry;
|
|
640
683
|
you may inspect related files for context. The supplied evidence is a starting
|
|
641
|
-
point, not a limit on your research.
|
|
684
|
+
point, not a limit on your research. Supplied commits leave out lockfiles and
|
|
685
|
+
directories named build, coverage, dist, or changelog at the top of a package,
|
|
686
|
+
which usually hold generated output. Judge those directories yourself: when one
|
|
687
|
+
holds a project's source rather than generated output, its changes belong in the
|
|
688
|
+
changelog like any others.
|
|
642
689
|
|
|
643
690
|
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
|
-
|
|
691
|
+
historical changelog. When mode is update, cover all missing changes in the
|
|
692
|
+
supplied range. When mode is revise, the existing entries are your own earlier
|
|
693
|
+
draft of this release, describing it up to repository.from, and the supplied
|
|
694
|
+
range is what has happened since. Return the complete entries for the release as
|
|
695
|
+
it now stands; they replace the draft. Carry over what still holds, fold later
|
|
696
|
+
fixes and refinements into the entries they affect, combine entries that turn
|
|
697
|
+
out to describe one change, and drop an entry only when the range reverts or
|
|
698
|
+
supersedes it. Keep the wording of entries that need no change. In every mode,
|
|
699
|
+
review every commit, diff, and associated PR in the supplied range before
|
|
700
|
+
deciding what to include. A changelog records the evolution of the project,
|
|
701
|
+
including maintenance releases; it is not just a list of new application
|
|
702
|
+
features.
|
|
648
703
|
|
|
649
704
|
Cover distinct changes to behavior, public APIs and types, CLI flags and output,
|
|
650
705
|
runtime compatibility, installation and distribution, documentation and examples,
|
|
@@ -673,9 +728,16 @@ Give distinct changes separate readable bullets;
|
|
|
673
728
|
do not compress unrelated features or breaking changes into one long entry. Keep
|
|
674
729
|
each entry concise without sacrificing coverage. Consolidate related commits,
|
|
675
730
|
not unrelated changes. There is no three-entry limit for package scope, including
|
|
676
|
-
a single-package repository's root changelog.
|
|
677
|
-
|
|
678
|
-
|
|
731
|
+
a single-package repository's root changelog.
|
|
732
|
+
|
|
733
|
+
Only scope=repository asks for a summary: an entry in a monorepo's root
|
|
734
|
+
changelog, which the caller ends with links to the package changelogs that hold
|
|
735
|
+
the details. Write what a reader of the whole repository most needs to know, in
|
|
736
|
+
at most three entries, rather than repeating package entries one by one. Combine
|
|
737
|
+
related changes across packages and name the packages they affect. When
|
|
738
|
+
repository.directory is a package, summarize that package's release; otherwise
|
|
739
|
+
summarize the repository, including changes outside any package. Do not write
|
|
740
|
+
the links yourself.
|
|
679
741
|
|
|
680
742
|
Use commit messages and PRs to understand intent, then check the changed files
|
|
681
743
|
and diffs. Inspect public interfaces at both ends of the range before claiming
|
|
@@ -714,25 +776,37 @@ Put breaking public changes in the appropriate standard category and set breakin
|
|
|
714
776
|
to true; do not create a Breaking Changes category.
|
|
715
777
|
Descriptions are single-line Markdown suitable for a list item,
|
|
716
778
|
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
|
-
|
|
779
|
+
Every change must cite, in sources, at least one evidence ID supplied for its own
|
|
780
|
+
release or the full ID of a commit within that release's range. Prefer the commit
|
|
781
|
+
that made the change, including one found through your own Git research; use the
|
|
782
|
+
release's range ID when no single commit establishes it. When mode is revise,
|
|
783
|
+
cite the draft evidence ID for an entry carried over or rewritten from the
|
|
784
|
+
existing entries, along with any commit that changed it. Include a PR
|
|
720
785
|
link only when its URL and association are supplied. Do not fabricate links.
|
|
721
786
|
|
|
722
|
-
Existing entries are provided
|
|
723
|
-
|
|
724
|
-
|
|
787
|
+
Existing entries are provided for deduplication and preservation, and when mode
|
|
788
|
+
is revise, as the draft to rewrite. They are not independent evidence. Unless
|
|
789
|
+
mode is revise, omit changes already expressed by those entries, even if you
|
|
790
|
+
would phrase them differently. GitHub release-note bodies are deliberately
|
|
725
791
|
absent because this output will itself become GitHub release notes.
|
|
726
792
|
Do not read existing changelog files, generated release-note files, or GitHub
|
|
727
793
|
release bodies as source material, even when researching the repository directly.
|
|
728
794
|
|
|
795
|
+
When styleReference is present, it is a recent release section of the changelog
|
|
796
|
+
these entries will join. Match its writing conventions: voice and tense,
|
|
797
|
+
capitalization, terminal punctuation, and how code, issues, and PRs are
|
|
798
|
+
referenced. It is a style sample and nothing more. It describes another release,
|
|
799
|
+
so it is not evidence, and its length and choice of categories say nothing about
|
|
800
|
+
this one.
|
|
801
|
+
|
|
729
802
|
Before returning, check coverage across behavior/API, compatibility, dependencies,
|
|
730
803
|
documentation, build/test/CI, and distribution. Do not drop a change just because
|
|
731
804
|
its commit is called chore, docs, build, or dependency updates. Return an empty
|
|
732
805
|
changes array only when the evidence is entirely release-version bookkeeping,
|
|
733
806
|
lockfile-only resolution changes, generated artifacts, formatting noise, or
|
|
734
|
-
changes already covered by existing entries.
|
|
735
|
-
|
|
807
|
+
changes already covered by existing entries. When mode is revise, an empty array
|
|
808
|
+
removes the draft, so return one only when nothing in the draft still holds. Do
|
|
809
|
+
not pad the result with unsupported changes.`;
|
|
736
810
|
/** Invalidates cached drafts whenever the instructions change. */
|
|
737
811
|
const instructionVersion = hash(instructions);
|
|
738
812
|
|
|
@@ -762,6 +836,7 @@ const rootChangelogs = [
|
|
|
762
836
|
];
|
|
763
837
|
|
|
764
838
|
// src/lib/agents.ts
|
|
839
|
+
const COMMIT_ID = /^[\da-f]{7,64}$/iv;
|
|
765
840
|
const CONTROL_CHARACTERS = /\p{Cc}/v;
|
|
766
841
|
const HEADING = /^#{1,6}\s/v;
|
|
767
842
|
const LIST_MARKER = /^(?:[\-*]|\d+\.)(?:\s+|$)/v;
|
|
@@ -780,13 +855,10 @@ const responseJsonSchema = JSON.stringify(z.toJSONSchema(responseSchema, { targe
|
|
|
780
855
|
function normalizeDescription(description) {
|
|
781
856
|
return description.replaceAll(WHITESPACE_RUN, " ").trim().replace(LIST_MARKER, "");
|
|
782
857
|
}
|
|
783
|
-
function
|
|
784
|
-
const ids = new Set(release.evidence.map((item) => item.id));
|
|
858
|
+
function validateDescriptions(changes, id) {
|
|
785
859
|
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
860
|
const description = normalizeDescription(change.description);
|
|
789
|
-
if (description === "" || CONTROL_CHARACTERS.test(description) || HEADING.test(description) || description.includes("<!--")) throw new Error(`Agent output for ${
|
|
861
|
+
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
862
|
return {
|
|
791
863
|
...change,
|
|
792
864
|
description
|
|
@@ -794,8 +866,8 @@ function validateChanges(changes, release) {
|
|
|
794
866
|
});
|
|
795
867
|
}
|
|
796
868
|
/**
|
|
797
|
-
* Validate structure, completeness, release identity
|
|
798
|
-
*
|
|
869
|
+
* Validate structure, completeness, and release identity before accepting any
|
|
870
|
+
* adapter's output. Citations are checked separately, by `verifyCitations`.
|
|
799
871
|
*
|
|
800
872
|
* @returns The change sets, with descriptions normalized.
|
|
801
873
|
*/
|
|
@@ -809,10 +881,53 @@ function validateResponse(value, releases) {
|
|
|
809
881
|
const omitted = expected.keys().filter((id) => !ids.includes(id)).toArray();
|
|
810
882
|
if (omitted.length > 0) throw new Error(`Agent output omitted requested releases: ${omitted.join(", ")}.`);
|
|
811
883
|
return response.data.releases.map(({ changes, id }) => ({
|
|
812
|
-
changes:
|
|
884
|
+
changes: validateDescriptions(changes, id),
|
|
813
885
|
id
|
|
814
886
|
}));
|
|
815
887
|
}
|
|
888
|
+
/** List every commit in a release's range. */
|
|
889
|
+
async function rangeCommits(root, { repository }) {
|
|
890
|
+
const { from, to } = repository;
|
|
891
|
+
return (await git(root, ["rev-list", from === void 0 ? to : `${from}..${to}`])).split("\n");
|
|
892
|
+
}
|
|
893
|
+
async function verifyChanges(root, release, changes) {
|
|
894
|
+
const supplied = new Set(release.evidence.map((item) => item.id));
|
|
895
|
+
let commits;
|
|
896
|
+
const verified = [];
|
|
897
|
+
for (const change of changes) {
|
|
898
|
+
const sources = /* @__PURE__ */ new Set();
|
|
899
|
+
for (const source of change.sources) if (supplied.has(source)) sources.add(source);
|
|
900
|
+
else if (COMMIT_ID.test(source)) {
|
|
901
|
+
commits ??= await rangeCommits(root, release);
|
|
902
|
+
const matches = commits.filter((commit) => commit.startsWith(source.toLowerCase()));
|
|
903
|
+
if (matches.length === 1) sources.add(matches[0]);
|
|
904
|
+
}
|
|
905
|
+
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.`);
|
|
906
|
+
verified.push({
|
|
907
|
+
...change,
|
|
908
|
+
sources: [...sources]
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
return verified;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Check citations against Git instead of trusting them. A change may cite
|
|
915
|
+
* evidence supplied for its release or any commit within that release's range,
|
|
916
|
+
* which lets an agent cite what its own research found. Abbreviated commit IDs
|
|
917
|
+
* are completed, and sources that hold up are kept when others do not. Pass the
|
|
918
|
+
* requested releases with their complete evidence.
|
|
919
|
+
*
|
|
920
|
+
* @throws {Error} When a change is left without a source.
|
|
921
|
+
*/
|
|
922
|
+
async function verifyCitations(root, changeSets, releases) {
|
|
923
|
+
const requested = new Map(releases.map((release) => [release.id, release]));
|
|
924
|
+
const verified = [];
|
|
925
|
+
for (const { changes, id } of changeSets) verified.push({
|
|
926
|
+
changes: await verifyChanges(root, requested.get(id), changes),
|
|
927
|
+
id
|
|
928
|
+
});
|
|
929
|
+
return verified;
|
|
930
|
+
}
|
|
816
931
|
function prompt({ releases, repair, repositoryRoot }) {
|
|
817
932
|
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
933
|
return `${instructions}${correction}\n\nInput data (JSON):\n${JSON.stringify({
|
|
@@ -936,6 +1051,83 @@ function createAgent(name) {
|
|
|
936
1051
|
};
|
|
937
1052
|
}
|
|
938
1053
|
|
|
1054
|
+
// src/lib/config.ts
|
|
1055
|
+
const configSchema = z.strictObject({
|
|
1056
|
+
agent: z.enum(agentNames).optional(),
|
|
1057
|
+
cache: z.boolean().optional(),
|
|
1058
|
+
effort: z.enum(efforts).optional(),
|
|
1059
|
+
exclude: z.array(z.string()).optional(),
|
|
1060
|
+
github: z.boolean().optional(),
|
|
1061
|
+
/** Maximum evidence characters per agent request. */
|
|
1062
|
+
maxInputChars: z.number().int().min(4e3).optional(),
|
|
1063
|
+
model: z.string().min(1).optional(),
|
|
1064
|
+
/** Workspace directory globs, overriding manifest workspace discovery. */
|
|
1065
|
+
packages: z.array(z.string()).optional(),
|
|
1066
|
+
/**
|
|
1067
|
+
* What a monorepo's root changelog records: a summary of package releases,
|
|
1068
|
+
* the root package's own changes, or nothing.
|
|
1069
|
+
*/
|
|
1070
|
+
rootChangelog: z.enum(rootChangelogs).optional(),
|
|
1071
|
+
/** Workspace package directory globs that get no changelog of their own. */
|
|
1072
|
+
skipPackage: z.array(z.string()).optional(),
|
|
1073
|
+
/** Templates containing {version} and optionally {name}. */
|
|
1074
|
+
tagPatterns: z.array(z.string().includes("{version}")).optional(),
|
|
1075
|
+
/** Minutes each agent session may run. */
|
|
1076
|
+
timeout: z.number().positive().max(maxTimeoutMinutes).optional()
|
|
1077
|
+
});
|
|
1078
|
+
const resolvedConfigSchema = configSchema.required({
|
|
1079
|
+
agent: true,
|
|
1080
|
+
cache: true,
|
|
1081
|
+
effort: true,
|
|
1082
|
+
github: true,
|
|
1083
|
+
maxInputChars: true,
|
|
1084
|
+
rootChangelog: true,
|
|
1085
|
+
timeout: true
|
|
1086
|
+
}).transform(({ timeout, ...config }) => ({
|
|
1087
|
+
...config,
|
|
1088
|
+
timeoutMs: timeout * 6e4
|
|
1089
|
+
}));
|
|
1090
|
+
function invalidConfig(source, error) {
|
|
1091
|
+
return new Error(`Invalid options in ${source}:\n${z.prettifyError(error)}`, { cause: error });
|
|
1092
|
+
}
|
|
1093
|
+
/** Define optional repository configuration with TypeScript completion. */
|
|
1094
|
+
function defineConfig(config) {
|
|
1095
|
+
return config;
|
|
1096
|
+
}
|
|
1097
|
+
/** Load configuration from the repository root or an explicitly selected file. */
|
|
1098
|
+
async function loadConfig(root, file) {
|
|
1099
|
+
const explorer = cosmiconfig("chagent", {
|
|
1100
|
+
searchPlaces: [
|
|
1101
|
+
"chagent.config.ts",
|
|
1102
|
+
"chagent.config.mjs",
|
|
1103
|
+
"chagent.config.js",
|
|
1104
|
+
"chagent.config.json",
|
|
1105
|
+
"package.json"
|
|
1106
|
+
],
|
|
1107
|
+
searchStrategy: "none"
|
|
1108
|
+
});
|
|
1109
|
+
const result = file === void 0 ? await explorer.search(root) : await explorer.load(file);
|
|
1110
|
+
if (!result) {
|
|
1111
|
+
log.debug("No configuration file found.");
|
|
1112
|
+
return {};
|
|
1113
|
+
}
|
|
1114
|
+
log.debug(`Loaded configuration from ${result.filepath}.`);
|
|
1115
|
+
const parsed = configSchema.safeParse(result.config);
|
|
1116
|
+
if (!parsed.success) throw invalidConfig(result.filepath, parsed.error);
|
|
1117
|
+
return parsed.data;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Apply configuration layers over the defaults; later layers win and undefined
|
|
1121
|
+
* values do not.
|
|
1122
|
+
*/
|
|
1123
|
+
function resolveConfig(...layers) {
|
|
1124
|
+
const merged = { ...defaults };
|
|
1125
|
+
for (const layer of layers) for (const [key, value] of Object.entries(layer)) if (value !== void 0) merged[key] = value;
|
|
1126
|
+
const parsed = resolvedConfigSchema.safeParse(merged);
|
|
1127
|
+
if (!parsed.success) throw invalidConfig("generation settings", parsed.error);
|
|
1128
|
+
return parsed.data;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
939
1131
|
// src/lib/format.ts
|
|
940
1132
|
const FORMATTER_PACKAGES = ["@kitschpatrol/shared-config", "@kitschpatrol/prettier-config"];
|
|
941
1133
|
const manifestSchema = z.object({ exports: z.object({ ".": z.object({ import: z.string() }) }) });
|
|
@@ -1038,9 +1230,11 @@ async function writeChangelog(plan) {
|
|
|
1038
1230
|
const edit = plan.files.find((file) => file.path === checkpoint.path);
|
|
1039
1231
|
try {
|
|
1040
1232
|
const written = edit === void 0 ? void 0 : await readOptional(edit.path);
|
|
1233
|
+
const edited = edit !== void 0 && changelogHash(edit.after) !== checkpoint.contentHash;
|
|
1041
1234
|
await writeCache(path.join(stateDirectory, "checkpoints"), checkpoint.path, {
|
|
1042
1235
|
...checkpoint,
|
|
1043
|
-
contentHash: written === void 0 ? checkpoint.contentHash : changelogHash(written)
|
|
1236
|
+
contentHash: written === void 0 ? checkpoint.contentHash : changelogHash(written),
|
|
1237
|
+
generated: checkpoint.generated && !edited
|
|
1044
1238
|
});
|
|
1045
1239
|
} catch {
|
|
1046
1240
|
plan.diagnostics.push("Could not save incremental metadata; the next run will recheck the full release range.");
|
|
@@ -1075,83 +1269,6 @@ async function acquireLock(file) {
|
|
|
1075
1269
|
}
|
|
1076
1270
|
}
|
|
1077
1271
|
|
|
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
1272
|
// src/lib/changelog.ts
|
|
1156
1273
|
const LINE_BOUNDARY = /(?<=\n)/v;
|
|
1157
1274
|
const FENCE = /^\s{0,3}(`{3,}|~{3,})/v;
|
|
@@ -1163,9 +1280,14 @@ const WHITESPACE = /\s+/gv;
|
|
|
1163
1280
|
const TERMINAL_PUNCTUATION = /[.!]$/v;
|
|
1164
1281
|
const NEWLINE = /\r?\n/v;
|
|
1165
1282
|
const BULLET = /^\s*[\-*] /v;
|
|
1283
|
+
const ENTRY = /^\s*[\-*] /mv;
|
|
1284
|
+
const STYLE_REFERENCE_LENGTH = 2e3;
|
|
1166
1285
|
const CATEGORY_HEADING = /^### /mv;
|
|
1286
|
+
const PACKAGES_HEADING = /^### Packages\s*$/mv;
|
|
1287
|
+
const LINK_TARGET = /\]\(([^\)]*)\)/gv;
|
|
1167
1288
|
const SEMVER_LINE = /^.*\[Semantic Versioning\].*(?:\r?\n|$)/mv;
|
|
1168
1289
|
const UNRELEASED_REFERENCE = /^\[unreleased\]:[^\r\n]*(?:\r?\n)?/imv;
|
|
1290
|
+
const V_PREFIX = /^v\d/v;
|
|
1169
1291
|
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
1292
|
/** The body of a release section without any changes to record. */
|
|
1171
1293
|
const noNotableChanges = "No notable changes.";
|
|
@@ -1234,6 +1356,21 @@ function parseChangelog(content) {
|
|
|
1234
1356
|
sections
|
|
1235
1357
|
};
|
|
1236
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Pick the newest release section with entries, to show an agent how this
|
|
1361
|
+
* changelog is written. Long sections are cut at a line boundary.
|
|
1362
|
+
*/
|
|
1363
|
+
function styleReference({ sections }) {
|
|
1364
|
+
const body = sections.filter(({ version }) => version !== "Unreleased").map((section) => withoutPackageLinks(section.body)).find((entries) => ENTRY.test(entries));
|
|
1365
|
+
if (body === void 0) return;
|
|
1366
|
+
const lines = body.trim().split(LINE_BOUNDARY);
|
|
1367
|
+
let sample = "";
|
|
1368
|
+
for (const line of lines) {
|
|
1369
|
+
if (sample.length + line.length > STYLE_REFERENCE_LENGTH) break;
|
|
1370
|
+
sample += line;
|
|
1371
|
+
}
|
|
1372
|
+
return sample.trim() === "" ? void 0 : sample.trimEnd();
|
|
1373
|
+
}
|
|
1237
1374
|
function normalized(description) {
|
|
1238
1375
|
return description.trim().toLowerCase().replaceAll(WHITESPACE, " ").replace(TERMINAL_PUNCTUATION, "");
|
|
1239
1376
|
}
|
|
@@ -1273,6 +1410,46 @@ function laterCategoryIndex(body, category) {
|
|
|
1273
1410
|
const indexes = categories.slice(categories.indexOf(category) + 1).map((item) => categoryHeading(body, item)?.index).filter((index) => index !== void 0);
|
|
1274
1411
|
return indexes.length === 0 ? void 0 : Math.min(...indexes);
|
|
1275
1412
|
}
|
|
1413
|
+
/** Locate the package links that end a root summary's section. */
|
|
1414
|
+
function packagesBlock(body) {
|
|
1415
|
+
const heading = PACKAGES_HEADING.exec(body);
|
|
1416
|
+
if (heading === null) return;
|
|
1417
|
+
const after = heading.index + heading[0].length;
|
|
1418
|
+
const next = CATEGORY_HEADING.exec(body.slice(after));
|
|
1419
|
+
return {
|
|
1420
|
+
start: heading.index,
|
|
1421
|
+
end: next === null ? body.length : after + next.index
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
/** Remove the package links from a root summary's section, leaving its entries. */
|
|
1425
|
+
function withoutPackageLinks(body) {
|
|
1426
|
+
const block = packagesBlock(body);
|
|
1427
|
+
return block === void 0 ? body : body.slice(0, block.start) + body.slice(block.end);
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Check whether a section links to other package releases than it should.
|
|
1431
|
+
* Targets are compared, since a formatter may restyle the list around them.
|
|
1432
|
+
*/
|
|
1433
|
+
function packageLinksDiffer(body, links) {
|
|
1434
|
+
const block = body === void 0 ? void 0 : packagesBlock(body);
|
|
1435
|
+
const targets = body === void 0 || block === void 0 ? [] : body.slice(block.start, block.end).matchAll(LINK_TARGET).map((match) => match[1]).toArray();
|
|
1436
|
+
return !isDeepStrictEqual(targets, links.map(({ url }) => url));
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Merge new changes into a section's draft, or replace a revised draft with
|
|
1440
|
+
* them, and end the section with its package links.
|
|
1441
|
+
*/
|
|
1442
|
+
function sectionBody({ changes, existingBody = "", links, revise }, newline) {
|
|
1443
|
+
const draft = links === void 0 ? existingBody : withoutPackageLinks(existingBody);
|
|
1444
|
+
const body = mergeChanges(revise === true ? "" : draft, changes, newline);
|
|
1445
|
+
if (links === void 0 || links.length === 0) return body;
|
|
1446
|
+
const list = links.map(({ label, url }) => `- [${label}](${url})`);
|
|
1447
|
+
return body.trimEnd() + newline + newline + [
|
|
1448
|
+
"### Packages",
|
|
1449
|
+
"",
|
|
1450
|
+
...list
|
|
1451
|
+
].join(newline);
|
|
1452
|
+
}
|
|
1276
1453
|
/** Render a release heading with an optional comparison link. */
|
|
1277
1454
|
function releaseLink(github, release) {
|
|
1278
1455
|
if (github === void 0) return;
|
|
@@ -1293,12 +1470,16 @@ function updateMarkdown(original, entries, nodeException, github) {
|
|
|
1293
1470
|
const draft = parsed.sections.find((section) => section.version === "Unreleased");
|
|
1294
1471
|
const released = parsed.sections.filter((section) => section.version !== "Unreleased").map((section) => content.slice(section.start, section.end));
|
|
1295
1472
|
let draftText = draft === void 0 ? "" : content.slice(draft.start, draft.end);
|
|
1296
|
-
for (const
|
|
1473
|
+
for (const entry of entries) {
|
|
1474
|
+
const { existingBody, release } = entry;
|
|
1297
1475
|
const link = releaseLink(github, release);
|
|
1298
1476
|
const heading = `## ${link === void 0 ? release.version : `[${release.version}]`}${release.date === void 0 ? "" : ` - ${release.date}`}`;
|
|
1299
|
-
let body =
|
|
1477
|
+
let body = sectionBody(entry, newline);
|
|
1300
1478
|
if (body.trim() === "") {
|
|
1301
|
-
if (release.version === "Unreleased")
|
|
1479
|
+
if (release.version === "Unreleased") {
|
|
1480
|
+
if (existingBody !== void 0) draftText = "";
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1302
1483
|
body = newline + noNotableChanges + newline;
|
|
1303
1484
|
}
|
|
1304
1485
|
const section = heading + newline + newline + body.trim() + newline + newline;
|
|
@@ -1335,8 +1516,15 @@ function addNodeNote(header, newline, nodeException) {
|
|
|
1335
1516
|
}
|
|
1336
1517
|
return header;
|
|
1337
1518
|
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Read a heading's version the way default tag patterns read a tag, so `1.2`
|
|
1521
|
+
* and `v1` name the first release of their series and a bare `1` names none.
|
|
1522
|
+
*/
|
|
1338
1523
|
function normalizeVersion(raw) {
|
|
1339
|
-
|
|
1524
|
+
if (raw.toLowerCase() === "unreleased") return "Unreleased";
|
|
1525
|
+
const prefixed = V_PREFIX.test(raw);
|
|
1526
|
+
const parsed = parseVersion(prefixed ? raw.slice(1) : raw);
|
|
1527
|
+
return parsed === void 0 || !prefixed && parsed.precision === 1 ? raw : parsed.version;
|
|
1340
1528
|
}
|
|
1341
1529
|
function releaseHeadingVersion(line) {
|
|
1342
1530
|
const match = RELEASE_HEADING.exec(line.trimEnd());
|
|
@@ -1352,17 +1540,26 @@ function sectionSpacing(content, newline) {
|
|
|
1352
1540
|
|
|
1353
1541
|
// src/lib/evidence.ts
|
|
1354
1542
|
const COMMIT_MARKER = "";
|
|
1355
|
-
const
|
|
1356
|
-
const
|
|
1543
|
+
const OUTPUT_DIRECTORY = /^(?:build|changelog|coverage|dist)(?:\/|$)/iv;
|
|
1544
|
+
const NEVER_SOURCE = /(?:^|\/)(?:changelog(?:\.md)?$|node_modules(?:\/|$))/iv;
|
|
1545
|
+
/**
|
|
1546
|
+
* Changelogs are this tool's own output, installed dependencies are another
|
|
1547
|
+
* project's source, and lockfiles record resolved versions, not declared
|
|
1548
|
+
* dependency ranges. None of them is evidence of a change.
|
|
1549
|
+
*/
|
|
1550
|
+
function excluded(file) {
|
|
1551
|
+
return LOCKFILE_PATH.test(file) || NEVER_SOURCE.test(file);
|
|
1552
|
+
}
|
|
1357
1553
|
/**
|
|
1358
|
-
*
|
|
1359
|
-
* `src/dist` remain source.
|
|
1360
|
-
*
|
|
1554
|
+
* Recognize directories at the top of a package that usually hold generated
|
|
1555
|
+
* output, so `packages/build` and `src/dist` remain source. Supplied commits
|
|
1556
|
+
* leave them out, but they still count as changes to research: only the agent
|
|
1557
|
+
* can tell a build artifact from a project whose source lives in `coverage`.
|
|
1361
1558
|
*/
|
|
1362
|
-
function
|
|
1559
|
+
function likelyGenerated(file, packages) {
|
|
1363
1560
|
const owner = packages.findLast(({ directory }) => file.startsWith(`${directory}/`));
|
|
1364
1561
|
const relative = owner === void 0 ? file : file.slice(owner.directory.length + 1);
|
|
1365
|
-
return
|
|
1562
|
+
return OUTPUT_DIRECTORY.test(relative);
|
|
1366
1563
|
}
|
|
1367
1564
|
/**
|
|
1368
1565
|
* A package's directory and, transitively, those of the workspace packages it
|
|
@@ -1435,7 +1632,10 @@ async function commitsInRange(root, range, paths) {
|
|
|
1435
1632
|
}
|
|
1436
1633
|
return commits.values().toArray();
|
|
1437
1634
|
}
|
|
1438
|
-
/**
|
|
1635
|
+
/**
|
|
1636
|
+
* Collect a release's starting evidence, or nothing when its range holds only
|
|
1637
|
+
* changelogs, lockfiles, and version bookkeeping.
|
|
1638
|
+
*/
|
|
1439
1639
|
async function collectEvidence(repository, pkg, release, excludeDirectories = []) {
|
|
1440
1640
|
const { packages, root } = repository;
|
|
1441
1641
|
const paths = [...(pkg.directory === "." ? ["."] : await packageScope(repository, pkg, release.ref)).map((directory) => `:(literal)${directory}`), ...excludeDirectories.map((directory) => `:(exclude,literal)${directory}`)];
|
|
@@ -1457,20 +1657,25 @@ async function collectEvidence(repository, pkg, release, excludeDirectories = []
|
|
|
1457
1657
|
release.ref,
|
|
1458
1658
|
"--",
|
|
1459
1659
|
...paths
|
|
1460
|
-
])).split("\0").filter((file) => file !== "" && !
|
|
1660
|
+
])).split("\0").filter((file) => file !== "" && !excluded(file));
|
|
1461
1661
|
const rangeBlobs = await readBlobs(root, manifestObjects(candidates, from, release.ref));
|
|
1462
1662
|
const changed = new Set(candidates.filter((file) => !isManifest(file) || !isVersionOnlyEdit(rangeBlobs.get(`${from}:${file}`), rangeBlobs.get(`${release.ref}:${file}`))));
|
|
1463
1663
|
if (changed.size === 0) return [];
|
|
1464
1664
|
const sourceCommits = (await commitsInRange(root, range, paths)).map((item) => ({
|
|
1465
1665
|
...item,
|
|
1466
|
-
files: item.files.filter((file) => !
|
|
1666
|
+
files: item.files.filter((file) => !excluded(file) && !likelyGenerated(file, packages))
|
|
1467
1667
|
}));
|
|
1468
1668
|
const commitBlobs = await readBlobs(root, sourceCommits.flatMap(({ files, id }) => manifestObjects(files, `${id}^`, id)));
|
|
1469
|
-
const evidence =
|
|
1669
|
+
const evidence = [{
|
|
1670
|
+
id: `range:${from}..${release.ref}`,
|
|
1671
|
+
kind: "range",
|
|
1672
|
+
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.`
|
|
1673
|
+
}];
|
|
1674
|
+
for (const { files, id, message } of sourceCommits) if (files.some((file) => changed.has(file)) && hasSourceChanges(files, id, commitBlobs)) evidence.push({
|
|
1470
1675
|
id,
|
|
1471
1676
|
kind: "commit",
|
|
1472
1677
|
text: `${message}\nFiles: ${files.join(", ")}`
|
|
1473
|
-
})
|
|
1678
|
+
});
|
|
1474
1679
|
if (release.pending === true) {
|
|
1475
1680
|
const file = path.posix.join(pkg.directory, "package.json");
|
|
1476
1681
|
const diff = await git(root, [
|
|
@@ -1487,11 +1692,6 @@ async function collectEvidence(repository, pkg, release, excludeDirectories = []
|
|
|
1487
1692
|
text: diff
|
|
1488
1693
|
});
|
|
1489
1694
|
}
|
|
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
1695
|
return evidence;
|
|
1496
1696
|
}
|
|
1497
1697
|
function githubUnavailable(error) {
|
|
@@ -1628,7 +1828,7 @@ function releaseBatches(requests, limit) {
|
|
|
1628
1828
|
for (const release of requests) {
|
|
1629
1829
|
const minimum = {
|
|
1630
1830
|
...release,
|
|
1631
|
-
evidence: release.evidence.filter((item) => item
|
|
1831
|
+
evidence: release.evidence.filter((item) => required(item))
|
|
1632
1832
|
};
|
|
1633
1833
|
const length = JSON.stringify(minimum).length + 1;
|
|
1634
1834
|
if (size + length > limit && batch.length > 0) {
|
|
@@ -1644,7 +1844,7 @@ function releaseBatches(requests, limit) {
|
|
|
1644
1844
|
const allowance = Math.max(0, Math.floor((limit - JSON.stringify(items).length) / items.length));
|
|
1645
1845
|
const seeded = items.map((item) => ({
|
|
1646
1846
|
...item,
|
|
1647
|
-
evidence: [...item.evidence, ...researchEvidence(full.get(item.id).filter((entry) => entry
|
|
1847
|
+
evidence: [...item.evidence, ...researchEvidence(full.get(item.id).filter((entry) => !required(entry)), allowance + 2)]
|
|
1648
1848
|
}));
|
|
1649
1849
|
let remaining = limit - JSON.stringify(seeded).length;
|
|
1650
1850
|
for (const item of seeded) {
|
|
@@ -1657,6 +1857,10 @@ function releaseBatches(requests, limit) {
|
|
|
1657
1857
|
return seeded;
|
|
1658
1858
|
});
|
|
1659
1859
|
}
|
|
1860
|
+
/** Keep what a request cannot do without: its range, and a draft it may cite. */
|
|
1861
|
+
function required(item) {
|
|
1862
|
+
return item.kind === "range" || item.kind === "draft";
|
|
1863
|
+
}
|
|
1660
1864
|
/** Bound the starting context; adapters can read the complete range themselves. */
|
|
1661
1865
|
function researchEvidence(evidence, limit) {
|
|
1662
1866
|
const seed = [];
|
|
@@ -1671,19 +1875,32 @@ function researchEvidence(evidence, limit) {
|
|
|
1671
1875
|
}
|
|
1672
1876
|
|
|
1673
1877
|
// src/lib/jobs.ts
|
|
1878
|
+
/** Lets a revised entry cite the draft it came from. */
|
|
1879
|
+
const draftEvidence = {
|
|
1880
|
+
id: "draft",
|
|
1881
|
+
kind: "draft",
|
|
1882
|
+
text: "The existing entries, which describe this release up to repository.from. Cite this ID for entries carried over or rewritten from them."
|
|
1883
|
+
};
|
|
1884
|
+
/** Check whether a package's changelog is a monorepo's root summary. */
|
|
1885
|
+
function summarizesPackages(context, pkg) {
|
|
1886
|
+
return pkg.directory === "." && context.repository.packages.length > 0 && context.options.rootChangelog === "summary";
|
|
1887
|
+
}
|
|
1674
1888
|
/**
|
|
1675
1889
|
* Prepare one release without launching a model, so repository research can be
|
|
1676
1890
|
* shared.
|
|
1677
1891
|
*/
|
|
1678
|
-
async function prepareRelease(context, pkg, release,
|
|
1892
|
+
async function prepareRelease(context, pkg, release, details) {
|
|
1679
1893
|
const { options, repository } = context;
|
|
1894
|
+
const { existingEntries = "", from, mode, onlyRepositoryFiles = false, styleReference } = details;
|
|
1680
1895
|
options.signal?.throwIfAborted();
|
|
1681
1896
|
const excludeDirectories = await excludedPackages(context, pkg, release.ref, onlyRepositoryFiles);
|
|
1897
|
+
const evidence = await collectEvidence(repository, pkg, from === void 0 ? release : {
|
|
1898
|
+
...release,
|
|
1899
|
+
previousRef: from
|
|
1900
|
+
}, excludeDirectories);
|
|
1901
|
+
if (mode === "revise" && evidence.length > 0) evidence.splice(1, 0, draftEvidence);
|
|
1682
1902
|
return {
|
|
1683
|
-
evidence
|
|
1684
|
-
...release,
|
|
1685
|
-
previousRef: from
|
|
1686
|
-
}, excludeDirectories),
|
|
1903
|
+
evidence,
|
|
1687
1904
|
existingEntries,
|
|
1688
1905
|
id: `${pkg.directory}@${release.version}`,
|
|
1689
1906
|
initialRelease: release.previousRef === void 0 && from === void 0,
|
|
@@ -1695,7 +1912,8 @@ async function prepareRelease(context, pkg, release, existingEntries, mode, from
|
|
|
1695
1912
|
from: from ?? release.previousRef,
|
|
1696
1913
|
to: release.ref
|
|
1697
1914
|
},
|
|
1698
|
-
scope: pkg
|
|
1915
|
+
scope: summarizesPackages(context, pkg) ? "repository" : "package",
|
|
1916
|
+
styleReference,
|
|
1699
1917
|
version: release.version
|
|
1700
1918
|
};
|
|
1701
1919
|
}
|
|
@@ -1761,9 +1979,11 @@ function progressMessage(pending, sessions) {
|
|
|
1761
1979
|
}
|
|
1762
1980
|
/**
|
|
1763
1981
|
* Run one agent session. A long session is worth saving, so invalid output gets
|
|
1764
|
-
* one correction request before the run fails.
|
|
1982
|
+
* one correction request before the run fails. Citations are checked against
|
|
1983
|
+
* the releases as they were before their evidence was trimmed to fit, since an
|
|
1984
|
+
* agent researching a range can cite what trimming left out.
|
|
1765
1985
|
*/
|
|
1766
|
-
async function runSession(context, releases) {
|
|
1986
|
+
async function runSession(context, releases, untrimmed) {
|
|
1767
1987
|
const { agent, options, repository } = context;
|
|
1768
1988
|
const request = {
|
|
1769
1989
|
releases,
|
|
@@ -1776,24 +1996,24 @@ async function runSession(context, releases) {
|
|
|
1776
1996
|
}
|
|
1777
1997
|
};
|
|
1778
1998
|
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.`);
|
|
1999
|
+
const accept = async (value) => verifyCitations(repository.root, validateResponse(value, releases), untrimmed);
|
|
1779
2000
|
const started = performance.now();
|
|
1780
2001
|
const response = await agent.generate(request);
|
|
1781
2002
|
log.debug(`The ${agent.name} session finished in ${Math.round(performance.now() - started)} ms.`);
|
|
1782
2003
|
try {
|
|
1783
|
-
return
|
|
2004
|
+
return await accept(response);
|
|
1784
2005
|
} catch (error) {
|
|
1785
2006
|
const message = error instanceof Error ? error.message : String(error);
|
|
1786
2007
|
log.warn(`Requesting a correction of invalid agent output. ${message}`);
|
|
1787
2008
|
options.onProgress?.("Requesting a correction of invalid agent output.");
|
|
1788
2009
|
options.signal?.throwIfAborted();
|
|
1789
|
-
|
|
2010
|
+
return accept(await agent.generate({
|
|
1790
2011
|
...request,
|
|
1791
2012
|
repair: {
|
|
1792
2013
|
error: message,
|
|
1793
2014
|
response
|
|
1794
2015
|
}
|
|
1795
|
-
});
|
|
1796
|
-
return validateResponse(corrected, releases);
|
|
2016
|
+
}));
|
|
1797
2017
|
}
|
|
1798
2018
|
}
|
|
1799
2019
|
/**
|
|
@@ -1826,7 +2046,7 @@ async function generateReleases(context, requests) {
|
|
|
1826
2046
|
options.onProgress?.(status);
|
|
1827
2047
|
for (const releases of batches) {
|
|
1828
2048
|
options.signal?.throwIfAborted();
|
|
1829
|
-
const changeSets = await runSession(context, releases);
|
|
2049
|
+
const changeSets = await runSession(context, releases, releases.map(({ id }) => unabridged.get(id)));
|
|
1830
2050
|
for (const { changes, id } of changeSets) {
|
|
1831
2051
|
results.set(id, changes);
|
|
1832
2052
|
await saveChanges(context, unabridged.get(id), changes);
|
|
@@ -1839,8 +2059,9 @@ async function generateReleases(context, requests) {
|
|
|
1839
2059
|
const checkpointSchema = z.object({
|
|
1840
2060
|
baseRef: z.string(),
|
|
1841
2061
|
contentHash: z.string(),
|
|
2062
|
+
generated: z.boolean(),
|
|
1842
2063
|
head: z.string(),
|
|
1843
|
-
instructionVersion: z.
|
|
2064
|
+
instructionVersion: z.string(),
|
|
1844
2065
|
path: z.string()
|
|
1845
2066
|
});
|
|
1846
2067
|
const HEADING_PREFIX = /^#{1,3}\s+/v;
|
|
@@ -1849,27 +2070,43 @@ const ANCHOR_PUNCTUATION = /[^\p{L}\p{N}_\s\-]/gv;
|
|
|
1849
2070
|
const ANCHOR_SPACE = /\s/gv;
|
|
1850
2071
|
/**
|
|
1851
2072
|
* Remember what this run covered, so the next one can research only newer
|
|
1852
|
-
* commits.
|
|
2073
|
+
* commits, and whether the draft it leaves is entirely generated.
|
|
1853
2074
|
*/
|
|
1854
|
-
function recordCheckpoint(context, file, content, release) {
|
|
2075
|
+
function recordCheckpoint(context, file, content, release, generated) {
|
|
1855
2076
|
if (release === void 0 || !context.options.cache) return;
|
|
1856
2077
|
context.plan.checkpoints.push({
|
|
1857
2078
|
baseRef: release.previousRef ?? "initial",
|
|
1858
2079
|
contentHash: changelogHash(content),
|
|
2080
|
+
generated,
|
|
1859
2081
|
head: context.repository.head,
|
|
1860
2082
|
instructionVersion,
|
|
1861
2083
|
path: file
|
|
1862
2084
|
});
|
|
1863
2085
|
}
|
|
1864
2086
|
/**
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
2087
|
+
* Read what the last run recorded about a changelog, when the file still reads
|
|
2088
|
+
* as that run left it.
|
|
1867
2089
|
*/
|
|
1868
|
-
async function
|
|
2090
|
+
async function readCheckpoint(context, file, content) {
|
|
1869
2091
|
if (content === void 0 || !context.options.cache) return;
|
|
1870
2092
|
const result = checkpointSchema.safeParse(await readCache(path.join(context.repository.gitDirectory, "chagent", "checkpoints"), file));
|
|
1871
|
-
|
|
1872
|
-
|
|
2093
|
+
return result.success && result.data.contentHash === changelogHash(content) ? result.data : void 0;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Find the commit a release's entries already cover, when the checkpoint
|
|
2097
|
+
* describes the same release.
|
|
2098
|
+
*/
|
|
2099
|
+
async function incrementalBase(context, checkpoint, release) {
|
|
2100
|
+
if (checkpoint?.instructionVersion !== instructionVersion || checkpoint.baseRef !== (release.previousRef ?? "initial")) return;
|
|
2101
|
+
return await isAncestor(context.repository.root, checkpoint.head, release.ref) ? checkpoint.head : void 0;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Decide whether the draft a run leaves is entirely generated: one that was
|
|
2105
|
+
* before stays so, and a draft written from nothing or revised becomes so.
|
|
2106
|
+
*/
|
|
2107
|
+
function leavesGeneratedDraft(entries, previously) {
|
|
2108
|
+
const draft = entries.find((entry) => entry.release.version === "Unreleased");
|
|
2109
|
+
return previously || draft !== void 0 && (draft.existingBody === void 0 || draft.revise === true);
|
|
1873
2110
|
}
|
|
1874
2111
|
/** Select the releases newer than anything the changelog already records. */
|
|
1875
2112
|
function missingReleases(releases, content = "") {
|
|
@@ -1886,8 +2123,28 @@ function missingReleases(releases, content = "") {
|
|
|
1886
2123
|
function releaseContext(options, pkg) {
|
|
1887
2124
|
return pkg.directory === options.releaseDirectory ? options.release : void 0;
|
|
1888
2125
|
}
|
|
1889
|
-
/**
|
|
1890
|
-
async function
|
|
2126
|
+
/** Prepare the request for a release the changelog still needs. */
|
|
2127
|
+
async function prepareWork(context, pkg, release, details) {
|
|
2128
|
+
const { checkpoint, existingBody, mode } = details;
|
|
2129
|
+
const from = existingBody === void 0 ? void 0 : await incrementalBase(context, checkpoint, release);
|
|
2130
|
+
const revise = from !== void 0 && checkpoint?.generated === true;
|
|
2131
|
+
return {
|
|
2132
|
+
existingBody,
|
|
2133
|
+
release,
|
|
2134
|
+
request: await prepareRelease(context, pkg, release, {
|
|
2135
|
+
existingEntries: existingBody !== void 0 && summarizesPackages(context, pkg) ? withoutPackageLinks(existingBody) : existingBody,
|
|
2136
|
+
from,
|
|
2137
|
+
mode: revise ? "revise" : mode,
|
|
2138
|
+
styleReference: details.styleReference
|
|
2139
|
+
})
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Resolve a package's releases and prepare requests for those its changelog
|
|
2144
|
+
* still needs. Summaries cover those releases alone, or with `allSummaries`,
|
|
2145
|
+
* every release, so that a root changelog can link to any of them.
|
|
2146
|
+
*/
|
|
2147
|
+
async function preparePackage(context, pkg, allSummaries = false) {
|
|
1891
2148
|
const { repository } = context;
|
|
1892
2149
|
const file = await findChangelog(path.join(repository.root, pkg.directory));
|
|
1893
2150
|
const content = context.options.rebuild ? void 0 : file.content;
|
|
@@ -1895,15 +2152,17 @@ async function preparePackage(context, pkg, aggregateHistory = false, recordedAt
|
|
|
1895
2152
|
const releases = await resolveReleases(repository, pkg, context.options, releaseContext(context.options, pkg));
|
|
1896
2153
|
const missing = missingReleases(releases, content);
|
|
1897
2154
|
log.debug(`${pkg.name}: ${releases.length} releases, missing [${missing.map((release) => release.version).join(", ")}], changelog ${file.path}.`);
|
|
1898
|
-
const
|
|
2155
|
+
const parsed = parseChangelog(content ?? "");
|
|
2156
|
+
const draft = parsed.sections.find((section) => section.version === "Unreleased");
|
|
2157
|
+
const checkpoint = draft === void 0 ? void 0 : await readCheckpoint(context, file.path, content);
|
|
1899
2158
|
const newest = releases.findLast((release) => release.version !== "Unreleased");
|
|
1900
2159
|
const draftPromoted = draft !== void 0 && newest !== void 0 && missing.includes(newest);
|
|
1901
2160
|
const work = [];
|
|
1902
2161
|
const summaries = [];
|
|
1903
2162
|
for (const release of releases) {
|
|
1904
2163
|
const shouldWrite = missing.includes(release);
|
|
1905
|
-
if (!shouldWrite && !
|
|
1906
|
-
|
|
2164
|
+
if (!shouldWrite && !allSummaries) continue;
|
|
2165
|
+
summaries.push({
|
|
1907
2166
|
changelogPath: file.path,
|
|
1908
2167
|
changes: [],
|
|
1909
2168
|
date: release.date,
|
|
@@ -1912,41 +2171,49 @@ async function preparePackage(context, pkg, aggregateHistory = false, recordedAt
|
|
|
1912
2171
|
ref: release.ref,
|
|
1913
2172
|
tag: release.tag,
|
|
1914
2173
|
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
2174
|
});
|
|
2175
|
+
if (!shouldWrite) continue;
|
|
2176
|
+
const ownsDraft = draftPromoted ? release === newest : release.version === "Unreleased";
|
|
2177
|
+
work.push(await prepareWork(context, pkg, release, {
|
|
2178
|
+
checkpoint,
|
|
2179
|
+
existingBody: ownsDraft ? draft?.body : void 0,
|
|
2180
|
+
mode,
|
|
2181
|
+
styleReference: styleReference(parsed)
|
|
2182
|
+
}));
|
|
1927
2183
|
}
|
|
2184
|
+
const exception = await hasNodeException(repository, pkg, releases);
|
|
1928
2185
|
return {
|
|
1929
2186
|
content,
|
|
1930
|
-
|
|
2187
|
+
draftGenerated: checkpoint?.generated === true,
|
|
2188
|
+
exception,
|
|
1931
2189
|
file,
|
|
2190
|
+
pkg,
|
|
1932
2191
|
releases,
|
|
1933
2192
|
summaries,
|
|
1934
2193
|
work
|
|
1935
2194
|
};
|
|
1936
2195
|
}
|
|
1937
|
-
/**
|
|
1938
|
-
|
|
2196
|
+
/**
|
|
2197
|
+
* Write a package's edit into the plan and complete its summaries. A root
|
|
2198
|
+
* summary passes `linksFor` to end each section it writes with links to the
|
|
2199
|
+
* package releases that section covers.
|
|
2200
|
+
*/
|
|
2201
|
+
function renderPackage(context, prepared, results, linksFor) {
|
|
1939
2202
|
const { plan, repository } = context;
|
|
1940
2203
|
const { content, exception, file, releases, summaries, work } = prepared;
|
|
1941
2204
|
const entries = [];
|
|
1942
2205
|
const changesByVersion = /* @__PURE__ */ new Map();
|
|
1943
|
-
for (const { existingBody, release, request
|
|
2206
|
+
for (const { existingBody, release, request } of work) {
|
|
1944
2207
|
const changes = results.get(request.id) ?? [];
|
|
2208
|
+
const links = linksFor?.(release);
|
|
2209
|
+
const revise = request.mode === "revise" && results.has(request.id);
|
|
1945
2210
|
changesByVersion.set(release.version, changes);
|
|
1946
|
-
if (
|
|
2211
|
+
if (revise || release.version !== "Unreleased" || changes.length > 0 || links !== void 0 && packageLinksDiffer(existingBody, links)) entries.push({
|
|
1947
2212
|
changes,
|
|
1948
2213
|
existingBody,
|
|
1949
|
-
|
|
2214
|
+
links,
|
|
2215
|
+
release,
|
|
2216
|
+
revise
|
|
1950
2217
|
});
|
|
1951
2218
|
}
|
|
1952
2219
|
const after = updateMarkdown(content, entries, exception, repository.github);
|
|
@@ -1955,7 +2222,7 @@ function renderPackage(context, prepared, results) {
|
|
|
1955
2222
|
before: file.content,
|
|
1956
2223
|
path: file.path
|
|
1957
2224
|
});
|
|
1958
|
-
recordCheckpoint(context, file.path, after, releases.at(-1));
|
|
2225
|
+
recordCheckpoint(context, file.path, after, releases.at(-1), leavesGeneratedDraft(entries, prepared.draftGenerated));
|
|
1959
2226
|
const { sections } = parseChangelog(after);
|
|
1960
2227
|
return summaries.map((summary) => ({
|
|
1961
2228
|
...summary,
|
|
@@ -1984,74 +2251,40 @@ function missingPackageSummaries(summaries, content = "") {
|
|
|
1984
2251
|
}
|
|
1985
2252
|
|
|
1986
2253
|
// src/lib/workspace.ts
|
|
2254
|
+
/** Link to a package's release section, when its changelog has one. */
|
|
1987
2255
|
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) {
|
|
2256
|
+
if (summary.anchor === void 0) return [];
|
|
2257
|
+
const target = `${path.relative(repository.root, summary.changelogPath).split(path.sep).join("/")}#${summary.anchor}`;
|
|
2258
|
+
return [{
|
|
2259
|
+
label: `${summary.packageName} ${summary.version}`,
|
|
2260
|
+
url: repository.github === void 0 ? target : `https://github.com/${repository.github}/blob/HEAD/${target}`
|
|
2261
|
+
}];
|
|
2262
|
+
}
|
|
2263
|
+
/** Name the root's summary of a package release. */
|
|
2264
|
+
function summaryId(summary) {
|
|
2265
|
+
return `.@${summary.packageName}@${summary.version}`;
|
|
2266
|
+
}
|
|
2267
|
+
async function prepareIndependent(context, packages) {
|
|
2049
2268
|
const { repository } = context;
|
|
2050
2269
|
const file = await findChangelog(repository.root);
|
|
2051
2270
|
const content = context.options.rebuild ? void 0 : file.content;
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2271
|
+
const parsed = parseChangelog(content ?? "");
|
|
2272
|
+
const details = {
|
|
2273
|
+
mode: content === void 0 ? "initialize" : "update",
|
|
2274
|
+
styleReference: styleReference(parsed)
|
|
2275
|
+
};
|
|
2276
|
+
const releases = [];
|
|
2277
|
+
for (const item of packages) for (const summary of missingPackageSummaries(item.summaries, content)) {
|
|
2278
|
+
const release = item.releases.find((candidate) => candidate.version === summary.version);
|
|
2279
|
+
const request = await prepareRelease(context, item.pkg, release, details);
|
|
2280
|
+
releases.push({
|
|
2281
|
+
...request,
|
|
2282
|
+
evidence: request.evidence.filter((evidence) => evidence.kind === "range"),
|
|
2283
|
+
id: summaryId(summary),
|
|
2284
|
+
scope: "repository"
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
const releaseRefs = new Set(packages.flatMap((item) => item.summaries).filter((summary) => summary.version !== "Unreleased").map((summary) => summary.ref));
|
|
2055
2288
|
const release = {
|
|
2056
2289
|
previousRef: (await git(repository.root, [
|
|
2057
2290
|
"rev-list",
|
|
@@ -2061,44 +2294,54 @@ async function prepareIndependent(context, summaries) {
|
|
|
2061
2294
|
ref: repository.head,
|
|
2062
2295
|
version: "Unreleased"
|
|
2063
2296
|
};
|
|
2064
|
-
const draft =
|
|
2065
|
-
const
|
|
2297
|
+
const draft = parsed.sections.find((section) => section.version === "Unreleased")?.body;
|
|
2298
|
+
const checkpoint = await readCheckpoint(context, file.path, content);
|
|
2299
|
+
const from = await incrementalBase(context, checkpoint, release);
|
|
2300
|
+
const draftGenerated = checkpoint?.generated === true;
|
|
2066
2301
|
return {
|
|
2067
2302
|
content,
|
|
2303
|
+
draft,
|
|
2304
|
+
draftGenerated,
|
|
2068
2305
|
file,
|
|
2069
|
-
|
|
2070
|
-
|
|
2306
|
+
releases,
|
|
2307
|
+
unreleased: {
|
|
2308
|
+
release,
|
|
2309
|
+
request: await prepareRelease(context, repository.rootPackage, release, {
|
|
2310
|
+
...details,
|
|
2311
|
+
existingEntries: draft === void 0 ? void 0 : withoutPackageLinks(draft),
|
|
2312
|
+
from,
|
|
2313
|
+
mode: draft !== void 0 && from !== void 0 && draftGenerated ? "revise" : details.mode,
|
|
2314
|
+
onlyRepositoryFiles: true
|
|
2315
|
+
})
|
|
2316
|
+
}
|
|
2071
2317
|
};
|
|
2072
2318
|
}
|
|
2073
|
-
|
|
2319
|
+
/**
|
|
2320
|
+
* Write a section for each package release the root does not record yet, and
|
|
2321
|
+
* keep the draft's links to packages with unreleased changes current.
|
|
2322
|
+
*/
|
|
2323
|
+
function renderIndependent(context, prepared, results, summaries, exception) {
|
|
2074
2324
|
const { plan, repository } = context;
|
|
2075
|
-
const { file } = prepared;
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
const label = `${summary.packageName}@${summary.version}`;
|
|
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,
|
|
2325
|
+
const { content, draft, file, unreleased } = prepared;
|
|
2326
|
+
const entries = Array.from(missingPackageSummaries(summaries, content), (summary) => ({
|
|
2327
|
+
changes: results.get(summaryId(summary)) ?? [],
|
|
2328
|
+
links: packageLink(repository, summary),
|
|
2098
2329
|
release: {
|
|
2099
|
-
|
|
2100
|
-
|
|
2330
|
+
date: summary.date,
|
|
2331
|
+
ref: summary.ref,
|
|
2332
|
+
tag: summary.tag,
|
|
2333
|
+
version: `${summary.packageName}@${summary.version}`
|
|
2101
2334
|
}
|
|
2335
|
+
}));
|
|
2336
|
+
const changes = results.get(unreleased.request.id) ?? [];
|
|
2337
|
+
const links = summaries.filter((summary) => summary.version === "Unreleased").flatMap((summary) => packageLink(repository, summary));
|
|
2338
|
+
const revise = unreleased.request.mode === "revise" && results.has(unreleased.request.id);
|
|
2339
|
+
if (revise || changes.length > 0 || packageLinksDiffer(draft, links)) entries.push({
|
|
2340
|
+
changes,
|
|
2341
|
+
existingBody: draft,
|
|
2342
|
+
links,
|
|
2343
|
+
release: unreleased.release,
|
|
2344
|
+
revise
|
|
2102
2345
|
});
|
|
2103
2346
|
const after = updateMarkdown(content, entries, exception, repository.github);
|
|
2104
2347
|
if (after !== file.content) plan.files.push({
|
|
@@ -2106,7 +2349,7 @@ function aggregateIndependent(context, prepared, results, summaries, exception)
|
|
|
2106
2349
|
before: file.content,
|
|
2107
2350
|
path: file.path
|
|
2108
2351
|
});
|
|
2109
|
-
recordCheckpoint(context, file.path, after, prepared.
|
|
2352
|
+
recordCheckpoint(context, file.path, after, unreleased.release, leavesGeneratedDraft(entries, prepared.draftGenerated));
|
|
2110
2353
|
}
|
|
2111
2354
|
/**
|
|
2112
2355
|
* Find package releases whose own section records no notable changes, so a
|
|
@@ -2120,18 +2363,6 @@ function quietReleases(plan, packages) {
|
|
|
2120
2363
|
}
|
|
2121
2364
|
return quiet;
|
|
2122
2365
|
}
|
|
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
2366
|
/** Generate package changelogs and the root changelog that summarizes them. */
|
|
2136
2367
|
async function generateWorkspace(context) {
|
|
2137
2368
|
const { options, plan, repository } = context;
|
|
@@ -2139,21 +2370,24 @@ async function generateWorkspace(context) {
|
|
|
2139
2370
|
const shared = rootReleases.some((release) => release.version !== "Unreleased");
|
|
2140
2371
|
const rootFile = await findChangelog(repository.root);
|
|
2141
2372
|
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);
|
|
2373
|
+
const linksHistory = !shared || missingReleases(rootReleases, rootContent).some((release) => release.version !== "Unreleased");
|
|
2145
2374
|
const packages = [];
|
|
2146
|
-
for (const pkg of describedPackages(repository)) packages.push(await preparePackage(context, pkg,
|
|
2375
|
+
for (const pkg of describedPackages(repository)) packages.push(await preparePackage(context, pkg, linksHistory));
|
|
2147
2376
|
const root = shared ? await preparePackage(context, repository.rootPackage) : void 0;
|
|
2148
|
-
const independent = shared ? void 0 : await prepareIndependent(context, packages
|
|
2377
|
+
const independent = shared ? void 0 : await prepareIndependent(context, packages);
|
|
2149
2378
|
const requests = [...packages, ...root === void 0 ? [] : [root]].flatMap((pkg) => pkg.work.map((item) => item.request));
|
|
2150
|
-
if (independent !== void 0) requests.push(independent.request);
|
|
2379
|
+
if (independent !== void 0) requests.push(...independent.releases, independent.unreleased.request);
|
|
2151
2380
|
const results = await generateReleases(context, requests);
|
|
2152
2381
|
const summaries = packages.flatMap((pkg) => renderPackage(context, pkg, results));
|
|
2153
2382
|
plan.releases.push(...summaries);
|
|
2154
2383
|
const exception = packages.some((pkg) => pkg.exception);
|
|
2155
|
-
if (root !== void 0)
|
|
2156
|
-
|
|
2384
|
+
if (root !== void 0) {
|
|
2385
|
+
const quiet = quietReleases(plan, packages);
|
|
2386
|
+
renderPackage(context, {
|
|
2387
|
+
...root,
|
|
2388
|
+
exception: exception || root.exception
|
|
2389
|
+
}, results, (release) => summaries.filter((summary) => summary.version === release.version && summary.ref === release.ref && !quiet.has(`${summary.changelogPath}@${summary.version}`)).flatMap((summary) => packageLink(repository, summary)));
|
|
2390
|
+
} else if (independent !== void 0) renderIndependent(context, independent, results, summaries, exception);
|
|
2157
2391
|
}
|
|
2158
2392
|
|
|
2159
2393
|
// src/lib/generate.ts
|
|
@@ -2379,4 +2613,4 @@ async function reviewChangelog(plan, options = {}) {
|
|
|
2379
2613
|
}
|
|
2380
2614
|
}
|
|
2381
2615
|
|
|
2382
|
-
export {
|
|
2616
|
+
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 };
|