@rubytech/create-maxy-code 0.1.573 → 0.1.574

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.
Files changed (127) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/admins-write/__tests__/index.test.ts +97 -0
  3. package/payload/platform/lib/admins-write/dist/index.d.ts +12 -0
  4. package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/admins-write/dist/index.js +27 -5
  6. package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
  7. package/payload/platform/lib/admins-write/src/index.ts +35 -5
  8. package/payload/platform/lib/admins-write/vitest.config.ts +14 -0
  9. package/payload/platform/lib/dispatch-read/dist/session.d.ts.map +1 -1
  10. package/payload/platform/lib/dispatch-read/dist/session.js +19 -4
  11. package/payload/platform/lib/dispatch-read/dist/session.js.map +1 -1
  12. package/payload/platform/lib/dispatch-read/src/__tests__/session.test.ts +91 -0
  13. package/payload/platform/lib/dispatch-read/src/session.ts +25 -6
  14. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  15. package/payload/platform/plugins/admin/PLUGIN.md +8 -3
  16. package/payload/platform/plugins/admin/hooks/__tests__/html-structure-gate.test.sh +264 -0
  17. package/payload/platform/plugins/admin/hooks/__tests__/quote-render-pdf-conformance.test.sh +122 -24
  18. package/payload/platform/plugins/admin/hooks/__tests__/specialist-dispatch-gate.test.sh +158 -0
  19. package/payload/platform/plugins/admin/hooks/html-structure-gate.sh +165 -0
  20. package/payload/platform/plugins/admin/hooks/quote-render-pdf-conformance.sh +111 -19
  21. package/payload/platform/plugins/admin/hooks/specialist-dispatch-gate.sh +159 -0
  22. package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.d.ts +27 -0
  23. package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.d.ts.map +1 -0
  24. package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.js +29 -0
  25. package/payload/platform/plugins/admin/mcp/dist/admin-bind-classify.js.map +1 -0
  26. package/payload/platform/plugins/admin/mcp/dist/binding-validate.d.ts +11 -0
  27. package/payload/platform/plugins/admin/mcp/dist/binding-validate.d.ts.map +1 -0
  28. package/payload/platform/plugins/admin/mcp/dist/binding-validate.js +20 -0
  29. package/payload/platform/plugins/admin/mcp/dist/binding-validate.js.map +1 -0
  30. package/payload/platform/plugins/admin/mcp/dist/index.js +133 -1
  31. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  32. package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +32 -2
  33. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +157 -46
  34. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +8 -0
  35. package/payload/platform/plugins/docs/references/admin-ui.md +71 -31
  36. package/payload/platform/plugins/docs/references/channel-wake-and-prompt.md +38 -13
  37. package/payload/platform/plugins/docs/references/internals.md +6 -0
  38. package/payload/platform/plugins/docs/references/outlook-guide.md +31 -1
  39. package/payload/platform/plugins/docs/references/troubleshooting.md +10 -0
  40. package/payload/platform/plugins/outlook/PLUGIN.md +3 -1
  41. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.d.ts +2 -0
  42. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.d.ts.map +1 -0
  43. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.js +177 -0
  44. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger-write-sites.test.js.map +1 -0
  45. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.d.ts +2 -0
  46. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.d.ts.map +1 -0
  47. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.js +130 -0
  48. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-ledger.test.js.map +1 -0
  49. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.d.ts +2 -0
  50. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.d.ts.map +1 -0
  51. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.js +162 -0
  52. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-reconcile.test.js.map +1 -0
  53. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.d.ts +2 -0
  54. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.d.ts.map +1 -0
  55. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.js +168 -0
  56. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-state-fields.test.js.map +1 -0
  57. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.d.ts +64 -0
  58. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.d.ts.map +1 -0
  59. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.js +110 -0
  60. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-ledger.js.map +1 -0
  61. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.d.ts +119 -0
  62. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.d.ts.map +1 -0
  63. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.js +0 -0
  64. package/payload/platform/plugins/outlook/mcp/dist/lib/draft-reconcile.js.map +1 -0
  65. package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.d.ts +8 -0
  66. package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.d.ts.map +1 -0
  67. package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.js +247 -0
  68. package/payload/platform/plugins/outlook/mcp/dist/scripts/draft-reconcile.js.map +1 -0
  69. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.d.ts.map +1 -1
  70. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.js +10 -0
  71. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.js.map +1 -1
  72. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  73. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +21 -0
  74. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  75. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.d.ts +16 -0
  76. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.d.ts.map +1 -1
  77. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js +5 -1
  78. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js.map +1 -1
  79. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts +4 -0
  80. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts.map +1 -1
  81. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js +2 -1
  82. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js.map +1 -1
  83. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  84. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +24 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  86. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts.map +1 -1
  87. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js +2 -1
  88. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -1
  89. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
  90. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +29 -0
  91. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
  92. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.d.ts +2 -0
  93. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.d.ts.map +1 -0
  94. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.js +21 -0
  95. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/draft-reconcile-throttle.test.js.map +1 -0
  96. package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.d.ts +13 -0
  97. package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.d.ts.map +1 -0
  98. package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.js +10 -0
  99. package/payload/platform/plugins/scheduling/mcp/dist/lib/draft-reconcile-throttle.js.map +1 -0
  100. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +54 -0
  101. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  102. package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.d.ts +32 -0
  103. package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.d.ts.map +1 -0
  104. package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.js +144 -0
  105. package/payload/platform/services/claude-session-manager/dist/authored-specialist-tools.js.map +1 -0
  106. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  107. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  108. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  109. package/payload/platform/services/claude-session-manager/dist/html-structure-census.d.ts +44 -0
  110. package/payload/platform/services/claude-session-manager/dist/html-structure-census.d.ts.map +1 -0
  111. package/payload/platform/services/claude-session-manager/dist/html-structure-census.js +185 -0
  112. package/payload/platform/services/claude-session-manager/dist/html-structure-census.js.map +1 -0
  113. package/payload/platform/services/claude-session-manager/dist/index.js +125 -0
  114. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  115. package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.d.ts.map +1 -1
  116. package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js +7 -0
  117. package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js.map +1 -1
  118. package/payload/platform/services/claude-session-manager/dist/quote-print-census.d.ts +42 -0
  119. package/payload/platform/services/claude-session-manager/dist/quote-print-census.d.ts.map +1 -0
  120. package/payload/platform/services/claude-session-manager/dist/quote-print-census.js +254 -0
  121. package/payload/platform/services/claude-session-manager/dist/quote-print-census.js.map +1 -0
  122. package/payload/platform/services/claude-session-manager/dist/specialist-census.d.ts +92 -0
  123. package/payload/platform/services/claude-session-manager/dist/specialist-census.d.ts.map +1 -0
  124. package/payload/platform/services/claude-session-manager/dist/specialist-census.js +227 -0
  125. package/payload/platform/services/claude-session-manager/dist/specialist-census.js.map +1 -0
  126. package/payload/platform/templates/account-settings.json +8 -4
  127. package/payload/server/server.js +465 -361
