fix(agents): what the review of dependency notes found

A review of the feature turned up the following, fixed here.

Writing. AGENTS.md was replaced through a temporary renamed over it,
which swaps in another file: a read-only AGENTS.md was rewritten
anyway, a hard link such as a CLAUDE.md beside it kept the old text,
ACLs and ownership were dropped, and without an fsync a power cut could
still leave it empty. The temporary was named after the PID, which
every Flatpak instance shares as PID 2. The file is now written in
place, from the first byte that differs, then truncated and synced,
under an flock on the file itself. A write that fails partway is undone
where the disk allows; with the section last, where it is appended,
only the section can be damaged. A read-only file is opened for writing
only when something would change, so it is refused then and silent
otherwise.

Reading. libc++'s filebuf reports a failed read as the end of the file,
so an I/O error came back as a short file and the section was written
over what was left. Reads go through read(2).

Code fences. ```make``` was taken for a fence, and a fence in a list
item was missed while its indented closer opened one, so markers a user
quoted in a code block could be taken for the section, or the real one
hidden and appended again. Backtick info strings, block quotes and list
items are now followed, and a block ends with its container.

Warnings with nothing to write. With no notes, nothing short of a
well-formed section is reported, so an AGENTS.md that ends in an open
code block, or a project set to None, no longer warns on every build.
An unclosed fence after a section no longer stops it being rewritten
in place.

Labels. The `from` line carried the git URL, credentials and all, or a
path relative to the consumer that changed with the machine, the cache
location and --local. It is now the repository's or the directory's
name. Notes are deduplicated by repository rather than by clone, so one
library pinned two ways is described once, as the nearer spec has it.

The walk. It no longer looks at the project's tests: a plain build
never did, and a test's dependency can point at the Configuration the
project file returned by value. It goes ring by ring over projects
rather than configurations, so what the project names itself comes
first even with the library/executable split. The project's own
AGENTS.consumer.md is excluded by path, also when a dependency's
project file sits beside the project's.

Removing a section dropped a blank line even from between two
paragraphs, joining them; only a section that ends the file takes it
now. A CRLF file gets a CRLF section.

A note was checked on its raw bytes and again, under other whitespace
rules, on the trimmed ones, so it could pass the first check and fail
the merge, which then dropped every note. It is normalised once and
checked on exactly what is merged.

`test --list` only lists, and no longer writes AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 05:02:28 +01:00
commit 7815856d60
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
5 changed files with 446 additions and 194 deletions

View file

@ -20,7 +20,7 @@ export namespace Crafter {
// The projects this one builds against, and the projects those build
// against, all the way down.
Transitive,
// Only the projects this one's own configurations, tests and
// Only the projects this one's own configurations and
// `#pragma crafter dependency` lines name.
Direct,
// None. A section an earlier build wrote is removed.
@ -29,28 +29,33 @@ export namespace Crafter {
// One dependency's AGENTS.consumer.md, ready to merge.
struct DependencyNote {
// Where it came from, written into the comment above it: the git URL,
// or for a local project the file's path relative to the consumer.
std::string source;
// What the comment above the note calls the dependency: its
// repository's name, or a local project's directory name.
std::string name;
// The file's content, without leading blank lines or trailing
// whitespace.
// whitespace. Lines end in LF; the merge writes the line ending the
// document uses.
std::string text;
};
// `agentsMd` with its dependency notes section holding `notes`, in order.
// A file without a section gets one appended after a blank line; one
// with a section has it rewritten in place, wherever it was moved to;
// and empty `notes` remove it together with that blank line, so a file
// that only ever gained the section gets its own bytes back.
// A document without a section gets one appended after a blank line; one
// with a section has it rewritten in place, wherever it was moved to; and
// empty `notes` remove it. Removing a section that ends the document takes
// the blank line in front of it too, so a document that ended in a newline
// before it gained the section gets its own bytes back. The section uses
// the document's line ending, so a CRLF file stays CRLF.
//
// The two marker lines count only as whole lines outside fenced code
// blocks, which is what lets a document quote them. Throws, naming the
// line, when the markers are unbalanced or the document ends inside a
// code block: guessing which part of someone's file is generated would
// overwrite their text, and missing the section would add a second one
// on every build. Also throws for a note that would break the section
// when it is read back: one with a marker line of its own, or a code
// block it never closes.
// line, when there are notes to write and the markers are unbalanced, or
// when the section would have to be appended to a document that ends
// inside a code block: guessing which part of someone's file is generated
// would overwrite their text, and missing the section would add another on
// every build. With no notes it never throws, since leaving the document
// alone loses nothing. Also throws for a note that would break the section
// when it is read back: one with a marker line of its own, or a code block
// it never closes.
std::string MergeDependencyNotes(std::string_view agentsMd, std::span<const DependencyNote> notes);
// Refresh the dependency notes section of the AGENTS.md beside
@ -58,8 +63,10 @@ export namespace Crafter {
// project.dependencyNotes selects. `project` is the Configuration that
// file returned, after LoadProject: loading is what found the
// dependencies. Does nothing when there is no AGENTS.md, which is never
// created. Writes only when the text changes, and returns whether it
// did. An unreadable file, a failed write, malformed markers and a
// dependency's unusable notes are warnings: the notes never fail a build.
// created. Writes only when the text changes, in place, so hard links,
// ownership and a read-only AGENTS.md are respected; returns whether it
// wrote. A read or write that fails, a section that cannot be placed and
// a dependency's unusable notes are warnings: the notes never fail a
// build, and nothing is reported when nothing would have been written.
bool UpdateAgentsMd(const Configuration& project, const fs::path& projectFile);
}