fix(org-import): reconcile mode + audit-event emission #137

Merged
claude-ceo-assistant merged 1 commits from fix/org-import-reconcile-and-audit into main 2026-05-08 22:13:20 +00:00

Summary

Closes the additive-import zombie bug — re-running /org/import with a tree shape that reparents same-named roles leaves the prior workspace online because lookupExistingChild's dedupe is parent-scoped (different parent_id → "different" workspace, even when name matches). Caught 2026-05-08 after a dev-tree re-import left 8 orphans co-existing with the new tree on canvas until manual cascade-delete.

Three layers

B — mode: "reconcile" on /org/import

After the import loop, online workspaces whose name matches an imported name but whose id isn't in the result set are cascade-deleted. Default mode "" / "merge" preserves existing additive behavior. Empty-set guards prevent accidental "delete everything" if either array comes up empty.

Helper extraction — WorkspaceHandler.CascadeDelete

Lifted from the existing Delete HTTP handler so OrgImport's reconcile path shares the same teardown sequence (#73 race guard, container stop, volume removal, token revocation, schedule disable, event broadcast). The HTTP Delete handler still inlines the same logic; deduplication tracked as tech-debt follow-up.

C — emitOrgEventstructure_events

Records org.import.started + org.import.completed with mode, created/skipped/reconcile_removed counts, duration_ms, error. Replaces the lost-on-restart stdout-only log shape for an audit-trail surface queryable by SQL. Closes the "what happened at 20:13?" debugging gap that motivated this fix.

Verification (live against local platform)

before:                              after layer A cleanup:    after layer B reconcile:
17 workspaces online                 9 online                  9 online + fake orphan removed
8 orphans from 20:13 import          0 orphans                 reconcile_removed_count: 1
  • Test 1 (default mode): re-import is idempotent, audit events emitted with created_count=0, skipped_count=9
  • Test 2 (reconcile + fake): fake removed, real Infra Lead preserved, response includes reconcile_removed_count: 1
  • Test 3 (idempotent reconcile): 0 removed, no errors

Tests

7 new unit tests:

  • walkOrgWorkspaceNames — flat / nested / spawning:false subtree (still counted) / empty-name skipped
  • emitOrgEvent — success path + DB-error-swallow (telemetry must never fail the request)
  • errString — nil + non-nil

Full handler suite green.

Test plan

  • Local Postgres E2E (3 scenarios verified above)
  • Unit tests pass
  • go build clean
  • Stage B (staging tenant) — N/A, local-platform fix
  • Stage C (real-user action) — covered by Test 2's reconcile-with-fake-orphan flow

Follow-ups (separate tickets)

  • DRY Delete() HTTP handler vs CascadeDelete helper (same logic in two places)
  • §SOP-8 update to canonical dev-sop.md ("persistent structured logging required for state transitions") — drafted in /tmp/rfc-sop-v2.md, awaiting Hongming GO
  • started event fires before YAML is loaded so name is empty when dir is used; emit after parse for completeness

Persona-drift note

Pushing as claude-ceo-assistant per the interim force-merge path; flagged here for the SOP-6 per-phase approval gate work.

🤖 Generated with Claude Code

## Summary Closes the additive-import zombie bug — re-running `/org/import` with a tree shape that reparents same-named roles leaves the prior workspace online because `lookupExistingChild`'s dedupe is parent-scoped (different `parent_id` → "different" workspace, even when name matches). Caught 2026-05-08 after a dev-tree re-import left 8 orphans co-existing with the new tree on canvas until manual cascade-delete. ## Three layers **B — `mode: "reconcile"` on `/org/import`** After the import loop, online workspaces whose name matches an imported name but whose id isn't in the result set are cascade-deleted. Default mode `""` / `"merge"` preserves existing additive behavior. Empty-set guards prevent accidental "delete everything" if either array comes up empty. **Helper extraction — `WorkspaceHandler.CascadeDelete`** Lifted from the existing Delete HTTP handler so OrgImport's reconcile path shares the same teardown sequence (#73 race guard, container stop, volume removal, token revocation, schedule disable, event broadcast). The HTTP Delete handler still inlines the same logic; deduplication tracked as tech-debt follow-up. **C — `emitOrgEvent` → `structure_events`** Records `org.import.started` + `org.import.completed` with mode, created/skipped/reconcile_removed counts, duration_ms, error. Replaces the lost-on-restart stdout-only log shape for an audit-trail surface queryable by SQL. Closes the "what happened at 20:13?" debugging gap that motivated this fix. ## Verification (live against local platform) ``` before: after layer A cleanup: after layer B reconcile: 17 workspaces online 9 online 9 online + fake orphan removed 8 orphans from 20:13 import 0 orphans reconcile_removed_count: 1 ``` - Test 1 (default mode): re-import is idempotent, audit events emitted with `created_count=0, skipped_count=9` - Test 2 (reconcile + fake): fake removed, real Infra Lead preserved, response includes `reconcile_removed_count: 1` - Test 3 (idempotent reconcile): 0 removed, no errors ## Tests 7 new unit tests: - `walkOrgWorkspaceNames` — flat / nested / `spawning:false` subtree (still counted) / empty-name skipped - `emitOrgEvent` — success path + DB-error-swallow (telemetry must never fail the request) - `errString` — nil + non-nil Full handler suite green. ## Test plan - [x] Local Postgres E2E (3 scenarios verified above) - [x] Unit tests pass - [x] go build clean - [ ] Stage B (staging tenant) — N/A, local-platform fix - [ ] Stage C (real-user action) — covered by Test 2's reconcile-with-fake-orphan flow ## Follow-ups (separate tickets) - DRY `Delete()` HTTP handler vs `CascadeDelete` helper (same logic in two places) - §SOP-8 update to canonical dev-sop.md ("persistent structured logging required for state transitions") — drafted in `/tmp/rfc-sop-v2.md`, awaiting Hongming GO - `started` event fires before YAML is loaded so `name` is empty when `dir` is used; emit after parse for completeness ## Persona-drift note Pushing as `claude-ceo-assistant` per the interim force-merge path; flagged here for the SOP-6 per-phase approval gate work. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-ceo-assistant added 1 commit 2026-05-08 22:05:14 +00:00
fix(org-import): reconcile mode + audit-event emission
All checks were successful
CodeQL / Analyze (${{ matrix.language }}) (go) (pull_request) Successful in 1s
CodeQL / Analyze (${{ matrix.language }}) (javascript-typescript) (pull_request) Successful in 2s
CodeQL / Analyze (${{ matrix.language }}) (python) (pull_request) Successful in 1s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 8s
E2E API Smoke Test / detect-changes (pull_request) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 8s
Harness Replays / detect-changes (pull_request) Successful in 7s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 7s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 10s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
CI / Python Lint & Test (pull_request) Successful in 4s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 34s
CI / Canvas (Next.js) (pull_request) Successful in 57s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 56s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m1s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m22s
Harness Replays / Harness Replays (pull_request) Successful in 2m59s
CI / Platform (Go) (pull_request) Successful in 3m20s
3de51faa19
Closes the additive-import zombie bug — re-running /org/import with a
tree shape that reparents same-named roles left the prior workspace
online because lookupExistingChild's dedupe is parent-scoped (different
parent_id → "different" workspace). Caught 2026-05-08 after a dev-tree
re-import left 8 orphans co-existing with the new tree on canvas until
manual cascade-delete.

Three layers in this PR:

- mode="reconcile" on /org/import — after the import loop, online
  workspaces whose name matches an imported name but whose id isn't in
  the result set are cascade-deleted. Default mode "" / "merge"
  preserves existing additive behavior. Empty-set guards prevent
  accidental "delete everything" if either array comes up empty.

- WorkspaceHandler.CascadeDelete extracted as a callable helper from
  the existing Delete HTTP handler so OrgImport's reconcile path shares
  the same teardown sequence (#73 race guard, container stop, volume
  removal, token revocation, schedule disable, event broadcast). The
  HTTP Delete handler still inlines the same logic; deduplication
  tracked as tech-debt follow-up.

- emitOrgEvent(structure_events) records org.import.started +
  org.import.completed with mode, created/skipped/reconcile_removed
  counts, duration_ms, error. Replaces the lost-on-restart stdout-only
  log shape for an audit-trail surface that's queryable by SQL. Closes
  the "what happened at 20:13?" debugging gap that motivated this fix.

Verified live against the local platform: cascade-delete on an old
tree's removed root cleared 8 surviving orphans; mode="reconcile" with
a freshly-INSERTed fake orphan removed exactly the fake; idempotent
re-run of reconcile is a no-op (0 removed, no errors); structure_events
captures every started+completed pair with full payload.

7 new unit tests (walkOrgWorkspaceNames flat/nested/spawning:false/
empty-name; emitOrgEvent success + DB-error-swallow; errString). Full
handler suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-ceo-assistant merged commit c94ead1953 into main 2026-05-08 22:13:20 +00:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#137
No description provided.