Policies: enforce on upload or export (#6614)

Follow-up to #6604 (merged). Builds the Security policy out so it
actually enforces, driven from the editor.

## What it does
- **Run on upload or export** — a single choice in the wizard: enforce
when a file is uploaded, or just before it's exported.
- **Output** — enforced result is a **new version** of the file
(default) or a **new file**, with optional filename
prefix/suffix/auto-number ("Output filename" subsection; auto-number
only for new files).
- **Export enforcement** — exporting an export-mode file runs the policy
first and downloads the enforced result; never hard-blocks (on failure
the original downloads). For new-version policies the in-editor file is
versioned too. Covers every export path incl. multi-file ZIP. A toast
(glowing in the policy's accent while it runs) reports progress and
fades after ~10s.
- **Affordances** — a freshly enforced file briefly glows its policy
accent and carries a shield badge.
- **Config tidy-up** — removed the unwired Security setting fields + the
wizard's review step; "Upgrade to enterprise" on locked categories;
category accent in the detail/wizard headers.

## Notes
- Builds on the manual-only (client-driven) policy model from #6587
(`trigger: null`, metadata in `output.options`), adding the `runOn`
field + export-time enforcement.
- The page-editor merge-export (no single source file) enforces +
downloads but doesn't version in place.

## Verification
typecheck (core + proprietary), eslint, prettier; proprietary suite
(105) green; flows checked in-app.
This commit is contained in:
Reece Browne
2026-06-11 18:12:01 +01:00
committed by GitHub
parent 5fa5e12c64
commit 9ee0bc4b32
31 changed files with 529 additions and 123 deletions
@@ -5,7 +5,9 @@ describe("policy definitions integrity", () => {
it("every category has a matching config entry", () => {
for (const cat of POLICY_CATEGORIES) {
expect(POLICY_CONFIG[cat.id], `config for ${cat.id}`).toBeDefined();
expect(POLICY_CONFIG[cat.id].fields.length).toBeGreaterThan(0);
// A category may have no policy-level setting fields (e.g. Security's were
// all unwired and removed); fields is required but can be empty.
expect(Array.isArray(POLICY_CONFIG[cat.id].fields)).toBe(true);
expect(POLICY_CONFIG[cat.id].rules.length).toBeGreaterThan(0);
// Every preset seeds a real, non-empty pipeline (the category→steps map).
expect(