Skip to content

Drop the per-statement COPY advice - #60

Open
asubbarao wants to merge 8 commits into
mainfrom
fix/drop-per-statement-copy
Open

asubbarao wants to merge 8 commits into
mainfrom
fix/drop-per-statement-copy

Conversation

@asubbarao

Copy link
Copy Markdown
Owner

Two lines told agents to make each dispatched statement write its own file and read the files back. The query already returns the rows; COPY around it is plain DuckDB. Both lines deleted:

  • skills/duckstack-review/SKILL.md check 1: flagged COPY around the outer dispatch.
  • skills/self-dispatch/references/compendium.md: sent each statement's rows to a file.

🤖 Generated with Claude Code

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao
asubbarao enabled auto-merge October 18, 2026 21:41
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

1 similar comment
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/agent-dispatch/SKILL.md:158 Check 1 / RULES.md 5; the 2026-10-17 rejection 'COPY (…) TO x.parquet then read it back → unnest the receipts'; SKILL.md How to run it, step 4 ('delete before adding'): This PR removes the advice to write each dispatched statement to its own file and read the files back, but this new line rewords that same advice for the verification block: the receipt only reports the write and the rows have to be read back from the agent-log, when they should come back as receipt columns.
    Rewrite: Rewrite the explanation as: 'The dispatched statement is the program under test, so both sides come back as receipt columns. To keep a record, wrap the outer query in the agent-log COPY.' The query becomes: WITH verification AS (...), sides AS (...), rendered AS (SELECT *, tera_render($tera${{ both_sides }}$tera$, s::JSON, autoescape := false) AS sql FROM sides AS s), dispatched AS (SELECT agent, session_id, skill, notes, log_dir, array_agg({sql: sql, receipt: selfdispatch(sql)}) AS receipts FROM rendered GROUP BY ALL) SELECT uuidv7() AS row_id, 'verification' AS type, agent, session_id, agent || '-' || session_id AS agent_signature, skill, r.sql AS query_was_ran, unnest(r.receipt), notes AS markdown_notes, now() AS ts FROM dispatched CROSS JOIN UNNEST(receipts) AS t(r). Anyone who wants the log row wraps that query in COPY (…) TO '<agent_log_dir>' (FORMAT parquet, PARTITION_BY (type, agent_signature), OVERWRITE_OR_IGNORE true, FILENAME_PATTERN '{uuid}').

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/agent-dispatch/SKILL.md:170 RULES.md 7 (declare everything in the first CTE); manifesto §1: Line 156 says the signature is declared once in the first CTE, but the query builds it in the final SELECT with concat_ws('-', agent, session_id), so the prose and the code disagree and the intent CTE does not hold every value.
    Rewrite: WITH verification AS ( -- 1. plain values, declared once
    SELECT [] AS raws, to_json(raws)::VARCHAR AS raws_json,
    '' AS agent, '' AS session_id, concat_ws('-', agent, session_id) AS agent_signature,
    '' AS skill, $n$$n$ AS notes
    ), ...
    -- and in the final SELECT project agent_signature as a plain column
  • skills/agent-dispatch/SKILL.md:172 RULES.md 5; compendium 'Results are receipt.*'; RULES.md 10 (lossless): The query takes the receipt apart by hand. It casts receipt.body::JSON[], unnests that into an untyped result_row JSON, and keeps only receipt.status, which drops the receipt's other fields such as its error. The rubric says results come back as columns via receipt.* and the body is never parsed.
    Rewrite: SELECT uuidv7() AS row_id, 'verification' AS type, agent, session_id, agent_signature, skill,
    both_sides AS query_was_ran, receipt.*, notes AS markdown_notes, now() AS ts
    FROM dispatched
    CROSS JOIN UNNEST(receipts) AS t(receipt);

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/duckstack-review/SKILL.md:49 RULES.md 11; SQL-MANIFESTO §5 ("derived state is a view"); CLAUDE.md ("derive state as views"): The new checklist question says "Objects as views, derived as tables". That reverses the rubric, where derived state is a view and a table needs a proposal first. Reviewers using this checklist would ask for derived tables.
    Rewrite: 9. Views first, nothing written to read back? Raw files, shellfs and hostfs output, and derived state are unmaterialized views; a table only after Alok's yes on its name, columns and why a view cannot do it (1–2 staging tables at most). No COPY to a file the next step reads; the receipts carry the rows (10-18). New macros only with his yes.
  • skills/agent-dispatch/SKILL.md:172 RULES.md 5; check 7 (compendium: "Results are receipt.*: never parse the body"): The added verification query casts receipt.body::JSON[] and unnests it into an opaque result_row JSON column. That parses the receipt body by hand when the receipt should already return the side, raw value and compared columns.
    Rewrite: SELECT uuidv7() AS row_id, 'verification' AS type, agent, session_id, concat_ws('-', agent, session_id) AS agent_signature,
    skill, both_sides AS query_was_ran, notes AS markdown_notes, now() AS ts, receipt.*
    FROM dispatched CROSS JOIN UNNEST(receipts) AS t(receipt);

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/duckstack-review/SKILL.md:39 RULES 17; compendium lines 5-6: Requiring "a template file called with template_path :=" makes every inline template a "no". That fails the owner's own example in this same file and the inline template in the verification query this PR adds, and it contradicts the compendium ("a named template when the statement has that shape, an inline template otherwise").
    Rewrite: 4. Is Tera only a constructor? One of the named templates via template_path := when the statement has that shape, an inline template otherwise; no {% for %} / {% if %}; a new template file only for a new shape. "tera is just to make code readable… a helper" (10-17).
  • skills/duckstack-review/SKILL.md:45 RULES 5; compendium line 46: The exception tells agents to read fields out of the receipt body, which brings back the field-name parsing RULES 5 forbids and that compendium line 46 bans ("never parse the body with … ->>"). It also contradicts the ->> ban on line 44 just above it.
    Rewrite: structure strings, columns := {…}, ->> ladders. Unknown shape stays raw in read_lines (10-17). Results are
    receipt.*; a receipt body is JSON text, so unnest it once as JSON[] and keep each element whole, never ->> its fields.
  • skills/duckstack-review/SKILL.md:50 RULES 11; SQL-MANIFESTO §5; CLAUDE.md 'derive state as views': "Objects as views, derived as tables" tells reviewers to want derived state as tables, but the manifesto says "derived state is a view" and RULES 11 says views come first and tables later.
    Rewrite: 9. Views first, nothing written to read back? Raw objects and derived state are views; a table only with his yes (CTAS replacement when it earns one). No COPY to a file the next step reads; the receipts carry the rows (10-18).
  • skills/agent-dispatch/SKILL.md:172 RULES 4, RULES 10; compendium line 47 (added in this PR): When a dispatch fails, receipt.body::JSON[] is not a JSON array, so the second CROSS JOIN UNNEST returns no rows and the failed verification disappears from the log; the final SELECT also keeps only receipt.status and drops the receipt's error and other fields.
    Rewrite: SELECT uuidv7() AS row_id, 'verification' AS type, agent, session_id, concat_ws('-', agent, session_id) AS agent_signature,
    skill, both_sides AS query_was_ran, receipt.*, result_row, notes AS markdown_notes, now() AS ts
    FROM dispatched
    CROSS JOIN UNNEST(receipts) AS t(receipt)
    CROSS JOIN UNNEST(coalesce(try_cast(receipt.body AS JSON[]), [to_json(receipt)])) AS r(result_row);

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • warn sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/duckstack-review/SKILL.md:44 RULES.md 5 (results come back as columns); checklist 6; compendium 'Results are receipt.*': The added exception tells reviewers to unnest the receipt body as JSON text and read its fields. That is the manual field-name parsing RULES.md 5 bans, it contradicts the ->> ban in the same item and in compendium.md line 46, and the PR gives no measured reason for it. A missing typed reply is a change to quackapi, not an exception in the checklist.
    Rewrite: 6. Does every reader bring its own schema? read_json / read_csv with their := options; no from_json structure strings, columns := {…}, ->> ladders. Unknown shape stays raw in read_lines (10-17). Results come back as columns: SELECT receipt.* FROM dispatched CROSS JOIN UNNEST(receipts) AS t(receipt). If a receipt body still arrives as JSON text, keep it whole as one column (lossless) and fix quackapi (asubbarao/quackapi) to return typed rows. Do not pick fields out of it.

