@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,119 +0,0 @@
|
|
|
1
|
-
# Single-source the `/graph` top-level label allowlist from the vertical ontology — Design
|
|
2
|
-
|
|
3
|
-
Task: `.tasks/1624-single-source-graph-labels-top-level-from-vertical-ontology.md`.
|
|
4
|
-
|
|
5
|
-
## Problem
|
|
6
|
-
|
|
7
|
-
A businessType's top-level operator-entry node types live in the `## Top-level node
|
|
8
|
-
types` table of each vertical's `schema-<vertical>.md` (established by 1622). A third
|
|
9
|
-
encoding of "top-level entity" still lives apart: `platform/ui/app/lib/graph-labels.ts`
|
|
10
|
-
`FILTER_TOP_LEVEL_LABELS` is a hand-maintained global union of every brand's top-level
|
|
11
|
-
labels — the `/graph` filter-popover chip allowlist. It drifts from the ontology by
|
|
12
|
-
construction: a top-level label added to a vertical schema does not become a chip until
|
|
13
|
-
someone also edits this TypeScript set.
|
|
14
|
-
|
|
15
|
-
## Goal
|
|
16
|
-
|
|
17
|
-
Derive the `/graph` chip allowlist from the same per-vertical ontology source so a
|
|
18
|
-
top-level label added to a vertical schema surfaces as a chip with no second edit, while
|
|
19
|
-
preserving today's exact chip visibility as the acceptance baseline.
|
|
20
|
-
|
|
21
|
-
## Blast radius (evidence)
|
|
22
|
-
|
|
23
|
-
`FILTER_TOP_LEVEL_LABELS` has four real code consumers:
|
|
24
|
-
|
|
25
|
-
1. `server/routes/admin/graph-labels-in-graph.ts` — popover chip allowlist (the route the
|
|
26
|
-
task names). Passes the set as the `$allowed` cypher param; the cypher count-gates.
|
|
27
|
-
2. `server/routes/admin/graph-subgraph.ts:280` — default-view label set.
|
|
28
|
-
3. `server/routes/admin/graph-default-view.ts:158` — default-view eligibility check.
|
|
29
|
-
4. `app/data/searchHelpers.ts:46` — client-side `/data` search chip pre-filter, a
|
|
30
|
-
build-time static import that cannot read files.
|
|
31
|
-
|
|
32
|
-
Scope decision (operator-confirmed): **the three server routes switch to the
|
|
33
|
-
ontology-derived source. The client `searchHelpers.ts` keeps the static set**, with a
|
|
34
|
-
follow-up task filed to close that last second source.
|
|
35
|
-
|
|
36
|
-
## Facts that shape the mechanism
|
|
37
|
-
|
|
38
|
-
- Only `schema-construction.md` and `schema-estate-agent.md` currently carry a
|
|
39
|
-
`## Top-level node types` section. Every other vertical (and 1626) adds theirs later.
|
|
40
|
-
- `schema-base.md` has no top-level section; its `## Node Types` table mixes top-level and
|
|
41
|
-
child types (Message, Section, Chunk). Base labels therefore **cannot** be derived — they
|
|
42
|
-
stay an explicit list. This resolves the task's open base-list decision.
|
|
43
|
-
- The construction top-level table includes `WhatsAppGroup` typed `cdm:Channel`. The 1622
|
|
44
|
-
Python file-bucket generator excludes non-`schema:` rows (a channel gets no file bucket),
|
|
45
|
-
but `WhatsAppGroup` **is** a current graph chip. So the graph derivation takes **every**
|
|
46
|
-
Neo4j Label row and applies **no** namespace filter.
|
|
47
|
-
- The construction table has 5 columns, estate-agent 4. The Neo4j Label column is resolved
|
|
48
|
-
by header name, never by index.
|
|
49
|
-
|
|
50
|
-
## Architecture
|
|
51
|
-
|
|
52
|
-
New server-only module `platform/ui/server/lib/top-level-labels.ts`:
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
getTopLevelLabelAllowlist(opts?: { referencesDir?: string }): ReadonlySet<string>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
- Resolves `referencesDir` to `${MAXY_PLATFORM_ROOT}/plugins/memory/references` (same
|
|
59
|
-
env/`__dirname` fallback the UI server already uses), or the injected override.
|
|
60
|
-
- Reads every `schema-*.md`. For each, locates the `## Top-level node types` heading by
|
|
61
|
-
case-insensitive prefix, reads the first pipe-table beneath it (before the next `##`),
|
|
62
|
-
and extracts the `Neo4j Label` column by header name, stripping backticks.
|
|
63
|
-
- Unions parsed labels with the module const `STATIC_TOP_LEVEL_LABELS` — the base-schema
|
|
64
|
-
infra plus any vertical whose schema has no top-level section yet (knowledge-work today).
|
|
65
|
-
Documented to shrink as 1626 adds sections.
|
|
66
|
-
- Caches the result at module level (schema files are static per install). An injected
|
|
67
|
-
`referencesDir` bypasses the cache so tests are isolated.
|
|
68
|
-
- Emits one observability line on computation:
|
|
69
|
-
`op=top-level-labels source=ontology count=<n> files=<...> derived=<d> static=<s>`.
|
|
70
|
-
|
|
71
|
-
The three server routes replace `FILTER_TOP_LEVEL_LABELS` with
|
|
72
|
-
`getTopLevelLabelAllowlist()` (a `ReadonlySet<string>`, so `.has()` and spread are
|
|
73
|
-
preserved). Cypher count-gating is unchanged: a non-resident label surfaces no chip.
|
|
74
|
-
|
|
75
|
-
Unchanged: `FILTER_TOP_LEVEL_LABELS` (client), `HIDDEN_BY_DEFAULT_LABELS`,
|
|
76
|
-
`AGENT_ACTION_LABELS`, `EXCLUDED_EDGE_TYPES`.
|
|
77
|
-
|
|
78
|
-
## Byte-identical guarantee
|
|
79
|
-
|
|
80
|
-
Derived union = construction table (11) ∪ estate table (4) ∪ `STATIC_TOP_LEVEL_LABELS`
|
|
81
|
-
(33) = the current 48-member `FILTER_TOP_LEVEL_LABELS`. Because the sets are equal, the
|
|
82
|
-
existing route tests (which assert membership against `FILTER_TOP_LEVEL_LABELS`) keep
|
|
83
|
-
passing unchanged.
|
|
84
|
-
|
|
85
|
-
`STATIC_TOP_LEVEL_LABELS` (top-level labels not yet sourced from a vertical section):
|
|
86
|
-
|
|
87
|
-
- Base-schema infra: `LocalBusiness`, `Service`, `PriceSpecification`,
|
|
88
|
-
`OpeningHoursSpecification`, `Organization`, `Person`, `UserProfile`, `Preference`,
|
|
89
|
-
`AdminUser`, `AccessGrant`, `KnowledgeDocument`, `DigitalDocument`, `CreativeWork`,
|
|
90
|
-
`Question`, `FAQPage`, `DefinedTerm`, `Review`, `ImageObject`, `Invoice`, `Task`,
|
|
91
|
-
`Project`, `Event`, `Workflow`, `Email`, `EmailAccount`, `Agent`.
|
|
92
|
-
- Knowledge-work vertical (no top-level section yet; 1626): `Objective`, `KeyResult`,
|
|
93
|
-
`Decision`, `Risk`, `Source`, `Finding`, `Hypothesis`.
|
|
94
|
-
|
|
95
|
-
## Testing
|
|
96
|
-
|
|
97
|
-
Committed, colocated `platform/ui/server/lib/top-level-labels.test.ts`:
|
|
98
|
-
|
|
99
|
-
1. `getTopLevelLabelAllowlist()` against the real references dir deep-equals
|
|
100
|
-
`FILTER_TOP_LEVEL_LABELS` (regression baseline).
|
|
101
|
-
2. A fixture references dir with an added top-level row surfaces that label with no change
|
|
102
|
-
to any TypeScript set.
|
|
103
|
-
3. A `cdm:`-namespaced row (e.g. `WhatsAppGroup`) is included (no namespace filter).
|
|
104
|
-
|
|
105
|
-
The pre-existing `labels-in-graph` and `graph-default-topLevel` tests must still pass
|
|
106
|
-
after the swap (count-gating and eligibility unchanged).
|
|
107
|
-
|
|
108
|
-
## Observability
|
|
109
|
-
|
|
110
|
-
`op=top-level-labels source=ontology count=<n>` on the server log, emitted when the
|
|
111
|
-
allowlist is resolved, so an operator can confirm the chips come from the ontology, not a
|
|
112
|
-
stale hardcoded list.
|
|
113
|
-
|
|
114
|
-
## Out of scope
|
|
115
|
-
|
|
116
|
-
- Client `searchHelpers.ts` static set — filed as a follow-up task.
|
|
117
|
-
- `HIDDEN_BY_DEFAULT_LABELS`, `AGENT_ACTION_LABELS`, `EXCLUDED_EDGE_TYPES`.
|
|
118
|
-
- Adding `## Top-level node types` sections to verticals that lack one (1626).
|
|
119
|
-
- The file-schema generator and quotation reification (landed in 1622).
|
package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# Task 1736 — retire `mcp-stderr-tee`
|
|
2
|
-
|
|
3
|
-
**Date:** 2026-07-17
|
|
4
|
-
**Task:** [`.tasks/1736-*.md`](../../../../.tasks/1736-nothing-sets-stream-log-path-so-the-per-conversation-mcp-stream-log-does-not-exist.md)
|
|
5
|
-
**Verdict:** retire the module, its call sites, and every doc claim that a per-conversation
|
|
6
|
-
MCP stream log exists.
|
|
7
|
-
|
|
8
|
-
## Why retire, precisely
|
|
9
|
-
|
|
10
|
-
`mcp-stderr-tee` reads `STREAM_LOG_PATH` and mirrors MCP stderr into a per-conversation
|
|
11
|
-
stream log. Nothing sets that variable, so `initStderrTee` takes its skip path on every
|
|
12
|
-
production spawn and 21 call sites wire up a no-op.
|
|
13
|
-
|
|
14
|
-
The reason to retire is **not** that the surface was deliberately retired elsewhere. It is
|
|
15
|
-
that `mcp-spawn-tee` already serves the same consumer, strictly better:
|
|
16
|
-
|
|
17
|
-
- It is a shim Claude Code runs *in place of* each server's entry, so it replaces
|
|
18
|
-
`process.stderr.write` before the real entry imports. It captures every stderr byte with
|
|
19
|
-
no cooperation from the plugin — including module-load throws that happen before
|
|
20
|
-
`initStderrTee` would ever run.
|
|
21
|
-
- It writes `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log`, keyed by session, which *is* the
|
|
22
|
-
conversation. Per-conversation correlation already exists.
|
|
23
|
-
- It handles the sessionless spawn explicitly (`mcp-<name>-nosession.log`).
|
|
24
|
-
- It uses `appendFileSync`, which survives an immediate `process.exit`. `mcp-stderr-tee`
|
|
25
|
-
used an async `createWriteStream`, which does not.
|
|
26
|
-
|
|
27
|
-
The two modules were never complements. Task 1721's docstring records that they wrote the
|
|
28
|
-
same file and "every line landed twice". 1721 removed the duplication and left the loser
|
|
29
|
-
holding a destination nothing produces.
|
|
30
|
-
|
|
31
|
-
## Correcting the record
|
|
32
|
-
|
|
33
|
-
The task file and its LANES row currently argue "retire because Tasks 287 and 288
|
|
34
|
-
dismantled the consumers." That is wrong and it is load-bearing: 287 retired the
|
|
35
|
-
`script_stream` surface — shell scripts narrating phase lines into the chat UI. It never
|
|
36
|
-
evaluated the MCP-stderr surface, whose consumer is an investigator grepping a file. That
|
|
37
|
-
consumer still exists; it is simply already served by `mcp-spawn-tee`.
|
|
38
|
-
|
|
39
|
-
Both records get the correct argument before the task archives. A wrong reason in an
|
|
40
|
-
archive licenses the next removal that the reason does not actually cover.
|
|
41
|
-
|
|
42
|
-
## What changes
|
|
43
|
-
|
|
44
|
-
**Code.**
|
|
45
|
-
|
|
46
|
-
- Delete `platform/lib/mcp-stderr-tee/`.
|
|
47
|
-
- Delete its two `tsc -p lib/mcp-stderr-tee/tsconfig.json` entries in
|
|
48
|
-
`platform/package.json` (`build` and `build:lib`).
|
|
49
|
-
- Remove 21 `initStderrTee` call sites — 20 plugin MCP entry files plus
|
|
50
|
-
`platform/lib/graph-mcp/src/index.ts`. Each loses one import line and one call line.
|
|
51
|
-
`graph-mcp` is inside the 21, not additional. Premium plugins have zero call sites.
|
|
52
|
-
- Remove the now-dead `STREAM_LOG_PATH` assignment in
|
|
53
|
-
`platform/plugins/memory/mcp/scripts/boot-smoke.sh` — the last assignment in the tree once
|
|
54
|
-
the reader is gone.
|
|
55
|
-
- Strip the dangling module name from two comment-only references
|
|
56
|
-
(`services/claude-session-manager/src/rc-daemon.ts`, `platform/scripts/logs-rotate.sh`).
|
|
57
|
-
Neither imports it.
|
|
58
|
-
- `packages/create-maxy-code/scripts/bundle.js` names `mcp-stderr-tee` as the example in its
|
|
59
|
-
lib-rewrite comment. The mechanism is generic and collects whatever premium plugins import;
|
|
60
|
-
the example just becomes stale. Point it at a lib that is actually imported.
|
|
61
|
-
|
|
62
|
-
**Docs.** `platform/plugins/docs/references/plugins-guide.md` loses every claim that a
|
|
63
|
-
per-conversation MCP stream log exists:
|
|
64
|
-
|
|
65
|
-
- the `initStderrTee` opt-in block;
|
|
66
|
-
- "How the tee decides which file to write to" (asserts the platform sets `STREAM_LOG_PATH`
|
|
67
|
-
on every spawn — it does not);
|
|
68
|
-
- the tee-state markers paragraph (`[mcp-tee-attach]` / `[mcp-tee-skip]` /
|
|
69
|
-
`[mcp-tee-detach]` "land in the stream log");
|
|
70
|
-
- the `logs-read { type: "system", sessionId }` → grep `[mcp:<name>]` retrieval instruction;
|
|
71
|
-
- the "Main-subprocess stderr" `[subproc-stderr]` paragraph, which describes the retired
|
|
72
|
-
admin server;
|
|
73
|
-
- the `initStderrTee` `createWriteStream` write-race paragraph in the failure-path contract;
|
|
74
|
-
- the premium-plugins paragraph's `mcp-stderr-tee` bundler example.
|
|
75
|
-
|
|
76
|
-
What replaces them is short: a plugin author calls `console.error`; the `mcp-spawn-tee` shim
|
|
77
|
-
captures every byte into `mcp-<name>-<SESSION_ID>.log`; that file is where you grep. No
|
|
78
|
-
import, no call, no opt-in.
|
|
79
|
-
|
|
80
|
-
`platform/plugins/admin/skills/platform-architecture/SKILL.md` is generated from that
|
|
81
|
-
reference doc. Regenerate it; never hand-edit.
|
|
82
|
-
|
|
83
|
-
## Scope boundaries
|
|
84
|
-
|
|
85
|
-
**In scope:** the `STREAM_LOG_PATH` contract — the module, its 21 call sites, its build
|
|
86
|
-
entries, and the doc claims asserting the surface works.
|
|
87
|
-
|
|
88
|
-
**Out of scope:**
|
|
89
|
-
|
|
90
|
-
- The raw sink. Task 1721 settled it and this removal does not touch it.
|
|
91
|
-
- `mcp-spawn-tee`'s stale marker names (`[mcp-spawn-tee-attached]` / `[mcp-spawn-tee-exit]`,
|
|
92
|
-
retired by Task 989 in favour of `[mcp-helper] op=spawn|boot|exit`) and its stale
|
|
93
|
-
two-process description. Owned by [`.tasks/1737-*.md`](../../../../.tasks/1737-mcp-spawn-tee-docs-promise-lifecycle-markers-and-a-child-process-that-task-989-removed.md),
|
|
94
|
-
which is designed but unimplemented on branch `worktree-task-1737-mcp-spawn-tee-docs`.
|
|
95
|
-
This sprint must not restate those names when it points authors at the sink, or it
|
|
96
|
-
re-introduces the exact bug 1737 exists to fix.
|
|
97
|
-
- Reviving `claude-agent-stream-<id>.log` or any per-conversation agent stream file. It has
|
|
98
|
-
no producer in the PTY architecture; building one is a different task with a different
|
|
99
|
-
premise.
|
|
100
|
-
- The `script_stream` surface and its banned-token gate. Tasks 287/288 settled it.
|
|
101
|
-
|
|
102
|
-
## Open question to resolve during implementation
|
|
103
|
-
|
|
104
|
-
`logs-read { type: "system", sessionId }` is documented as returning the conversation's
|
|
105
|
-
stream log for `[mcp:<name>]` grepping. The doc instruction goes either way — grepping
|
|
106
|
-
`[mcp:<name>]` on that log returns nothing, so the sentence is false regardless of what the
|
|
107
|
-
tool does internally.
|
|
108
|
-
|
|
109
|
-
What is unresolved is the *tool*. If `logs-read type:"system"` reads a file with no producer,
|
|
110
|
-
that is a live bug and not this task's doc defect. Establish which during implementation. If
|
|
111
|
-
it is a bug, file a `.tasks/NNN-*.md` for it — deleting the doc sentence while leaving a tool
|
|
112
|
-
that silently returns nothing would hide the same false-clean shape one layer down, which is
|
|
113
|
-
the exact failure this task exists to close.
|
|
114
|
-
|
|
115
|
-
## Verification
|
|
116
|
-
|
|
117
|
-
- `npm run build` at `maxy-code/platform/` is clean after the tsconfig entries are removed.
|
|
118
|
-
- The 21 affected MCP packages typecheck.
|
|
119
|
-
- `initStderrTee` and `mcp-stderr-tee` return zero hits outside `.tasks/archive/` and
|
|
120
|
-
`docs/superpowers/specs/`.
|
|
121
|
-
- The architecture-skill regeneration gate passes with the regenerated `SKILL.md` committed.
|
|
122
|
-
- `STREAM_LOG_PATH` returns zero hits in the live tree.
|
|
123
|
-
|
|
124
|
-
## Deployment
|
|
125
|
-
|
|
126
|
-
Every one of the 21 call sites ships in the bundled `create-maxy-code` payload, so this
|
|
127
|
-
requires an installer publish to reach the fleet. Per the sprint gating rule, publish awaits
|
|
128
|
-
an explicit operator command.
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
# Graph-native cash book + AR/AP ledger — design
|
|
2
|
-
|
|
3
|
-
Spec for [Task 1771](../../../../.tasks/1771-graph-native-cash-book-ledger.md). Written 2026-07-18.
|
|
4
|
-
|
|
5
|
-
## Why
|
|
6
|
-
|
|
7
|
-
On 2026-07-18 an admin asked the agent to record a £2,000 payment. The agent began inventing `Invoice`
|
|
8
|
-
properties (`total`, `amountPaid`, `balance`, `status:"part-paid"`), then reported that no ledger exists
|
|
9
|
-
and offered QuickBooks, a bespoke design, or a reminder task.
|
|
10
|
-
|
|
11
|
-
The report was partly wrong. `:Invoice` is in the base ontology (`schema-base.md:20`). But
|
|
12
|
-
`:InvoicePayment`, `:Credit`, `:InvoiceLine` and `:InboundInvoice` are declared only for the construction
|
|
13
|
-
vertical (`schema-construction.md:63-64`), so on the other nine `businessType` values an invoice can be
|
|
14
|
-
raised and a payment against it has nowhere to land. `paymentStatus` is free text. No arithmetic anywhere
|
|
15
|
-
relates an invoice total to what has been paid. The QuickBooks plugin performs zero graph writes. Write
|
|
16
|
-
Cypher is specialist-only (`platform/plugins/graph/PLUGIN.md:42`), so the admin agent has no deterministic
|
|
17
|
-
path even where the labels exist.
|
|
18
|
-
|
|
19
|
-
## Corrections to the task file
|
|
20
|
-
|
|
21
|
-
Three statements in Task 1771 were checked against the tree and do not hold. The implementation follows
|
|
22
|
-
the tree, not the task file.
|
|
23
|
-
|
|
24
|
-
1. **`ADMIN_CORE_TOOLS` does not exist.** `grep -rn "ADMIN_CORE_TOOLS\s*[=:]"` returns zero hits. It is
|
|
25
|
-
named in `CLAUDE.md:35` but `.docs/superpowers/plans/2026-06-26-email-body-extraction.md:15` records it
|
|
26
|
-
as stale. The live mechanism is `eagerTool` (`platform/lib/mcp-eager/src/index.ts:63`), which sets
|
|
27
|
-
`_meta["anthropic/alwaysLoad"]`, plus the generated canonical-tool-names artifact.
|
|
28
|
-
2. **`packages/create-maxy-code/payload/` must not be hand-edited.** It is gitignored and
|
|
29
|
-
`scripts/bundle.js:276-280` wipes it on every run.
|
|
30
|
-
3. **The ledger edges do not belong in `TYPED_EDGE_ALLOWLIST`.** See § Ontology below. The task file's
|
|
31
|
-
list of eight triples is dropped; five patterns go to `schema-base.md` § Relationship Patterns instead,
|
|
32
|
-
and `typed-edge-schema.ts` is not touched.
|
|
33
|
-
4. **A tool alone is not a standing check.** There is no plugin-level API for recurring jobs. The two real
|
|
34
|
-
homes are a `setInterval` module wired into `platform/ui/server/index.ts`, or code called from
|
|
35
|
-
`platform/plugins/scheduling/mcp/src/scripts/check-due-events.ts`. The SiteDesk censuses cited as
|
|
36
|
-
precedent in the task file (`quote-generation.md:241-243`, `issue-flow.md:103-115`) have **no emitter in
|
|
37
|
-
code**; they are documented log lines nothing produces. Shipping `ledger-reconcile` as only a tool would
|
|
38
|
-
reproduce that defect.
|
|
39
|
-
|
|
40
|
-
## Architecture
|
|
41
|
-
|
|
42
|
-
Three units. The arithmetic invariant is needed by two processes, so it lives in neither of them.
|
|
43
|
-
|
|
44
|
-
### 1. `platform/lib/ledger-core/` — pure
|
|
45
|
-
|
|
46
|
-
No Neo4j, no filesystem, no environment reads. Exports:
|
|
47
|
-
|
|
48
|
-
- `computeOutstanding(totalPaymentDue, payments, credits)` — the invariant
|
|
49
|
-
`outstanding = totalPaymentDue - Σpayments - Σcredits`.
|
|
50
|
-
- `applyPayment(invoice, existing, candidate)` — returns `{accepted}` or
|
|
51
|
-
`{rejected, reason: 'over-application', remaining}`. Never clamps.
|
|
52
|
-
- `ageBuckets(openInvoices, now)` — 0-30 / 31-60 / 61-90 / 90+ days.
|
|
53
|
-
- `reconcileLedger(rows, now)` — returns the census finding, including `alarm`.
|
|
54
|
-
|
|
55
|
-
`platform/ui` already imports `platform/lib/*/dist` (`server/index.ts:45`,
|
|
56
|
-
`app/lib/graph-health.ts:25`), so one shared lib serves both consumers and the invariant is defined once.
|
|
57
|
-
ledger-core computes in integer minor units so the equality is exact and never subject to float drift.
|
|
58
|
-
Graph storage is unchanged: `amount` and `totalPaymentDue` stay decimal numbers, matching the existing
|
|
59
|
-
JobLogic-exact construction rows. The tools convert on the way in and on the way out, so no stored value
|
|
60
|
-
changes shape and existing construction data stays readable.
|
|
61
|
-
|
|
62
|
-
### 2. `platform/plugins/ledger/` — the MCP
|
|
63
|
-
|
|
64
|
-
Seven tools, registered with `eagerTool`, which wraps each handler in `wrapWithLifeline` and therefore
|
|
65
|
-
emits `op=request` and `op=result` with a correlation token and a handle census for free. This also
|
|
66
|
-
satisfies `platform/scripts/check-no-raw-mcp-registrations.mjs`.
|
|
67
|
-
|
|
68
|
-
| Tool | Purpose |
|
|
69
|
-
|---|---|
|
|
70
|
-
| `ledger-invoice-record` | Raise an AR `:Invoice` or record an AP `:InboundInvoice`, selected by a required `direction` |
|
|
71
|
-
| `ledger-payment-record` | Apply a payment to either invoice label |
|
|
72
|
-
| `ledger-credit-record` | Apply a credit to an `:Invoice` |
|
|
73
|
-
| `ledger-cash-record` | A `:CashEntry` not attached to any invoice |
|
|
74
|
-
| `ledger-balance` | One invoice: total, applied, outstanding |
|
|
75
|
-
| `ledger-statement` | Open receivables in ageing buckets, open payables, net cash position |
|
|
76
|
-
| `ledger-reconcile` | On-demand form of the census |
|
|
77
|
-
|
|
78
|
-
Structure copies `platform/plugins/contacts/`: own `mcp/src/lib/neo4j.ts`, per-tool files under
|
|
79
|
-
`mcp/src/tools/`, writes through `writeNodeWithEdges` from `platform/lib/graph-write/dist/index.js`.
|
|
80
|
-
`accountId` is read once from `process.env.ACCOUNT_ID` at `index.ts` and passed in, never derived inside a
|
|
81
|
-
tool. `createdBy` is `{agent: AGENT_SLUG, session: SESSION_ID, tool: <name>}`. Every arithmetic decision
|
|
82
|
-
delegates to ledger-core; the plugin contains no money arithmetic of its own.
|
|
83
|
-
|
|
84
|
-
### 3. `platform/ui/app/lib/ledger-census.ts` — the standing check
|
|
85
|
-
|
|
86
|
-
Mirrors `platform/ui/app/lib/timeentry-census.ts`: pure reconcile imported from ledger-core, impure
|
|
87
|
-
`runLedgerCensusOnce(session, now)`, and `startLedgerCensus(openSession, opts)` returning a stop function.
|
|
88
|
-
The interval is `unref()`'d so it never holds the process open, defaults to 15 minutes matching its
|
|
89
|
-
sibling, and takes a boot tick. A Neo4j error logs and returns null rather than throwing into the timer.
|
|
90
|
-
Wired at `platform/ui/server/index.ts` alongside `startTimeEntryCensus` (`:2208-2215`).
|
|
91
|
-
|
|
92
|
-
`ledger-reconcile` and the interval both call the same `reconcileLedger`, so the on-demand and standing
|
|
93
|
-
forms cannot drift apart.
|
|
94
|
-
|
|
95
|
-
## Ontology
|
|
96
|
-
|
|
97
|
-
Promote to `schema-base.md` so all ten verticals are covered by one definition:
|
|
98
|
-
|
|
99
|
-
| Label | Natural key |
|
|
100
|
-
|---|---|
|
|
101
|
-
| `:InboundInvoice` | `(accountId, supplier, confirmationNumber)` |
|
|
102
|
-
| `:InvoiceLine` | `(accountId, invoiceId, lineId)` |
|
|
103
|
-
| `:InvoicePayment` | `(accountId, invoiceId, paymentId)` |
|
|
104
|
-
| `:Credit` | `(accountId, creditId)` |
|
|
105
|
-
| `:CashEntry` | `(accountId, entryId)` — new |
|
|
106
|
-
|
|
107
|
-
`:CashEntry` requires `accountId`, `entryId`, `direction` (`'in'|'out'`), `amount`, `date`, `description`.
|
|
108
|
-
|
|
109
|
-
Two derivations, not stored fields:
|
|
110
|
-
|
|
111
|
-
- **Direction of a payment comes from the parent label.** An `:InvoicePayment` under `:Invoice` is money
|
|
112
|
-
received; under `:InboundInvoice` it is money paid. No `direction` property on `:InvoicePayment`.
|
|
113
|
-
- **Outstanding is computed on read.** `outstanding`, `balance` and `amountPaid` join § Forbidden
|
|
114
|
-
Properties (`schema-base.md:119`) for `:Invoice` and `:InboundInvoice`.
|
|
115
|
-
|
|
116
|
-
`schema.cypher` additionally gains the `:Invoice` `(accountId, confirmationNumber)` uniqueness constraint
|
|
117
|
-
that base already claims as its natural key but never declared, the `:CashEntry` constraint and index, and
|
|
118
|
-
entries in the `entity_search_admin` fulltext union (`:465-481`).
|
|
119
|
-
|
|
120
|
-
`schema-construction.md` drops the rows now owned by base. Construction-only labels stay.
|
|
121
|
-
|
|
122
|
-
The line-item `amount` carve-out against the base `totalPaymentDue` synonym ban
|
|
123
|
-
(`schema-construction.md:66-70`) moves to `schema-base.md` § Property Naming Rules with the labels.
|
|
124
|
-
|
|
125
|
-
Five edge patterns join `schema-base.md` § Relationship Patterns (`:168`), beside the
|
|
126
|
-
`(:Invoice)-[:BILLED_TO]->` pattern already there:
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
(:Invoice|:InboundInvoice)-[:HAS_LINE]->(:InvoiceLine)
|
|
130
|
-
(:Invoice|:InboundInvoice)-[:HAS_PAYMENT]->(:InvoicePayment)
|
|
131
|
-
(:Invoice)-[:HAS_CREDIT]->(:Credit)
|
|
132
|
-
(:InboundInvoice)-[:FROM_SUPPLIER]->(:Organization)
|
|
133
|
-
(:CashEntry)-[:COUNTERPARTY]->(:Person|Organization)
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Not `TYPED_EDGE_ALLOWLIST`.** That constant is imported by exactly two consumers,
|
|
137
|
-
`plugins/memory/mcp/src/tools/memory-ingest.ts:25` and `scripts/generate-edge-docs.ts:20`. It governs what
|
|
138
|
-
the typed-edge classifier may propose from free-text document ingest. `lib/graph-write/src/index.ts` never
|
|
139
|
-
references it and `lib/schema-validator.ts` performs no edge-type validation, so the ledger tools' writes
|
|
140
|
-
are not checked against it. Adding ledger triples there would make the classifier extract ledger edges out
|
|
141
|
-
of ingested documents, which is not intended. The allowlist header states the rule directly: edge types
|
|
142
|
-
outside it "are governed by schema-base.md / vertical-schema rules".
|
|
143
|
-
|
|
144
|
-
## Data flow — one payment
|
|
145
|
-
|
|
146
|
-
1. Resolve the invoice by reference; capture its label and `totalPaymentDue`.
|
|
147
|
-
2. Fetch existing `:InvoicePayment` and `:Credit` children.
|
|
148
|
-
3. ledger-core validates the candidate.
|
|
149
|
-
4. Write through `writeNodeWithEdges`, or reject.
|
|
150
|
-
5. Re-read outstanding from the graph.
|
|
151
|
-
6. Emit `op=verify`.
|
|
152
|
-
|
|
153
|
-
Step 5 is the point of the design. `op=write` proves intention; only the re-read proves outcome.
|
|
154
|
-
|
|
155
|
-
## Errors
|
|
156
|
-
|
|
157
|
-
Over-application rejects, is logged, and is reported to the user verbatim. It is never clamped and never
|
|
158
|
-
silently recorded. An unresolvable invoice reference rejects. Neo4j failures surface as `isError` payloads,
|
|
159
|
-
which `wrapWithLifeline` logs as `outcome=error` (`mcp-lifeline/src/index.ts:20-26`). The census swallows
|
|
160
|
-
nothing but never throws into the interval.
|
|
161
|
-
|
|
162
|
-
## Observability
|
|
163
|
-
|
|
164
|
-
Failure modes and the signal for each. Four are no-event failures owned by the census, not by a log line.
|
|
165
|
-
|
|
166
|
-
| # | Failure | Event? | Signal |
|
|
167
|
-
|---|---|---|---|
|
|
168
|
-
| 1 | Narrated as recorded, nothing written | No | `[ledger-census]` counts flat across a window containing `op=request` lines |
|
|
169
|
-
| 2 | Payment with no parent-invoice edge | No | `[ledger-census] orphanPayments=<N>` |
|
|
170
|
-
| 3 | A stored balance field reappears and drifts | No | `[ledger-census] storedBalanceProps=<N>` |
|
|
171
|
-
| 4 | Duplicate payment via non-deterministic `paymentId` | No | `[ledger-census] suspectDuplicates=<N>` |
|
|
172
|
-
| 5 | Over-application | Yes | `op=reject reason=over-application` |
|
|
173
|
-
| 6 | Write rejected, resolved vertical lacks the label | Yes | `op=reject reason=unknown-label vertical=<file>` |
|
|
174
|
-
| 7 | `schema.cypher` edited, `seed-neo4j.sh` not re-run | No | `[ledger-census] declaredNotLive=<labels>` from `live-schema-source.ts:180` |
|
|
175
|
-
| 8 | Tool in one registry, absent from the other | No | `[ledger-census] toolsResolved=7/7` |
|
|
176
|
-
|
|
177
|
-
Lifecycle for `ledger-payment-record`, correlated by `paymentId`. Lines 1 and 6 come free from
|
|
178
|
-
`wrapWithLifeline`; the rest are domain lines the tool emits.
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
[ledger] op=request paymentId=… invoiceRef=… direction=… amount=… date=…
|
|
182
|
-
[ledger] op=resolve paymentId=… found=<bool> label=<Invoice|InboundInvoice> vertical=… totalPaymentDue=…
|
|
183
|
-
[ledger] op=arithmetic paymentId=… priorApplied=… priorCredits=… amount=… outstandingBefore=… outstandingAfter=…
|
|
184
|
-
[ledger] op=write paymentId=… nodesCreated=… edgesCreated=…
|
|
185
|
-
[ledger] op=reject paymentId=… reason=<over-application|unknown-label|no-parent> detail=…
|
|
186
|
-
[ledger] op=verify paymentId=… outstandingReRead=…
|
|
187
|
-
[ledger] op=exit paymentId=… ranMs=… outcome=<applied|rejected|error>
|
|
188
|
-
[ledger-census] invoices=… payments=… orphanPayments=… storedBalanceProps=… suspectDuplicates=… declaredNotLive=… toolsResolved=…/7
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Success:** `op=verify` present, `outstandingReRead` equal to `op=arithmetic`'s `outstandingAfter`; census
|
|
192
|
-
with every counter zero and `toolsResolved=7/7`.
|
|
193
|
-
|
|
194
|
-
**Failure:** `op=request` with no matching `op=exit`; `op=write` with no `op=verify`; any non-zero census
|
|
195
|
-
counter; **absence of a census line** in the expected window; `op=resolve found=false` on a reference the
|
|
196
|
-
user just supplied.
|
|
197
|
-
|
|
198
|
-
**Diagnostic path:** the plugin MCP tee log, `grep -E '\[ledger\]|\[ledger-census\]'`, then filter by one
|
|
199
|
-
`paymentId` for that operation's full lifeline.
|
|
200
|
-
|
|
201
|
-
## Registration
|
|
202
|
-
|
|
203
|
-
- `platform/plugins/ledger/PLUGIN.md` frontmatter is the source of truth for tool names.
|
|
204
|
-
- `cd platform && npm run gen:canonical-tools` regenerates
|
|
205
|
-
`services/claude-session-manager/src/canonical-tool-names.generated.ts`.
|
|
206
|
-
`platform/scripts/check-canonical-tool-names.mjs` fails the build on drift.
|
|
207
|
-
- `plugins.core[]` in all four brand JSONs: `maxy-code`, `realagent-code`, `sitedesk-code`,
|
|
208
|
-
`property-administrators`. A plugin absent from a brand's list does not ship to that brand.
|
|
209
|
-
- Specialist templates `database-operator.md` and `project-manager.md`.
|
|
210
|
-
- `platform/package.json` build chain gains `lib/ledger-core`. The `plugins/*/mcp` glob picks the plugin up
|
|
211
|
-
automatically.
|
|
212
|
-
|
|
213
|
-
## Testing
|
|
214
|
-
|
|
215
|
-
- **ledger-core** — pure unit tests, no infrastructure. Part payment leaves the exact remainder; two
|
|
216
|
-
payments summing to the total leave zero; a credit reduces outstanding identically to a payment; an
|
|
217
|
-
over-application is rejected with the remaining figure; a zero-total invoice accepts no payment; the same
|
|
218
|
-
`paymentId` twice does not double-count; ageing buckets at the 30/60/90 boundaries.
|
|
219
|
-
- **Tools** — stubbed session; asserts the reject branch writes nothing and that `op=verify` re-reads.
|
|
220
|
-
- **Per-vertical coverage** — iterates all ten `businessType` values resolved by
|
|
221
|
-
`resolve-active-vertical.ts:38,67-81` including the `trades → schema-construction` alias, validating an
|
|
222
|
-
`:Invoice` + `:InvoicePayment` + `:CashEntry` write against each. Fails before the promotion, passes
|
|
223
|
-
after. This is the test that would have caught the reported defect.
|
|
224
|
-
- **Census** — pure `reconcileLedger` cases for each non-zero counter.
|
|
225
|
-
- **Drift gate** — `schema-cypher-drift.test.ts:60` green with no new `PROSE_ONLY_LABELS` entries.
|
|
226
|
-
|
|
227
|
-
Baseline in this worktree before any change: 508 vitest tests pass across the memory plugin, 56 node:test
|
|
228
|
-
tests pass. Three suites report as failed under vitest because they are `node:test` files vitest cannot
|
|
229
|
-
collect. That is pre-existing and unrelated.
|
|
230
|
-
|
|
231
|
-
## Regression boundary
|
|
232
|
-
|
|
233
|
-
Existing construction-vertical tests unchanged. `resolve-active-vertical.test.ts` cases at `:69,:82,:95,:108`
|
|
234
|
-
unchanged. `account-schema-owned-dirs.test.sh` confirms no new account file-schema bucket is minted, since
|
|
235
|
-
`schema-base.md` has no `## Top-level node types` section. The SiteDesk `:Job -> :Quote -> :QuoteLine`
|
|
236
|
-
reification is untouched.
|
|
237
|
-
|
|
238
|
-
## Out of scope
|
|
239
|
-
|
|
240
|
-
Double-entry. QuickBooks-to-graph mirroring. VAT, tax and multi-currency. Bank feed import. Any UI.
|
|
241
|
-
SiteDesk quoting figures. Backfill of existing construction accounts.
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
# Task 1704 — data portal standing audit
|
|
2
|
-
|
|
3
|
-
**Date:** 2026-07-20 · **Lane:** cloudflare + storage-broker · data-portal observability
|
|
4
|
-
**Depends on:** 1689 (portal, landed), 1694 (broker R2 object ops, landed), 1810 (loop registry, landed)
|
|
5
|
-
|
|
6
|
-
## The problem
|
|
7
|
-
|
|
8
|
-
The `[data-portal]` lifeline traces one upload end to end and ends in a re-read post-condition, so
|
|
9
|
-
every failure that emits an event is already covered. Three conditions emit nothing:
|
|
10
|
-
|
|
11
|
-
- **An orphan object** — an object in R2 with no manifest row. Stored, invisible to ingestion
|
|
12
|
-
forever, and the upload that produced it returned `200`.
|
|
13
|
-
- **A phantom row** — a manifest row whose object is absent. Ingestion will pull a missing file,
|
|
14
|
-
and nothing says so until ingestion runs, which may be never.
|
|
15
|
-
- **A stalled ingestion sweep** — an ingestion that never runs logs nothing, so a climbing backlog
|
|
16
|
-
and an empty one look identical.
|
|
17
|
-
|
|
18
|
-
Only a standing reconcile reveals any of them. Absence of the audit's line is itself the signal that
|
|
19
|
-
all three are unobservable.
|
|
20
|
-
|
|
21
|
-
## Premise corrections carried from the task file
|
|
22
|
-
|
|
23
|
-
The task brief predates three changes. All three point the same way; none change its intent.
|
|
24
|
-
|
|
25
|
-
1. **The `[storage-audit]` idiom is now `registerLoop`.** Task 1810 moved every standing loop onto
|
|
26
|
-
`platform/ui/server/loop-registry.ts`, and `scripts/check-loops-registered.mjs` fails the build on
|
|
27
|
-
a bare `setInterval`. "Copy the `[storage-audit]` shape" therefore means registering a loop, not
|
|
28
|
-
hand-rolling two unref'd timers.
|
|
29
|
-
2. **Every line number in the brief is stale**, though every cited thing still exists.
|
|
30
|
-
`runStorageAudit` is `storage-broker.ts:773`; the "silent when the config names no portal" idiom
|
|
31
|
-
is the `hasBookingSite` check in the availability loop; `r2ObjectList`'s drain-to-exhaustion
|
|
32
|
-
comment at `cf-exec.ts:443` names this audit as its own reason for existing.
|
|
33
|
-
3. **`reconcilePages` (Task 1728) is a closer template than the brief knew.** It is already a pure
|
|
34
|
-
orphan/phantom set difference over two lists in `lib/storage-broker/src/audit.ts`, carrying the
|
|
35
|
-
concurrency contract and the refusal to print `orphan=<everything>` off a failed read. This audit
|
|
36
|
-
is that shape one level down: objects against rows instead of projects against records.
|
|
37
|
-
|
|
38
|
-
## Components
|
|
39
|
-
|
|
40
|
-
### 1. `reconcileDataPortal` — pure, in `lib/storage-broker/src/audit.ts`
|
|
41
|
-
|
|
42
|
-
Third reconcile beside `reconcileStorage` and `reconcilePages`. It is D1 rows against R2 objects,
|
|
43
|
-
which is that lib's domain, and the ui server imports the lib by source path so there is no dist
|
|
44
|
-
rebuild in the loop.
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
interface DataPortalReconcile {
|
|
48
|
-
objects: number
|
|
49
|
-
rows: number
|
|
50
|
-
orphanObjects: string[] // object keys with no manifest row
|
|
51
|
-
phantomRows: string[] // manifest objectKeys with no object
|
|
52
|
-
oldestUningestedHrs: number | null // null renders as `na`
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function reconcileDataPortal(
|
|
56
|
-
objects: Array<{ key: string }>,
|
|
57
|
-
rows: Array<{ objectKey: string; ingested: number; uploadedAt: string }>,
|
|
58
|
-
nowMs: number,
|
|
59
|
-
): DataPortalReconcile
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
`nowMs` is injected so the age arithmetic is deterministic under test rather than clock-dependent.
|
|
63
|
-
|
|
64
|
-
`oldestUningestedHrs` is computed from rows with `ingested === 0` only, and is `null` when there are
|
|
65
|
-
none. **An `uploadedAt` that will not parse is excluded from the minimum rather than allowed to
|
|
66
|
-
produce `NaN`** — that is the `snapshotAgeMs=NaN` lesson from the availability loop, where a truthy
|
|
67
|
-
but non-ISO value slipped past a `??` guard and logged `NaN`. If exclusion leaves no usable row, the
|
|
68
|
-
result is `null`.
|
|
69
|
-
|
|
70
|
-
### 2. `parseManifestRows` — pure, exported from the route module
|
|
71
|
-
|
|
72
|
-
`cf.d1Query` shells `npx wrangler d1 execute --json` and is typed `unknown`. This function parses
|
|
73
|
-
that envelope into the row shape above and **throws on anything it does not recognise**.
|
|
74
|
-
|
|
75
|
-
Throwing rather than returning `[]` is the load-bearing decision. Zero rows against a populated
|
|
76
|
-
bucket reads as "every object is an orphan" — a fabricated defect count off a failed read, which is
|
|
77
|
-
precisely what `reconcilePages` refuses to print. A throw lands in the d1-degraded branch, where the
|
|
78
|
-
line says the read failed instead of asserting a number it does not have.
|
|
79
|
-
|
|
80
|
-
### 3. `runDataPortalAudit(root)` — the caller, in `ui/server/routes/storage-broker.ts`
|
|
81
|
-
|
|
82
|
-
Sits with `runStorageAudit` and `runPagesAudit` at the foot of the same file.
|
|
83
|
-
|
|
84
|
-
**Discovery.** Walks `data/accounts/*/data-portal.json`. A config that is absent, unparseable, or
|
|
85
|
-
parses but names no `portalDbName` and `bucketName` is **silent** — the `hasBookingSite` idiom. An
|
|
86
|
-
account with no portal is not a fault and must not produce a line.
|
|
87
|
-
|
|
88
|
-
**It returns before minting a house token when no account has a portal.** Most installs have no data
|
|
89
|
-
portal and no house Cloudflare credential; minting unconditionally would make this loop report an
|
|
90
|
-
error forever on every one of them.
|
|
91
|
-
|
|
92
|
-
**Reads.** One `makeHouseCfExec(root)` for the whole run, then per account:
|
|
93
|
-
|
|
94
|
-
- rows — `d1Query(portalDbName, 'SELECT objectKey, ingested, uploadedAt FROM manifest')`
|
|
95
|
-
- objects — `r2ObjectList(bucketName)`
|
|
96
|
-
|
|
97
|
-
The two halves are caught **separately**, and the line is emitted unconditionally afterwards. This is
|
|
98
|
-
`.tasks/backlog/1351` made structural: the log is not downstream of either read, so neither read can
|
|
99
|
-
skip it. An audit that a data-source failure silences is the same blind spot one layer up, in a task
|
|
100
|
-
that exists because failures emit nothing.
|
|
101
|
-
|
|
102
|
-
**Degradation.** Each line carries exactly what is still measurable:
|
|
103
|
-
|
|
104
|
-
| Failure | Line still carries |
|
|
105
|
-
|---|---|
|
|
106
|
-
| R2 read fails | `rows=N oldestUningestedHrs=N`, remainder `na`, `degraded=r2` |
|
|
107
|
-
| D1 read fails | `objects=N`, remainder `na`, `degraded=d1` |
|
|
108
|
-
| Both fail | all `na`, `degraded=d1+r2` |
|
|
109
|
-
|
|
110
|
-
`oldestUningestedHrs` surviving an R2 failure is not incidental: the backlog age is computable from
|
|
111
|
-
the manifest alone, and it is the one of the three conditions that a half-broken audit can still
|
|
112
|
-
report.
|
|
113
|
-
|
|
114
|
-
**Line format.** Counts only:
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
[data-portal-audit] account=<id> objects=N rows=N orphanObjects=N phantomRows=N oldestUningestedHrs=N
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Healthy is `orphanObjects=0 phantomRows=0` with a bounded age. **Any non-zero orphan or phantom count
|
|
121
|
-
is a defect, not noise.**
|
|
122
|
-
|
|
123
|
-
`reconcilePages` reports orphan and phantom *names*, on the grounds that a count says something is
|
|
124
|
-
wrong and a name says what to fix. This audit deliberately does not, and the pure function returning
|
|
125
|
-
the keys does not change that: the keys here are `<ownerId>/<filename>`, real people's filenames,
|
|
126
|
-
and the journal is not where they belong. The task's success criterion specifies counts, and the
|
|
127
|
-
audit owning its own ownership discipline is called out in the brief as load-bearing precisely
|
|
128
|
-
because the in-process path holds the account-wide credential and bypasses `objectGate` entirely.
|
|
129
|
-
|
|
130
|
-
### 4. Arming, in `ui/server/index.ts`
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
registerLoop({
|
|
134
|
-
name: 'data-portal-audit',
|
|
135
|
-
intervalMs: 300_000,
|
|
136
|
-
firstRunDelayMs: 50_000,
|
|
137
|
-
run: runDataPortalAuditSafe,
|
|
138
|
-
})
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Same interval as the storage and pages audits. The first run is offset past storage (20s), pages
|
|
142
|
-
(35s) and google-account (45s) so a boot does not fire four token mints in the same instant.
|
|
143
|
-
|
|
144
|
-
**Degradation is not an error.** A degraded run returns a note (`portals=N degraded=M`) and the
|
|
145
|
-
registry records `ok`, because the audit did the job it could and said so. A genuine throw — no
|
|
146
|
-
house credential where a portal exists, an unreachable registry — is logged and **rethrown**, the
|
|
147
|
-
`runStorageAuditSafe` shape, so /activity shows a fresh Last run against a stale Last OK.
|
|
148
|
-
|
|
149
|
-
## Testing
|
|
150
|
-
|
|
151
|
-
**Unit, `lib/storage-broker/src/__tests__/audit.test.ts`** — an object with no row counts one
|
|
152
|
-
`orphanObjects`; a row with no object counts one `phantomRows`; a matched pair counts neither;
|
|
153
|
-
`oldestUningestedHrs` comes from the oldest `ingested=0` row; `ingested=1` rows are excluded from it;
|
|
154
|
-
`na` when there are no uningested rows; an unparseable `uploadedAt` does not yield `NaN`.
|
|
155
|
-
|
|
156
|
-
**Envelope, same run** — `parseManifestRows` on a well-formed wrangler payload, and a throw on a
|
|
157
|
-
malformed one. The throw case is what stops a parse failure reading as an empty manifest.
|
|
158
|
-
|
|
159
|
-
**Route, new `ui/server/__tests__/data-portal-audit.test.ts`** — modelled on
|
|
160
|
-
`storage-broker-object-routes.test.ts`: `// @vitest-environment node`, `vi.mock` the neo4j store,
|
|
161
|
-
partially mock the lib via `importOriginal`, stub `makeHouseCfExec` with a fake carrying
|
|
162
|
-
`r2ObjectList` and `d1Query`. Cases: two accounts, one with a portal, yields one line; a D1 throw
|
|
163
|
-
still emits a line carrying `degraded=d1`; an R2 throw emits `degraded=r2` with `rows` still counted;
|
|
164
|
-
a malformed D1 payload degrades rather than reporting zero rows; a config naming no portal is silent;
|
|
165
|
-
no portal anywhere mints no token.
|
|
166
|
-
|
|
167
|
-
**Mutation check** — reverting the reconcile to compare in one direction only must fail the opposite
|
|
168
|
-
direction's test.
|
|
169
|
-
|
|
170
|
-
## Scope boundaries
|
|
171
|
-
|
|
172
|
-
**In scope:** the reconcile and its tests, the caller, the arming, the route-level tests, and
|
|
173
|
-
`.docs/data-portal.md` § Observability, which currently states the audit is not built.
|
|
174
|
-
|
|
175
|
-
**Out of scope:** the device-side ingestion sweep (unbuilt, agent-side); any change to the portal
|
|
176
|
-
template or the gate (1689, landed); escalation beyond a log line — `.tasks/backlog/1351` records
|
|
177
|
-
that a non-zero count escalating nowhere is its own problem, and that is lane-wide, not this task's.
|