fix(audit): berth rules/recommender — M4 (bundle-wide status), M5 (berth_unlinked target), M20/L27 (interest_berths invariant + cross-port guard), L3 (recommender stage-scale), L4 (dead branch)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -418,17 +418,24 @@ export async function removeInterestBerth(
|
||||
if (!interestRow || !berthRow) {
|
||||
throw new NotFoundError('interest or berth');
|
||||
}
|
||||
await db
|
||||
.delete(interestBerths)
|
||||
.where(and(eq(interestBerths.interestId, interestId), eq(interestBerths.berthId, berthId)));
|
||||
|
||||
// G-C4: fire the berth_unlinked berth-rule. Default mode is 'off' so this
|
||||
// is a silent no-op unless an admin opted in via system_settings.berth_rules.
|
||||
// Dynamic import avoids a static cycle: berth-rules-engine imports this file
|
||||
// (getPrimaryBerth). meta is optional so older callers that haven't been
|
||||
// threaded through can still call this without triggering the rule.
|
||||
//
|
||||
// Audit M5: evaluate BEFORE the delete and pass the just-unlinked `berthId`
|
||||
// as an explicit target override. Firing after the delete would let the rule
|
||||
// re-resolve its target via `getPrimaryBerth`, which — with the row already
|
||||
// gone — points at a DIFFERENT still-linked berth and would corrupt that
|
||||
// unrelated berth's status if an admin enabled auto/suggest mode.
|
||||
if (meta) {
|
||||
const { evaluateRule } = await import('@/lib/services/berth-rules-engine');
|
||||
void evaluateRule('berth_unlinked', interestId, portId, meta);
|
||||
await evaluateRule('berth_unlinked', interestId, portId, meta, berthId);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(interestBerths)
|
||||
.where(and(eq(interestBerths.interestId, interestId), eq(interestBerths.berthId, berthId)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user