Opus 5.5, duckstack review queue (duckstack-review skill)

asubbarao and others added 8 commits October 19, 2026 09:39
A self-dispatch query already returns its rows; an agent that wants a file writes COPY around it. The review check that flagged that, and the compendium line that sent each statement to its own file, are deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The by-hand run rewrites each finding on a branch, runs old and new on dev and compares the rows, deletes rules that teach the antipattern, and opens a PR. A finding left as a comment is not done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OPY wraps the outer query

Both sides come back as rows in the receipts. Rerun on dev 2026-10-18: four rows, Python's ValueError kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions from 737 of his messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… replies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mary, one call

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eld after the unnest (RULES.md 5)

The verification declares agent_signature in the first CTE, keeps receipt.* and each
returned row as result_row (a failed dispatch stays one row), and names the compared
fields with one ->> each. Checklist 4, 6 and 9 follow RULES.md 5, 11 and 17; the
compendium's receipt line matches RULES.md 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@asubbarao
asubbarao force-pushed the fix/drop-per-statement-copy branch from 51d9113 to 06bfc5d Compare October 19, 2026 16:43
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • existing sql_vague_cte_names in skills/duckstack-review/SKILL.md#2.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • existing sql_vague_cte_names in skills/duckstack-review/SKILL.md#5.sql: Name every CTE for what it holds (content_first_100, self_dispatch), not t / x / sd / tmp.
  • existing sql_dispatch_shape in skills/self-dispatch/references/compendium.md#1.sql: Self-dispatch has one shape: source rows, one generated statement per row, array_agg(selfdispatch(sql)), then CROSS JOIN UNNEST of the receipts.

