Repository navigation
feat(feedback): corrections, a checked fix filed as a verified-query proposal (F1) - #341
Conversation
Review of F1 (corrections)On the PR head,
Invariants all hold: no dialect SQL outside adapters, lazy imports keep Verdict: fix first. Findings 1 and 2 are cross-role disclosures of SQL text that D60's scope filter is meant to prevent. |
…ctions only, union refiled tables (F1 review)
|
Thanks for the review. All seven are fixed in b2e53a6, each with a test written first and seen failing.
Docs updated: F1's Built note in Checks: |
…boundary test reads it (F1)
|
Follow-up in cac4136. CI on the first push failed one test: |
Design:
docs/architecture/wave_b.md(design PR #310), section 10 "Learn from corrections", decision D55, row F1 of the PR plan. Builds on Q1 (#337): the correction is filed withProposal.build(...)+nl2sql.review.queue.propose(ctx, proposal).What changed
engine.feedback.correct(trace_id, *, sql=None, plan=None, question=None, note=None, user_context) -> Proposal(nl2sql/feedback/corrections.py, new;api/feedback_api.pyonly adds thecorrectmethod that calls it,record/_proposeuntouched).questionoverrides) and its one datasource; a run on several datasources is refused. The run is rateddownwith the note unless it already is (throughengine.feedback.record).run_query(plan=SuppliedPlan(...), execute=False, user_context=corrector), as Wave A's seeding does:run.before,plan.after, the logical validator (with the corrector's scope) and the generator, no model call. A rejected plan is refused with the run's messages.inspect_verified_sql(exactly one read-only query, the curated config's own check) and every table it reads must be allowed by the corrector'sScope.hooks_of(ctx).call("execute.before", ...)with the corrector as the caller and no approvals: aDeny, a hook failure or aNeedsApprovalrefuses it.CuratedAPI.testdoes (read-only, first batch only, nothing kept, no artifact written). A failure is refused with the database's message; an empty result is allowed.kind="verified_query",origin="correction",changeincurated.yaml's shape (idslugged from the question, the question, the SQL, the note or the placeholder as description),tableseverydatasource.schema.tablethe fix and the original SQL read,evidence{trace_id, original_sql, ran, row_count, columns}. Nothing is written to a person's file before accept.nl2sql feedback correct TRACE_ID (--sql FILE | --plan FILE) [--question TEXT] [--note TEXT] --as ROLE [--user ID](cli/commands/feedback.py). A plan file is JSON or YAML. A refusal prints the reason and exits 1; no fix or both exits 2.feedback/corrections.pyjoinsACCESS_DECISIONS(it asks aScope), andFeedbackAPI.correctjoinsWAVE_B_READ_METHODS(a requiredUserContextcaller).Where the code differs from the doc (small gaps filled; the code on main wins)
CorrectionRefused(ValueError)lives infeedback/corrections.py, withcode(no_run,multi_datasource,invalid_plan,not_read_only,denied,hook_denied,needs_approval,failed) andmessages. It is aValueError, so F1 adds noErrorCodeand no top-level export (both are B0's files).settings.traces.mode(on_failure) keeps no trace of a successful run, so the run's stored rating is the fallback for the question, datasource and SQL. Neither isno_run.:nameplaceholders is refused (not_read_only): it runs once with no values; the reviewer adds params on accept.tablesincludes the original SQL's tables, becauseevidence.original_sqlshows that SQL; a caller denied any of them does not see the proposal.proposewould complete the fix's tables anyway; F1 passes them itself, as Q1's hand-off asks.execute.beforeis asked (step 4);execute.afteris not.For C2
C2's
test_correction_check_is_cost_guardedcan callengine.feedback.correct(trace_id, sql=..., user_context=...)and expectnl2sql.feedback.corrections.CorrectionRefusedwithcode == "needs_approval"(over_limit: confirm, the message holds the hook's reason) orcode == "hook_denied"(refuse, the message is the hook'sDenyreason). The correction askshooks_of(ctx).call("execute.before", trace_id=..., question=..., user=..., datasource_id=..., sub_query=<id "correction">, plan=<PlanModel or None>, sql=..., params={}, tables=[...], approvals=[]), so the built-inpolicy.costapplies as soon as the runtime puts it first in that runner. A test needs a kept trace (write one toengine._runtime.traces, astests/unit/test_corrections.py::_rundoes) or a stored rating.Tests
packages/nl2sql/tests/unit/test_corrections.py(new, 25 tests): every test the row names (test_plan_fix_goes_through_validator_and_generator,test_rejected_plan_fix_is_refused_with_the_messages,test_sql_fix_must_be_one_read_only_select,test_sql_fix_on_a_denied_table_is_refused,test_correction_denied_by_an_execute_before_hook_is_refused,test_correction_needing_approval_is_refused,test_failing_fix_is_refused_with_the_database_message,test_multi_datasource_run_is_refused,test_correction_records_a_down_rating,test_nothing_is_written_before_accept), plustest_a_caller_denied_a_table_the_fix_names_does_not_see_it,test_a_caller_denied_a_table_the_original_sql_names_does_not_see_it, the proposal's shape, the no-trace fallback, the caller and one-fix checks, and the CLI. Written first and seen failing (no module). No model is called: a plan fix builds model clients pointed at a dead local address and calls none.Docs
observability/feedback.md(new section "Correcting an answer"),configuration/review.md(corrections file proposals; evidence; source refs),api/core/public-facade.md(feedback.correct),README.md(the command), and F1's "Built" note inarchitecture/wave_b.mdsection 10.Checks
tests/unit/test_corrections.pyandtests/architecture/test_boundaries.py: 78 passed. Also run locally, as they share the touched files:test_review_queue.py,test_playground_feedback.py,test_feedback_store.py: passed.mkdocs build --strict: clean.