Skip to content

Seed loader and batch-row withholding: a declared 4xx now discloses the platform sentence, and the seed loader's operator log loses the driver's own text because it reads err.message and never walks cause #14403

Description

@os-musk

Measured while implementing #14095 (the insert door's DUPLICATE_RECORD envelope). This is a metadata-protocol consequence of that change, outside that card's declared file surface, filed rather than ridden on its PR. It must be routed before or with #14095: two pins in packages/runtime go red without it.

What moved, and why it is the designed behaviour rather than an accident

Both sinks decide disclosure by a POSITIVE test — "would the boundary that serves this throw call it a client refusal?", answered by resolveThrownHttpError:

  • clientFacingRowFailureText (packages/metadata-protocol/src/protocol.ts): if (status >= 400 && status < 500) return err.message — otherwise the generic The of this record failed. The reason is in the server log.
  • quotableSeedFailureDetail / declaresSeedClientRefusal (packages/metadata-protocol/src/seed-loader.ts): the same shape, otherwise WITHHELD_WRITE_REASON.

The insert door's envelope declares status: 409, so a duplicate row is no longer in the withheld population. That is the sink's own stated remedy — its docblock says the undeclared case is withheld deliberately and "the remedy is at the producer … declaring is cheaper than the workaround, which is the direction that makes authored code hard to get wrong". So the DISCLOSURE half looks correct and is arguably the point.

Measured, on real drivers through the real doors:

batch row   before: { code: 'INTERNAL_ERROR', message: 'The create of this record failed. The reason is in the server log.' }
batch row   after : { code: 'DUPLICATE_RECORD', httpStatus: 409,
                      message: "Duplicate record refused on 'bd_note': a unique constraint on 'email' already holds this value. No record was written." }

seed row    before: 'Failed to write dt_acct record #1 (name=second): the data engine rejected the write; the reason is in the server log'
seed row    after : "Failed to write dt_acct record #1 (name=second): Duplicate record refused on 'dt_acct': a unique constraint on 'email' already holds this value. No record was written."

⭐ No leak is introduced. The envelope's message carries no statement, no bound values and no dialect text — the driver's error stays whole on cause. Every leak assertion in both suites (not.toContain('insert into'), 'dup@example.com', 'UNIQUE constraint failed', 'SQLITE_CONSTRAINT') still holds against the new sentence.

⚠️ The half that is a real regression, not a pin rename

seedFailureLogLine builds the OPERATOR line from err.message alone:

function seedFailureLogLine(payloadMessage: string, err: unknown): string {
  const cause = seedFailureCause(err);          // err.message — never `err.cause`
  return payloadMessage.includes(cause)
    ? `[SeedLoader] ${payloadMessage}`
    : `[SeedLoader] ${payloadMessage} ${seedCauseLabel(err)}: ${cause}`;
}

With the envelope, cause is the PLATFORM sentence and payloadMessage already contains it, so the line collapses to [SeedLoader] Failed to write … and the driver's own text — UNIQUE constraint failed: dt_acct.email, or MySQL's index name — reaches neither the response nor the log. seed-loader-driver-text-real-driver.integration.test.ts asserts exactly that operator half (logged contains UNIQUE constraint failed and Cause (withheld from the seed response)), which is why it is a pin worth keeping rather than re-baselining.

⚠️ Derivation note: the payload strings above are measured; this log-line consequence is read off the source branch plus the measured payload, not off a captured log line. Confirm it in place when taking the card.

The insert door met the same problem one layer down and fixed it the same way it should be fixed here: the engine now logs e instanceof DuplicateRecordError ? e.cause : e, because the platform logger serializes only message and stack. seedFailureCause wants the equivalent — prefer the driver's own sentence off cause when the caught error carries one.

What the work is

  1. packages/metadata-protocol/src/seed-loader.tsseedFailureCause (and seedCauseLabel's question with it) should reach through cause so the operator keeps the driver's sentence when a platform envelope replaced it. The equivalent question is worth asking of the batch-row sink's console.warn half.
  2. packages/runtime/src/batch-row-driver-text-real-driver.integration.test.ts and packages/runtime/src/seed-loader-driver-text-real-driver.integration.test.ts — retriage the two sentence pins onto the declared-refusal population, keeping every leak assertion and the operator half intact. ⛔ Not a re-baseline: the leak assertions are what those files exist for and none of them moved.

The decision, if the disclosure reading is disputed

The alternative is to keep both rows withheld even for a declared 4xx — which would mean the sinks' positive-list rule is wrong, or that the insert envelope should not declare a status. Neither looks right: the ruling on #14095 requires the status (it is what makes REST answer 409 instead of a sanitised 500), and withholding a producer-authored sentence that discloses nothing is the "declared ≠ enforced" shape pointed the other way. Recorded here so the maintainer can rule if they read it differently.


Triage housekeeping (R+99): the Blocked-by: #14095 line has been removed from this body — #14095 closed as completed on 2026-09-02T06:52Z via PR #14405, so the line was inert. It is the fourth exhausted #14095 line found; the other three (#14389, #14390, #14419) were stripped in R+98 and this card was not on that list. See the triage comment below for the grading and for a correction to the seedFailureCause reading above.

Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions