feat(config): lens sections - a repository's own text appended to a shipped charter

A lens has one charter, the one this pipeline ships; what a repository
knows that is not general to the lens - its platform and layers, the
record it keeps its decisions in, what its users already have, the
calibration entries that are its own - goes in a file the config names
for the lens (lenses.<name>.section), and Charter appends it after one
blank line. The charter sha in every reuse key covers the combined text.

The config refuses a path outside the repository - absolute, or
climbing out with `..` - by the check slots.dir had, now one both
settings share; a file missing or one the run could not use (a dangling
link, unreadable, not UTF-8) is a problem too, checked through the
reader Charter uses (Prompt::ReadSection), so a section is never
dropped in silence, and a name that is no lens is the unknown key it
always was. Nothing lists a directory, so what lies beside the files is
no concern of the config's: a directory listed would have to tolerate or
refuse every foreign entry. The run logs which charters got one.

The better-solution charter gains a consumer's general entry
(2026-10-31: a mechanism reworked a second time in one submission is the
signal to derive the fix at every layer the data crosses). The procedure
names the section wherever it names the charter: among the config's keys,
in the lens inputs, in the reuse rule (an edit to either re-runs the lens)
and in the feedback loop, which routes a repository's own lesson to its
section and a general one to the shipped charter.
This commit is contained in:
Jorijn van der Graaf 2026-11-01 01:30:22 +01:00
commit 6cf15919db
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
16 changed files with 1235 additions and 524 deletions

View file

@ -122,9 +122,24 @@ namespace Crafter::Review::Prompt {
}
}
namespace {
std::map<std::string, std::filesystem::path>& Sections() {
static std::map<std::string, std::filesystem::path> files;
return files;
}
}
void SetLensSections(std::map<std::string, std::filesystem::path> files) {
Sections() = std::move(files);
}
std::filesystem::path LensFile(std::string_view name) {
return LensDir() / std::string(name);
}
std::string SystemText(std::string_view mode) {
if (mode == "reply" || mode == "series") return Dyn::Replace(Dyn::ReadText(LensDir() / "_common.md"), "{{ARTIFACT_KIND}}", mode == "reply" ? "mailing-list reply" : "patch series");
return Dyn::Replace(Dyn::ReadText(LensDir() / "_common-repo.md"), "{{ARTIFACT_KIND}}", "commit range");
if (mode == "reply" || mode == "series") return Dyn::Replace(Dyn::ReadText(LensFile("_common.md")), "{{ARTIFACT_KIND}}", mode == "reply" ? "mailing-list reply" : "patch series");
return Dyn::Replace(Dyn::ReadText(LensFile("_common-repo.md")), "{{ARTIFACT_KIND}}", "commit range");
}
namespace {
@ -166,8 +181,43 @@ namespace Crafter::Review::Prompt {
Dyn::Join(inputs, "\n"), hash, ArtifactLines(lines), units.size(), InventoryLines(units), refDigest);
}
Section ReadSection(const std::filesystem::path& file) {
Section out;
std::error_code error;
const std::filesystem::file_status entry = std::filesystem::symlink_status(file, error);
if (error && error != std::errc::no_such_file_or_directory && error != std::errc::not_a_directory) {
out.present = true;
out.problem = std::format("cannot be read ({})", error.message());
return out;
}
if (!std::filesystem::exists(entry)) return out;
out.present = true;
if (!std::filesystem::is_regular_file(std::filesystem::status(file, error))) {
out.problem = std::format("cannot be read ({})", error ? error.message() : std::string("not a regular file"));
return out;
}
try {
out.text = Dyn::ReadText(file);
} catch (const Dyn::Error& failure) {
out.problem = failure.Kind() == "UnicodeDecodeError" ? std::string("is not UTF-8") : std::string("cannot be read");
}
return out;
}
std::string Charter(std::string_view lensFile) {
return Dyn::ReadText(LensDir() / std::string(lensFile));
std::string text = Dyn::ReadText(LensFile(lensFile));
if (const auto own = Sections().find(std::string(lensFile)); own != Sections().end()) {
// Through the reader the config checks it with: a section the
// config reported never reaches a run, and none is dropped here.
const Section section = ReadSection(own->second);
if (!section.problem.empty() || !section.present) {
throw Dyn::Error("ValueError", std::format("lenses.{}.section: {} {}", Plan::LensName(lensFile), own->second.string(),
section.present ? section.problem : std::string("does not exist")));
}
while (text.ends_with('\n')) text.pop_back();
text += std::format("\n\n{}", section.text);
}
return text;
}
std::string RunLayer(std::string_view artifactRel, std::string_view base, std::string_view date, std::span<const std::string> trees, std::string_view hash,