@rubytech/create-sitedesk-code 0.1.524 → 0.1.526
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/samba-provision.test.js +46 -1
- package/dist/__tests__/websockify-bind-wiring.test.js +47 -0
- package/dist/index.js +39 -1
- package/dist/samba-provision.js +37 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +8 -7
- package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/memory/PLUGIN.md +10 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +173 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +73 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +194 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +25 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
- package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
- package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
- package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
- package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
- package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
- package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
- package/payload/platform/scripts/vnc.sh +45 -1
- package/payload/server/{chunk-VUXKXOJU.js → chunk-7MGR5M6G.js} +4 -2
- package/payload/server/{manager-4TJK3YOY.js → manager-6XOWHREK.js} +1 -1
- package/payload/server/server.js +10 -4
- package/payload/platform/.docs/search-surface-contract.md +0 -58
- package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
- package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
- package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
- package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
- package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
- package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
- package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
- package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
- package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
- package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
- package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
- package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
- package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
- package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
- package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
- package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
- package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
- package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
- package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
- package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
- package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
- package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
- package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
- package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
- package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
- package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# Task 1942 — Sub-account switcher moves to the sidebar brand head
|
|
2
|
-
|
|
3
|
-
Design doc. Source task: `.tasks/1942-subaccount-switcher-moves-to-sidebar-brand-head.md`.
|
|
4
|
-
|
|
5
|
-
## Goal
|
|
6
|
-
|
|
7
|
-
Open the sub-account picker by clicking the account name at the top of the sidebar, not from the footer kebab. Remove the switcher from the footer menu.
|
|
8
|
-
|
|
9
|
-
## Resolved decisions (brainstorm)
|
|
10
|
-
|
|
11
|
-
1. **Click target:** the whole brand head (`side-brand`: icon + two-line lockup) becomes one button when the switcher is available.
|
|
12
|
-
2. **Affordance:** a small down-chevron renders after the business name to signal the dropdown.
|
|
13
|
-
3. **Close:** re-clicking the head toggles it shut, and a transparent full-screen scrim behind the panel closes it on any outside click. Mirrors the footer-menu scrim it replaces.
|
|
14
|
-
|
|
15
|
-
## Availability gate (unchanged from today)
|
|
16
|
-
|
|
17
|
-
Switcher is available only when `subAccounts.length > 1 && Boolean(onSwitchAccount)`. Unavailable → the head stays a plain non-interactive `<div>`, no chevron, no picker. This is the current `switcherAvailable` gate, moved from `AccountMenu` to `Sidebar`.
|
|
18
|
-
|
|
19
|
-
## Components and flow
|
|
20
|
-
|
|
21
|
-
- **`Sidebar.tsx`** owns `switcherOpen` state. The brand head renders as `<button aria-haspopup="menu" aria-expanded={switcherOpen}>` when available, else the current `<div>`. Click toggles `switcherOpen`; the closed→open transition fires `refreshAccounts?.()` and emits `[admin-ui] op=subaccount-switcher-toggle anchor=brand-head open=<bool> available=<bool> accounts=<N>`. `SubAccountPicker` renders inside `.side-brand`, anchored beneath it, with a sibling scrim that closes on click.
|
|
22
|
-
- **`AccountMenu.tsx`** loses the `switcherAvailable`/`switcherOpen` block, the `SubAccountPicker` import, and the five switcher props. `UserRound` import dropped if otherwise unused.
|
|
23
|
-
- **`globals.css`** gives `.side-brand` `position: relative` and re-anchors `.sub-account-picker-panel` left-aligned under the head (drop the retired header-title `translateX(-50%)` centering). Adds `.side-brand` button-reset styling (transparent, inherit, pointer) and the chevron. Deletes the dead `.side-foot-menu .sub-account-picker-panel` / `-list` overrides.
|
|
24
|
-
|
|
25
|
-
## Collapsed rail (edge, in scope to not break)
|
|
26
|
-
|
|
27
|
-
When `sidebar-collapsed`, existing CSS hides `.side-brand-lockup` (name + chevron), leaving only the icon. The head-button still functions on the icon, but no new collapsed affordance is added (per task out-of-scope). The picker anchors under `.side-brand` in both states.
|
|
28
|
-
|
|
29
|
-
## Testing
|
|
30
|
-
|
|
31
|
-
vitest + jsdom (Node 22). New `Sidebar` test: available → head is a button, click mounts the picker and fires `refreshAccounts`, re-click closes; unavailable → not a button, no picker. `AccountMenu.test.tsx`: no "Switch account", switcher props dropped. `SubAccountPicker.test.tsx` unchanged.
|
|
32
|
-
|
|
33
|
-
## Out of scope
|
|
34
|
-
|
|
35
|
-
`SubAccountPicker` internals, the switch mechanism, other `AccountMenu` items, a dedicated collapsed-rail affordance, maxy-lite, Task 1940's mobile lite tab bar.
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
# Task 1902 — every account-root entry is declared by the component that owns it
|
|
2
|
-
|
|
3
|
-
Design agreed 2026-07-26. Source task: `.tasks/pending/1902-account-owned-entry-declarations-are-incomplete.md`.
|
|
4
|
-
|
|
5
|
-
## The problem this removes
|
|
6
|
-
|
|
7
|
-
`account-schema-owned-dirs.py` unions each plugin's `account-owned-dirs`
|
|
8
|
-
declaration into the account's `SCHEMA.md` `allowed-top-level` block on provision,
|
|
9
|
-
and the standing reconcile
|
|
10
|
-
(`platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`)
|
|
11
|
-
names every root entry outside that block on the `op=strays` line. Three plugins
|
|
12
|
-
declare anything today: `storage-broker/PLUGIN.md:4` (`pages`),
|
|
13
|
-
`memory/PLUGIN.md:4` (`archive`), and
|
|
14
|
-
`premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md:34` (`quoting`).
|
|
15
|
-
|
|
16
|
-
Two classes of entry are outside that mechanism.
|
|
17
|
-
|
|
18
|
-
Platform-written control-plane files at the account root are kept out of the stray
|
|
19
|
-
count by `PROTECTED_TOP_LEVEL`, a hand-maintained constant in the reconcile that
|
|
20
|
-
has been extended once per casualty (`calendar-availability.json` was Task 1877,
|
|
21
|
-
`agents-disabled.json` was Task 1996). It holds seven such files. The mechanism
|
|
22
|
-
that should own them covers directories only, so no plugin can declare a file, and
|
|
23
|
-
`data-portal.json` was never added to either surface and reports as a stray on
|
|
24
|
-
every run.
|
|
25
|
-
|
|
26
|
-
Directories an agent created at an account root are in no declaration and have no
|
|
27
|
-
writer in the shipped tree. Since Task 1899 removed the move, each costs one
|
|
28
|
-
`op=strays` name per five-minute cycle rather than a broken feature. The cost is a
|
|
29
|
-
permanently noisy signal an operator learns to ignore, which is how the next real
|
|
30
|
-
stray gets missed.
|
|
31
|
-
|
|
32
|
-
## Evidence
|
|
33
|
-
|
|
34
|
-
Grep over `platform/plugins`, `platform/scripts`, `platform/services`,
|
|
35
|
-
`platform/templates` and `premium-plugins` for an account-root write of each
|
|
36
|
-
undeclared name:
|
|
37
|
-
|
|
38
|
-
- `e-sign/` has one writer: `platform/plugins/business-assistant/skills/e-sign/SKILL.md:452`
|
|
39
|
-
(`mkdir -p "${ACCOUNT_DIR}/e-sign/<DOC_REF>"`), plus the persisted base and the
|
|
40
|
-
per-signer stamped copies alongside it.
|
|
41
|
-
- `clients`, `workers`, `specs`, `templates`, `sites-staging` and `repo` have no
|
|
42
|
-
writer. By the source task's own rule an entry with no creator in the tree is
|
|
43
|
-
agent work product, recorded as such rather than declared.
|
|
44
|
-
|
|
45
|
-
Writers for the seven protected files and for `data-portal.json`:
|
|
46
|
-
|
|
47
|
-
| Root file | Writer | Declared on |
|
|
48
|
-
|---|---|---|
|
|
49
|
-
| `calendar-availability.json` | `plugins/scheduling/mcp/src/scripts/publish-availability.ts` | scheduling |
|
|
50
|
-
| `data-portal.json` | `plugins/cloudflare/bin/portal-index-push.mjs` | cloudflare |
|
|
51
|
-
| `wa-channel-bindings.json` | `services/claude-session-manager/src/wa-channel-store.ts` | admin |
|
|
52
|
-
| `telegram-channel-bindings.json` | `services/claude-session-manager/src/telegram-channel-store.ts` | admin |
|
|
53
|
-
| `webchat-channel-bindings.json` | `services/claude-session-manager/src/webchat-channel-store.ts` | admin |
|
|
54
|
-
| `canonical-webchat-session.json` | `ui/server/canonical-webchat-override.ts` | admin |
|
|
55
|
-
| `session-titles.json` | `services/claude-session-manager/src/account-title-stores.ts` | admin |
|
|
56
|
-
| `agents-disabled.json` | `ui/server/routes/admin/agents.ts` | admin |
|
|
57
|
-
|
|
58
|
-
Six of the eight are written by a shared platform service or the UI server, not by
|
|
59
|
-
the plugin they are declared on. The session manager and the UI server carry no
|
|
60
|
-
manifest the resolver reads, so their files are declared on `admin`, and the rule
|
|
61
|
-
is reach rather than naming: **a declaration is only present on a brand whose
|
|
62
|
-
payload carries the declaring plugin.** `brand.json` `plugins.excluded` drops the
|
|
63
|
-
directory outright (`packages/create-maxy-code/scripts/bundle.js`), and
|
|
64
|
-
`realagent-code` and `property-administrators` both exclude `telegram` while the
|
|
65
|
-
session manager still constructs `TelegramChannelStore`
|
|
66
|
-
(`services/claude-session-manager/src/index.ts`). Declaring
|
|
67
|
-
`telegram-channel-bindings.json` on the telegram plugin would therefore leave
|
|
68
|
-
those two brands reporting it as a stray forever, which is the failure this task
|
|
69
|
-
exists to remove. `admin` is excluded by no brand.
|
|
70
|
-
|
|
71
|
-
The three remaining declarations sit on the plugin whose own code writes them
|
|
72
|
-
(`scheduling`, `cloudflare`, `business-assistant`). That is self-consistent: when
|
|
73
|
-
such a plugin is excluded, its writer is excluded with it and the file is never
|
|
74
|
-
created, so the declaration is present exactly when the file can be.
|
|
75
|
-
|
|
76
|
-
`setup-account.sh:69` runs `reconcile_all_accounts_owned_dirs "$ACCOUNTS_DIR"` on
|
|
77
|
-
every install, and that sweep calls `merge_owned_dirs_into_schema` for every
|
|
78
|
-
account carrying an `account.json`. A declaration therefore reaches every existing
|
|
79
|
-
account on the next install, not only accounts provisioned after it lands.
|
|
80
|
-
|
|
81
|
-
## Units
|
|
82
|
-
|
|
83
|
-
### 1. `platform/scripts/lib/account-schema-owned-dirs.py`
|
|
84
|
-
|
|
85
|
-
`_parse_plugin_md` gains a second frontmatter key, `account-owned-files`, parsed
|
|
86
|
-
with the same regex-then-`json.loads` shape as `account-owned-dirs` and keyed
|
|
87
|
-
`file` instead of `dir`:
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
account-owned-files: [{"file": "data-portal.json", "description": "..."}]
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
A declaration that fails to parse is skipped and the remaining declarations still
|
|
94
|
-
merge, matching how the dirs key already behaves (`except Exception: pass`).
|
|
95
|
-
|
|
96
|
-
`resolve` returns one list carrying both kinds, each entry gaining a `kind` field
|
|
97
|
-
of `dir` or `file`, so a single ordered union feeds the merge. Dirs keep their
|
|
98
|
-
existing position ahead of files from the same manifest.
|
|
99
|
-
|
|
100
|
-
`merge` unions declared file names into the same `allowed-top-level` block as
|
|
101
|
-
dirs. The block is already a flat list of names that carries `SCHEMA.md`,
|
|
102
|
-
`account.json` and `AGENTS.md`, and `parseAllowedTopLevel` in the reconcile
|
|
103
|
-
compares it against `readdirSync` entries without distinguishing files from
|
|
104
|
-
directories, so no consumer changes. The prune rule is unchanged: an entry the
|
|
105
|
-
resolver no longer targets is dropped when empty or absent and kept-and-logged
|
|
106
|
-
when it holds content, and `_dir_holds_files` already treats a non-empty regular
|
|
107
|
-
file as content.
|
|
108
|
-
|
|
109
|
-
The plugin-owned descriptive region renders a file without the trailing slash a
|
|
110
|
-
directory gets:
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
- `pages/` — Cloudflare Pages deploy staging for the account's public sites.
|
|
114
|
-
- `data-portal.json` — the account's data-portal index, written by portal-index-push.
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
`reconcile` prints `ownedFile=<name> present=<true|false>`, the file twin of the
|
|
118
|
-
existing `ownedDir=` line, so a declaration that has not reached an account is
|
|
119
|
-
visible per name.
|
|
120
|
-
|
|
121
|
-
### 2. Declarations
|
|
122
|
-
|
|
123
|
-
One frontmatter line added per plugin, in the shape the resolver parses:
|
|
124
|
-
|
|
125
|
-
- `plugins/business-assistant/PLUGIN.md` — `account-owned-dirs` gains `e-sign`.
|
|
126
|
-
- `plugins/cloudflare/PLUGIN.md` — `account-owned-files` gains `data-portal.json`.
|
|
127
|
-
- `plugins/scheduling/PLUGIN.md` — `calendar-availability.json`.
|
|
128
|
-
- `plugins/admin/PLUGIN.md` — `wa-channel-bindings.json`,
|
|
129
|
-
`telegram-channel-bindings.json`, `webchat-channel-bindings.json`,
|
|
130
|
-
`canonical-webchat-session.json`, `session-titles.json`, `agents-disabled.json`.
|
|
131
|
-
|
|
132
|
-
The resolver reads every `$PROJECT_DIR/plugins/*/PLUGIN.md` present in the
|
|
133
|
-
installed tree, with only premium bundles gated by `shipsPremiumBundles`. A brand
|
|
134
|
-
that excludes a plugin has no manifest to read, which is why the six
|
|
135
|
-
service-written files sit on `admin`: every brand then allows all nine names,
|
|
136
|
-
which is what `PROTECTED_TOP_LEVEL` did for seven of them.
|
|
137
|
-
|
|
138
|
-
### 3. `platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`
|
|
139
|
-
|
|
140
|
-
`PROTECTED_TOP_LEVEL` drops the seven file entries and keeps `.git`, `.claude`,
|
|
141
|
-
`.quarantine`, `SCHEMA.md`, `account.json` and `AGENTS.md`. Its comment is
|
|
142
|
-
rewritten to state the new division: the constant holds the account's own
|
|
143
|
-
machinery, which exists whatever the schema says, and every feature file is held
|
|
144
|
-
by the declaration that owns it.
|
|
145
|
-
|
|
146
|
-
The behavioural consequence, stated rather than hidden: those seven files are now
|
|
147
|
-
kept out of the stray count by the account's `SCHEMA.md`, not by a constant. An
|
|
148
|
-
account whose schema was never re-merged reports them. The sweep in
|
|
149
|
-
`setup-account.sh` closes that window on the install that ships this change, and
|
|
150
|
-
the `ownedFile=` reconcile line names any account it has not reached.
|
|
151
|
-
|
|
152
|
-
### 4. Tests
|
|
153
|
-
|
|
154
|
-
`platform/scripts/__tests__/account-schema-owned-dirs.test.sh` gains, on a
|
|
155
|
-
synthetic tree in the style of the existing shape-b case:
|
|
156
|
-
|
|
157
|
-
- a plugin declaring `account-owned-files: [{"file": "x.json", ...}]` puts `x.json`
|
|
158
|
-
in the merged `allowed-top-level` block, describes it in the plugin-owned region
|
|
159
|
-
without a trailing slash, and reports `ownedFile=x.json present=true` from
|
|
160
|
-
`reconcile`;
|
|
161
|
-
- a plugin whose `account-owned-files` value is malformed JSON is skipped while a
|
|
162
|
-
sibling plugin's valid declaration still merges;
|
|
163
|
-
- the merge stays byte-identical on a second run with a file declaration present.
|
|
164
|
-
|
|
165
|
-
**Mutation check.** Drop the files branch from the resolver and the new cases must
|
|
166
|
-
fail. Revert.
|
|
167
|
-
|
|
168
|
-
`platform/services/claude-session-manager/src/__tests__/account-dir-schema-reconcile.test.ts`
|
|
169
|
-
carries two tests that prove the code belt: `never counts platform-written
|
|
170
|
-
control-plane root files absent from the allowed block` and `does not count the
|
|
171
|
-
disabled store itself as a stray`. Both seed an account whose `SCHEMA.md` lists no
|
|
172
|
-
such file, which after this change is a stale account rather than a current one.
|
|
173
|
-
They are rewritten to seed the schema a reprovisioned account carries, listing the
|
|
174
|
-
declared file names, and then assert the same outcome: the control-plane files are
|
|
175
|
-
absent from `strayNames` and a genuine operator stray alongside them is still
|
|
176
|
-
counted and named. That is the surface the reconcile reads, so the rewritten tests
|
|
177
|
-
prove the delivered behaviour rather than the retired constant.
|
|
178
|
-
|
|
179
|
-
### 5. The region parsers and the doc
|
|
180
|
-
|
|
181
|
-
Three readers parse the plugin-owned region and each anchors on the trailing
|
|
182
|
-
slash inside the backticks: `platform/lib/account-schema-regions/src/index.ts:65`
|
|
183
|
-
(shared by the `/data` page grouping and the reconcile's parity audit) and
|
|
184
|
-
`platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs:42-43` (the public
|
|
185
|
-
data-portal exposure set, which keeps its own copy under a parity test). A file
|
|
186
|
-
line therefore yields no bucket in any of them, which is the required behaviour
|
|
187
|
-
and needs no code change. It is pinned by a case in
|
|
188
|
-
`platform/ui/server/lib/__tests__/account-schema-regions.test.ts`, because a
|
|
189
|
-
future edit relaxing that regex would put a control-plane file in front of a
|
|
190
|
-
client.
|
|
191
|
-
|
|
192
|
-
`.docs/data-portal-folder-index.md` is the authoritative description of how the
|
|
193
|
-
allowed set is built and currently describes plugin-owned as directories. It
|
|
194
|
-
gains the second kind and the trailing-slash rule.
|
|
195
|
-
|
|
196
|
-
## Scope boundaries
|
|
197
|
-
|
|
198
|
-
### In scope
|
|
199
|
-
|
|
200
|
-
Units 1 to 5.
|
|
201
|
-
|
|
202
|
-
### Out of scope
|
|
203
|
-
|
|
204
|
-
- Reprovisioning the laptop's accounts, and the two device checks that depend on
|
|
205
|
-
it (`op=strays` naming only recorded work product, and the negative check that
|
|
206
|
-
removing one declaration makes its entry reappear). Both need this change
|
|
207
|
-
published and installed, and publish awaits an explicit operator command. Task
|
|
208
|
-
`2027-reprovision-laptop-accounts-after-1902-declarations.md`.
|
|
209
|
-
- Declaring `clients`, `workers`, `specs`, `templates`, `sites-staging` or `repo`.
|
|
210
|
-
No shipped component writes them, so declaring them would make the allowed list
|
|
211
|
-
a record of what happens to exist rather than of what is owned. They continue to
|
|
212
|
-
appear in `op=strays`, which is the correct report for debris.
|
|
213
|
-
- Relocating agent work product into schema buckets. Task 1894 covers the glsmith
|
|
214
|
-
case.
|
|
215
|
-
- Teaching `fs-schema-guard.sh` to tell a declared file from an operator bucket.
|
|
216
|
-
The guard reads the same `allowed-top-level` fence as an allow-list for
|
|
217
|
-
`Write`/`Edit`/`NotebookEdit`, so declaring these eight files makes them
|
|
218
|
-
agent-writable where they were blocked before. Measured: the same envelope
|
|
219
|
-
returns exit 2 against the pre-merge schema and exit 0 against the merged one.
|
|
220
|
-
The fence already permits `account.json`, `SCHEMA.md`, `secrets/` and
|
|
221
|
-
`.claude/`, so it is a layout guard rather than a trust boundary, and this
|
|
222
|
-
widens it rather than breaches it. Task
|
|
223
|
-
`2028-fs-schema-guard-cannot-tell-a-declared-file-from-an-operator-bucket.md`.
|
|
224
|
-
- The reconcile's report-only posture. Task 1899 settled it.
|
|
225
|
-
|
|
226
|
-
## Observability
|
|
227
|
-
|
|
228
|
-
The `op=strays` line from Task 1899 is the instrument. A declared entry never
|
|
229
|
-
appears in it, so anything that does is either debris or an undeclared owner, and
|
|
230
|
-
the two are separated by asking what created it. The `ownedDir=` and `ownedFile=`
|
|
231
|
-
lines from `reconcile` are the second instrument: they name, per account, a
|
|
232
|
-
declaration that has not yet reached that account's `SCHEMA.md`.
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# Task 1926 — same-account portal push concurrency guard (design)
|
|
2
|
-
|
|
3
|
-
**Date:** 2026-07-26
|
|
4
|
-
**Task:** [`.tasks/1926-concurrent-same-account-portal-push-lets-the-pre-stage-clear-delete-the-live-tree.md`](../../../../.tasks/1926-concurrent-same-account-portal-push-lets-the-pre-stage-clear-delete-the-live-tree.md)
|
|
5
|
-
**Touches:** `platform/plugins/cloudflare/bin/portal-index-push.mjs`, `platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts`
|
|
6
|
-
|
|
7
|
-
## Problem
|
|
8
|
-
|
|
9
|
-
`pushAccount` stages an account's file index at `next = currentGeneration + 1`, flips
|
|
10
|
-
the `directory_state` pointer to `next`, then sweeps every other generation. Task 1923
|
|
11
|
-
added a pre-stage `DELETE FROM directory WHERE accountId = ? AND generation = ?` bound
|
|
12
|
-
to `next`, so a stage that died mid-batch could self-heal instead of colliding forever
|
|
13
|
-
on `UNIQUE (accountId, relPath, generation)`.
|
|
14
|
-
|
|
15
|
-
Two pushes of one account break that. The second writer reads `current = 5` before the
|
|
16
|
-
first writer's flip, computes `next = 6`, and by the time its pre-stage DELETE runs the
|
|
17
|
-
pointer already names generation 6. The DELETE removes the live tree. A client listing
|
|
18
|
-
in the gap between that DELETE and the re-insert sees an empty portal.
|
|
19
|
-
|
|
20
|
-
The overlap is reachable: the admin server's 60s loop calls `pushAllAccounts`, and a
|
|
21
|
-
hand-run `node portal-index-push.mjs --account <id>` calls the same `pushAccount` from a
|
|
22
|
-
separate OS process.
|
|
23
|
-
|
|
24
|
-
## What success looks like
|
|
25
|
-
|
|
26
|
-
Two concurrent pushes of one account never leave a reader seeing an empty or partial
|
|
27
|
-
tree, and never delete rows at the pointer's generation.
|
|
28
|
-
|
|
29
|
-
## Why the three candidate approaches in the task file were rejected
|
|
30
|
-
|
|
31
|
-
**A per-account advisory lock.** The two racers are separate OS processes. A JavaScript
|
|
32
|
-
mutex inside the admin server does not exist in the CLI process, so it guards nothing.
|
|
33
|
-
A lock that does work across processes has to live in D1, which means new columns, a
|
|
34
|
-
lease, and a lease expiry. A lease expiry reintroduces the wedge class Task 1923 just
|
|
35
|
-
removed: a holder that dies wedges the account until the lease runs out, and a push
|
|
36
|
-
slower than its lease keeps writing after another writer has taken over.
|
|
37
|
-
|
|
38
|
-
**`next` from `MAX(generation)` alone.** This stops two writers sharing a staging
|
|
39
|
-
generation, but the slower writer still flips the pointer down onto its own generation
|
|
40
|
-
after the faster writer has already swept it. The reader still sees an empty tree, so
|
|
41
|
-
the success criterion is not met.
|
|
42
|
-
|
|
43
|
-
**Scoping the clear to `generation = next AND generation != current`.** `current` is a
|
|
44
|
-
value read at the top of the push. By the time the DELETE runs, the pointer has moved.
|
|
45
|
-
Re-reading the pointer immediately before the DELETE narrows the window without closing
|
|
46
|
-
it, because the peer can flip between the re-read and the DELETE.
|
|
47
|
-
|
|
48
|
-
## Design
|
|
49
|
-
|
|
50
|
-
Three changes to `pushAccount`. No lock, no lease, no schema change, no new table.
|
|
51
|
-
|
|
52
|
-
### 1. Claim a generation above everything that exists
|
|
53
|
-
|
|
54
|
-
Keep the existing pointer read, and add one statement beside it:
|
|
55
|
-
|
|
56
|
-
```sql
|
|
57
|
-
SELECT MAX(generation) AS g FROM directory WHERE accountId = ?
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`next = Math.max(current, maxStagedGeneration) + 1`.
|
|
61
|
-
|
|
62
|
-
A peer that has already inserted at least one row is visible in that MAX, so the two
|
|
63
|
-
writers claim different generations and neither stages into the other's rows.
|
|
64
|
-
|
|
65
|
-
Two statements rather than one nested `UNION ALL`: each is trivially readable, each is
|
|
66
|
-
trivially fakeable in a test, and the pointer read stays the push's first statement,
|
|
67
|
-
which an existing test asserts.
|
|
68
|
-
|
|
69
|
-
### 2. The flip only ever moves the pointer forward
|
|
70
|
-
|
|
71
|
-
```sql
|
|
72
|
-
INSERT INTO directory_state (accountId, currentGeneration) VALUES (?, ?)
|
|
73
|
-
ON CONFLICT (accountId) DO UPDATE SET currentGeneration = excluded.currentGeneration
|
|
74
|
-
WHERE excluded.currentGeneration > directory_state.currentGeneration
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
SQLite has supported a `WHERE` clause on upsert's `DO UPDATE` since 3.24, and D1 is
|
|
78
|
-
SQLite. This is what makes a losing writer harmless: it cannot drag readers back onto a
|
|
79
|
-
generation the winner has already swept.
|
|
80
|
-
|
|
81
|
-
The D1 client returns rows, not affected-row meta, so the flip is followed by a pointer
|
|
82
|
-
readback. That readback is the branch point:
|
|
83
|
-
|
|
84
|
-
- **Pointer equals `next`.** We won. Log `op=flip`, sweep, verify, return. Unchanged
|
|
85
|
-
behaviour.
|
|
86
|
-
- **Pointer is above `next`.** A peer won. Log
|
|
87
|
-
`op=stage-contended account=<id> generation=<next> pointer=<actual> action=superseded`
|
|
88
|
-
and return without sweeping and without verifying. Both of those would measure the
|
|
89
|
-
peer's tree against our expected row count, and `op=verify` would read as a failure
|
|
90
|
-
when nothing failed.
|
|
91
|
-
|
|
92
|
-
Our staged rows at the losing generation are left in place. They are invisible (no
|
|
93
|
-
reader sees a generation the pointer does not name) and the winner's next sweep collects
|
|
94
|
-
them.
|
|
95
|
-
|
|
96
|
-
### 3. The sweep only ever removes older generations
|
|
97
|
-
|
|
98
|
-
```sql
|
|
99
|
-
DELETE FROM directory WHERE accountId = ? AND generation < ?
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
`< next` rather than `!= next`, so a writer never deletes a peer's higher in-flight
|
|
103
|
-
stage. Rows above `next` from an abandoned stage still converge: the following cycle
|
|
104
|
-
claims a generation above them and sweeps them once it flips.
|
|
105
|
-
|
|
106
|
-
### 4. The Task 1923 pre-stage clear is removed
|
|
107
|
-
|
|
108
|
-
Because `next` is now always above every existing generation for the account, the orphan
|
|
109
|
-
`SELECT COUNT(*) ... generation = next` can only ever return 0 and the pre-stage DELETE
|
|
110
|
-
can never match a row. Leaving unreachable code whose comment claims it is the wedge
|
|
111
|
-
heal would be false narration, so the orphan count, the DELETE, the `op=stage-clear` log
|
|
112
|
-
line and its two tests go.
|
|
113
|
-
|
|
114
|
-
Task 1923's outcome is preserved by construction and is stronger than before. A stage
|
|
115
|
-
that died mid-batch leaves rows at generation 6; the next run claims 7, so there is no
|
|
116
|
-
UNIQUE collision to freeze on, and the gen-6 orphans are swept after the flip. The heal
|
|
117
|
-
now happens with no DELETE before the flip at all, which is exactly what removes this
|
|
118
|
-
task's hazard rather than narrowing it.
|
|
119
|
-
|
|
120
|
-
This was confirmed with the operator before the spec was written, because Task 1926
|
|
121
|
-
lists Task 1923's heal as out of scope.
|
|
122
|
-
|
|
123
|
-
## Cases covered, and the one left fail-closed
|
|
124
|
-
|
|
125
|
-
**Peer reads the generation after we have staged a row.** It claims a higher generation.
|
|
126
|
-
Both pushes complete. The forward-only flip picks the winner. The reader sees the old
|
|
127
|
-
complete tree, then one complete new tree. Neither push fails.
|
|
128
|
-
|
|
129
|
-
**Both read the generation before either inserts.** Both claim the same generation. The
|
|
130
|
-
second writer's first INSERT hits `UNIQUE (accountId, relPath, generation)` and that push
|
|
131
|
-
throws. Both walks resolve the same exposed set and `walkExposed` emits the exposed dirs
|
|
132
|
-
in the same order, so the second writer collides on its first row and contributes
|
|
133
|
-
nothing. The reader is safe: the winner's tree is complete and the pointer never moved
|
|
134
|
-
backward. The loser logs `op=failed` and the next 60s cycle succeeds.
|
|
135
|
-
|
|
136
|
-
This second case is left fail-closed rather than retried. The task's success criterion is
|
|
137
|
-
about what a reader can see, and a loud failure that self-corrects one cycle later meets
|
|
138
|
-
it. A retry-on-collision path would be an invented requirement.
|
|
139
|
-
|
|
140
|
-
**The one residual, stated rather than fixed.** The claim above relies on both walks
|
|
141
|
-
starting with the same row. If the account's `SCHEMA.md` or `data-portal.json` changes in
|
|
142
|
-
the window between the two generation reads, the two walks can begin with different rows,
|
|
143
|
-
so the second writer contributes some non-overlapping rows at the shared generation before
|
|
144
|
-
colliding on a shared one. The winner then flips onto its own complete tree plus those few
|
|
145
|
-
extra rows. That is not an empty tree and not a partial one, so it does not breach the
|
|
146
|
-
success criterion, and the next cycle's sweep removes the whole generation. No task file is
|
|
147
|
-
filed for it because nothing is deferred: the case is inside what this design covers, and
|
|
148
|
-
its outcome is a stale row for one cycle rather than a reader-visible break.
|
|
149
|
-
|
|
150
|
-
## Observability
|
|
151
|
-
|
|
152
|
-
`op=stage-contended account=<id> generation=<next> pointer=<actual> action=superseded`
|
|
153
|
-
is emitted when a push's flip was superseded by a peer. This deviates from the task
|
|
154
|
-
file's `action=<waited|skipped>` wording: the design never waits and never skips work, so
|
|
155
|
-
neither value describes what happened. `superseded` names the actual outcome, and
|
|
156
|
-
`pointer=` names the generation that won, which is what an operator needs to correlate
|
|
157
|
-
the two runs.
|
|
158
|
-
|
|
159
|
-
`op=stage-clear` is retired with the code that emitted it. The task file's line that a
|
|
160
|
-
`stage-clear` with `cleared=` equal to the live tree size must never appear is satisfied
|
|
161
|
-
absolutely: no `stage-clear` line can appear at all.
|
|
162
|
-
|
|
163
|
-
## Testing
|
|
164
|
-
|
|
165
|
-
Both new tests drive two interleaved `pushAccount` calls against one shared fake store
|
|
166
|
-
and snapshot the reader's view after every statement. The assertion is the success
|
|
167
|
-
criterion stated directly: the visible tree is never empty and never a strict subset of
|
|
168
|
-
a complete tree.
|
|
169
|
-
|
|
170
|
-
1. **The task file's interleave.** Writer B reads the generation, then pauses. Writer A
|
|
171
|
-
runs to completion. B resumes. Red on current code, because B's pre-stage DELETE binds
|
|
172
|
-
the generation the pointer now names and the snapshot goes empty. Green after, because
|
|
173
|
-
B collides on UNIQUE and A's tree is untouched. This test also asserts that no DELETE
|
|
174
|
-
is ever issued naming the pointer's current generation, which is the other half of the
|
|
175
|
-
success criterion.
|
|
176
|
-
|
|
177
|
-
2. **The higher-claim interleave.** Writer A stages fully but pauses before its flip.
|
|
178
|
-
Writer B reads the generation, sees A's staged rows in the MAX, claims the next one up,
|
|
179
|
-
and completes. A resumes and its flip is a no-op. Asserts the pointer never moves
|
|
180
|
-
backward, that A emits `op=stage-contended` with `action=superseded`, and that A
|
|
181
|
-
issues no sweep.
|
|
182
|
-
|
|
183
|
-
The existing 27 tests stay green apart from the two Task 1923 stage-clear tests, which
|
|
184
|
-
are removed with the code they cover.
|
|
185
|
-
|
|
186
|
-
## Scope boundaries
|
|
187
|
-
|
|
188
|
-
**In scope:** the generation claim, the forward-only flip, the backward-only sweep, the
|
|
189
|
-
removal of the pre-stage clear, and the two-writer regression tests.
|
|
190
|
-
|
|
191
|
-
**Out of scope:** INSERT batching (Task 1842). Cross-account contention (separate
|
|
192
|
-
generation counters, no contention). Any change to `runTargets`, `recordSuccess`, or the
|
|
193
|
-
portal reader in `files.ts`.
|
|
194
|
-
|
|
195
|
-
## Deployment
|
|
196
|
-
|
|
197
|
-
`portal-index-push.mjs` ships inside the installer payload and is inlined into the admin
|
|
198
|
-
server bundle. No D1 migration is required: every statement runs against the existing
|
|
199
|
-
`directory` and `directory_state` tables as defined in
|
|
200
|
-
`platform/plugins/cloudflare/skills/data-portal/schema.sql`.
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# Task 2014 design — cut the jsdom the `platform/ui` suite does not use
|
|
2
|
-
|
|
3
|
-
Date: 2026-07-26
|
|
4
|
-
Task: `.tasks/pending/2014-platform-ui-suite-is-nondeterministic-under-parallel-load.md`
|
|
5
|
-
Supersedes brief: 1765, folded into 2014 on 2026-07-26
|
|
6
|
-
|
|
7
|
-
## The measurement this design rests on
|
|
8
|
-
|
|
9
|
-
All figures from the dev Mac, Node 22.22.0, vitest 4.1.2, worktree
|
|
10
|
-
`task-2014-ui-suite-nondeterminism`, 2026-07-26.
|
|
11
|
-
|
|
12
|
-
### Most apparent worktree flakiness is missing build output, not flakiness
|
|
13
|
-
|
|
14
|
-
`platform/node_modules` and `platform/lib/*/dist` are gitignored build output. A
|
|
15
|
-
fresh worktree has neither. Four full-suite runs, each after adding one more
|
|
16
|
-
piece of provisioning:
|
|
17
|
-
|
|
18
|
-
| Run | Worktree state | Failed | Duration |
|
|
19
|
-
|---|---|---|---|
|
|
20
|
-
| 1 | `platform/ui` deps only | 63 files / 32 tests | 435s |
|
|
21
|
-
| 2 | + 3 libs built | 5 files / 8 tests | 180s |
|
|
22
|
-
| 3 | + `platform` deps | 5 files / 3 tests | 111s |
|
|
23
|
-
| 4 | + all 24 libs built | 0 | 143s |
|
|
24
|
-
|
|
25
|
-
Runs 1 and 2 failed on `Cannot find module .../dist/index.js` and
|
|
26
|
-
`Failed to resolve import "neo4j-driver"`. Those are absent artifacts, not
|
|
27
|
-
timing. Only run 3's three failures are the defect this task describes, and all
|
|
28
|
-
three are in the brief's population: both `/login/start` sub-account cases in
|
|
29
|
-
`whatsapp-self-or-house-scope.test.ts` at 5013ms and 515ms, and
|
|
30
|
-
`whatsapp-conversation-graph-state-projection.test.ts` "sub-account all-senders
|
|
31
|
-
mode returns only its bound thread" at 5017ms. 5000ms is the vitest default
|
|
32
|
-
`testTimeout`.
|
|
33
|
-
|
|
34
|
-
### Environment setup costs six times what the tests cost
|
|
35
|
-
|
|
36
|
-
Run 4, fully green:
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
tests 267s
|
|
40
|
-
environment 1571s
|
|
41
|
-
import 257s
|
|
42
|
-
setup 196s
|
|
43
|
-
transform 42s
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
`vitest.config.ts` sets `environment: 'jsdom'` globally. 530 test files pay it.
|
|
47
|
-
|
|
48
|
-
### Roughly half the suite boots a DOM it never touches
|
|
49
|
-
|
|
50
|
-
266 `.test.ts` files do not declare `@vitest-environment node`. Both runs
|
|
51
|
-
back to back, same machine load:
|
|
52
|
-
|
|
53
|
-
| | Duration | environment CPU |
|
|
54
|
-
|---|---|---|
|
|
55
|
-
| as-is (jsdom) | 103.0s | 1317.6s |
|
|
56
|
-
| `--environment node` | 30.9s | 0.074s |
|
|
57
|
-
|
|
58
|
-
Three consecutive node-environment passes over those 266 files agreed on 17
|
|
59
|
-
`.ts` files that genuinely need a DOM. A file that appeared in only one pass
|
|
60
|
-
(`route-cache-watchers.test.ts`) is the known flake, not a DOM dependency. A
|
|
61
|
-
`.tsx` file in the failure list arrived because vitest CLI arguments are
|
|
62
|
-
substring filters rather than exact paths.
|
|
63
|
-
|
|
64
|
-
So the suite's problem is not 4757 tests. The tests are 267s of CPU. It is that
|
|
65
|
-
about half the files run a browser DOM environment for nothing, and that is the
|
|
66
|
-
CPU starving the workers the named tests race against.
|
|
67
|
-
|
|
68
|
-
### Machine context
|
|
69
|
-
|
|
70
|
-
The dev Mac ran these measurements at load average 100 to 350 with 60 `claude`
|
|
71
|
-
processes across six peer worktree sessions. Self-contention from 20 vitest
|
|
72
|
-
forks is not the only contender for CPU, and any verification has to record the
|
|
73
|
-
load it ran under or a red run cannot be attributed.
|
|
74
|
-
|
|
75
|
-
## Design
|
|
76
|
-
|
|
77
|
-
### 1. Route the test environment by file extension
|
|
78
|
-
|
|
79
|
-
`platform/ui/vitest.config.ts` replaces its single global `environment: 'jsdom'`
|
|
80
|
-
with two projects:
|
|
81
|
-
|
|
82
|
-
- `dom`: `**/*.{test,spec}.?(c|m)[jt]sx`, environment `jsdom`.
|
|
83
|
-
- `node`: `**/*.{test,spec}.?(c|m)[jt]s` plus `../scripts/__tests__/**`,
|
|
84
|
-
environment `node`.
|
|
85
|
-
|
|
86
|
-
The existing `resolve.dedupe`, `setupFiles`, `globals`, and `exclude` settings
|
|
87
|
-
carry into both projects unchanged.
|
|
88
|
-
|
|
89
|
-
The rule is structural, not a maintained list: a test that renders JSX gets a
|
|
90
|
-
DOM, and anything else opts in explicitly. No `.tsx` file is edited.
|
|
91
|
-
|
|
92
|
-
### 2. Seventeen `.ts` files opt back into jsdom
|
|
93
|
-
|
|
94
|
-
Each gets `// @vitest-environment jsdom` with a short reason. Verified on vitest
|
|
95
|
-
4.1.2 that a per-file docblock overrides a project's `environment` setting.
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
app/__tests__/spinning-class-global.test.ts
|
|
99
|
-
app/__tests__/wa-viewer-width.test.ts
|
|
100
|
-
app/graph/__tests__/side-panel-helpers.test.ts
|
|
101
|
-
app/hooks/__tests__/useCopyFeedback.test.ts
|
|
102
|
-
app/hooks/__tests__/useMediaQuery.test.ts
|
|
103
|
-
app/hooks/__tests__/useVoiceRecorder.deviceId.test.ts
|
|
104
|
-
app/hooks/__tests__/useVoiceRecorder.mic-result.test.ts
|
|
105
|
-
app/lib/__tests__/admin-auth-login-remote-auth.test.ts
|
|
106
|
-
app/lib/__tests__/switch-account-scope.test.ts
|
|
107
|
-
app/lib/__tests__/use-admin-auth-claude-connected.test.ts
|
|
108
|
-
app/lib/__tests__/use-admin-auth-heartbeat-remote-auth.test.ts
|
|
109
|
-
app/lib/__tests__/useAdminFetch.test.ts
|
|
110
|
-
app/lib/__tests__/useSubAccountSwitcher.test.ts
|
|
111
|
-
app/public/__tests__/extract-seed-question.test.ts
|
|
112
|
-
app/whatsapp/__tests__/useTranscriptStream-activity.test.ts
|
|
113
|
-
app/whatsapp/__tests__/useTranscriptStream-endpoint.test.ts
|
|
114
|
-
app/whatsapp/__tests__/useTranscriptStream.test.ts
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
A `.ts` test that needs a DOM and lacks the docblock fails loudly on
|
|
118
|
-
`document is not defined`. It does not pass silently.
|
|
119
|
-
|
|
120
|
-
### 3. A prebuild gate so the waste cannot creep back
|
|
121
|
-
|
|
122
|
-
New `platform/ui/scripts/check-jsdom-budget.mjs`, joining the 24 checks already
|
|
123
|
-
in the `prebuild` script. It counts the files that will boot jsdom, which is
|
|
124
|
-
every `.test.tsx`/`.spec.tsx` plus every `.ts` test declaring the jsdom
|
|
125
|
-
docblock, and fails the build when that count exceeds a ceiling written in the
|
|
126
|
-
script. Adding a jsdom file becomes a deliberate one-line bump with a reason,
|
|
127
|
-
not an accident. The failure message names the files above the ceiling and the
|
|
128
|
-
CPU cost per jsdom file measured here, so a reader knows what the number buys.
|
|
129
|
-
|
|
130
|
-
### 4. The 141 existing `@vitest-environment node` docblocks stay
|
|
131
|
-
|
|
132
|
-
They remain accurate and are now redundant. Removing them is 141 files of churn
|
|
133
|
-
for no behaviour change.
|
|
134
|
-
|
|
135
|
-
## What this does not claim
|
|
136
|
-
|
|
137
|
-
Removing the environment waste reduces the contention the named tests race
|
|
138
|
-
against. It is not proven, before measurement, that the ten named tests stop
|
|
139
|
-
failing. If group A's long-press tests still race React Testing Library's
|
|
140
|
-
1000ms `findBy` default once contention drops, that is a second and separate
|
|
141
|
-
lever, and it gets measured and brought back rather than stacked on top as a
|
|
142
|
-
speculative second fix.
|
|
143
|
-
|
|
144
|
-
## Verification
|
|
145
|
-
|
|
146
|
-
**Group A and the suite.** Ten consecutive `npm test` runs in `platform/ui` on
|
|
147
|
-
the merged branch, `uptime` captured before and after each run, exit code and
|
|
148
|
-
any `×` lines collected to one file. Success is ten `exit=0` and zero `×`
|
|
149
|
-
matches across the collection, reported alongside the load each ran under. A red
|
|
150
|
-
run with its load recorded is attributable; a red run without is not.
|
|
151
|
-
|
|
152
|
-
**Group B.** Ten runs do not test a one-in-twenty failure rate. Instead measure
|
|
153
|
-
the wall-clock cost of the four group B seed calls under full-suite parallel
|
|
154
|
-
load, before and after the change, and state the headroom inside the 5000ms
|
|
155
|
-
default. The measurement is the evidence, not the run count.
|
|
156
|
-
|
|
157
|
-
**The jsdom cut itself.** The suite must still report 530 files and 4757 tests.
|
|
158
|
-
A file silently dropping out of the run because a glob missed it would look like
|
|
159
|
-
a pass.
|
|
160
|
-
|
|
161
|
-
## Out of scope
|
|
162
|
-
|
|
163
|
-
- The `platform/lib` build artifacts that made runs 1 to 3 look flaky. That is
|
|
164
|
-
worktree provisioning friction and gets its own task file.
|
|
165
|
-
- A blanket `testTimeout` bump, excluded by the brief.
|
|
166
|
-
- Deleting, skipping, or loosening the four group B tests, excluded by the
|
|
167
|
-
brief: they are the only place the cross-account 403 ordering is asserted.
|
|
168
|
-
- Removing the 141 redundant node docblocks.
|
|
169
|
-
- Any product change to the whatsapp routes, the data browser, or graph-write.
|