Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ missing historical endpoint. Snapshot times can differ from the requested window
marked approximate/incomplete rather than asserted as exact daily totals. GitHub title enrichment
preserves event-time status; a later merge cannot turn an earlier close into a merge event.

The owner still reviews every draft. The local LLM fact-check is best effort: if it fails, the draft
is retained and the failure is logged. GitHub feeds/search and Discord gateway observations are
The owner still reviews every draft. If the enabled local LLM fact-check fails, the generated prose is replaced with a
source-based activity list and the approval message explains that the factual review failed.
Supporter-update creation stops when its factual review fails; retry after the model is available. GitHub feeds/search and Discord gateway observations are
bounded sources, not a complete audit of all work. This does not establish inactivity outside the
recorded evidence. No new environment settings are required for these measurement rules.
19 changes: 11 additions & 8 deletions src/Newsletter/Writer.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,20 @@ public function write(Window $window, array $reports, array $previous = []): Pro
->then(function () use ($window, $reports, &$notes, $previous): PromiseInterface {
return $this->compose($window, $reports, $notes, $previous);
})
->then(function (array $composed) use (&$notes, $previous, $reports): PromiseInterface {
->then(function (array $composed) use (&$notes, $previous, $reports, $window): PromiseInterface {
if ($composed['fallback'] !== null || ! $this->factCheck || $notes === []) {
return resolve($composed);
}

return $this->review($composed['draft'], $notes, $previous, $reports)
->then(static fn(Draft $draft): array => ['draft' => $draft, 'fallback' => null]);
->then(
static fn(Draft $draft): array => ['draft' => $draft, 'fallback' => null],
function (\Throwable $error) use ($window, $reports, &$notes): array {
$this->logger->warning('Fact-check failed; using the source-based fallback: ' . $error->getMessage());
return ['draft' => self::fallback($window, $reports, $notes, $this->author),
'fallback' => 'The factual review could not be completed. This draft uses the recorded source facts instead.'];
},
);
})
->then(function (array $composed) use (&$notes): array {
return ['draft' => $composed['draft'], 'notes' => $notes, 'fallback' => $composed['fallback']];
Expand Down Expand Up @@ -203,7 +210,7 @@ function (\Throwable $e) use ($window, $reports, $notes): array {
/**
* Fact-checks $draft against original reports (saved notes for legacy editions)
* and returns a corrected draft. Any
* failure keeps the original: a missed check is better than a lost draft.
* failure rejects; callers must explicitly choose a source-based fallback or retry.
*
* @param array<string, string> $notes
* @param list<array<string, mixed>> $previous Published editions, newest first.
Expand Down Expand Up @@ -239,11 +246,7 @@ public function review(Draft $draft, array $notes, array $previous = [], array $
author's voice; do not pad the corrected draft to its original length or publish raw error diagnostics.
Reply with ONLY the corrected newsletter, in exactly the same Markdown format.
PROMPT],
], 0.1, false)->then(null, function (\Throwable $e) use ($draft): Draft {
$this->logger->warning("Fact-check pass failed, keeping the draft: {$e->getMessage()}");

return $draft;
});
], 0.1, false);
}

/**
Expand Down
20 changes: 17 additions & 3 deletions tests/WriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,25 @@ public function testQuietDayNeedsNoModel(): void
$this->assertSame([], $this->prompts);
}

public function testFactCheckFailureKeepsTheDraft(): void
public function testFactCheckFailureFallsBackToOriginalSourcesAndWarnsTheOwner(): void
{
$writer = new Writer($this->fakeLlm(['notes', self::DRAFT, 'garbage']), 'Valithor');
$invented = '{"headline":"Released version 99","intro":"I released version 99 with every API supported.","sections":[],"signoff":""}';
foreach (['garbage', new \RuntimeException('private transport diagnostic')] as $failure) {
$writer = new Writer($this->fakeLlm(['Invented notes', $invented, $failure]), 'Valithor');
$written = self::settle($writer->write($this->window(), $this->reports()));
$text = $written['draft']->toMarkdown();
self::assertStringNotContainsString('version 99', $text);
self::assertStringContainsString('merged pull request #1400', $text);
self::assertStringContainsString('factual review could not be completed', $written['fallback']);
self::assertStringNotContainsString('private transport diagnostic', $written['fallback']);
}
}

$this->assertSame('Polls land in DiscordPHP', self::settle($writer->write($this->window(), $this->reports()))['draft']->headline);
public function testSupporterReviewFailureRejectsInsteadOfReturningUncheckedContent(): void
{
$writer = new Writer($this->fakeLlm([self::DRAFT, new \RuntimeException('Review unavailable')]), 'Valithor');
$this->expectExceptionMessage('Review unavailable');
self::settle($writer->supporterUpdate(Draft::fromLlmJson(self::DRAFT)));
}

public function testReviseSendsTheInstructionsAndCurrentDraft(): void
Expand Down
Loading