diff --git a/web/src/lib/components/collections/CreateCollectionModal.svelte b/web/src/lib/components/collections/CreateCollectionModal.svelte
index 9ec81639f..2da76e778 100644
--- a/web/src/lib/components/collections/CreateCollectionModal.svelte
+++ b/web/src/lib/components/collections/CreateCollectionModal.svelte
@@ -10,6 +10,7 @@
coerceDefault,
fieldFromDef,
isSafeDoneFieldKey,
+ keptAbandoned,
typeSupportsDefault,
validateFieldKey,
type EditableField
@@ -316,6 +317,8 @@
) {
const terms = f.terminalOptions.filter((t) => def.options!.includes(t));
if (terms.length > 0) def.terminal_options = terms;
+ const ab = keptAbandoned(f.abandonedOptions, terms);
+ if (ab.length > 0) def.abandoned_options = ab;
}
// Advanced controls (T3 / TASK-596). Only emit when set so
// payloads stay compact and round-trip with existing schemas.
diff --git a/web/src/lib/components/collections/EditCollectionModal.svelte b/web/src/lib/components/collections/EditCollectionModal.svelte
index 106c2f54b..6eee51bed 100644
--- a/web/src/lib/components/collections/EditCollectionModal.svelte
+++ b/web/src/lib/components/collections/EditCollectionModal.svelte
@@ -12,6 +12,7 @@
coerceDefault,
defaultsEqual,
isSafeDoneFieldKey,
+ keptAbandoned,
typeSupportsDefault,
validateFieldKey,
type EditableField
@@ -316,6 +317,7 @@
options: f.options ? [...f.options] : [],
originalOptions: f.options ? [...f.options] : [],
terminalOptions: f.terminal_options ? [...f.terminal_options] : [],
+ abandonedOptions: f.abandoned_options ? [...f.abandoned_options] : [],
required: f.required,
computed: f.computed,
collection: f.collection,
@@ -541,6 +543,8 @@
) {
const terms = f.terminalOptions.filter((t) => def.options!.includes(t));
if (terms.length > 0) def.terminal_options = terms;
+ const ab = keptAbandoned(f.abandonedOptions, terms);
+ if (ab.length > 0) def.abandoned_options = ab;
}
// Gate type-specific advanced values by the current type so
// stale hidden values from a previous type (e.g. a number
@@ -619,6 +623,8 @@
) {
const terms = f.terminalOptions.filter((t) => def.options!.includes(t));
if (terms.length > 0) def.terminal_options = terms;
+ const ab = keptAbandoned(f.abandonedOptions, terms);
+ if (ab.length > 0) def.abandoned_options = ab;
}
// Advanced controls (T3 / TASK-596). Only emit when set so
// payloads stay compact and round-trip with existing schemas.
diff --git a/web/src/lib/components/collections/FieldEditor.svelte b/web/src/lib/components/collections/FieldEditor.svelte
index 5cc42dedd..c0ebd9246 100644
--- a/web/src/lib/components/collections/FieldEditor.svelte
+++ b/web/src/lib/components/collections/FieldEditor.svelte
@@ -105,6 +105,24 @@
field.keyTouched = true;
}
+ // Markings are keyed by option VALUE, so an edited option must carry its
+ // terminal and abandoned marks to the new value (codex round 1 on #1468):
+ // a plain bind left them on the old value, which the save then filtered
+ // out, silently un-marking a renamed "cancelled" and counting it shipped.
+ function renameOption(optIndex: number, next: string) {
+ const prev = field.options[optIndex];
+ field.options[optIndex] = next;
+ if (prev === next) return;
+ for (const marks of [field.terminalOptions, field.abandonedOptions]) {
+ const at = marks.indexOf(prev);
+ if (at < 0) continue;
+ // Another row may still hold the old value; its marking stays.
+ if (field.options.includes(prev)) continue;
+ if (marks.includes(next)) marks.splice(at, 1);
+ else marks[at] = next;
+ }
+ }
+
function removeOption(optIndex: number) {
field.options.splice(optIndex, 1);
}
@@ -117,6 +135,11 @@
const idx = field.terminalOptions.indexOf(option);
if (idx >= 0) {
field.terminalOptions.splice(idx, 1);
+ // An option that no longer closes an item cannot close it without
+ // delivering either: the server refuses abandoned_options outside
+ // terminal_options (BUG-2347).
+ const ab = field.abandonedOptions.indexOf(option);
+ if (ab >= 0) field.abandonedOptions.splice(ab, 1);
} else {
field.terminalOptions.push(option);
}
@@ -126,6 +149,23 @@
return field.terminalOptions.includes(option);
}
+ // "Counts as abandoned" (BUG-2347): a terminal option that closes an item
+ // WITHOUT delivering it (cancelled, rejected, overturned…). The changelog,
+ // standup and report throughput leave those items out. Offered only on a
+ // terminal option.
+ function toggleAbandoned(option: string) {
+ const idx = field.abandonedOptions.indexOf(option);
+ if (idx >= 0) {
+ field.abandonedOptions.splice(idx, 1);
+ } else {
+ field.abandonedOptions.push(option);
+ }
+ }
+
+ function isAbandoned(option: string): boolean {
+ return field.abandonedOptions.includes(option);
+ }
+
// ── Advanced section ─────────────────────────────────────────────────────
// Contains: required, default value, suffix (number only), relation target.
// Auto-expanded when the field already has any advanced value configured so
@@ -330,6 +370,11 @@
Saved
{/if}
+ Abandoned?
{#if !isActiveDoneField}
@@ -348,7 +393,8 @@
renameOption(oi, e.currentTarget.value)}
placeholder="option name"
/>
+ {#if showsTerminalColumn}
+ {#if isTerminal(field.options[oi])}
+
+ {:else}
+
+ {/if}
+ {/if}