feat(layout): forest layout patch β apply targeted changes without pull#784
Open
Gawtier wants to merge 7 commits into
Open
feat(layout): forest layout patch β apply targeted changes without pull#784Gawtier wants to merge 7 commits into
Gawtier wants to merge 7 commits into
Conversation
Declarative layout-as-code for the CLI: pull the live rendering of an environment/team into a versionable forest-layout.yml, diff a local file against the live layout, and apply the changes as an array of JSON-Patch operations to the existing PATCH /api/:domain endpoint. - Pure engine (no IO): rendering->canonical mapper, identity-based diff (match by id then name, idempotent, add->replace->remove), a mirror of the server patch whitelist that pre-validates every op, and YAML (de)serialization with a scope header. - HTTP adapter and scope resolver reuse the toolbelt's existing authenticator / environment / team managers (no new auth). - oclif commands under `forest layout` extending AbstractAuthenticatedCommand. - 41 unit tests covering the mapper, diff, whitelist, YAML round-trip and scope resolver. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦) via patch Extends the layout commands from edit-only to create+edit, across every patchable domain in one pull/diff/apply round. - Add path: keep the client-supplied `id` (generate a uuid only when absent) instead of stripping it β the server requires it on add, and a stable id makes re-apply / cross-env copy idempotent. - Per-type add-value completeness: fill the write-required fields the rendering read omits so an add validates server-side (segments need defaultSortingFieldName/defaultSortingFieldOrder/isVisible/position/columns/ hasColumnsConfiguration + filter|query; dashboards need charts; workflow shells need segmentIds/isVisible/position). - Multi-domain pull/diff/apply: layout + folders + workflows in a single round (new sync orchestrator); apply sends one atomic PATCH per domain. - Workflows domain wired end to end: create/edit a workflow shell incl. the bpmnAwsS3Identifier link (the BPMN artifact authoring stays out of scope). Validated live on a real env: creating a segment + dashboard + workflow from minimal input applies (3 PATCHes), round-trips on re-pull, and reverts cleanly. Not in this change: workspaces/inboxes modeling and name<->id resolution inside complex values (tracked as follow-ups). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the layout engine to the two remaining top-level rendering elements. - Mapper: capture workspaces (with their components; component `options` kept opaque) and inboxes (with collection/segment references) from the rendering. - Diff rules: workspaces (name/icon/position + components add/remove/edit of name/displaySettings/visibility) and inboxes (premium-gated; editable scalars, with type/dispatchRule/sortingFields/collectionId/segmentId as add-only). Component/inbox `options`-style values ride opaquely on add. - addDefaults fill the write-required fields the read omits (workspace icon + collectionId/components/position; inbox position/canUsersReassign). Validated live: pull round-trips 12 workspaces (250 components) + 6 inboxes idempotently; created a workspace, edited an inbox, and created an inbox (type=workflow), each verified on re-pull and reverted cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦load) Closes the last gap: a workflow's internal elements (steps) can now be declared in the file and pushed, not just the shell. - workflow-bpmn.ts: compile a declarative `steps` graph (read/update/action/ condition/guidance/escalation/mcp/load-related/end) into BPMN XML β ported and verified against the exact request trace the Forest UI makes. - LayoutManager: `uploadWorkflowBpmn` mirrors the UI β POST generate-presigned-request (forest-rendering-id header), multipart-upload the BPMN to the presigned S3 URL, read `x-amz-version-id`. - apply: when a workflow carries `steps`, after the shell patch it compiles + uploads the BPMN and links the version via `bpmnAwsS3Identifier`. `steps` is stripped from the shell value (authoring input, not a server field). diff shows the planned compile/upload. Validated live: created a workflow with a 3-step graph (read β guidance β end) β shell created, BPMN compiled + uploaded to S3, `bpmnAwsS3Identifier` linked, verified on re-pull, reverted cleanly. 67 unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦elds as removals Two idempotency fixes surfaced by re-applying a file: - Workflow BPMN: before uploading, compile the step graph and byte-compare it against the version currently stored (download via the presigned GET URL). Unchanged graphs are skipped β no new S3 version, no `bpmnAwsS3Identifier` patch. `diff`/`apply` only report a (re)upload when the BPMN actually differs. Relies on the compiler being deterministic + S3 storing the object verbatim. - Scalar diff: a field absent from the local file now means "leave alone" (partial-authoring / patch semantics) instead of "remove" β which removes the misleading "this field cannot be removed" warnings when a hand-authored item omits server-managed fields. Clear a value explicitly with `null`. Validated live: applying a workflow-with-steps file twice β first run creates the shell + uploads the BPMN; second run is a clean no-op (no re-upload, no warnings, "Nothing to apply"). 68 unit tests (compiler determinism covered). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦ place Addresses review on PR #776 (D1, T1): - D1: matchItems() resolves a local item to a remote one by id, then by name, so a hand-authored item that omits its id updates the existing element instead of being treated as add + remove (silent data loss). Guards against ambiguous matches; header comment corrected. - T1: unit tests for the orchestration core β sync.planWorkflowBpmn idempotency (unchanged / changed / new / unreadable) and stepWorkflows; plus a diff regression test for the name-only edit case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses Macroscope review on #777: - Wrap the presigned S3 BPMN upload in try/catch -> toLayoutApiError, so S3 failures surface as LayoutApiError like every other network call in the class (and are caught by the apply command handler) instead of a raw superagent error. - Remove the unused `removable` flag from ScalarRule (and CHART_PROPS): it was dead (diffScalarLike treats an absent field as "leave alone"). Emitting a remove on omission, as the bot suggested, would reintroduce silent deletion and break partial-authoring. Clearing a scalar is done via an explicit null. Added a test locking the leave-alone behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| if (Array.isArray(parsed.errors) && parsed.errors.length > 0) { | ||
| const first = parsed.errors[0]; | ||
|
|
||
| return first.detail || first.message || JSON.stringify(body); |
There was a problem hiding this comment.
π’ Low layout/errors.ts:29
first.detail throws a TypeError when parsed.errors[0] is null or undefined, causing malformed API responses to crash the error handler instead of returning a detail string. Consider using optional chaining (first?.detail and first?.message) to fall back to JSON.stringify(body).
Suggested change
| return first.detail || first.message || JSON.stringify(body); | |
| return first?.detail || first?.message || JSON.stringify(body); |
π Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/services/layout/errors.ts around line 29:
`first.detail` throws a `TypeError` when `parsed.errors[0]` is `null` or `undefined`, causing malformed API responses to crash the error handler instead of returning a detail string. Consider using optional chaining (`first?.detail` and `first?.message`) to fall back to `JSON.stringify(body)`.
Evidence trail:
src/services/layout/errors.ts lines 20-36 at REVIEWED_COMMIT: `extractDetail(body: unknown)` casts body to a typed shape without runtime validation. Line 26 checks `Array.isArray(parsed.errors) && parsed.errors.length > 0` but doesn't guard against nullish array elements. Line 27 `const first = parsed.errors[0]` can be null/undefined for inputs like `{errors: [null]}`. Line 29 `first.detail` would throw TypeError. The function is called from `toLayoutApiError` (line 47) with `body` sourced from HTTP response bodies (`superagentError.response?.body`), making the trigger condition reachable with malformed API responses.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
forest layout patchβ a new command that sends a JSON-Patch array directly to an environment without requiring apullfirstadd /folders/-from the whitelist (server rejects it with a business rule error; the correct path is/folders/<mainId>/children/-)Motivation
The existing
pull β diff β applycycle always fetches the full layout before writing. For the forest-layout AI skill, this is unnecessary for pure creation: adding a workspace, a workflow shell, or a dashboard doesn't require knowing the current state. This command exposes the underlyingPATCH /api/:domainendpoint directly.Usage
Input format β a JSON object keyed by domain:
{ "layout": [{ "op": "add", "path": "/workspaces/-", "value": {...} }], "workflows": [{ "op": "replace", "path": "/workflows/<id>/isVisible", "value": false }], "folders": [{ "op": "add", "path": "/folders/<mainId>/children/-", "value": {...} }] }Behaviours
layout β folders β workflows), consistent withapplyexplainApiErrorreceives only the ops for the domain that failed, so path correlation is accurate--dry-runβ prints ops without sending (scope resolution is also skipped)/folders/-whitelist removalValidated against the deployed server:
add /folders/-is always rejected with"Cannot create a folder without adding it to the main folder". The correct path is/folders/<mainId>/children/-. Removing the entry from the whitelist surfaces this constraint as a client-side error before sending anything.Testing
Validated live against BaaSDemo/Development:
displayName,icon, columns)selectARecord+sourceWorkspaceComponentId)scopes)inbox)multipleDashboards)layout+workflowsin one call)Fixes PRD-592
Note
Add
forest layout pull,diff, andapplycommands for targeted layout patchingforest layout:pullfetches the live rendering and writes aforest-layout.yml;diffshows what changes would be applied;applycomputes a JSON-Patch plan and pushes it to the target environment/team without a full pull.ScopeErrorfor actionable failures.forestheader and tolerant parsing, using the newyamldependency.applysends atomic per-domain PATCH requests to the server; a mid-run failure leaves domains applied up to the failure point with no rollback.π Macroscope summarized 36db700. 15 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
ποΈ Filtered Issues
No issues evaluated.