@@ -517,6 +517,12 @@ The `brand-excluded` branch closes the recurring crash-restart loop on brands th
517
517
 
518
518
  **Per-spawn signals (server.log).** Every spawn emits `pty-spawn-mcp-config servers=<N> tools=<M> bytes=<B> path=<…>` once, plus one `pty-spawn-agents-dir role=<admin|public> path=<…>` per added directory. Specialist spawns additionally emit `pty-spawn-allowlist specialist=<name> count=<N> stripped=<S> sourced-from=agent-frontmatter` where `stripped` is the count of brand-excluded tool names removed before argv emission. The diagnostic one-liner is `grep -E 'pty-spawn-mcp-config|pty-spawn-agents-dir|pty-spawn-allowlist|mcp-config-allowlist-coverage|specialist-tool-strip|boot-failed reason=' ~/.<brand>/logs/server.log | tail -50`.
519
519
 
520
+ **Is the account's own specialist actually being used? (server.log).** An account can have its own specialists, have them registered and resolvable, and still never use them. Every reachability signal reads green through that state — the registry reconcile says a specialist CAN be reached, the roster census says it is advertised, the preference audit says whether the right one wrote an owned artefact — and none of them counts a dispatch. Skipped delegation writes no record either: the admin doing the work inline logs an ordinary success. Two signals close it, both in `server.log`.
521
+
522
+ Every five minutes, one line per provisioned account, emitted including all-zero so the line's presence proves the check ran: `[specialist-census] account=<id8> declared=<n> dispatched7d=<n> bypassed7d=<n>`. `declared` counts the specialists the account authored itself, not the ones the platform ships — provisioning copies every shipped template into each account, and one of them declares roughly 130 tools, so counting every card would make the other two numbers meaningless. `dispatched7d` counts dispatches in the last seven days whose subagent `agentType` names one of those authored specialists; `bypassed7d` counts direct main-conversation calls to a tool one of them declares, native tools excluded. **`declared` above zero with `dispatched7d` at zero is the fault.** An account whose card set cannot be read emits `[specialist-census] account=<id8> op=degraded reason=<r>` and no counts at all, because a count nobody measured is worse than no count.
523
+
524
+ Against that, a PreToolUse gate ([`specialist-dispatch-gate.sh`](../../admin/hooks/specialist-dispatch-gate.sh)) refuses the admin a tool one of the account's own specialists declares, and names the specialist in the refusal: `[specialist-gate] op=refuse account=<id8> tool=<t> specialist=<name>`. The caller is identified by `agent_type` in the hook envelope, which Claude Code sets inside a subagent and leaves empty in the main conversation, so the gate constrains one seat and never filters a subagent's tool surface. It fails open on an uninspectable envelope and says so with `op=degraded reason=<r>` when the card set cannot be read. Rising refusals against a flat `dispatched7d` is a distinct fault from silence: the admin is being stopped and still not delegating, which needs a change to how it is briefed rather than to the gate. The diagnostic one-liner is `grep -E '\[specialist-census\]|\[specialist-gate\]' ~/.<brand>/logs/server.log | tail -50`.
525
+
520
526
  **Channel follower cold-start retry.** Each `dispatchOnce`-based channel PTY session has one JSONL follower ([`platform/ui/app/lib/channel-pty-bridge/follower.ts`](../../../ui/app/lib/channel-pty-bridge/follower.ts)) reading `GET /<sessionId>/log?follow=1` and fanning each assistant `end_turn` out to the awaiting `dispatchOnce`. (WhatsApp and public webchat have since migrated to the native event channel — an MCP notification in, a `reply`-tool call out — and no longer use this follower; the remaining `dispatchOnce` channels are email and telegram. The webchat detail below is the historical record of the bug the retry fixed.) A freshly-spawned PTY has no JSONL on disk until claude flushes its first line; during that window the manager answers `202 {pending:true}`. The follower retries every `CHANNEL_PTY_FOLLOWER_RETRY_MS` (default 1000) until a 200 stream opens or `CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS` elapses. The follower is shared across channels, so that window defaults to the longest channel turn window (whatsapp's `WHATSAPP_PTY_TURN_TIMEOUT_MS`, 300000 — longer than webchat's 120000) so it never abandons a turn the caller is still awaiting. Because public sessions idle-reap, every webchat greeting is the first turn of a fresh spawn and crosses this window — before the retry, a 202 (which satisfies `res.ok`) was consumed as a single non-event line, the stream ended, and the follower died silently, timing out every public turn. The lifecycle is greppable as `follower-connect status=<code>` → `follower-retry attempt=N reason=pending` → `follower-open` → `outbound bytes=N`; `follower-give-up reason=pending-timeout` marks the JSONL never appearing. A `reject reason=turn-timeout` with no preceding `follower-open` (and no manager `log-follow-open`) for that sessionId is the signature. See `.docs/gated-public-agents.md` "Webchat turn lifecycle" for the full tag list.
