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
13 changes: 8 additions & 5 deletions .agents/skills/proof/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,14 @@ lists), one-hop related records, and an edge table. Body policy:
from these digests — use `proof get` for the payload. Citation bodies
(usually short) still excerpt normally.

Caps: 25 primary records, one hop, 50 edges, 64 KiB; hitting a cap sets
`complete: false` with named `cap_reasons` — narrow the query or page rather
than expecting more. If a `get` body alone exceeds the 64 KiB digest byte
cap, the digest fails closed with a byte-cap banner (it does **not** fake a
full body via the 600/12 excerpt).
Caps: 25 primary records, one hop, 50 edges, 64 KiB. Hitting a cap sets
`complete: false` with named `cap_reasons`. Page only when `page.has_more` is
true. Non-empty hard `cap_reasons` that paging cannot clear mean narrow the
query or fail closed. `primary_records` is a defensive in-process signal after
the CLI pre-slices to at most 25 primary records; `proof list` and
`proof records` do not emit it. If a `get` body alone exceeds the 64 KiB
digest byte cap, the digest fails closed with a byte-cap banner (it does **not**
fake a full body via the 600/12 excerpt).

Every read envelope carries `complete` and `cap_reasons`. Read it in this
order:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
id: cit-pr-254-grouped-review-22-aug--cpbe5anhpby3h625
effort: eff-proof-and-contributor-operating-system--ahhgtafvdhg4dfve
title: PR 254 grouped review 22 Aug
role: evidence
created_at: '2026-08-22T16:46:24.804Z'
---

https://github.com/FlatbreadLabs/flatbread/pull/254#pullrequestreview-5000573079
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
id: dec-address-pr-254-review-as-five-disjoint-file-grou--bx44enbv52ztnrnn
effort: eff-proof-and-contributor-operating-system--ahhgtafvdhg4dfve
title: Address PR 254 review as five disjoint file groups
state: accepted
created_at: '2026-08-22T16:46:40.750Z'
derives_from:
- fnd-pr-254-still-omitted-paging-only-has-more-map-an--hk8r9xfee39s64vc
---

Context: PR 254 already exposes complete and cap_reasons. The 22 Aug review asked to document that page.has_more is pagination-only and to lock two missing tests, plus optional refuse of hasMore without a cursor.

Choice: one follow-up branch with five exclusive file owners.

1. CHANGELOG Unreleased now says page.has_more is pagination-only; use cap_reasons and complete for hard caps. primary_records stays an in-process signal after the CLI slice.
2. Both reference.md copies (source plus skills:sync) now say page only when page.has_more; hard caps that paging cannot clear mean narrow or fail closed.
3. CLI completeness spawn now asserts a non-null next_cursor on page-only list, a null cursor on bytes, and summary names displayed_edges and pagination together on relations.
4. Digest unit now covers displayed_edges plus hasMore/nextCursor, and refuses hasMore without a cursor.
5. renderDigest treats pagination as present only when hasMore is true and nextCursor is a non-empty string. It does not OR the 25-record wall back into has_more.

Alternatives: amend PR 254 in place; skip optional refuse. We kept the refuse because docs already call a null cursor an error.

Reversal: revert this follow-up. Digest cache rebuilds on the next read.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
id: fnd-pr-254-still-omitted-paging-only-has-more-map-an--hk8r9xfee39s64vc
effort: eff-proof-and-contributor-operating-system--ahhgtafvdhg4dfve
title: PR 254 still omitted paging-only has_more map and two tests
kind: gap
created_at: '2026-08-22T16:46:30.481Z'
derives_from:
- eff-proof-and-contributor-operating-system--ahhgtafvdhg4dfve
cites:
- cit-pr-254-grouped-review-22-aug--cpbe5anhpby3h625
---

The 22 Aug grouping review of PR 254 listed four open 19 Aug notes and one optional harden.

1. CHANGELOG advertised complete and cap_reasons but not that page.has_more is pagination-only.
2. Both reference.md copies still said narrow or page when a hard cap hit. Paging cannot clear displayed_edges.
3. The CLI page-only spawn checked has_more but not next_cursor, and skipped a null cursor on bytes plus summary co-list on relations.
4. Digest unit cases never set hasMore with a hard cap, so summary pagination plus a hard reason was unproven.
5. Optional: renderDigest could still emit has_more true with a null cursor if DigestInput was mis-paired.

None of these said the feature was wrong. They asked to say the Load more rule out loud and lock it.
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
## Unreleased

- The DAG runner is now `@flatbread/oven` (`pnpm exec oven`); the memory package is now `@flatbread/proof` with the `flatbread proof` CLI.
- Proof read envelopes now expose `complete` and `cap_reasons`. Callers can
tell paging from the `primary_records`, `displayed_edges`, and `bytes` caps
without parsing the digest Markdown or `summary` text.
- Proof read envelopes now expose `complete` and `cap_reasons`. `page.has_more`
is pagination-only and no longer signals the 25-record wall; use
`cap_reasons` / `complete` for hard caps on `displayed_edges` and `bytes`.
The `primary_records` limit remains an in-process defensive signal because
the CLI read bridge slices to 25 records before rendering. Callers can tell
paging from hard caps without parsing the digest Markdown or `summary` text.
- `@flatbread/source-filesystem` reads a content directory that does not exist
as an empty collection instead of throwing `ENOENT`. Git cannot store an
empty directory, and a Proof write creates only the directory it writes, so
Expand Down
4 changes: 4 additions & 0 deletions packages/flatbread/src/cli/proof.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export default {
t.false(paged.complete);
t.deepEqual(paged.cap_reasons, []);
t.true(paged.page.has_more);
t.truthy(paged.page.next_cursor);

const cappedResult = await runCli(
cwd,
Expand All @@ -437,6 +438,7 @@ export default {
t.false(capped.complete);
t.deepEqual(capped.cap_reasons, ['bytes']);
t.false(capped.page.has_more);
t.is(capped.page.next_cursor, null);

const edgeResult = await runCli(
cwd,
Expand All @@ -453,6 +455,8 @@ export default {
t.deepEqual(edgeCapped.cap_reasons, ['displayed_edges']);
t.true(edgeCapped.page.has_more);
t.truthy(edgeCapped.page.next_cursor);
t.true(edgeCapped.summary.includes('displayed_edges'));
t.true(edgeCapped.summary.includes('pagination'));
}
);

Expand Down
13 changes: 8 additions & 5 deletions packages/proof/skills/proof/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,14 @@ lists), one-hop related records, and an edge table. Body policy:
from these digests — use `proof get` for the payload. Citation bodies
(usually short) still excerpt normally.

Caps: 25 primary records, one hop, 50 edges, 64 KiB; hitting a cap sets
`complete: false` with named `cap_reasons` — narrow the query or page rather
than expecting more. If a `get` body alone exceeds the 64 KiB digest byte
cap, the digest fails closed with a byte-cap banner (it does **not** fake a
full body via the 600/12 excerpt).
Caps: 25 primary records, one hop, 50 edges, 64 KiB. Hitting a cap sets
`complete: false` with named `cap_reasons`. Page only when `page.has_more` is
true. Non-empty hard `cap_reasons` that paging cannot clear mean narrow the
query or fail closed. `primary_records` is a defensive in-process signal after
the CLI pre-slices to at most 25 primary records; `proof list` and
`proof records` do not emit it. If a `get` body alone exceeds the 64 KiB
digest byte cap, the digest fails closed with a byte-cap banner (it does **not**
fake a full body via the 600/12 excerpt).

Every read envelope carries `complete` and `cap_reasons`. Read it in this
order:
Expand Down
67 changes: 67 additions & 0 deletions packages/proof/src/__tests__/digest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,73 @@ test('displayed-edge caps are machine readable', async (t) => {
t.true(result.summary.includes('incomplete: displayed_edges'));
});

test('renderDigest refuses hasMore without a next cursor', async (t) => {
const cacheRoot = await mkdtemp(join(tmpdir(), 'eg-digest-refuse-page-'));
const result = await renderDigest({
query: { type: 'listEfforts', page: { limit: 1 } },
queryHash: 'refuse-page',
generation: '4',
consistency: { mode: 'eventual' as const, min_generation: null },
cacheRoot,
edges: [],
records: [
{
id: 'eff-one--0123456789abcdef',
kind: 'effort' as const,
path: 'efforts/one.md',
frontmatter: { title: 'One' },
body_excerpt: '',
relations: {},
},
],
hasMore: true,
});
const digest = await readFile(result.artifact_path, 'utf8');
t.true(result.complete);
t.deepEqual(result.cap_reasons, []);
t.false(result.page.has_more);
t.is(result.page.next_cursor, null);
t.true(
digest.includes('primary: {"returned":1,"total_known":1,"has_more":false}')
);
t.true(result.summary.includes('complete'));
t.false(result.summary.includes('pagination'));
});

test('renderDigest reports displayed-edge caps with pagination', async (t) => {
const cacheRoot = await mkdtemp(join(tmpdir(), 'eg-digest-edge-page-cap-'));
const cursor = 'next-edge-page';
const result = await renderDigest({
query: { type: 'relations', effort_id: 'eff-one--0123456789abcdef' },
queryHash: 'edge-page-cap',
generation: '4',
consistency: { mode: 'eventual' as const, min_generation: null },
cacheRoot,
records: [
{
id: 'dec-one--0123456789abcdef',
kind: 'decision' as const,
path: 'decisions/one.md',
frontmatter: { title: 'One' },
body_excerpt: '',
relations: {},
},
],
edges: Array.from({ length: 51 }, (_, index) => ({
from_id: 'dec-one--0123456789abcdef',
relation: 'derives_from' as const,
to_id: `fnd-edge-page-${index}--0123456789abcdef`,
})),
hasMore: true,
nextCursor: cursor,
});
t.false(result.complete);
t.deepEqual(result.cap_reasons, ['displayed_edges']);
t.true(result.page.has_more);
t.is(result.page.next_cursor, cursor);
t.true(result.summary.includes('incomplete: displayed_edges, pagination'));
});

test('stacked byte and record caps survive the byte rebuild', async (t) => {
const cacheRoot = await mkdtemp(join(tmpdir(), 'eg-digest-stacked-caps-'));
const result = await renderDigest({
Expand Down
27 changes: 18 additions & 9 deletions packages/proof/src/digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ function digestCompleteness(
return { complete: !hasMore && capReasons.length === 0, capReasons };
}

function yamlHeader(input: DigestInput, state: DigestCompleteness) {
function yamlHeader(
input: DigestInput,
state: DigestCompleteness,
hasMore: boolean
): string {
const query = JSON.stringify(input.query);
return [
'---',
Expand All @@ -122,7 +126,7 @@ function yamlHeader(input: DigestInput, state: DigestCompleteness) {
`primary: ${JSON.stringify({
returned: Math.min(input.records.length, CAP_RECORDS),
total_known: input.totalKnown ?? input.records.length,
has_more: Boolean(input.hasMore),
has_more: hasMore,
})}`,
`complete: ${state.complete}`,
`caps: ${JSON.stringify({
Expand Down Expand Up @@ -259,6 +263,11 @@ async function durableWrite(path: string, bytes: Buffer): Promise<void> {

export async function renderDigest(input: DigestInput): Promise<ReadEnvelope> {
const primaryBodyMode: RecordBodyMode = input.fullBody ? 'full' : 'excerpt';
const nextCursor =
typeof input.nextCursor === 'string' && input.nextCursor.length > 0
? input.nextCursor
: null;
const hasMore = Boolean(input.hasMore) && nextCursor !== null;
const records = [...input.records].sort((a, b) =>
`${String(a.frontmatter.created_at ?? '')}\0${a.id}`.localeCompare(
`${String(b.frontmatter.created_at ?? '')}\0${b.id}`
Expand All @@ -275,7 +284,7 @@ export async function renderDigest(input: DigestInput): Promise<ReadEnvelope> {
const reasons: ReadCapReason[] = [];
if (records.length > CAP_RECORDS) reasons.push('primary_records');
if (input.edges.length > CAP_EDGES) reasons.push('displayed_edges');
let completeness = digestCompleteness(Boolean(input.hasMore), reasons);
let completeness = digestCompleteness(hasMore, reasons);
const checkpoints = input.checkpointLines?.length
? ['## Lineage checkpoints', ...input.checkpointLines, '']
: [];
Expand All @@ -287,7 +296,7 @@ export async function renderDigest(input: DigestInput): Promise<ReadEnvelope> {
const renderRelated = (record: ReadRecord) =>
renderRecord(record, { bodyMode: 'excerpt' });
let markdown = [
yamlHeader(input, completeness),
yamlHeader(input, completeness, hasMore),
...(input.anomaly ? [`> anomaly: ${input.anomaly}`, ''] : []),
'# Proof read',
'## Index',
Expand All @@ -308,7 +317,7 @@ export async function renderDigest(input: DigestInput): Promise<ReadEnvelope> {
].join('\n');
if (Buffer.byteLength(markdown) > CAP_BYTES) {
reasons.push('bytes');
completeness = digestCompleteness(Boolean(input.hasMore), reasons);
completeness = digestCompleteness(hasMore, reasons);
// Full-body digests must not silently fall back to the 600/12 excerpt.
// Prefer a visible byte-cap miss banner over a fake "full" body.
const overflowBodyMode: RecordBodyMode = input.fullBody
Expand All @@ -321,7 +330,7 @@ export async function renderDigest(input: DigestInput): Promise<ReadEnvelope> {
? 'body exceeded digest byte cap'
: input.anomaly;
const header = [
yamlHeader(input, completeness),
yamlHeader(input, completeness, hasMore),
...(anomaly ? [`> anomaly: ${anomaly}`, ''] : []),
'# Proof read',
'## Index',
Expand Down Expand Up @@ -383,7 +392,7 @@ export async function renderDigest(input: DigestInput): Promise<ReadEnvelope> {
visible,
completeness.complete,
completeness.capReasons,
Boolean(input.hasMore)
hasMore
),
artifact_path: path,
artifact_sha256: createHash('sha256').update(bytes).digest('hex'),
Expand All @@ -393,8 +402,8 @@ export async function renderDigest(input: DigestInput): Promise<ReadEnvelope> {
cap_reasons: completeness.capReasons,
page: {
returned: visible.length,
has_more: Boolean(input.hasMore),
next_cursor: input.hasMore ? input.nextCursor ?? null : null,
has_more: hasMore,
next_cursor: hasMore ? nextCursor : null,
},
hints: (
input.hints ?? ids.slice(0, 10).map((id) => `getRecord("${id}")`)
Expand Down
Loading