Skip to content

feat(effort-graph): add Citation + Blob with optional blob cites - #225

Merged
tonyketcham merged 2 commits into
mainfrom
toeknee/blob-collection-cites-1b07
Jul 26, 2026
Merged

tonyketcham merged 2 commits into
mainfrom
toeknee/blob-collection-cites-1b07

Conversation

@tonyketcham

Copy link
Copy Markdown
Collaborator

Summary

Implements the Blob collection issue from #224 by adding Citation as the homogeneous cites target (keeps Flatbread core refs strength) and Blob as an optional longform payload behind a Citation.

Model

Finding.cites → Citation (body may be a URL alone)
                  └─ blob? → Blob (optional longform)
  • WriteCitation / WriteBlob mutations (15 total)
  • Optional cites: Citation[] on all epistemic creates
  • Bounded digests omit Blob bodies; effort get zooms in
  • Dogfood Finding + accepted Decision; Issue resolved

Decision: dec-ship-citation-collection-with-optional-blob--fyga3x876n7rcnmn
Issue: iss-implement-blob-collection-and-crumb-graph-cites--g2c7m6j39we5xy3z (resolved)

Checklist

  • Doc comments / skill glossary + reference updated
  • Tests for Citation URL-only, optional blob, cites validation, Blob digest omission
  • No new console errors locally

Backwards compatible?

  • Additive collections/mutations/refs (cites → Citation). Consumers must use the updated effortGraphContent() preset (includes citations/ + blobs/).
Open in Web Open in Cursor 

Resolve the Blob collection issue by shipping Citation as the homogeneous
`cites` target (Flatbread refs-safe). Citation body alone is valid (e.g. a
URL); optional `blob` attaches longform payloads. Epistemic writes gain
optional `cites`; digests omit Blob bodies by default.

Decision: dec-ship-citation-collection-with-optional-blob--fyga3x876n7rcnmn
Issue: iss-implement-blob-collection-and-crumb-graph-cites--g2c7m6j39we5xy3z
Change-Id: Id73687f3c3224ca3eb77b4fc870815811ebd754d
Base automatically changed from toeknee/citeable-blob-payload-989d to main July 26, 2026 02:45
@tonyketcham

Copy link
Copy Markdown
Collaborator Author

@Mergifyio queue

@mergify

mergify Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • ✅ Entered queue — 2026-07-26 02:47 UTC · Rule: owner-bypass · triggered by @tonyketcham with the @mergifyio queue command
  • ✅ Checks passed · in-place
  • ✅ Merged — 2026-07-26 02:51 UTC · at 5754e360901123211909dd67e22049361bb99b20

This pull request spent 3 minutes 48 seconds in the queue, including 3 minutes 34 seconds running CI.

Required conditions to merge
  • author = tonyketcham
  • check-success = build (20.x, ubuntu-latest)
  • check-success = build (22.x, ubuntu-latest)
  • check-success = integration-nextjs (20.x, macos-latest)
  • check-success = integration-nextjs (20.x, ubuntu-latest)
  • check-success = integration-nextjs (20.x, windows-latest)
  • check-success = integration-nextjs (22.x, macos-latest)
  • check-success = integration-nextjs (22.x, ubuntu-latest)
  • check-success = integration-nextjs (22.x, windows-latest)
  • check-success = integration-sveltekit (20.x, macos-latest)
  • check-success = integration-sveltekit (20.x, ubuntu-latest)
  • check-success = integration-sveltekit (20.x, windows-latest)
  • check-success = integration-sveltekit (22.x, macos-latest)
  • check-success = integration-sveltekit (22.x, ubuntu-latest)
  • check-success = integration-sveltekit (22.x, windows-latest)
  • check-success = lint (20.x, ubuntu-latest)
  • check-success = lint (22.x, ubuntu-latest)
  • check-success = test (20.x, ubuntu-latest)
  • check-success = test (22.x, ubuntu-latest)

@tonyketcham
tonyketcham marked this pull request as ready for review July 26, 2026 02:47
@tonyketcham

Copy link
Copy Markdown
Collaborator Author

@Mergifyio queue

@mergify

mergify Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

queue

☑️ Command queue ignored because it is already running from a previous command.

@mergify mergify Bot added the queued label Jul 26, 2026
@tonyketcham
tonyketcham merged commit ccca907 into main Jul 26, 2026
20 checks passed
@tonyketcham
tonyketcham deleted the toeknee/blob-collection-cites-1b07 branch July 26, 2026 02:51
@mergify mergify Bot removed the queued label Jul 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review verdict

REQUEST_CHANGES — consensus HIGHs on silent cites drop for Citation/Blob writes, incomplete same-effort cite/blob validation vs read-path filtering, and missing CLI/live write→read lock-in for cit-/blb-.

Adversarial DAG (5 perspectives + judge) on 5754e36 vs main. Not dependency-only.

Blocking themes

  1. Silent field drop — Zod strips unknown keys on WriteCitation/WriteBlob; planner also deletes cites/edge fields. Callers get success with no cites persisted.
  2. Write vs read contract — assertCites / Citation.blob check kind only; other edges throw Different effort, while relations() filters cross-effort away (returned: 0).
  3. Coverage — CLI/live tests mostly mkdir citations/blobs; no write→read round-trip for cites/Citation.blob.
  4. Docs drift — SKILL.md still opens with six primitives / 13 mutations and a six-folder layout while later text says 15 mutations (opening lines outside this diff hunk — fix in follow-up edit).

