Skip to content

fix(policies): create draft version on policy regenerate instead of overwriting published - #3471

Merged
tofikwest merged 4 commits into
mainfrom
tofik/cs-766-bug-policy-regeneration-changes-the
Jul 21, 2026
Merged

tofikwest merged 4 commits into
mainfrom
tofik/cs-766-bug-policy-regeneration-changes-the

Conversation

@tofikwest

@tofikwest tofikwest commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When regenerating a policy, the published version's content is overwritten immediately. This bypasses the approval workflow that should gate any content changes. Regenerating a published policy currently mutates its live content, signedBy metadata, and currentVersionId directly without creating a draft for review.

Root cause

updatePolicyInDatabase in update-policy-helpers.ts rewrites policy.content, currentVersionId, and signedBy on the live published policy record without changing status or requiring approval. Every other content-change path (createVersion, submitForApproval, acceptChanges) creates a draft version first and only publishes on explicit approval; regenerate was the sole exception.

Fix

For published or needs_review policies, regenerate now creates a new draft version (incrementing the version number) with the regenerated content, leaving the published version's content, currentVersionId, signedBy, and status unchanged. This mirrors the existing approval workflow: draft versions are created, then approved, then published with the signing flow triggered for all employees.

Explicitly NOT touched

  • Policies not yet published (status: draft)
  • The signing flow (runs only on explicit publish, unchanged)
  • Approval/rejection workflows
  • Version history or deletion logic

Verification

Added regression test asserting that regenerating a published policy creates a new draft version without modifying the published content or status. Existing policy regenerate and versioning unit tests pass locally ✅

Fixes CS-766


Summary by cubic

Regenerating a policy now creates a draft version for published/needs_review policies and preserves live content and signatures; draft policies are updated in place so the editor shows the new content immediately. Aligns with CS-766 by restoring the approval flow; publishing the draft triggers the signing flow as expected.

  • Bug Fixes
    • Published/needs_review: create a new draft version; keep published content, currentVersionId, signedBy, PDFs, and existing versions unchanged.
    • Draft: overwrite the current draft version and sync content and draftContent; do not append a new version; clear stale PDF references and switch displayFormat to EDITOR if the draft was a PDF.
    • Stop deleting existing versions and their PDFs (no S3 deletes).
    • Add retry on version creation to avoid unique-key races.
    • Update UI copy: toast says “New draft version created for review” and the dialog explains the new draft flow.
    • Add tests for both published and draft paths, including PDF-to-editor migration.

Written for commit cadbbe1. Summary will update on new commits.

Review in cubic

…verwriting published

## Problem

When regenerating a policy, the published version's content is overwritten immediately. This bypasses the approval workflow that should gate any content changes. Regenerating a published policy currently mutates its live content, signedBy metadata, and currentVersionId directly without creating a draft for review.

## Root cause

updatePolicyInDatabase in update-policy-helpers.ts rewrites policy.content, currentVersionId, and signedBy on the live published policy record without changing status or requiring approval. Every other content-change path (createVersion, submitForApproval, acceptChanges) creates a draft version first and only publishes on explicit approval; regenerate was the sole exception.

## Fix

For published or needs_review policies, regenerate now creates a new draft version (incrementing the version number) with the regenerated content, leaving the published version's content, currentVersionId, signedBy, and status unchanged. This mirrors the existing approval workflow: draft versions are created, then approved, then published with the signing flow triggered for all employees.

## Explicitly NOT touched

- Policies not yet published (status: draft)
- The signing flow (runs only on explicit publish, unchanged)
- Approval/rejection workflows
- Version history or deletion logic

## Verification

Added regression test asserting that regenerating a published policy creates a new draft version without modifying the published content or status. Existing policy regenerate and versioning unit tests pass locally ✅
@linear

linear Bot commented Jul 21, 2026

Copy link
Copy Markdown

CS-766

@vercel

vercel Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jul 21, 2026 10:58pm
comp-framework-editor Ready Ready Preview, Comment Jul 21, 2026 10:58pm
portal Ready Ready Preview, Comment Jul 21, 2026 10:58pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

All reported issues were addressed across 3 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.

Linked issue analysis

Linked issue: CS-766: [Bug] - Policy Regeneration, changes the published version

Status Acceptance criteria Notes
✅ Regenerating a policy creates a new version in draft status (does not overwrite the published policy's content, currentVersionId, signedBy, or PDFs) update-policy-helpers.ts now creates a new policyVersion in a transaction and intentionally avoids updating the live policy row. The regression test update-policy-helpers.spec.ts asserts a new version is appended and that the policy row is not mutated; UI copy and toast were updated to reflect 'draft created for review'.
⚠️ The new draft version can be approved through the normal approval workflow The PR preserves the approval workflow by creating a draft version and explicitly states it does not change approval/rejection logic, but there is no explicit test in this PR that exercises approving the newly created draft version.
⚠️ Publishing the new version triggers the policy signing flow for all employees in the org The PR states the signing flow is unchanged and that publishing the draft will re-trigger signing, and it avoids touching publishing/signing code here. However, this PR does not include a test that publishes the new draft and asserts the signing flow runs, so verification is indirect (claim + code avoidance) rather than explicit.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/policies/update-policy-helpers.ts Outdated
Address review findings.
@tofikwest

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@vercel
vercel Bot temporarily deployed to Preview – portal July 21, 2026 20:46 Inactive
@vercel
vercel Bot temporarily deployed to Preview – app July 21, 2026 20:46 Inactive
@cubic-dev-ai

cubic-dev-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 3 files

Confidence score: 5/5

  • In apps/api/src/trigger/policies/update-policy-helpers.ts, POLICY_VERSION_CREATE_RETRIES duplicates PoliciesService.versionCreateRetries, so these values can drift and cause inconsistent retry behavior between trigger and service paths after future tuning; centralize this constant in a shared source (or reference the service value directly) before merging to prevent subtle reliability mismatches.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/trigger/policies/update-policy-helpers.ts">

<violation number="1" location="apps/api/src/trigger/policies/update-policy-helpers.ts:72">
P3: The retry constant `POLICY_VERSION_CREATE_RETRIES` (line 72) duplicates `PoliciesService.versionCreateRetries` with a mirror comment but no shared source. If the service-layer retry count is tuned, the trigger path's count won't follow. Consider importing or referencing the value from `PoliciesService`, or extracting a shared constant, to prevent silent drift.</violation>
</file>

Linked issue analysis

Linked issue: CS-766: [Bug] - Policy Regeneration, changes the published version

Status Acceptance criteria Notes
✅ Regenerating a PUBLISHED policy creates a new DRAFT version instead of overwriting the published version update-policy-helpers.ts implements a published-path that creates a new policyVersion (incrementing the version) and leaves the published row unchanged; the spec contains a test that asserts a new version is created and the published row is not mutated.
✅ Published policy live fields (content, currentVersionId, signedBy, PDFs) are preserved and existing versions are not deleted when regenerating tests assert deleteMany is not called and policy update calls do not include signedBy/currentVersionId/content; code no longer deletes PDFs or overwrites the live policy for published status.
✅ Regenerating a DRAFT policy overwrites the current draft version in place and syncs policy.content/draftContent (no unattached extra version) the code path for PolicyStatus.draft updates the current version and policy.content/draftContent in a transaction, and the spec contains a test asserting version update, no new create, and policy content sync.
⚠️ The new draft version can be approved (follows the normal approval workflow) PR states regeneration now mirrors the existing approval workflow and creates a draft for review, but there is no direct test in this diff that exercises approving the newly created draft or end-to-end approval behavior.
⚠️ Publishing the new version triggers the policy signing flow for all employees in the org PR explicitly states the signing flow is unchanged and runs only on explicit publish, but this change does not include a test or code exercising the signing flow on publish to prove end-to-end behavior in this PR.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/policies/update-policy-helpers.ts
// Mirror PoliciesService.versionCreateRetries: retry version creation on a
// unique-constraint race so two near-simultaneous regenerations don't collide
// on the [policyId, version] key.
const POLICY_VERSION_CREATE_RETRIES = 3;

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The retry constant POLICY_VERSION_CREATE_RETRIES (line 72) duplicates PoliciesService.versionCreateRetries with a mirror comment but no shared source. If the service-layer retry count is tuned, the trigger path's count won't follow. Consider importing or referencing the value from PoliciesService, or extracting a shared constant, to prevent silent drift.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/trigger/policies/update-policy-helpers.ts, line 72:

<comment>The retry constant `POLICY_VERSION_CREATE_RETRIES` (line 72) duplicates `PoliciesService.versionCreateRetries` with a mirror comment but no shared source. If the service-layer retry count is tuned, the trigger path's count won't follow. Consider importing or referencing the value from `PoliciesService`, or extracting a shared constant, to prevent silent drift.</comment>

<file context>
@@ -67,6 +66,11 @@ export async function fetchOrganizationAndPolicy(
+// Mirror PoliciesService.versionCreateRetries: retry version creation on a
+// unique-constraint race so two near-simultaneous regenerations don't collide
+// on the [policyId, version] key.
+const POLICY_VERSION_CREATE_RETRIES = 3;
+
 export async function updatePolicyInDatabase(
</file context>
Fix with cubic

@tofikwest

Copy link
Copy Markdown
Contributor Author

re: POLICY_VERSION_CREATE_RETRIES duplicates the service-layer retry setting and can drift, so centralize it to a shared source.
→ Maintainability suggestion, not a defect introduced or exposed by this diff. The service value versionCreateRetries=3 is a private readonly instance field (apps/api/src/policies/policies.service.ts:73) that genuinely cannot be imported, and no shared policy constants file exists in apps/api, so the trigger helper cannot reference it directly. The duplicated constant (update-policy-helpers.ts:72) already equals 3 and carries an explicit mirror comment (:69-71). The retry count is best-effort P2002 unique-constraint-race handling; any future divergence would only change retry attempts under a rare collision, never produce incorrect behavior. Cubic itself frames it as 'can drift over time' — a hypothetical future maintenance risk, not a present bug. No correctness impact in the current code.

Address review findings.
@tofikwest

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@vercel
vercel Bot temporarily deployed to Preview – app July 21, 2026 21:04 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portal July 21, 2026 21:04 Inactive
@cubic-dev-ai

cubic-dev-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 3 files

Confidence score: 2/5

  • apps/api/src/trigger/policies/update-policy-helpers.ts was fixed, but the duplicated helper in apps/app/src/trigger/policies/update-policy-helpers.ts still appears to run during app Trigger policy regeneration, which can overwrite the published policy version and clear signatures for users. Update the app-side copy (or consolidate to a shared helper) and verify Trigger-driven regeneration preserves published versions/signatures before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/trigger/policies/update-policy-helpers.ts">

<violation number="1" location="apps/api/src/trigger/policies/update-policy-helpers.ts:153">
P1: Policy regeneration invoked through the app Trigger task still overwrites the published version and clears signatures, because only the API copy of this duplicated helper changed. Update or consolidate `apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts` too; according to linked Linear issue CS-766, regeneration must create a draft for approval rather than mutate the published policy.</violation>
</file>

Linked issue analysis

Linked issue: CS-766: [Bug] - Policy Regeneration, changes the published version

Status Acceptance criteria Notes
✅ Regenerating a published policy creates a new draft version instead of overwriting the published version updatePolicyInDatabase now appends a new policyVersion with the next version number for published/needs_review policies; tests assert a new version is created and its content/changelog are correct.
✅ Regenerating a published policy does not mutate the live published policy row (content, currentVersionId, signedBy, pdfs, existing versions) The published path no longer updates the policy row to overwrite content/currentVersionId/signedBy; tests explicitly assert the tx.policy.update calls do not contain those properties.
✅ Regenerating a DRAFT policy overwrites the current draft version in-place and clears stale PDF/displayFormat so the editor shows regenerated content For draft policies the code updates the current version and policy content/draftContent, clears pdfUrl and sets displayFormat to 'EDITOR'; tests verify version update, no extra version creation, and clearing of PDF/displayFormat.
⚠️ Publishing the new draft triggers the policy signing flow for all employees in the org The PR states the signing flow is unchanged and that publishing continues to trigger signing, but this diff does not add/tests the publish-to-signing behavior itself, so evidence is limited to the author’s statement and unchanged signing flow comment.
✅ User-facing messaging updated to indicate regeneration creates a draft for review UI toast and dialog copy were updated to inform users that regeneration creates a new draft version for review and preserves published signatures.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

});
const nextVersion = (latestVersion?.version ?? 0) + 1;

await tx.policyVersion.create({

@cubic-dev-ai cubic-dev-ai Bot Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Policy regeneration invoked through the app Trigger task still overwrites the published version and clears signatures, because only the API copy of this duplicated helper changed. Update or consolidate apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts too; according to linked Linear issue CS-766, regeneration must create a draft for approval rather than mutate the published policy.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/trigger/policies/update-policy-helpers.ts, line 153:

<comment>Policy regeneration invoked through the app Trigger task still overwrites the published version and clears signatures, because only the API copy of this duplicated helper changed. Update or consolidate `apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts` too; according to linked Linear issue CS-766, regeneration must create a draft for approval rather than mutate the published policy.</comment>

<file context>
@@ -75,79 +79,99 @@ export async function updatePolicyInDatabase(
+          });
+          const nextVersion = (latestVersion?.version ?? 0) + 1;
+
+          await tx.policyVersion.create({
+            data: {
+              policyId,
</file context>
Fix with cubic

@tofikwest

Copy link
Copy Markdown
Contributor Author

re: The duplicated helper in apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts still runs during app Trigger policy regeneration and can overwrite the published policy version / clear signatures for users.
→ App and API are SEPARATE Trigger.dev projects (apps/api/trigger.config.ts proj_zhioyrusqertqgafqgpj vs apps/app/trigger.config.ts proj_lhxjliiqgcdyqbgtucda). tasks.trigger('update-policy') resolves only within the calling process's own project, so it cannot reach the other project's task. Every LIVE regeneration of an existing policy routes to the FIXED API helper: the 'Regenerate policy' button (PolicyHeaderActions.tsx:111 -> usePolicy.ts:98) calls POST /v1/policies/:id/regenerate (policies.controller.ts:382), which runs tasks.trigger('update-policy') at policies.controller.ts:446 inside the API project -> apps/api/src/trigger/policies/update-policy.ts -> fixed update-policy-helpers.ts. Admin regenerate (admin-policies.controller.ts:182) and framework-driven generation (frameworks.service.ts:808) likewise trigger within the API project. The app-side buggy copy is reachable only through the APP project via triggerPolicyUpdates (onboard-organization-helpers.ts:1324), whose callers are (a) onboarding (onboard-organization.ts:108 -> updateOrganizationPolicies -> 1481) operating on freshly-seeded, unsigned draft policies where deleting versions and clearing signedBy is the correct initial behavior, and (b) generate-full-policies.ts:43, triggered only by regenerateFullPoliciesAction (regenerate-full-policies.ts:24) which has ZERO callers (dead code). The visible 'Regenerate all' button (FullPolicyHeaderActions.tsx:35 -> usePolicyActions.ts:8) posts to /v1/policies/regenerate-all, which does NOT exist in the API (no such route in policies.controller.ts), and FullPolicyHeaderActions is never mounted (only its own .test.tsx imports it). Thus no user regenerating a published/signed policy reaches the app-side copy, and this diff did not change app-side reachability. The duplication is a real maintenance smell (consolidating to a shared helper is good hygiene), but the specific defect claimed is not reachable, so it is not a genuine user-facing defect introduced or exposed by this diff. Residual uncertainty: 'Regenerate all' is clearly intended as a live feature, so if that endpoint/wiring is ever restored it would hit the buggy copy — but that is a separate, pre-existing, currently-dead path, not what this PR fixes or what the finding describes.

@tofikwest

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

No issues found across 3 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Linked issue analysis

Linked issue: CS-766: [Bug] - Policy Regeneration, changes the published version

Status Acceptance criteria Notes
✅ Regenerating a published policy creates a new draft version instead of overwriting the published version updatePolicyInDatabase now appends a new policyVersion with the next version number for published/needs_review policies; tests assert a new version is created and its content/changelog are correct.
✅ Regenerating a published policy does not mutate the live published policy row (content, currentVersionId, signedBy, pdfs, existing versions) The published path no longer updates the policy row to overwrite content/currentVersionId/signedBy; tests explicitly assert the tx.policy.update calls do not contain those properties.
✅ Regenerating a DRAFT policy overwrites the current draft version in-place and clears stale PDF/displayFormat so the editor shows regenerated content For draft policies the code updates the current version and policy content/draftContent, clears pdfUrl and sets displayFormat to 'EDITOR'; tests verify version update, no extra version creation, and clearing of PDF/displayFormat.
⚠️ Publishing the new draft triggers the policy signing flow for all employees in the org The PR states the signing flow is unchanged and that publishing continues to trigger signing, but this diff does not add/tests the publish-to-signing behavior itself, so evidence is limited to the author’s statement and unchanged signing flow comment.
✅ User-facing messaging updated to indicate regeneration creates a draft for review UI toast and dialog copy were updated to inform users that regeneration creates a new draft version for review and preserves published signatures.

Re-trigger cubic

@tofikwest
tofikwest merged commit ff31dbd into main Jul 21, 2026
11 checks passed
@tofikwest
tofikwest deleted the tofik/cs-766-bug-policy-regeneration-changes-the branch July 21, 2026 23:00
claudfuen pushed a commit that referenced this pull request Jul 22, 2026
# [3.106.0](v3.105.0...v3.106.0) (2026-07-22)

### Bug Fixes

* **auth:** attribute API-key mutations to the key's creator, not the org owner ([#3472](#3472)) ([206ed96](206ed96)), closes [hi#risk](https://github.com/hi/issues/risk)
* **deps:** bump adm-zip 0.5.18 -> 0.6.0 in apps/api (Dependabot [#88](https://github.com/trycompai/comp/issues/88)/[#89](https://github.com/trycompai/comp/issues/89)) ([#3462](#3462)) ([300f2a1](300f2a1)), closes [#3451](#3451)
* **deps:** override tar to ^7.5.19 to clear node-tar Dependabot alerts ([#94](https://github.com/trycompai/comp/issues/94)-[#104](https://github.com/trycompai/comp/issues/104)) ([#3466](#3466)) ([8ab5709](8ab5709))
* **deps:** patch engine.io ([#93](#93)) and body-parser ([#92](#92)) Dependabot alerts ([#3464](#3464)) ([94c33b1](94c33b1))
* **isms:** harden internal-audit validation and edge cases from deploy review ([#3473](#3473)) ([c6c7379](c6c7379))
* **policies:** create draft version on policy regenerate instead of overwriting published ([#3471](#3471)) ([ff31dbd](ff31dbd))
* **policies:** delete detached PDF objects when regenerating a draft ([#3474](#3474)) ([ecd1bd0](ecd1bd0))
* **policies:** rename CreateVersionDto to avoid swagger collision with automations ([#3469](#3469)) ([2d5290a](2d5290a))

### Features

* **isms:** internal audit programme, plan and report — clause 9.2 (CS-724) ([#3468](#3468)) ([42e5ebd](42e5ebd)), closes [hi#impact](https://github.com/hi/issues/impact)
@claudfuen

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.106.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

This branch was successfully deployed

3 active deployments
Preview – app — cadbbe10 Deployed Jul 21, 2026 by vercel[bot]
Preview – portal — cadbbe10 Deployed Jul 21, 2026 by vercel[bot]
Preview – comp-framework-editor — cadbbe10 Deployed Jul 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants