Unify CPQ Agent and Admin V3 catalog writes
DEA-6655 moves product and custom-attribute writes onto shared draft-write seams so the Config Agent and Admin V3 UI stop defining catalog persistence differently.
A new shared dealops3/attributes/write.ts module validates and assembles org custom-attribute documents.
It includes 12 new unit tests covering add, remove, reserved keys, duplicates, category checks, and system-key collisions.
The Config Agent now calls the same catalogProduct/write.ts product payload builder and draft upsert helper used by Admin-facing write code.
Attribute writes are unified for both tRPC publishing and agent draft staging.
The shared seam is intentionally publish-agnostic.
Admin V3 still publishes immediately; the agent still stages drafts for review, approval, receipts, and later publish.
clean pnpm run typecheck for apps/server.
341 pass full Config Agent Jest suite, plus 12 pass new attribute write specs.
1. Why this exists
Config Agent persisted catalog changes through near-duplicate private writers.
- Private product
writeData. - Private
upsertDraftRow. - Inline bulk
catalogProduct.upsert. - Weaker custom-attribute validation than the Admin Attributes UI.
Both surfaces share the definition of a valid catalog write.
- Product writes use exported
writeDataandupsertDraftProductWith. - Attribute writes use
attributes/write.ts. - Each caller keeps its own publish policy.
publishManualSkuChange or saveSpecVersion + createGlobalVersion. The PR shares the lower draft-write layer instead.
2. What changes
stageCreateCatalogProduct
stageUpdateCatalogProduct
stageDeleteCatalogProduct
bulkStageCatalogProducts
writeData
upsertDraftProductWith
catalogProduct row in state: "draft".
Receipts hash the same payload that is written.
| File | Meaningful change | Reviewer read |
|---|---|---|
dealops3/catalogProduct/write.ts |
Exports writeData and documents why the agent needs it. |
This is the product payload single source of truth. |
configAgent/catalogProductContext.ts |
Deletes private writeData, private upsertDraftRow, and inline bulk upsert. |
All create, update, delete, and bulk staging now use the shared helper. |
configAgent/tools/catalogProductTableTools.ts |
Attribute tools switch to shared validation and document assembly. | Agent now rejects the same invalid custom attributes as Admin V3. |
The agent verification path remains stable because validate-time plans, persisted-row receipts from readStagedCatalogWriteFingerprints, and the actual draft write all hash the same shared writeData shape.
The PR notes canonicalized keys and concrete pricebookId values keep the column payload byte-identical.
v3Admin.updateAttributeSchema
Full-array replace, then publish.
assertValidCustomAttributeDefs
buildOrgAttributesDoc
addCustomAttribute
deleteCustomAttribute
Incremental add/remove, then stage draft.
AttributeValidationErrorassertValidCustomAttributeDefswithAddedCustomAttributewithRemovedCustomAttributebuildOrgAttributesDoc
- No
productIdorpricebookId. - No duplicate custom keys.
- No collision with system attributes.
- Every persisted custom attribute must have
category: "custom".
- tRPC maps
AttributeValidationErrortoBAD_REQUEST. - Agent tools return
{ success: false, error }. - Unexpected errors still propagate as server faults.
attributes/reader.ts now exports RESERVED_KEYS and orgSchemaFile so write validation and full-doc parsing use the canonical definitions.
The branch carries earlier routing work around approved product writes in orchestrator.ts and productCatalogAgent.ts.
Reviewer note says this predates the unification work but is included because it is the branch base.
Approved non-file mutations now force directMutation capability even if the model selected a file action.
New test covers the case where a plain approved product create is misclassified as fileAdd.
Two additive tsx scripts were swept in from local development.
They are guarded against accidental remote DB writes unless --allow-remote is passed.
scripts/createLocalPreviewOpportunity.tsscripts/syncV3FlowSpecToV2.tsconfigAgent/__tests__/productCatalogAgentRouting.spec.tsconfigAgent/agents/orchestrator.tsconfigAgent/agents/productCatalogAgent.ts
3. How it works
Agent plan builders still compute identity, content, deletion state, and pricebookId.
Persistence now funnels through:
upsertDraftProductWith(
ctx.prisma,
ctx.orgId,
ctx.userId,
plan.skuId,
plan.identity,
plan.content,
false,
plan.pricebookId,
)
The agent creates the proposed AttributeDef, merges it purely, validates the full list, then stages a draft.
const doc = await buildOrgAttributesDoc(
orgSlug,
withAddedCustomAttribute(orgData, newAttr).attributes,
);
await saveDraft(orgSlug, userId, 'attributes', doc);
Deletion only removes custom attributes. A system attribute with the same key is treated as not removable.
const { doc, removed } =
withRemovedCustomAttribute(orgData, key);
if (!removed) {
return { success: false, error: 'not found' };
}
| Surface | Shared code used | What happens after shared write assembly |
|---|---|---|
| Admin V3 Attributes tRPC | buildOrgAttributesDoc |
Saves spec version, creates global version, deletes draft, returns merged system + custom schema. |
| Config Agent attribute tools | withAddedCustomAttribute, withRemovedCustomAttribute, buildOrgAttributesDoc |
Saves attributes draft for review and approval. |
| Config Agent product tools | writeData, upsertDraftProductWith |
Upserts draft catalogProduct rows and keeps receipt fingerprints aligned. |
4. What it doesn't change
- No new REST route; this stays inside Dealops 3 server code and existing tRPC/Admin flows.
- No change to the Admin V3 immediate-publish policy for attribute schema edits.
- No change to the Config Agent draft-first review → approve → publish flow.
- No dead-code removal for
saveOrgAttributesor unused legacy tool sets; the PR leaves that for follow-up. - No change to
upsertConfirmedProduct2ForAgentbecause it writesCrmProduct, not catalog product rows. - No Prisma migration or schema change is included.
5. Risks / rollback / open questions
loadOrgAttributes to stamp orgId and orgName, and falls back to the org slug if that read fails. That preserves existing handler behavior, but reviewers should confirm this fallback is acceptable for agent draft writes too.
Product rollback is straightforward: restore the agent-local writer and inline bulk upsert, then unexport writeData if no longer needed.
Attribute rollback means moving validation back into updateAttributeSchema.ts and restoring the agent's weaker checks.
The two local dev helper scripts are additive but unrelated to DEA-6655.
If the PR should stay narrowly scoped, they can be dropped without affecting the write unification.
Existing Config Agent catalog verification specs pass unchanged, including exact readback and fingerprint assertions.
New focused tests cover the shared attribute write module directly.