521
527
 
522
528
  **Brand-process start counter.** `platform/ui/server-init.cjs` increments a persistent counter at `/tmp/server-init-<accountId>-restart.count` on every fresh start and emits `[server-init] start count=<N> account=<accountId> counter-path=<…>` to `server.log`. /tmp clears on reboot, so a clean reboot starts the count fresh; any value `>1` between operator-observed reboots means the brand process (driven by its `Requires=<brand>-claude-session-manager.service` clause) is restarting. The diagnostic one-liner is `grep '\[server-init\] start' ~/.<brand>/logs/server.log | tail -5` — the trailing `count=` value is the loop depth without counting SIGTERMs.
@@ -63,9 +63,19 @@ Mail only: calendar and contacts tools refuse on a shared mailbox and say so. If
63
63
 
64
64
  When the admin agent drafts or sends a message to a known contact, it reads that contact's record first and applies any routing the record carries, for example a preferred send-to address, showing the result for your approval before anything is sent.
65
65
 
66
+ ## What an empty Drafts folder does not prove
67
+
68
+ If a draft is gone from Drafts and a copy of it is in Sent Items, that does **not** mean this platform sent it. An external mail client signed in to the same mailbox produces exactly the same folder state, and so does a person clicking send on their phone. Never report that Maxy sent something on the strength of folder state alone.
69
+
70
+ Two other things look alarming and are the same blind spot. A message that has left the Drafts folder still fetches by id, and while it remains a draft object it still comes back with no `from` — so "it still fetches" is not "it is still a draft". Read `isDraft` and `parentFolderId`, which every mail read now returns.
71
+
72
+ The one statement to trust is the hourly reconcile's verdict, `op=draft-reconcile … outcome=`. It compares a ledger of every draft this platform created and sent against what the mailbox actually holds. `outcome=sent-not-by-maxy` means the platform made the draft, never sent it, and it left the folder anyway.
73
+
74
+ Naming *what* sent it needs the mailbox tenant's Microsoft 365 unified audit log, where a `Send` record carries the client string. This plugin's delegated permissions cannot read it — the mailbox owner's M365 administrator pulls it. Say that, rather than guessing.
75
+
66
76
  ## Observability
67
77
 
68
- All log lines start with `[outlook-mcp]` and write to `server.log`. They are key=value, account-scoped:
78
+ All log lines start with `[outlook-mcp]` and write to `server.log`, except the draft reconcile, which the scheduling heartbeat spawns and whose lines land in `check-due-events.log`. They are key=value, account-scoped:
69
79
 
70
80
  | Event | Line shape |
71
81
  |-------|------------|
@@ -117,6 +127,13 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
117
127
  | Draft-send request | `draft-send-request account=<id> id=<draftId>` |
118
128
  | Draft sent | `draft-sent account=<id> id=<draftId> status=202` |
119
129
  | Draft-send failed | `draft-send-failed account=<id> id=<draftId> error=<msg>` |
130
+ | Ledger row written | `draft-ledger op=<created\|sent> account=<id> mailbox=<addr> draftId=<id> tool=<t> route=<r\|null>` — one per draft created and per draft sent, the left-hand side the reconcile compares the mailbox against |
131
+ | Ledger row lost | `draft-ledger-failed op=<created\|sent> account=<id> draftId=<id> error=<msg>` — the row was not written; the mail still went. A `sent` row lost here can make a real Maxy send read as `sent-not-by-maxy` later |
132
+ | Draft reconciled | `draft-reconcile account=<id> mailbox=<addr> draftId=<id> outcome=<still-draft\|sent-by-maxy\|sent-not-by-maxy\|deleted\|unreadable> createdTs=<iso> sentDateTime=<iso\|null> parentFolderId=<id\|null> ageMs=<N>` — plus `sentCopyId=`/`sentCopyTo=`/`sentCopyAt=` on `sent-not-by-maxy`, and `status=`/`code=` on `unreadable`. **In `check-due-events.log`, not `server.log`** |
133
+ | Reconcile census | `draft-reconcile-census account=<id> total=<N> stillDraft=<N> sentByMaxy=<N> sentNotByMaxy=<N> deleted=<N> unreadable=<N>` — **every pass including an all-zero one, so an absent census means the reconcile stopped running** |
134
+ | Reconcile pruned | `draft-reconcile-pruned accounts=<N> removed=<N> kept=<N>` |
135
+ | Reconcile skipped | `draft-reconcile-skipped reason=no-vendor-app` — this brand ships no Outlook app; distinct from "did not run" |
136
+ | Reconcile could not run | `draft-reconcile-failed error=<msg>` — exit 1 |
120
137
 
121
138
  ## Diagnostic paths
122
139
 
@@ -133,6 +150,19 @@ grep -E "^\[outlook-mcp\] (mail-reply|draft-created)" ~/.maxy/logs/server.log \
133
150
  | grep -E "threaded=true|op=reply" | grep -E "convId=(null)?( |$)" | head
134
151
  ```
135
152
 
153
+ ```bash
154
+ # One draft's whole lifeline: created, then a verdict per hourly pass.
155
+ grep -E "op=draft-ledger|op=draft-reconcile" ~/.maxy/logs/check-due-events.log | grep "draftId=<id>"
156
+
157
+ # Did the reconcile stop? An absent census IS the failure signal.
158
+ grep -c "op=draft-reconcile-census" ~/.maxy/logs/check-due-events.log
159
+
160
+ # Confirm a sent-not-by-maxy: this must be EMPTY for the window in question.
161
+ grep -E "op=request tool=outlook-(draft-send|mail-send|mail-reply)" ~/.maxy/logs/mcp-outlook-*.log
162
+ ```
163
+
164
+ Attribution triage: `outcome=sent-not-by-maxy` with the send grep above empty means this platform made the draft, never sent it, and it left the folder anyway — an external mail client on the same mailbox. If the send grep is **not** empty for that draft id, suspect a lost ledger row instead and check for `draft-ledger-failed`.
165
+
136
166
  Threading triage: **absence of a `convId` on a threaded write means threading was lost.** `draft-created threaded=false convId=null` is correct and expected — a new message has no parent conversation. `draft-created threaded=true convId=null` is a fault: Graph returned a reply draft that names no conversation, and the reply will land detached from the original.
137
167
 
138
168
  Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions issue; `elapsedMs > 5000` → Graph slowness or DNS.
@@ -48,6 +48,16 @@ Note: passing an explicit second argument (e.g. `logs-read.sh <key> agent-stream
48
48
 
49
49
  **Diagnose if it ever recurs:** grep the per-conversation stream log for `[browser-render]`. `rendered=true domBytes=<n>` is the healthy signal. `rendered=false outcome=cdp-unreachable` means no Chromium is listening on the brand's CDP port — confirm with `curl 127.0.0.1:<cdpPort>/json/version`. Other outcomes (`navigate-failed`, `load-timeout`, `evaluate-failed`) name the failed CDP step.
50
50
 
51
+ ## Stylesheet text is visible on a client document, or a styled box has lost its rules
52
+
53
+ **Symptom:** A client-facing HTML document shows raw CSS as text on the page, usually above the header, and a box further down the same document renders unstyled (no border, no padding). Both are one fault, not two.
54
+
55
+ **Cause:** A `<style>` element opened inside another one. HTML parses style content as raw text, so the two do not nest: the first `</style>` closes the outer element, everything after it becomes body content and renders as visible text, and the rule immediately after the stray opener is eaten as part of an invalid selector.
56
+
57
+ **Resolution:** Delete the stray opener. Put the block in the body as its own `<style>` element, or merge its rules into the one already open. If the document is deployed, redeploy it, then confirm the rule is present in the parsed stylesheet — a changed byte count is not proof.
58
+
59
+ **Diagnose:** `grep -F '[html-structure-census]' ~/.<brand>/logs/server.log | tail -1` reports `accounts=`, `documents=` and `malformed=` from the last standing tick, and each malformed document has its own `op=malformed account=<id8> file="<path>" line=<n>` line naming the stray opener. The summary is emitted on every run including a zero count, so no line at all means the walk is broken rather than the install being clean, and `documents=0` means the same. A write that would produce the shape is refused before it lands, logging `[html-structure-gate] op=refuse` to the same file.
60
+
51
61
  ## First user-domain write rejected by `[graph-write-gate] reject reason=no-admin-user`
52
62
 
53
63
  **Symptom:** Admin chat reports "couldn't save that — set up your business profile first" or `[graph-write-gate] reject reason=no-admin-user` appears in `server.log` on the operator's first non-bootstrap write (a website, service, opening hours, etc.). Reproduces on Minimal-onboarded installs from before the seed-stamping fix shipped.
@@ -137,6 +137,7 @@ Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read (
137
137
  - **Mail (ingest to graph):** `outlook-mail-ingest` writes operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads, the analogue of the IMAP `email-ingest`. Input is `decisions`: one `{messageId, disposition:'ingest'|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched via `runMailFetchBody`, threads are grouped by Graph `conversationId`, and participant resolution is closed-set — every From/To/Cc address must already resolve to a `:Person.email` or `:AdminUser.email` for the `accountId`, or its thread is skipped and the address is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch one at a time through the shared `plugins/memory/bin/conversation-archive-ingest.sh --source email` (same RFC822 thread-JSON shape the `email` normaliser consumes). This is the plugin's only Neo4j coupling (`neo4j-driver`). Human-in-the-loop: nothing enters the graph without an explicit per-message decision; the `outlook-mail-ingest` skill drives the list → summarise → ask → ingest loop. Requires `Mail.Read`.
138
138
  - **Mail (OTP):** `outlook-mail-otp-extract` polls the inbox for a one-time verification code from a named sender, reading full bodies to extract it — used during service authentication.
139
139
  - **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id — either a new message (`to` + `subject` required) or, with `replyToMessageId`, a threaded reply created via `POST /me/messages/{id}/createReply` that lands under the original conversation (the only way to draft a reply without sending it; `outlook-mail-reply` threads but always sends). On the threaded path Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote — the same email `outlook-mail-reply` sends — so that path is plain text only and `to`, `subject`, and `isHtml: true` are refused rather than silently dropped; `replyAll` and `cc`/`bcc` work as on reply. The draft result carries `to`, the recipients the draft holds, so the self-addressed case above is visible without fetching the draft back; on this path it is reported rather than refused, because a draft is not sent. `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and edits its attachments — `attachments` adds files, `removeAttachments` takes them off by name or attachment id, and both together replace a file in one call; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
140
+ - **Draft attribution:** every draft the plugin creates and every draft it sends is recorded in an append-only ledger at `platform/config/outlook-draft-ledger.jsonl`. The `created` row is written the moment Graph returns the draft id, before any attach or send, so a draft stranded by a later failure is still watched; `outlook-mail-send`'s no-attachment route drafts nothing and writes nothing. An hourly reconcile spawned by the scheduling heartbeat classifies each open draft against live mailbox state: `still-draft`, `sent-by-maxy`, `sent-not-by-maxy`, `deleted`, `unreadable`. **An empty Drafts folder beside a copy in Sent Items is not evidence this plugin sent anything** — an external mail client signed in to the same mailbox produces identical folder state, and a message that has left the Drafts folder still fetches by id and still returns `from: null` while it remains a draft object. The reconcile's `outcome=` is the only sanctioned statement; agents must not assert a send from folder state. `parentFolderId` is the discriminator, not `isDraft`, and every mail read now returns both. Naming the external client requires the tenant's Microsoft 365 unified audit log, which these delegated scopes cannot read (see out of scope). See `.docs/outlook-draft-attribution.md`.
140
141
  - **Calendar (read + control):** `outlook-calendar-list` and `outlook-calendar-event` read events; `outlook-calendar-create` books an event; `outlook-calendar-update` reschedules or edits one; `outlook-calendar-cancel` cancels a meeting (notifying attendees when the account organises it) or deletes a personal appointment; `outlook-calendar-respond` accepts, declines, or tentatively accepts an invite; `outlook-calendar-freebusy` reports availability for one or more addresses over a window. Writes require the `Calendars.ReadWrite` consent; a mailbox on stale read-only consent returns a distinct signal naming the scope and `outlook-account-register`.
141
142
  - **Contacts (read-only):** `outlook-contacts-list` returns contacts.
142
143
  - **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count for one mailbox (name it with `mailbox` when several are attached). `outlook-mailbox-list` lists every attached mailbox with its email, scopes, and token health, reading local stores only, never the network. It reports an `addressable` count and a per-mailbox `status` (`ok` / `blank-email` / `refresh-expired` / `unreadable`) so an authenticated, addressable mailbox is distinguishable from one that is merely present: a bare `count` of 1 is not "connected" unless that mailbox is `status:ok`. Use these to answer "did mailbox X auth?" without grepping logs.
@@ -151,7 +152,8 @@ Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read (
151
152
  - OneDrive, Files, Sites — separate plugin; different endpoints.
152
153
  - Push notifications via `/subscriptions` — first cut polls.
153
154
  - On-premises Exchange — Microsoft Graph deprecated hybrid REST 2023-07. The plugin detects on-prem mailboxes and routes the operator to the IMAP path (`email` plugin).
154
- - M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`) — separate plugin.
155
+ - M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`) — separate plugin. This is why the draft reconcile can report that a draft left the Drafts folder without a Maxy send, but cannot name what sent it: the client string lives on a `Send` record in the tenant's unified audit log, which the mailbox owner's M365 administrator pulls.
156
+ - Blocking or refusing a send when the draft ledger disagrees with the mailbox. The reconcile observes; enforcement is deliberately not built.
155
157
 
156
158
  ## References
157
159
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=draft-ledger-write-sites.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-ledger-write-sites.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/draft-ledger-write-sites.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,177 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { TokenStore } from "../auth/token-store.js";
7
+ import { runDraft } from "../tools/draft.js";
8
+ import { runDraftSend } from "../tools/draft-send.js";
9
+ import { runMailSend } from "../tools/mail-send.js";
10
+ import { runMailReply } from "../tools/mail-reply.js";
11
+ import { readDraftLedger } from "../lib/draft-ledger.js";
12
+ /**
13
+ * Task 2530. The ledger's own unit tests prove the file works; these prove the
14
+ * four tools actually write to it. A send path that quietly stopped writing
15
+ * would make a real Maxy send read as `sent-not-by-maxy` later, which is the
16
+ * one wrong answer this whole mechanism exists to avoid.
17
+ */
18
+ function configWithToken(accountId, mailbox) {
19
+ const dir = mkdtempSync(join(tmpdir(), "outlook-ledgersite-"));
20
+ const tokenStore = new TokenStore(accountId, dir);
21
+ tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
22
+ return { clientId: "test-client", tenantId: "common", accountId, tokenStore, mailboxAddress: mailbox };
23
+ }
24
+ /** Every Graph write answers 201 with a fixed id; sends answer 202. */
25
+ function mockGraph() {
26
+ const original = globalThis.fetch;
27
+ globalThis.fetch = (async (input, init) => {
28
+ const req = input instanceof Request ? input : null;
29
+ const url = req ? req.url : typeof input === "string" ? input : input.toString();
30
+ if (!url.includes("graph.microsoft.com"))
31
+ return original(input, init);
32
+ if (url.endsWith("/send") || url.endsWith("/sendMail"))
33
+ return new Response(null, { status: 202 });
34
+ return new Response(JSON.stringify({ id: "GRAPHID", conversationId: "CONV1", toRecipients: [{ emailAddress: { address: "g@example.com" } }] }), { status: 201, headers: { "content-type": "application/json" } });
35
+ });
36
+ return {
37
+ restore: () => {
38
+ globalThis.fetch = original;
39
+ },
40
+ };
41
+ }
42
+ /**
43
+ * A temp tree shaped like a real install: `<base>/platform` is PLATFORM_ROOT and
44
+ * `<base>/data/accounts/<id>` is where attachment paths must resolve, because
45
+ * `resolveAccountDir` reads `join(platformRoot, "..", "data", "accounts", id)`.
46
+ * A flat temp dir would put the account dir in the OS temp root itself.
47
+ *
48
+ * `fn` receives the account directory so an attachment test can write into it.
49
+ */
50
+ async function withLedger(accountId, fn) {
51
+ const base = mkdtempSync(join(tmpdir(), "outlook-ledgerroot-"));
52
+ const root = join(base, "platform");
53
+ const accountDir = join(base, "data", "accounts", accountId);
54
+ mkdirSync(join(root, "config"), { recursive: true });
55
+ mkdirSync(accountDir, { recursive: true });
56
+ const prevRoot = process.env.PLATFORM_ROOT;
57
+ const prevAccountDir = process.env.ACCOUNT_DIR;
58
+ process.env.PLATFORM_ROOT = root;
59
+ delete process.env.ACCOUNT_DIR;
60
+ const mock = mockGraph();
61
+ try {
62
+ await fn(accountDir);
63
+ return readDraftLedger();
64
+ }
65
+ finally {
66
+ mock.restore();
67
+ if (prevRoot === undefined)
68
+ delete process.env.PLATFORM_ROOT;
69
+ else
70
+ process.env.PLATFORM_ROOT = prevRoot;
71
+ if (prevAccountDir !== undefined)
72
+ process.env.ACCOUNT_DIR = prevAccountDir;
73
+ rmSync(base, { recursive: true, force: true });
74
+ }
75
+ }
76
+ test("outlook-draft writes one created row carrying mailbox and subject", async () => {
77
+ const rows = await withLedger("acct-w1", async () => {
78
+ await runDraft(configWithToken("acct-w1", "rob@example.com"), {
79
+ to: ["g@example.com"],
80
+ subject: "Extending our work",
81
+ body: "hi",
82
+ });
83
+ });
84
+ assert.equal(rows.length, 1);
85
+ assert.equal(rows[0].event, "created");
86
+ assert.equal(rows[0].tool, "draft");
87
+ assert.equal(rows[0].draftId, "GRAPHID");
88
+ assert.equal(rows[0].mailbox, "rob@example.com");
89
+ assert.equal(rows[0].subject, "Extending our work");
90
+ assert.equal(rows[0].conversationId, null, "a standalone draft has no conversation");
91
+ });
92
+ test("a threaded draft carries the conversationId instead of a subject", async () => {
93
+ const rows = await withLedger("acct-w2", async () => {
94
+ await runDraft(configWithToken("acct-w2", "rob@example.com"), {
95
+ replyToMessageId: "PARENT",
96
+ body: "hi",
97
+ });
98
+ });
99
+ assert.equal(rows.length, 1);
100
+ assert.equal(rows[0].conversationId, "CONV1");
101
+ assert.equal(rows[0].subject, undefined, "Graph sets the subject on a threaded draft");
102
+ });
103
+ test("outlook-draft-send writes one sent row", async () => {
104
+ const rows = await withLedger("acct-w3", async () => {
105
+ await runDraftSend(configWithToken("acct-w3", "rob@example.com"), { draftId: "D1" });
106
+ });
107
+ assert.equal(rows.length, 1);
108
+ assert.equal(rows[0].event, "sent");
109
+ assert.equal(rows[0].tool, "draft-send");
110
+ assert.equal(rows[0].route, "draft-send");
111
+ assert.equal(rows[0].draftId, "D1");
112
+ });
113
+ test("the sendMail route writes NO row — it drafts nothing and Graph returns no id", async () => {
114
+ // A row here would carry an empty draftId that can never pair with a created
115
+ // row and can never be pruned by one: a line nothing reads, growing forever.
116
+ const rows = await withLedger("acct-w4", async () => {
117
+ await runMailSend(configWithToken("acct-w4", "rob@example.com"), {
118
+ to: ["g@example.com"],
119
+ subject: "No attachments",
120
+ body: "hi",
121
+ });
122
+ });
123
+ assert.deepEqual(rows, []);
124
+ });
125
+ test("outlook-mail-reply writes created then sent for the draft Graph composed", async () => {
126
+ const rows = await withLedger("acct-w5", async () => {
127
+ await runMailReply(configWithToken("acct-w5", "rob@example.com"), {
128
+ messageId: "PARENT",
129
+ body: "hi",
130
+ });
131
+ });
132
+ assert.equal(rows.length, 2);
133
+ assert.equal(rows[0].event, "created");
134
+ assert.equal(rows[0].tool, "mail-reply");
135
+ assert.equal(rows[0].conversationId, "CONV1");
136
+ assert.equal(rows[1].event, "sent");
137
+ assert.equal(rows[1].route, "reply");
138
+ assert.equal(rows[0].draftId, rows[1].draftId);
139
+ });
140
+ test("mail-send's attachment route writes created before the send, then sent", async () => {
141
+ // The route DOES draft. Anything throwing between the create and the send
142
+ // strands that draft in Drafts; without the created row the reconcile is
143
+ // blind to it.
144
+ const rows = await withLedger("acct-w7", async (accountDir) => {
145
+ // Inside the account dir: resolveOutboundAttachments refuses any path that
146
+ // does not realpath under it.
147
+ const file = join(accountDir, "quote.pdf");
148
+ writeFileSync(file, "pdf-bytes");
149
+ await runMailSend(configWithToken("acct-w7", "rob@example.com"), {
150
+ to: ["g@example.com"],
151
+ subject: "With attachment",
152
+ body: "hi",
153
+ attachments: [file],
154
+ });
155
+ });
156
+ assert.equal(rows.length, 2);
157
+ assert.equal(rows[0].event, "created");
158
+ assert.equal(rows[0].tool, "mail-send");
159
+ assert.equal(rows[0].subject, "With attachment");
160
+ assert.equal(rows[1].event, "sent");
161
+ assert.equal(rows[1].route, "draft-send");
162
+ assert.equal(rows[0].draftId, rows[1].draftId);
163
+ });
164
+ test("a draft and its send pair up on the same draftId", async () => {
165
+ // The pairing is what lets the reconcile answer sent-by-maxy from the ledger
166
+ // alone, with no Graph call.
167
+ const rows = await withLedger("acct-w6", async () => {
168
+ const config = configWithToken("acct-w6", "rob@example.com");
169
+ await runDraft(config, { to: ["g@example.com"], subject: "Paired", body: "hi" });
170
+ await runDraftSend(config, { draftId: "GRAPHID" });
171
+ });
172
+ assert.equal(rows.length, 2);
173
+ assert.equal(rows[0].draftId, rows[1].draftId);
174
+ assert.equal(rows[0].event, "created");
175
+ assert.equal(rows[1].event, "sent");
176
+ });
177
+ //# sourceMappingURL=draft-ledger-write-sites.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-ledger-write-sites.test.js","sourceRoot":"","sources":["../../src/__tests__/draft-ledger-write-sites.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=draft-ledger.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-ledger.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/draft-ledger.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync, statSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { appendDraftLedger, readDraftLedger, writeDraftLedger, ledgerPath, } from "../lib/draft-ledger.js";
7
+ function withRoot(fn) {
8
+ const root = mkdtempSync(join(tmpdir(), "outlook-ledger-"));
9
+ mkdirSync(join(root, "config"), { recursive: true });
10
+ const prev = process.env.PLATFORM_ROOT;
11
+ process.env.PLATFORM_ROOT = root;
12
+ try {
13
+ fn();
14
+ }
15
+ finally {
16
+ if (prev === undefined)
17
+ delete process.env.PLATFORM_ROOT;
18
+ else
19
+ process.env.PLATFORM_ROOT = prev;
20
+ rmSync(root, { recursive: true, force: true });
21
+ }
22
+ }
23
+ const CREATED = {
24
+ at: "2026-08-07T11:52:25.000Z",
25
+ accountId: "acc-1",
26
+ mailbox: "rob@example.com",
27
+ draftId: "AAA",
28
+ event: "created",
29
+ tool: "draft",
30
+ subject: "Extending our work",
31
+ conversationId: null,
32
+ };
33
+ test("append writes one JSON line per entry and read returns them in order", () => {
34
+ withRoot(() => {
35
+ appendDraftLedger(CREATED);
36
+ appendDraftLedger({ ...CREATED, at: "2026-08-07T12:00:00.000Z", draftId: "BBB" });
37
+ const rows = readDraftLedger();
38
+ assert.equal(rows.length, 2);
39
+ assert.equal(rows[0].draftId, "AAA");
40
+ assert.equal(rows[1].draftId, "BBB");
41
+ assert.equal(rows[0].subject, "Extending our work");
42
+ });
43
+ });
44
+ test("append never rewrites the file — a concurrent row is not lost", () => {
45
+ withRoot(() => {
46
+ appendDraftLedger(CREATED);
47
+ appendDraftLedger({
48
+ at: "2026-08-07T11:55:00.000Z",
49
+ accountId: "acc-1",
50
+ mailbox: "rob@example.com",
51
+ draftId: "BBB",
52
+ event: "sent",
53
+ tool: "draft-send",
54
+ route: "draft-send",
55
+ });
56
+ const raw = readFileSync(ledgerPath(), "utf8");
57
+ assert.equal(raw.trim().split("\n").length, 2);
58
+ });
59
+ });
60
+ test("a malformed line is skipped, never thrown on", () => {
61
+ withRoot(() => {
62
+ appendDraftLedger(CREATED);
63
+ writeFileSync(ledgerPath(), readFileSync(ledgerPath(), "utf8") + "{not json\n");
64
+ const rows = readDraftLedger();
65
+ assert.equal(rows.length, 1);
66
+ assert.equal(rows[0].draftId, "AAA");
67
+ });
68
+ });
69
+ test("reading an absent ledger returns an empty array", () => {
70
+ withRoot(() => {
71
+ assert.deepEqual(readDraftLedger(), []);
72
+ });
73
+ });
74
+ test("writeDraftLedger replaces the file with exactly the rows given", () => {
75
+ withRoot(() => {
76
+ appendDraftLedger(CREATED);
77
+ appendDraftLedger({ ...CREATED, draftId: "BBB" });
78
+ writeDraftLedger([{ ...CREATED, draftId: "CCC" }]);
79
+ const rows = readDraftLedger();
80
+ assert.deepEqual(rows.map((r) => r.draftId), ["CCC"]);
81
+ });
82
+ });
83
+ test("writeDraftLedger with no rows leaves a readable empty file", () => {
84
+ withRoot(() => {
85
+ appendDraftLedger(CREATED);
86
+ writeDraftLedger([]);
87
+ assert.deepEqual(readDraftLedger(), []);
88
+ });
89
+ });
90
+ test("the ledger file is written 0600", () => {
91
+ withRoot(() => {
92
+ appendDraftLedger(CREATED);
93
+ assert.equal(statSync(ledgerPath()).mode & 0o777, 0o600);
94
+ });
95
+ });
96
+ test("a write failure logs and returns rather than throwing", () => {
97
+ // PLATFORM_ROOT pointed at a path that cannot be created: the append must not
98
+ // propagate. Observability must never take out the mail.
99
+ const prev = process.env.PLATFORM_ROOT;
100
+ process.env.PLATFORM_ROOT = "/dev/null/nope";
101
+ try {
102
+ assert.doesNotThrow(() => appendDraftLedger(CREATED));
103
+ }
104
+ finally {
105
+ if (prev === undefined)
106
+ delete process.env.PLATFORM_ROOT;
107
+ else
108
+ process.env.PLATFORM_ROOT = prev;
109
+ }
110
+ });
111
+ test("no PLATFORM_ROOT means no ledger path, and nothing is written anywhere", () => {
112
+ // There is no directory-relative fallback by design: guessing a path is how
113
+ // the email ledger's fallback ends up writing a stray plugins/config/ into
114
+ // the source tree under test.
115
+ const prev = process.env.PLATFORM_ROOT;
116
+ delete process.env.PLATFORM_ROOT;
117
+ try {
118
+ assert.equal(ledgerPath(), null);
119
+ assert.doesNotThrow(() => appendDraftLedger(CREATED));
120
+ assert.deepEqual(readDraftLedger(), []);
121
+ assert.throws(() => writeDraftLedger([CREATED]), /PLATFORM_ROOT is not set/);
122
+ }
123
+ finally {
124
+ if (prev === undefined)
125
+ delete process.env.PLATFORM_ROOT;
126
+ else
127
+ process.env.PLATFORM_ROOT = prev;
128
+ }
129
+ });
130
+ //# sourceMappingURL=draft-ledger.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-ledger.test.js","sourceRoot":"","sources":["../../src/__tests__/draft-ledger.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=draft-reconcile.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-reconcile.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/draft-reconcile.test.ts"],"names":[],"mappings":""}