Coverage plan (priority)

  1. schemas.test.ts — negative: WriteCitation/WriteBlob with cites → reject (.strict()), not strip
  2. planner.test.ts — negative: unknown/blb-/cross-effort cites; foreign/missing/cross-effort Citation.blob
  3. effort.test.ts — positive: WriteBlob → WriteCitation(blob) → WriteFinding(cites) → get/records/relations
  4. effort.test.ts — negative: invalid cites rejected at CLI
  5. liveServerEffortGraph.test.ts — positive: GraphQL cites { id } + Citation.blob { id } after mutations

Reviewer scoreboard

  • correctness-and-contracts: 5 findings, 3 gaps, signal:HIGH
  • test-coverage-robustness: 6 findings, 6 gaps, signal:HIGH
  • cli-and-runtime: 5 findings, 3 gaps, signal:HIGH
  • effort-graph-schema-and-cites: 5 findings, 1 gap, signal:HIGH
  • docs-and-positioning: 5 findings, 0 gaps, signal:HIGH
Open in Web View Automation 

Sent by Cursor Automation: Flatbread PR Review

Comment on lines +60 to +73
export const WriteCitationSchema = z.object({
type: z.literal('WriteCitation'),
...common,
effort: id,
/** Optional longform/payload target; body alone (e.g. a URL) is valid. */
blob: id.optional(),
role: z.string().min(1).optional(),
});
export const WriteBlobSchema = z.object({
type: z.literal('WriteBlob'),
...common,
effort: id,
kind: z.string().min(1).optional(),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH (consensus) — WriteCitationSchema / WriteBlobSchema are plain z.objects, so unknown keys like cites / derives_from are stripped by EffortGraphMutationSchema.parse (CLI path) with no error.

Minimal fix: .strict() (or explicit reject) on these schemas; add negative parse tests for unknown edge fields and malformed blob / role / blob: null.

Comment on lines +35 to +49
function assertCites(
get: (
id: string
) => NonNullable<ReturnType<EffortGraphSnapshot['getRecord']>>,
cites: string[] | undefined
): void {
for (const citeId of cites ?? []) {
const target = get(citeId);
if (target.kind !== 'citation')
throw new EffortGraphValidationError(
`cites must target a Citation, got ${target.kind} (${citeId})`
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH (consensus) — assertCites only requires target.kind === 'citation'. ResolveIssue / SetRiskState throw Different effort for cross-effort refs, but cites do not — then relations() filters them out (effort !== effortId → empty).

Minimal fix: Same-effort check here (mirror those mutations); add negatives for unknown cit id, cites: [blb-…], and cross-effort cit with exact error strings.

Comment on lines +143 to +151
if (kind === 'citation' && raw.blob !== undefined) {
const blob = get(raw.blob);
if (blob.kind !== 'blob')
throw new EffortGraphValidationError(
`Citation.blob must target a Blob, got ${blob.kind}`
);
}
if (EPISTEMIC_CREATE.has(kind) || kind === 'effort')
assertCites(get, raw.cites);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH (consensus) — Citation.blob validates kind only. Cross-effort blob attachment is allowed at write time; wrong-kind / missing / effort-mismatch messages are under-tested.

Minimal fix: Reject when blob.frontmatter.effort !== citation.effort; planner tests for foreign kind, unknown blob, and effort mismatch.

Comment on lines +162 to +167
if (kind === 'blob' || kind === 'citation') {
delete fm.cites;
delete fm.derives_from;
delete fm.supersedes;
delete fm.invalidates;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — On citation/blob creates, unsupported edge fields are silently deleted. Combined with Zod strip (or writer.mutate(input: any) bypassing Zod), callers can pass cites and get a successful write with nothing persisted.

Minimal fix: Throw EffortGraphValidationError if these fields are present instead of deleting them.

Comment on lines 23 to +26
export const CreateEffortSchema = z.object({
type: z.literal('CreateEffort'),
...common,
...cites,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — CreateEffort accepts cites[], but Citations need an existing effort. Same-effort effort-level cites cannot be set at create time, and there is no post-create mutation to add them (cross-effort-only in practice).

Minimal fix: Remove cites from CreateEffort, or add an update path; until then docs must not imply same-effort effort-level cites work at create.

Comment on lines +133 to +134
'citations',
'blobs',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH (consensus) — Tests mkdir citations/blobs but never assert WriteBlob → WriteCitation(blob) → WriteFinding(cites) → get / records / relations --relations cites.

Minimal fix: One serial happy-path round-trip plus one invalid-cites CLI reject.

Comment on lines +22 to +23
'citations',
'blobs',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — Live GraphQL path never asserts cites { id } / Citation.blob { id } after mutation (dirs only in this touch).

Minimal fix: Extend read-your-writes with WriteCitation + cited finding and a GraphQL query on cites and blob.

Comment on lines +551 to +552
'citation',
'blob',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH (consensus) — Default effortRecords kinds now include citation/blob, but without CLI/live write→read lock-in that cites survive GraphQL→toRecord→digest/relations. Invalid --kinds failing open to empty results is a related gap.

Minimal fix: Round-trip coverage (see review body); validate --kinds against PrimitiveKind and raise EFFORT_GRAPH_INVALID_ARGUMENT on typos.

@@ -49,7 +49,7 @@ The write journal is `<root>/.journal/`; read digests cache under

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — Prerequisites still list only six record folders (efforts…risks). Opening copy above the hunk still says “Six primitives” / “13 typed mutations” while this file later documents 15 mutations and Citation/Blob.

Minimal fix: Extend this path list to include citations/blobs, and update the opening paragraph + YAML description to eight collections / 15 mutations (sync packages/effort-graph/skills/… copy).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants