feat(#93): IMP-55 u1~u12 frontend manual section swap detection (manual_section_assignment bool axis + drag-only marker gate + dual-axis persistence + backend manual-true gate)
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 9s
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 9s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9062931863
commit
4e281a20d8
13 files changed
+834
-52
No files matched your search
+100
-27
@@ -170,6 +170,16 @@ export default function Home() {
|
||||
const key = deriveUserOverridesKey(p.uploadedFile.name);
|
||||
void saveUserOverrides(key, { zone_geometries: null });
|
||||
}
|
||||
// IMP-55 (#93) u12 — persist the marker reset to disk so a stale
|
||||
// `manual_section_assignment: true` from a prior drag (written via
|
||||
// u6's co-PUT) cannot survive the layout apply. The in-memory reset
|
||||
// on line 192 protects the current session, but a page reload would
|
||||
// re-seed from disk via u3's restore branch and re-arm the u7 gate.
|
||||
// Unconditional — apply always resets, independent of hadPriorGeoms.
|
||||
if (p.uploadedFile) {
|
||||
const key = deriveUserOverridesKey(p.uploadedFile.name);
|
||||
void saveUserOverrides(key, { manual_section_assignment: false });
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
userSelection: {
|
||||
@@ -179,6 +189,17 @@ export default function Home() {
|
||||
layout_preset: layoutId,
|
||||
zone_sections: carriedZoneSections,
|
||||
zone_geometries: {},
|
||||
// IMP-55 (#93) u5 — reset the bool intent marker to `false` on
|
||||
// layout apply. `carriedZoneSections` above is auto-carry (old
|
||||
// zone.section_ids → new layout positions), NOT user drag-drop
|
||||
// intent. Without this explicit reset the spread of
|
||||
// `...p.userSelection.overrides` would carry a prior-drag `true`
|
||||
// into the new layout, causing handleGenerate (u7) to forward
|
||||
// auto-carried assignments as user overrides and re-trigger the
|
||||
// PARTIAL_COVERAGE regression. The marker flips back to `true`
|
||||
// only when the user actually drag-drops a section in the new
|
||||
// layout (u6 handleSectionDrop).
|
||||
manual_section_assignment: false,
|
||||
},
|
||||
selectedZoneId: null,
|
||||
selectedRegionId: null,
|
||||
@@ -193,10 +214,27 @@ export default function Home() {
|
||||
// pending 모드 취소 → 평소 (final.html iframe) 모드 복귀.
|
||||
const handleCancelPendingLayout = useCallback(() => {
|
||||
setPendingLayout(null);
|
||||
setState((p) => ({
|
||||
...p,
|
||||
userSelection: createInitialUserSelection(p.slidePlan),
|
||||
}));
|
||||
setState((p) => {
|
||||
// IMP-55 (#93) u12 — persist marker=false to disk on cancel. In-memory
|
||||
// the u3 seed via createInitialUserSelection already pins false (u5
|
||||
// contract), but if a prior drag-drop wrote `true` to disk via u6's
|
||||
// co-PUT, that value would survive a reopen and re-arm the u7
|
||||
// forwarding gate on the next page load. Symmetric with the apply
|
||||
// path's disk PUT above.
|
||||
if (p.uploadedFile) {
|
||||
const key = deriveUserOverridesKey(p.uploadedFile.name);
|
||||
void saveUserOverrides(key, { manual_section_assignment: false });
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
// IMP-55 (#93) u5 — cancel discards all pending overrides via
|
||||
// `createInitialUserSelection`, whose u3 seed pins
|
||||
// `manual_section_assignment: false`. In-memory reset is implicit
|
||||
// via the seed; u12 adds the disk-side PUT above to keep persisted
|
||||
// state consistent so a reopen does not re-arm the marker.
|
||||
userSelection: createInitialUserSelection(p.slidePlan),
|
||||
};
|
||||
});
|
||||
setHasPendingChanges(false);
|
||||
}, []);
|
||||
|
||||
@@ -354,29 +392,45 @@ export default function Home() {
|
||||
}
|
||||
}
|
||||
|
||||
// 2026-05-22 — IMP-08 B-3 원래 동작 (sameAsDefault with effectiveSlidePlan) 복귀.
|
||||
// 시연 안정성 우선. section swap 은 별 path (수동 drag detection) 로 풀어야 함.
|
||||
// 임시 over-aggressive fix 가 default flow 깨뜨려 PARTIAL_COVERAGE 발생했음.
|
||||
const userZoneSections = state.userSelection.overrides.zone_sections;
|
||||
if (userZoneSections) {
|
||||
const defaultByZone = new Map<string, string[]>();
|
||||
sourcePlan.zones.forEach((z) => {
|
||||
defaultByZone.set(z.zone_id, z.section_ids);
|
||||
});
|
||||
const zoneSectionsDiff: Record<string, string[]> = {};
|
||||
for (const [zoneId, sids] of Object.entries(userZoneSections)) {
|
||||
if (!Array.isArray(sids)) continue;
|
||||
const cleaned = sids.filter((s) => typeof s === "string" && s.trim());
|
||||
const defaults = defaultByZone.get(zoneId) ?? [];
|
||||
const sameAsDefault =
|
||||
cleaned.length === defaults.length &&
|
||||
cleaned.every((sid, i) => sid === defaults[i]);
|
||||
if (!sameAsDefault) {
|
||||
zoneSectionsDiff[zoneId] = cleaned;
|
||||
// IMP-55 (#93) u7 — Replace the IMP-08 B-3 self-compare with the bool
|
||||
// `manual_section_assignment` intent marker gate. The prior code built
|
||||
// `defaultByZone` from `sourcePlan.zones` and compared against the
|
||||
// user's `overrides.zone_sections`, but `sourcePlan === effectiveSlidePlan`
|
||||
// (Home.tsx:305) and `effectiveSlidePlan.zones === pendingZones`
|
||||
// (Home.tsx:649), which is itself derived from
|
||||
// `state.userSelection.overrides.zone_sections` via slidePlanUtils.ts.
|
||||
// The comparison was degenerate (user input vs itself), so real drag-drop
|
||||
// swaps were classified `sameAsDefault` and silently dropped from
|
||||
// `overrides.zoneSections` — the exact regression IMP-55 fixes.
|
||||
// - true → forward `zone_sections` filtered to zone_ids that exist in
|
||||
// `sourcePlan.zones` (cross-layout safety so foreign zone keys from a
|
||||
// stale persisted layout never reach backend `--override-section-
|
||||
// assignment`). u6 is the SOLE setter of true (real drag-drop).
|
||||
// - false → skip. Backend determines assignment from its own default
|
||||
// policy. u3 seeds false on first load, u5 resets false on layout
|
||||
// apply auto-carry, u12 persists false so a stale disk `true` cannot
|
||||
// survive a reopen-after-apply window.
|
||||
// No `sameAsDefault` heuristic — the marker is the source of intent.
|
||||
const manualMarker =
|
||||
state.userSelection.overrides.manual_section_assignment;
|
||||
if (manualMarker === true) {
|
||||
const userZoneSections = state.userSelection.overrides.zone_sections;
|
||||
if (userZoneSections) {
|
||||
const validZoneIds = new Set(
|
||||
sourcePlan.zones.map((z) => z.zone_id),
|
||||
);
|
||||
const zoneSectionsForward: Record<string, string[]> = {};
|
||||
for (const [zoneId, sids] of Object.entries(userZoneSections)) {
|
||||
if (!validZoneIds.has(zoneId)) continue;
|
||||
if (!Array.isArray(sids)) continue;
|
||||
const cleaned = sids.filter(
|
||||
(s) => typeof s === "string" && s.trim(),
|
||||
);
|
||||
zoneSectionsForward[zoneId] = cleaned;
|
||||
}
|
||||
if (Object.keys(zoneSectionsForward).length > 0) {
|
||||
overrides.zoneSections = zoneSectionsForward;
|
||||
}
|
||||
}
|
||||
if (Object.keys(zoneSectionsDiff).length > 0) {
|
||||
overrides.zoneSections = zoneSectionsDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,7 +532,21 @@ export default function Home() {
|
||||
const handleSectionDrop = useCallback((sectionId: string, zoneId: string) => {
|
||||
setState((p) => {
|
||||
const newSelection = moveSectionToZone(p.userSelection, sectionId, zoneId);
|
||||
const finalSelection = selectZone(newSelection, zoneId); // 이동된 존 자동 선택
|
||||
const zoneSelected = selectZone(newSelection, zoneId); // 이동된 존 자동 선택
|
||||
// IMP-55 (#93) u6 — flip the bool intent marker to `true` on real
|
||||
// user drag-drop. Inverse of the u5 reset (layout apply/cancel
|
||||
// auto-carry → false). handleGenerate (u7) gates `overrides.zoneSections`
|
||||
// forwarding on this marker, so an unflipped drop would never reach
|
||||
// the backend (the IMP-55 self-compare regression). The marker is
|
||||
// flipped BEFORE persistence so the in-memory selection and the
|
||||
// co-PUT body stay in sync atomically.
|
||||
const finalSelection = {
|
||||
...zoneSelected,
|
||||
overrides: {
|
||||
...zoneSelected.overrides,
|
||||
manual_section_assignment: true,
|
||||
},
|
||||
};
|
||||
// IMP-52 u7 — persist the post-drop zone_sections snapshot. The on-disk
|
||||
// schema axis (`zone_sections`) shares the in-memory shape (zone_id →
|
||||
// section_ids), so we forward the full mutated value; the u4 PUT path
|
||||
@@ -486,10 +554,15 @@ export default function Home() {
|
||||
// p.uploadedFile gate skips persistence before any MDX is loaded —
|
||||
// the demo-mode initial render path would otherwise PUT to the empty
|
||||
// key. saveUserOverrides is debounced (300ms) and per-key coalesced.
|
||||
// IMP-55 (#93) u6 — co-PUT `manual_section_assignment: true` in the
|
||||
// SAME body so the disk file never has the post-drop zone_sections
|
||||
// without the marker (would otherwise look like an unmotivated
|
||||
// IMP-52 zone_sections write to the u9 backend fallback).
|
||||
if (p.uploadedFile) {
|
||||
const key = deriveUserOverridesKey(p.uploadedFile.name);
|
||||
void saveUserOverrides(key, {
|
||||
zone_sections: finalSelection.overrides.zone_sections,
|
||||
manual_section_assignment: true,
|
||||
});
|
||||
}
|
||||
return { ...p, userSelection: finalSelection };
|
||||
|
||||
Reference in new issue
Block a user