Skip to content

Commit dbcb65d

Browse files
V48 Gate 3 (implementation-only): deposit options UX — anchor delete, Forced paths, product titles
Load-anchor rows delete via history API; path pickers and options section use Forced Inclusion/Exclusions, Synthesize AssetPack Options, and Source-Safe Proposals / AssetPack Options.
1 parent c0cf951 commit dbcb65d

3 files changed

Lines changed: 120 additions & 26 deletions

File tree

uapi/app/deposits/DepositPageClient.tsx

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ export default function DepositPageClient() {
372372
{
373373
id: "source-path-sensitive-scope-warning",
374374
label:
375-
"Source path hints include sensitive operational terms requiring review.",
375+
"Forced Inclusion paths include sensitive operational terms requiring review.",
376376
severity: "warning" as const,
377377
weight: 0.64,
378378
},
@@ -1048,6 +1048,38 @@ export default function DepositPageClient() {
10481048
sourcePathHints,
10491049
]);
10501050

1051+
// Delete a saved Obfuscations anchor from the activity ledger (hover-trash
1052+
// on the Load-anchor dropdown). Optimistic local removal; server is the
1053+
// fail-closed authority (own row + anchor-source only).
1054+
const handleDeleteObfuscationsAnchor = useCallback(async (anchorId: string) => {
1055+
if (!anchorId) return;
1056+
const previousRuns = liveRuns;
1057+
setLiveRuns((current) => current.filter((run) => run.id !== anchorId));
1058+
setObfuscationsAnchorMessage(null);
1059+
try {
1060+
const response = await fetch(
1061+
`/api/executions/history/${encodeURIComponent(anchorId)}`,
1062+
{ method: "DELETE" },
1063+
);
1064+
if (!response.ok) {
1065+
const payload = (await response.json().catch(() => null)) as {
1066+
error?: string;
1067+
} | null;
1068+
throw new Error(
1069+
payload?.error || "Unable to delete the Obfuscations anchor.",
1070+
);
1071+
}
1072+
setObfuscationsAnchorMessage("Obfuscations anchor deleted.");
1073+
} catch (error) {
1074+
setLiveRuns(previousRuns);
1075+
setObfuscationsAnchorMessage(
1076+
error instanceof Error
1077+
? error.message
1078+
: "Unable to delete the Obfuscations anchor.",
1079+
);
1080+
}
1081+
}, [liveRuns]);
1082+
10511083
// Real option synthesis via the AssetPacksSynthesis pipeline (deposit
10521084
// lens). The server route builds the exclusion-filtered source inventory,
10531085
// runs bounded inference, persists the execution row with real
@@ -1722,6 +1754,7 @@ export default function DepositPageClient() {
17221754
]
17231755
.filter(Boolean)
17241756
.join(" "),
1757+
deletable: true,
17251758
}))}
17261759
value={null}
17271760
onSelect={(key) => {
@@ -1734,6 +1767,12 @@ export default function DepositPageClient() {
17341767
setSourcePathHints(anchor.sourcePathHints);
17351768
setProtectedIpExclusions(anchor.protectedIpExclusions);
17361769
}}
1770+
onDeleteItem={(key) => {
1771+
void handleDeleteObfuscationsAnchor(key);
1772+
}}
1773+
// One-shot load-in: always shows the placeholder, never
1774+
// a selected value — no check indicator in the list.
1775+
showSelectionIndicator={false}
17371776
placeholder="Load anchor..."
17381777
searchPlaceholder="Search anchors..."
17391778
emptyMessage="No anchors yet."
@@ -1891,21 +1930,21 @@ export default function DepositPageClient() {
18911930
) : null}
18921931
</div>
18931932
{/* File-tree pickers over the selected repository·branch·
1894-
commit. Hints and exclusions are MUTUALLY EXCLUSIVE — a
1895-
path picked on one side is disabled on the other.
1896-
Concept-level withholding belongs to Obfuscations above. */}
1933+
commit. Forced Inclusion and Forced Exclusions are MUTUALLY
1934+
EXCLUSIVE — a path picked on one side is disabled on the
1935+
other. Concept-level withholding belongs to Obfuscations. */}
18971936
<div className="mt-4 grid gap-4 tablet:grid-cols-2">
18981937
<div className="block">
18991938
<span className="flex items-center gap-2 text-[0.62rem] uppercase tracking-[0.16em] text-neutral-500">
19001939
<DepositIncludePathsIcon />
1901-
<span>Source path hints</span>
1940+
<span>Forced Inclusion</span>
19021941
<span onClick={(event) => event.stopPropagation()}>
19031942
<BitcodeInlineExplainer explainer={DEPOSIT_SECTION_EXPLAINERS.sourcePathHints} triggerAriaLabel="More info about this field" />
19041943
</span>
19051944
</span>
19061945
<div className="mt-2">
19071946
<VCSFileTreePicker
1908-
aria-label="Source path hints file tree"
1947+
aria-label="Forced Inclusion file tree"
19091948
provider={repositoryContext?.provider ?? "github"}
19101949
repositoryFullName={
19111950
repositoryContext?.selectedRepository?.fullName ?? null
@@ -1918,21 +1957,21 @@ export default function DepositPageClient() {
19181957
selectedPaths={sourcePathHints}
19191958
onChange={setSourcePathHints}
19201959
conflictingPaths={protectedIpExclusions}
1921-
conflictLabel="Already a protected IP exclusion"
1960+
conflictLabel="Already a Forced Exclusion"
19221961
/>
19231962
</div>
19241963
</div>
19251964
<div className="block">
19261965
<span className="flex items-center gap-2 text-[0.62rem] uppercase tracking-[0.16em] text-neutral-500">
19271966
<DepositExcludePathsIcon />
1928-
<span>Protected IP exclusions</span>
1967+
<span>Forced Exclusions</span>
19291968
<span onClick={(event) => event.stopPropagation()}>
19301969
<BitcodeInlineExplainer explainer={DEPOSIT_SECTION_EXPLAINERS.protectedIpExclusions} triggerAriaLabel="More info about this field" />
19311970
</span>
19321971
</span>
19331972
<div className="mt-2">
19341973
<VCSFileTreePicker
1935-
aria-label="Protected IP exclusions file tree"
1974+
aria-label="Forced Exclusions file tree"
19361975
provider={repositoryContext?.provider ?? "github"}
19371976
repositoryFullName={
19381977
repositoryContext?.selectedRepository?.fullName ?? null
@@ -1945,14 +1984,14 @@ export default function DepositPageClient() {
19451984
selectedPaths={protectedIpExclusions}
19461985
onChange={setProtectedIpExclusions}
19471986
conflictingPaths={sourcePathHints}
1948-
conflictLabel="Already a source path hint"
1987+
conflictLabel="Already a Forced Inclusion"
19491988
/>
19501989
</div>
19511990
<span className="mt-1 block text-xs leading-5 text-neutral-500">
1952-
Excluded paths never enter AssetPack knowledge synthesis:
1953-
they are removed from the source inventory before
1954-
measurement, and candidates that touch them are dropped
1955-
fail-closed. Concept-level withholding belongs in
1991+
Forced Exclusions never enter AssetPack knowledge
1992+
synthesis: they are removed from the source inventory
1993+
before measurement, and candidates that touch them are
1994+
dropped fail-closed. Concept-level withholding belongs in
19561995
Obfuscations above.
19571996
</span>
19581997
</div>
@@ -1985,7 +2024,7 @@ export default function DepositPageClient() {
19852024
>
19862025
{synthesisStatus === "running"
19872026
? "Synthesizing with AssetPacksSynthesis…"
1988-
: "Synthesize options"}
2027+
: "Synthesize AssetPack Options"}
19892028
</button>
19902029
)}
19912030
</section>
@@ -2001,10 +2040,10 @@ export default function DepositPageClient() {
20012040
<div className="flex flex-wrap items-start justify-between gap-3">
20022041
<div>
20032042
<p className="text-[0.68rem] uppercase tracking-[0.22em] text-emerald-200/80">
2004-
Options
2043+
Source-Safe Proposals
20052044
</p>
20062045
<h2 className="mt-2 flex items-center gap-2 text-lg font-semibold text-white">
2007-
<span>Source-safe AssetPack proposals</span>
2046+
<span>AssetPack Options</span>
20082047
<BitcodeInlineExplainer explainer={DEPOSIT_SECTION_EXPLAINERS.options} />
20092048
</h2>
20102049
</div>

uapi/tests/depositPageClient.test.tsx

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -361,15 +361,15 @@ describe("DepositPageClient", () => {
361361
expect(lastHref).not.toContain("transactionId=");
362362
});
363363

364-
it("hides the Synthesize options button while a run's detail owns the page", async () => {
364+
it("hides the Synthesize AssetPack Options button while a run's detail owns the page", async () => {
365365
// A run is adopted by default (mockQuery) — dispatching from here would
366366
// yank the viewer off the loaded run's telemetry/results mid-review.
367367
render(<DepositPageClient />);
368368
expect(
369369
await screen.findByTestId("deposit-synthesis-telemetry"),
370370
).toBeInTheDocument();
371371
expect(
372-
screen.queryByRole("button", { name: "Synthesize options" }),
372+
screen.queryByRole("button", { name: "Synthesize AssetPack Options" }),
373373
).not.toBeInTheDocument();
374374
expect(
375375
screen.getByTestId("deposit-obfuscations-run-loaded-note"),
@@ -380,7 +380,7 @@ describe("DepositPageClient", () => {
380380
await screen.findByRole("button", { name: "Back to Deposit pipelines" }),
381381
);
382382
expect(
383-
await screen.findByRole("button", { name: "Synthesize options" }),
383+
await screen.findByRole("button", { name: "Synthesize AssetPack Options" }),
384384
).toBeInTheDocument();
385385
expect(
386386
screen.queryByTestId("deposit-obfuscations-run-loaded-note"),
@@ -679,15 +679,15 @@ describe("DepositPageClient", () => {
679679
// Pick the exclusion from the repository file tree (fetched at the
680680
// selected repo·branch·commit); a directory selects its prefix.
681681
const exclusionsTree = await screen.findByLabelText(
682-
"Protected IP exclusions file tree",
682+
"Forced Exclusions file tree",
683683
);
684684
const secretEngineRow = await within(exclusionsTree).findByText(
685685
"secret-engine/",
686686
);
687687
fireEvent.click(secretEngineRow);
688688

689689
const synthesizeButton = await screen.findByRole("button", {
690-
name: "Synthesize options",
690+
name: "Synthesize AssetPack Options",
691691
});
692692
await waitFor(() => expect(synthesizeButton).not.toBeDisabled());
693693
fireEvent.click(synthesizeButton);
@@ -894,7 +894,7 @@ describe("DepositPageClient", () => {
894894
await screen.findByRole("button", { name: "Back to Deposit pipelines" }),
895895
);
896896
const synthesizeButton = await screen.findByRole("button", {
897-
name: "Synthesize options",
897+
name: "Synthesize AssetPack Options",
898898
});
899899
await waitFor(() => expect(synthesizeButton).not.toBeDisabled());
900900
fireEvent.click(synthesizeButton);
@@ -1236,10 +1236,10 @@ describe("DepositPageClient", () => {
12361236
within(listbox).getByText("Withhold the billing module internals."),
12371237
).toBeInTheDocument();
12381238
expect(
1239-
within(listbox).getByLabelText("2 hint files"),
1239+
within(listbox).getByLabelText("2 forced inclusion paths"),
12401240
).toBeInTheDocument();
12411241
expect(
1242-
within(listbox).getByLabelText("1 exclusion file"),
1242+
within(listbox).getByLabelText("1 forced exclusion path"),
12431243
).toBeInTheDocument();
12441244
fireEvent.click(within(listbox).getByText("Billing withhold"));
12451245

@@ -1256,6 +1256,61 @@ describe("DepositPageClient", () => {
12561256
).toBe("Billing withhold");
12571257
});
12581258

1259+
it("deletes an Obfuscations anchor from the load dropdown without loading it", async () => {
1260+
withAnchorFixtures();
1261+
const fetchMock = jest.fn(async (url: string, init?: RequestInit) => {
1262+
if (
1263+
typeof url === "string" &&
1264+
url.includes("/api/executions/history/obfuscations-anchor-1") &&
1265+
init?.method === "DELETE"
1266+
) {
1267+
return {
1268+
ok: true,
1269+
status: 200,
1270+
json: async () => ({ deleted: true, id: "obfuscations-anchor-1" }),
1271+
};
1272+
}
1273+
return { ok: true, json: async () => ({}) };
1274+
});
1275+
global.fetch = fetchMock as unknown as typeof fetch;
1276+
1277+
render(<DepositPageClient />);
1278+
1279+
const anchorSelect = await screen.findByRole("combobox", {
1280+
name: "Load a previously anchored Obfuscations configuration",
1281+
});
1282+
fireEvent.click(anchorSelect);
1283+
const listbox = await screen.findByRole("listbox");
1284+
expect(within(listbox).getByText("Billing withhold")).toBeInTheDocument();
1285+
1286+
fireEvent.click(
1287+
within(listbox).getByRole("button", {
1288+
name: "Delete Billing withhold",
1289+
}),
1290+
);
1291+
1292+
await waitFor(() =>
1293+
expect(fetchMock).toHaveBeenCalledWith(
1294+
"/api/executions/history/obfuscations-anchor-1",
1295+
expect.objectContaining({ method: "DELETE" }),
1296+
),
1297+
);
1298+
await waitFor(() =>
1299+
expect(
1300+
screen.queryByRole("option", { name: /Billing withhold/i }),
1301+
).not.toBeInTheDocument(),
1302+
);
1303+
expect(
1304+
await screen.findByText("Obfuscations anchor deleted."),
1305+
).toBeInTheDocument();
1306+
// Delete must not load the anchor body into the textarea.
1307+
expect(
1308+
(screen.getByLabelText(
1309+
"What to obfuscate or withhold",
1310+
) as HTMLTextAreaElement).value,
1311+
).toBe("");
1312+
});
1313+
12591314
it("anchors the current Obfuscations text into the activity ledger with its name", async () => {
12601315
withAnchorFixtures();
12611316
const fetchMock = jest.fn(async (url: string) => {

uapi/tests/e2e/commercial-mvp.ip-exchange.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ test.describe('commercial MVP IP exchange browser proof', () => {
153153

154154
// Source connection precedes synthesis: the synthesize action stays
155155
// disabled until the route session resolves a repository source.
156-
const synthesizeButton = page.getByRole('button', { name: 'Synthesize options' });
156+
const synthesizeButton = page.getByRole('button', { name: 'Synthesize AssetPack Options' });
157157
await expect(synthesizeButton).toBeEnabled({ timeout: 30_000 });
158158

159159
await synthesizeButton.click();

0 commit comments

Comments
 (0)