duckstack review queue, rule gate (hooks/hook_checks.sql)

asubbarao added a commit that referenced this pull request Oct 19, 2026
…op or try

The inputs are rows in the first CTE; each renders a one-input Python statement that
is self-dispatched beside its duckdb_answer, and the receipts unnest back into columns.
An input the proposal raises on is its own failed receipt and counts as differs (the
old loop's NULL-vs-NULL compare marked raising inputs as agreeing). The example runs
on dev as written. Record: COPY the whole query, as in PR #60; nothing reads it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
asubbarao added a commit that referenced this pull request Oct 19, 2026
…cord

The body rows unnest once (coalesce(try_cast(body AS JSON[]), [NULL::JSON])), so a
failed dispatch stays as one row, and python_answer is read with a single ->> instead
of a $[*] path. The COPY-to-agent-log sentence is gone: the query's rows are the answer.
Same three rows as the previous version (table_diff_summary pk raw: 3 identical).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/duckstack-review/SKILL.md:40 RULES.md 16: The checklist bans {% for %} outright, but RULES.md 16 (which overrides) allows a {% for %} when the output is genuinely loop-shaped, such as an osascript or bash loop.
    Rewrite: shape, an inline template otherwise; no {% if %}, and a {% for %} only when the output is genuinely loop-shaped (an osascript or bash loop, RULES.md 16); a new template file only for a new shape. "tera is just to make code readable… a helper" (10-17).
  • skills/agent-dispatch/SKILL.md:171 RULES.md 10 (lossless) / checklist 7 (shortest): The dispatched CTE lists six columns by hand and GROUP BY ALL over them, which silently drops the declared raws and raws_json from the logged verification row, and it is longer than it needs to be.
    Rewrite: ), dispatched AS ( -- 3. run it, each receipt beside its row
    SELECT *, array_agg(selfdispatch(both_sides)) AS receipts
    FROM sides GROUP BY ALL

Opus 5.5, duckstack review queue (duckstack-review skill)

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.

1 participant