From 99c01bf11318165b861ad7bf5f43dfc241feecda Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 19:41:04 +0000 Subject: [PATCH] =?UTF-8?q?fix(metadata-protocol):=20seed-loader=20?= =?UTF-8?q?=E4=B8=AD=E3=80=8C=E8=AE=A1=E4=B8=BA=E9=94=99=E8=AF=AF=E3=80=8D?= =?UTF-8?q?=E7=9A=84=E5=A4=B1=E8=B4=A5=E4=B8=80=E5=BE=8B=E8=AE=B0=20error,?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=87=AA=E7=9B=B8=E7=9F=9B=E7=9B=BE=E5=9C=B0?= =?UTF-8?q?=E8=AE=B0=20warn=20(#4729)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SeedLoaderService` pass-2 延迟引用回填的 catch 上方写着「this must be a reported, counted error, never a silent warning」,紧跟着的调用却是 `this.logger.warn`。计数是对的(`recordDeferredError` 进 `allErrors` ⇒ `success: false`),但日志级别与它矛盾 —— 而这一行是一次 seed 在宿主控制台上 留下的唯一痕迹,`warn` 正是 #4420 证明没人读的那一级。 - 该行提到 `error`,并按 AGENTS.md「Degradation log levels」补齐一条 error 该有的两件东西:**后果**(`.` 停在 NULL、行本身已种下所以 行计数一切正常、循环关系半写入)与**修复动作**(没有任何东西会重试,修掉 写入错误 —— 超出重试预算的瞬时故障,或某条 validation 规则否决了这次 update —— 之后重跑 seed)。 - 按同一判据(这次失败是否计入 `errors` / 是否让 `success` 变 false)盘完本 文件其余 `logger.warn`:另有五处「计为错误、日志 warn」一并提到 `error` —— 批量插入失败行、`cel` 表达式解析失败被丢弃的记录、两处 DROP 引用字段的非法 引用路径(行落了、关联没落,而行计数干净 —— framework#3932),以及顺序写 与 update 两处 catch。两处 DROP 路径的日志行另补后果与修复动作。 - 三处**维持 warn**并把审计结论写进注释:`Halting on first error`(控制流通知, 它所halt 的错误各自已在 error 级别报过)、`NODE_ENV` 无法判定(功能性、 fail-open 降级)、roll-up summary 重算失败(记录确实写入了;陈旧汇总列是否 属于 #4632 第二类另开 #4998 由维护者定夺)。 - 让门禁而不只是测试钉住这个接缝:回填写入抽成 `writeDeferredReference` (原写入在 `withTransientRetry` 闭包里,AST 扫描进不去),与 `writeRecord` 一同登记进 `scripts/check-durability-degradation-log-level.mjs` 的 `DURABILITY_CRITICAL_CALLEES`,这两处 catch 再被降级即 CI 红。 结果对象、API、schema 均无变化,变的只是级别与措辞。 另记录两处不在本单判据内的同文件发现:#4997(无 pass-2 时整条记录被丢弃却 一行日志都不打)、#4998(roll-up summary 陈旧值不计数、只记 warn)。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX --- .../seed-loader-loud-failure-log-level.md | 44 +++++ .../src/seed-loader-deferred-failure.test.ts | 80 +++++++++ .../seed-loader-multi-value-reference.test.ts | 9 +- packages/metadata-protocol/src/seed-loader.ts | 162 +++++++++++++++--- packages/runtime/src/seed-loader.test.ts | 6 +- ...check-durability-degradation-log-level.mjs | 8 + 6 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 .changeset/seed-loader-loud-failure-log-level.md diff --git a/.changeset/seed-loader-loud-failure-log-level.md b/.changeset/seed-loader-loud-failure-log-level.md new file mode 100644 index 0000000000..c584610dd2 --- /dev/null +++ b/.changeset/seed-loader-loud-failure-log-level.md @@ -0,0 +1,44 @@ +--- +"@objectstack/metadata-protocol": patch +--- + +fix(metadata-protocol): a seed failure that is COUNTED as an error now logs at `error` (#4729) + +`SeedLoaderService`'s pass-2 deferred back-fill carried a comment stating that a +failed back-fill "must be a reported, counted error, **never** a silent warning" +— and the line under it called `logger.warn`. The count was right (the failure +lands in `result.errors`, flips `success: false`) but the level contradicted it, +and that log line is the only trace a seed leaves in a host's console. `warn` is +the level #4420 proved nobody reads. + +**What changed** + +- The failed back-fill logs at **`error`**, and the line now owes what + AGENTS.md → "Degradation log levels" requires of one: the **consequence** + (`.` stays NULL on a named record, the row itself was seeded so + every row counter reads clean, the circular relationship is half-written) and + the **fix** (nothing retries it — repair the write error, which is either a + transient failure that outlasted the retry budget or a validation rule vetoing + the update, then re-run the seed). +- The rest of the file was audited against the same criterion — *is this failure + counted in the load's `errors` (i.e. does it make `success: false`)?* Five more + sites answered yes while logging `warn`, and were raised to `error`: a failed + batch insert row, a record dropped because its `cel` expression could not + resolve, the two invalid-reference paths that DROP a reference field (the row + lands without its association and the row counters stay clean — framework#3932), + and the two write-failure catches on the sequential/update paths. The two + dropped-reference lines also gained the consequence and fix in the message. +- Deliberately left at `warn`, and now documented as audited: "Halting on first + error" (a control-flow notice about failures already reported at `error`), the + `NODE_ENV` scope warning (a functional, fail-open degradation), and the + roll-up-summary recompute (records *were* written; whether a stale summary + column is the same class is #4998). +- The seam is now pinned by CI, not only by tests: the back-fill write was + extracted as `writeDeferredReference` and added — with `writeRecord` — to + `DURABILITY_CRITICAL_CALLEES` in `scripts/check-durability-degradation-log-level.mjs`, + so `pnpm check:durability-log-level` fails if either catch is ever quietened + again. + +No API, schema or result-object change: the same errors are reported in +`SeedLoaderResult` exactly as before. What changed is the level and the wording +of what a seeding host sees in its log. diff --git a/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts b/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts index bcce90aa71..8e03efcd2b 100644 --- a/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts +++ b/packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts @@ -186,6 +186,86 @@ describe('seed deferred back-fill failure is reported, not swallowed (framework# expect(result.errors.some((e: { field: string }) => e.field === 'head_id')).toBe(true); }); + /** + * #4729 — the LOG LEVEL has to agree with the count. + * + * The comment above this catch has always said the failure "must be a + * reported, counted error, never a silent warning", and `recordDeferredError` + * duly counts it — but the call underneath it was `logger.warn`, i.e. the + * level #4420 proved nobody reads, on the ONE line this failure leaves in a + * seed's console output. AGENTS.md → "Degradation log levels" also requires + * that line to carry the consequence and the fix, not just a label. + */ + it('logs the failed back-fill at ERROR, naming object.field, the NULL consequence and the remedy (#4729)', async () => { + const { engine, store } = createFaithfulEngine(); + const metadata = createMetadata(); + const logger = createLogger(); + + const realUpdate = (engine.update as any).getMockImplementation(); + (engine.update as any).mockImplementation(async (obj: string, data: any, opts: any) => { + if (obj === 'audit_department') throw new Error('UPDATE rejected by validation rule'); + return realUpdate(obj, data, opts); + }); + + const result = await new SeedLoaderService(engine, metadata, logger).load({ + seeds: SEEDS, + config: CONFIG, + }); + + // The reference genuinely did not land. + expect(store.audit_department.find((r) => r.name === 'Engineering')!.head_id == null).toBe(true); + + const line = logger.error.mock.calls + .map((c: unknown[]) => String(c[0])) + .find((m: string) => m.includes('audit_department.head_id')); + expect(line, 'the failed back-fill was not reported at error level').toBeDefined(); + + // The consequence, concretely: which reference stays NULL, and that + // everything else looks fine. + expect(line).toContain('stays NULL'); + expect(line).toContain('HALF-WRITTEN'); + expect(line).toContain('audit_worker.name'); + // The fix. + expect(line).toMatch(/re-run the seed/); + // The cause travels on the same line (a `warn` reader is not owed a second look). + expect(line).toContain('UPDATE rejected by validation rule'); + // The structured error object is passed through for the logger's own + // error rendering, per the `Logger` contract's `(message, error, meta)`. + const [, err, meta] = logger.error.mock.calls.find((c: unknown[]) => + String(c[0]).includes('audit_department.head_id'), + )!; + expect(err).toBeInstanceOf(Error); + expect(meta).toMatchObject({ object: 'audit_department', field: 'head_id' }); + + // NOT at warn — the level this issue exists to correct. + expect( + logger.warn.mock.calls.some((c: unknown[]) => String(c[0]).includes('deferred reference')), + 'the back-fill failure is still being reported at warn', + ).toBe(false); + + // …and it is still COUNTED, which is what the level now agrees with. + expect(result.success).toBe(false); + expect(result.summary.totalErrored).toBeGreaterThan(0); + expect(result.errors.some((e: { field: string }) => e.field === 'head_id')).toBe(true); + }); + + it('a back-fill that SUCCEEDS logs nothing loud (#4729 — do not train readers to skim `error`)', async () => { + const { engine, store } = createFaithfulEngine(); + const metadata = createMetadata(); + const logger = createLogger(); + + const result = await new SeedLoaderService(engine, metadata, logger).load({ + seeds: SEEDS, + config: CONFIG, + }); + + const aliceId = store.audit_worker.find((r) => r.name === 'Alice')!.id; + expect(store.audit_department.find((r) => r.name === 'Engineering')!.head_id).toBe(aliceId); + expect(result.success).toBe(true); + expect(logger.error).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('a transient blip that recovers on retry still reports clean success', async () => { const { engine, store } = createFaithfulEngine(); const metadata = createMetadata(); diff --git a/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts b/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts index f59d5fe272..8dd853789e 100644 --- a/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts +++ b/packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts @@ -276,7 +276,14 @@ describe('seed reference resolution — multi-value lookup (multiple: true)', () // The unwritable value never reaches the driver; the record still lands. expect(store.book[0].reviewer).toBeUndefined(); expect(store.book[0].name).toBe('Refactoring'); - expect(logger.warn).toHaveBeenCalled(); + // #4729: the row landed WITHOUT its association and the row counters stay + // clean, so this is reported at `error` — the one level a reader of the + // console is not trained to skim — and the line says what was lost. + const dropped = logger.error.mock.calls.map((c: any[]) => String(c[0])).find((m: string) => m.includes('reviewer')); + expect(dropped, 'the dropped reference was not reported at error level').toBeDefined(); + expect(dropped).toContain('DROPPED'); + expect(dropped).toContain('re-run the seed'); + expect(logger.warn).not.toHaveBeenCalled(); // framework#3932: the row WAS written, so `errored` stays 0 and the row // counters all look healthy — the loss only shows up here. diff --git a/packages/metadata-protocol/src/seed-loader.ts b/packages/metadata-protocol/src/seed-loader.ts index 2197962741..b88e248975 100644 --- a/packages/metadata-protocol/src/seed-loader.ts +++ b/packages/metadata-protocol/src/seed-loader.ts @@ -213,6 +213,13 @@ export class SeedLoaderService implements ISeedLoaderService { allResults.push(result); if (config.haltOnError && result.errored > 0) { + // Deliberately `warn`, and audited as such in #4729: this line reports + // a CONTROL-FLOW decision, not a loss. Every error it halts on was + // already reported at `error` by the site that counted it, and the + // datasets skipped after it were never written — the load reports + // `success: false` and says which object stopped it. Escalating a + // second line about the same failures is the over-application AGENTS.md + // warns about (it trains readers to skim `error`). this.logger.warn('[SeedLoader] Halting on first error', { object: dataset.object }); break; } @@ -480,7 +487,15 @@ export class SeedLoaderService implements ISeedLoaderService { const error = this.buildWriteError(objectName, record, externalId, recordIndex, res.error); errors.push(error); allErrors.push(error); - this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex }); + // `error`, not `warn` (#4729 / #4632): this row is counted in + // `allErrors` — the load already reports `success: false` — and the + // consequence is that the record did NOT land. Count and log level + // must agree; the message names the row and the cause. + this.logger.error( + `[SeedLoader] ${error.message}`, + res.error instanceof Error ? res.error : undefined, + { recordIndex }, + ); } } }; @@ -542,7 +557,9 @@ export class SeedLoaderService implements ISeedLoaderService { }; errors.push(error); allErrors.push(error); - this.logger.warn(`[SeedLoader] ${error.message}`); + // `error`, not `warn` (#4729 / #4632): counted in `allErrors` and the + // record is dropped — nothing of it is persisted. + this.logger.error(`[SeedLoader] ${error.message}`, undefined, { recordIndex: i }); continue; } const record = { ...(seedResult.value as Record) }; @@ -609,6 +626,27 @@ export class SeedLoaderService implements ISeedLoaderService { return error; }; + /** + * Report a reference the loader had to DROP — the row still lands, so + * this is the one failure mode in the file whose row counters stay + * clean (`errored` never moves; only `referencesDropped` does, + * framework#3932). That is exactly the shape AGENTS.md → "Degradation + * log levels" reserves `error` for: the record looks seeded while an + * association it declared is not there. `warn` here was the same + * count/level contradiction #4729 fixed in pass 2, so the line states + * the CONSEQUENCE (row written without the association) and the FIX on + * top of the authored error's own advice. + */ + const reportDroppedReference = (error: ReferenceResolutionError): void => { + this.logger.error( + `[SeedLoader] ${error.message} The value was DROPPED, so ${objectName} record #${i} was written WITHOUT ` + + `its \`${ref.field}\` association: the row counters stay clean (only referencesDropped moves), and the ` + + `link is simply absent. Fix the seed value and re-run the seed to restore it.`, + undefined, + { recordIndex: i }, + ); + }; + // LOUD FAILURE: an ARRAY of natural keys is only writable by a field // that stores an array — `Field.lookup(..., { multiple: true })` (or a // multi `user` field). On a single-value field it can never resolve, so @@ -621,7 +659,7 @@ export class SeedLoaderService implements ISeedLoaderService { `or pass one natural key.`, fieldValue, ); - this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i }); + reportDroppedReference(error); // Drop the unwritable value so it never reaches the driver. Removing // the key (not writing null) matters on the upsert UPDATE path — see // the deferred-reference note below. The row itself still gets @@ -661,7 +699,7 @@ export class SeedLoaderService implements ISeedLoaderService { `${ref.targetObject}.${ref.targetField} natural-key string but got an object.${outcome.hint}`, item, ); - this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i }); + reportDroppedReference(error); invalidItem = true; break; } @@ -722,6 +760,12 @@ export class SeedLoaderService implements ISeedLoaderService { // (LOUD: counted + reported). Writing it anyway would either // carry the raw natural-key string into the FK column or, on // update, corrupt the existing row. + // + // "LOUD" here means counted + in `result.errors` ONLY — this path + // logs nothing at all, so a load that drops N records looks + // identical to a clean one in the console. Filed as #4997 rather + // than fixed under #4729, whose audit criterion was the file's + // `logger.warn` calls. pushError( `Cannot resolve reference: ${objectName}.${ref.field} = '${String(unresolvedItem)}' → ` + `${ref.targetObject}.${ref.targetField} not found`, @@ -779,7 +823,14 @@ export class SeedLoaderService implements ISeedLoaderService { const error = this.buildWriteError(objectName, record, externalId, i, err); errors.push(error); allErrors.push(error); - this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i }); + // `error`, not `warn` (#4729 / #4632): counted in `allErrors`, and + // the record did not land. `writeRecord` is in the durability + // gate's vocabulary, so this catch cannot regress to `warn`. + this.logger.error( + `[SeedLoader] ${error.message}`, + err instanceof Error ? err : undefined, + { recordIndex: i }, + ); } } else { const decision = this.decideWriteAction(record, mode, externalId, existingRecords); @@ -809,7 +860,15 @@ export class SeedLoaderService implements ISeedLoaderService { const error = this.buildWriteError(objectName, record, externalId, i, err); errors.push(error); allErrors.push(error); - this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i }); + // `error`, not `warn` (#4729 / #4632): counted in `allErrors`, + // and the row's declared values did not land — an upsert that + // fails here leaves the PREVIOUS row contents in place, which + // looks like a seeded record and is not one. + this.logger.error( + `[SeedLoader] ${error.message}`, + err instanceof Error ? err : undefined, + { recordIndex: i }, + ); } } else { // Insert: buffer for the batched flush rather than writing now. @@ -1015,16 +1074,7 @@ export class SeedLoaderService implements ISeedLoaderService { if (recordId) { try { - // Use SEED_OPTIONS like every other seed write: this pass is still - // seeding, so it must carry `skipTriggers` too. Inlining a bare - // `{ isSystem: true }` here re-fired record-change automation on - // freshly seeded rows — `isSystem` does NOT suppress trigger - // dispatch, only `skipTriggers` does — which is exactly the - // self-trigger vector SEED_OPTIONS exists to prevent (#3760). - await withTransientRetry(() => this.engine.update(deferred.objectName, { - id: recordId, - [deferred.field]: resolvedValue, - }, SeedLoaderService.SEED_OPTIONS as any)); + await this.writeDeferredReference(deferred, recordId, resolvedValue); // Update result stats const resultEntry = allResults.find(r => r.object === deferred.objectName); @@ -1033,18 +1083,34 @@ export class SeedLoaderService implements ISeedLoaderService { resultEntry.referencesDeferred--; } } catch (err: any) { - // LOUD FAILURE (framework#2805): the target resolved but the - // back-fill WRITE failed (a transient error that outlasted the - // retry budget, a validation veto, …). The reference stays NULL — - // the very corruption pass 2 exists to prevent — so this must be a - // reported, counted error, never a silent warning. Swallowing it - // returned `success: true` / `totalErrored: 0` over a load that - // left a circular relationship half-written. - this.logger.warn('[SeedLoader] Failed to write deferred reference', { - object: deferred.objectName, - field: deferred.field, - error: err?.message, - }); + // LOUD FAILURE (framework#2805; rule: #4632, accident: #4420): the + // target resolved but the back-fill WRITE failed (a transient error + // that outlasted the retry budget, a validation veto, …). The + // reference stays NULL — the very corruption pass 2 exists to + // prevent — so this must be a reported, counted `error`, never a + // warning. It is counted below (`recordDeferredError` → `allErrors` + // → `success: false`), and until #4729 the LOG line contradicted + // that count by sitting at `warn`: the one trace a seed leaves in + // the console was the level nobody reads (#4420). Count and level + // now agree, and the line owes the two things AGENTS.md + // ("Degradation log levels") requires of an `error`: the + // CONSEQUENCE and the FIX. + this.logger.error( + `[SeedLoader] Deferred reference back-fill FAILED — ${deferred.objectName}.${deferred.field} stays NULL ` + + `on record '${deferred.recordExternalId}'. The row itself was seeded, so every row counter looks healthy ` + + `while the circular relationship is HALF-WRITTEN: nothing links it to ${deferred.targetObject}.` + + `${deferred.targetField} = '${this.formatAttempted(deferred.attemptedValue)}'. Nothing retries this — ` + + `fix the write error below (a transient failure that outlasted the retry budget, or a validation rule ` + + `vetoing the update) and re-run the seed to complete the link. ` + + `Cause: ${err?.message ?? String(err)}`, + err instanceof Error ? err : undefined, + { + object: deferred.objectName, + field: deferred.field, + target: `${deferred.targetObject}.${deferred.targetField}`, + recordIndex: deferred.recordIndex, + }, + ); this.recordDeferredError(deferred, allResults, allErrors, `Failed to write deferred reference: ${deferred.objectName}.${deferred.field} = '${this.formatAttempted(deferred.attemptedValue)}' → ${deferred.targetObject}.${deferred.targetField}: ${err?.message ?? String(err)}`); } @@ -1059,6 +1125,37 @@ export class SeedLoaderService implements ISeedLoaderService { } } + /** + * Write ONE pass-2 back-fill — the update that turns a deferred reference + * from NULL into the resolved id. + * + * Uses SEED_OPTIONS like every other seed write: this pass is still seeding, + * so it must carry `skipTriggers` too. Inlining a bare `{ isSystem: true }` + * here re-fired record-change automation on freshly seeded rows — `isSystem` + * does NOT suppress trigger dispatch, only `skipTriggers` does — which is + * exactly the self-trigger vector SEED_OPTIONS exists to prevent (#3760). + * + * Extracted (rather than inlined at the call site) so the durability gate can + * SEE this seam: `scripts/check-durability-degradation-log-level.mjs` matches + * a guarded `try` by the callee name it finds in the block, and it + * deliberately does not descend into nested function bodies — the engine + * write here lives inside the `withTransientRetry` closure, where no AST scan + * of the try block can reach it. `writeDeferredReference` is listed in that + * script's `DURABILITY_CRITICAL_CALLEES`, so a future edit that quietly drops + * the caller's `logger.error` back to `warn` fails CI instead of shipping + * (#4729; the rule is #4632, the accident it comes from is #4420). + */ + private async writeDeferredReference( + deferred: DeferredUpdate, + recordId: string, + resolvedValue: unknown, + ): Promise { + await withTransientRetry(() => this.engine.update(deferred.objectName, { + id: recordId, + [deferred.field]: resolvedValue, + }, SeedLoaderService.SEED_OPTIONS as any)); + } + /** * Record a pass-2 (deferred) reference failure as a first-class error: it * lands in the object's per-result `errors`, bumps its `errored` count (so @@ -1129,6 +1226,15 @@ export class SeedLoaderService implements ISeedLoaderService { * and return the written value rather than re-writing (which would * duplicate). Matched by `code` so we needn't import objectql (which depends * on this package — importing back would cycle). Any other error propagates. + * + * Left at `warn` by #4729 deliberately, and the reasoning is filed rather + * than settled here: this is the one degradation in the file that is NOT + * counted as an error (the load still reports `success: true`), so it falls + * outside that issue's "count and level must agree" criterion — but a stale + * roll-up column IS persisted data disagreeing with the rows it summarizes, + * which is arguably the #4632 class. Whether it should become `error`, + * counted, or stay as-is is #4998 (needs a maintainer's call, since it + * changes what a SUCCESSFUL seed prints). */ private async writeRecoveringSummary(fn: () => Promise): Promise { try { diff --git a/packages/runtime/src/seed-loader.test.ts b/packages/runtime/src/seed-loader.test.ts index f21590549d..54159054a2 100644 --- a/packages/runtime/src/seed-loader.test.ts +++ b/packages/runtime/src/seed-loader.test.ts @@ -1251,7 +1251,11 @@ describe('SeedLoaderService', () => { }); expect(result.summary.totalErrored).toBe(1); - expect(logger.warn).toHaveBeenCalled(); + // #4729: a failed write is COUNTED as an error, so it logs at `error` — + // count and log level must agree (AGENTS.md → "Degradation log levels"). + expect(logger.error).toHaveBeenCalled(); + expect(logger.error.mock.calls.some(([m]) => String(m).includes('Failed to write account'))).toBe(true); + expect(logger.warn).not.toHaveBeenCalled(); }); it('should handle multiple references on same object', async () => { diff --git a/scripts/check-durability-degradation-log-level.mjs b/scripts/check-durability-degradation-log-level.mjs index be00f436c8..5269107f60 100644 --- a/scripts/check-durability-degradation-log-level.mjs +++ b/scripts/check-durability-degradation-log-level.mjs @@ -120,6 +120,14 @@ const DURABILITY_CRITICAL_CALLEES = new Map([ 'rearmSuspendedWaitTimers', 'Suspended runs survive on disk but nothing will ever resume them — the persisted state and the runtime disagree (ADR-0019, #4420).', ], + [ + 'writeDeferredReference', + "A seed's pass-2 back-fill never landed — the row was written but its reference column stays NULL, so a circular relationship is half-written while every row counter reads clean (#4729, framework#2805).", + ], + [ + 'writeRecord', + 'A seed record was not written — the row is simply absent (or, on the upsert/update path, still holds its pre-seed contents) while the load moves on to the next record (#4729).', + ], ]); /** Log levels that are ACCEPTABLE inside a durability-guarding catch. */