@rubytech/create-maxy-code 0.1.68 → 0.1.70
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/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +163 -102
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +127 -13
- package/payload/platform/plugins/docs/references/admin-session.md +22 -18
- package/payload/platform/plugins/docs/references/platform.md +4 -0
- package/payload/platform/plugins/memory/PLUGIN.md +9 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js +116 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +100 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +197 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js +44 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +29 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +57 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +9 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +36 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +7 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +22 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -2
- package/payload/platform/scripts/lib/read-brand-json.sh +69 -0
- package/payload/platform/scripts/seed-neo4j.sh +67 -8
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +73 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +8 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +49 -4
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +11 -3
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +45 -20
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +5 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +4 -67
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +48 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +2 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +284 -11
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/index.html +1 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/registers.md +66 -8
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +4 -1
- package/payload/server/public/assets/{admin-BPRjc4z9.js → admin-Bej-Q1IZ.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Asw7tNG5.js → architectureDiagram-Q4EWVU46-umb1qQ5e.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Blvay4_b.js → blockDiagram-DXYQGD6D-C8kHw50g.js} +1 -1
- package/payload/server/public/assets/{brand-CwHvVF8s.css → brand-DxX912hX.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-C2st6jZP.js → c4Diagram-AHTNJAMY-CtKxKvPu.js} +1 -1
- package/payload/server/public/assets/channel-BshbIIpE.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-Cp7B2yh1.js → chunk-336JU56O-_xUA1Yj-.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-B0x3RXvj.js → chunk-426QAEUC-d_A8vQqM.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-B0yM0pgq.js → chunk-4TB4RGXK-BgkVBpN_.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-CqGhfArO.js → chunk-5FUZZQ4R-CGOqTZDS.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-jSeD3Yrj.js → chunk-5PVQY5BW-CW78oYeu.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-C7HuvG2r.js → chunk-EDXVE4YY-D_w_ZcbQ.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-DzCrge6H.js → chunk-ENJZ2VHE-MIcdFlwQ.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-BPvCh0Yc.js → chunk-ICPOFSXX-BaRHp6jX.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DamxwrTn.js → chunk-OYMX7WX6-Cgo3QttE.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-DDiVd0qV.js → chunk-U2HBQHQK-BVZMJI1z.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CDof681v.js → chunk-X2U36JSP-DdgOyRDq.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-DYzaYjrN.js → chunk-YZCP3GAM-CO61zu06.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-j0bcHHzU.js → chunk-ZZ45TVLE-B3BNOUKS.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-mzmkY4eP.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Co3bBdih.js +1 -0
- package/payload/server/public/assets/clone-BRSMOear.js +1 -0
- package/payload/server/public/assets/{dagre-C8nbdG6y.js → dagre-BY0FcwR1.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-1RnGN_fy.js → dagre-KV5264BT-YvKOR5T0.js} +1 -1
- package/payload/server/public/assets/data-LkSd6fEQ.js +1 -0
- package/payload/server/public/assets/{device-url-actions-C_Keyr0L.js → device-url-actions-Cq5VIBKc.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-C6d3Rrui.js → diagram-5BDNPKRD-DL3SiNlA.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-aD4Dik01.js → diagram-G4DWMVQ6-C_IwINPH.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CAub49YE.js → diagram-MMDJMWI5-vumL8N8U.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CAKnAfEr.js → diagram-TYMM5635-ewBaf58r.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-BrqOworC.js → erDiagram-SMLLAGMA-kiidRkW3.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DV4eU-T2.js → flowDiagram-DWJPFMVM-C-1rI_Iw.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DayXUN42.js → ganttDiagram-T4ZO3ILL-B_9YdwWh.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CK1xV1OV.js → gitGraphDiagram-UUTBAWPF-BmwivVe3.js} +1 -1
- package/payload/server/public/assets/graph-CPXz4A4I.js +1 -0
- package/payload/server/public/assets/graph-labels-CZR9j5Ge.js +1 -0
- package/payload/server/public/assets/{graphlib-CkF9o1XT.js → graphlib-Dvt3mNtm.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CiepM2H6.js → infoDiagram-42DDH7IO-B2n7mCmp.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DwNkdQ6z.js → ishikawaDiagram-UXIWVN3A-C2ejJadN.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-BlDLMfu_.js → journeyDiagram-VCZTEJTY-BaIXYVwB.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-ayZTc0Oj.js → kanban-definition-6JOO6SKY-B1NCV7hH.js} +1 -1
- package/payload/server/public/assets/{line-LhPUiuH-.js → line-BEjGPKWc.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-DyZYsQv_.js → mermaid-parser.core-DwLM_Bkq.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-CauxgRdz.js → mermaid.core-BwedTSna.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CQzcaJuh.js → mindmap-definition-QFDTVHPH-D6q_XhF7.js} +1 -1
- package/payload/server/public/assets/{page-DAw3-U_s.js → page-BBRZF8Z8.js} +1 -1
- package/payload/server/public/assets/{page-BGv81dGN.js → page-CLr-z4E2.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BPhAeTm9.js → pieDiagram-DEJITSTG-CAKQ-eUh.js} +1 -1
- package/payload/server/public/assets/{public-CGI24wrL.js → public-CfVPuA1x.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CxrzL3ZF.js → quadrantDiagram-34T5L4WZ-B8_Xl5p8.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-ZBSC4fGX.js → requirementDiagram-MS252O5E-BWSq9eFs.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BTDBEisC.js → sankeyDiagram-XADWPNL6-V3uWSRXp.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Bv7fA5_J.js → sequenceDiagram-FGHM5R23-DJ_F3-vT.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-D07lcZrk.js → stateDiagram-FHFEXIEX-odlwuCTP.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DE5qFFDY.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DyeyUqKa.js → timeline-definition-GMOUNBTQ-DXE7m_zC.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-B9FqDmwS.js → vennDiagram-DHZGUBPP-DEiEepoV.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-4-5uCIAD.js → wardleyDiagram-NUSXRM2D-CRhVx95h.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-WdkP5T_y.js → xychartDiagram-5P7HB3ND-9Gv4VNaG.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +5 -5
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/public/assets/channel-DTh_uFbz.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-Dsupydrn.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DrwAXnBf.js +0 -1
- package/payload/server/public/assets/clone-COx5mf-H.js +0 -1
- package/payload/server/public/assets/data-DOI26n9p.js +0 -1
- package/payload/server/public/assets/graph-DP9P7TKZ.js +0 -1
- package/payload/server/public/assets/graph-labels-BoLyECfJ.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Cr1Qet-f.js +0 -1
- /package/payload/server/public/assets/{brand-B8JhGW9e.js → brand-BT-mNX7-.js} +0 -0
|
@@ -92,29 +92,32 @@ After every completed operator admin turn, the Stop hook `platform/plugins/admin
|
|
|
92
92
|
Body overrides relative to a plain Sidebar click:
|
|
93
93
|
- `specialist: 'database-operator'`
|
|
94
94
|
- `model: 'haiku'`
|
|
95
|
-
- `initialMessage: <
|
|
95
|
+
- `initialMessage: <filled-paragraph>` (the four-sentence database-operator template with `{schema}` and `{conversation}` substituted at hook time — see below)
|
|
96
96
|
|
|
97
97
|
**Spawn body.** The hook POSTs `{adminSessionId, channel: 'browser', specialist: 'database-operator', model: 'haiku', initialMessage}` to `POST /api/admin/claude-sessions`. `adminSessionId` is the loopback-bypass key — the wrapper resolves the operator's real `senderId` from `GET <managerBase>/<adminSessionId>/meta` and forwards a Sidebar-shape body to the manager.
|
|
98
98
|
|
|
99
|
-
**`initialMessage`
|
|
99
|
+
**`initialMessage` substitution contract (Task 195).** The agent body on disk is the user-specified four-sentence paragraph verbatim, with two placeholders `{schema}` and `{conversation}`:
|
|
100
100
|
|
|
101
101
|
```
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"toolCalls": [ { "tool": "...", "input": {...}, "output": ... } ] },
|
|
107
|
-
...
|
|
108
|
-
],
|
|
109
|
-
"conversationId": "<operator session_id>",
|
|
110
|
-
"accountId": "<ACCOUNT_ID env, stamped on the manager systemd unit>",
|
|
111
|
-
"occurredAt": "<hook fire time, ISO-8601 UTC, .000Z>"
|
|
112
|
-
}
|
|
102
|
+
You are an expert Neo4J graph operator. Here is the schema {schema}.
|
|
103
|
+
Use your expert judgement to update the graph in reaction to the
|
|
104
|
+
following conversation {conversation}, using the tools at your disposal.
|
|
105
|
+
You are not user-facing and your text goes nowhere — do not emit it.
|
|
113
106
|
```
|
|
114
107
|
|
|
115
|
-
|
|
108
|
+
At fire time the Stop hook substitutes both placeholders and sends the filled paragraph as `initialMessage` — a plain string, not a JSON envelope, no instruction prose, no leading object.
|
|
116
109
|
|
|
117
|
-
|
|
110
|
+
- `{schema}` ← the concatenated text of `platform/plugins/memory/references/schema-base.md` plus, when `brand.json#vertical` names a vertical file that exists in the same directory, that file's content joined with a blank-line separator (Task 193). When the brand declares `vertical: null`, the field is missing, or the named file is absent, the schema is `schema-base.md` alone — never loud-fail here; Task 193 owns the brand-config gate.
|
|
111
|
+
- `{conversation}` ← a flat rendering of the envelope's `turns` array. Format: one line per turn, `role: text`, wrapped in leading and trailing newlines so each line lands on its own line in the filled prompt. Assistant turns that called tools carry inline `[tool: name(args)]` markers at the end of their line, with `args` being the call's input as a JSON literal. Tool outputs are not included — only the call shape the model has been emitting. The user record carrying only `tool_result` blocks does not create a separate transcript line; thinking-only records contribute nothing.
|
|
112
|
+
|
|
113
|
+
The envelope walker still runs (so `[turn-recorder] envelope ...` continues to land for observability and the `trigger-skipped reason=conversation-empty` gate still fires on a turns-empty transcript), but its JSON shape never reaches the model.
|
|
114
|
+
|
|
115
|
+
Walker semantics for `turns` (preserved from Task 177): oldest first, newest last; every user / assistant message in the operator's JSONL from session start to the moment the recorder fired; no windowing, no truncation, no env knob; multi-record assistant messages collapse on `message.id`; `tool_use` and `tool_result` blocks are paired by `tool_use_id` so the result attaches to the assistant turn that owned the call rather than fabricating a separate user turn; if `turns` is empty after the walker runs the hook emits `trigger-skipped reason=conversation-empty` and no recorder is spawned. The Task 177 envelope shape — `{turns, conversationId, accountId, occurredAt}` — is now an internal hook structure; it is not what the model sees.
|
|
116
|
+
|
|
117
|
+
The hook emits two summary lines per fire immediately before `spawn-request`:
|
|
118
|
+
|
|
119
|
+
- `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` — the walker's output. `turnsCount` is monotone across spawns in the same operator session; a flat or decreasing series is the regression signature for "windowing crept back in".
|
|
120
|
+
- `[turn-recorder] substitution sessionId=<op> schemaBytes=<n> conversationBytes=<n> bodyBytes=<n>` — the per-spawn substitution. `schemaBytes` covers schema-base plus any vertical concatenated. `bodyBytes` equals the trailing `initialMessageBytes` field on the next `spawn-request` line — the two log lines read the same number.
|
|
118
121
|
|
|
119
122
|
**Manager-side specialist branches.** The recorder is the first specialist subagent that exercises the full specialist flag matrix:
|
|
120
123
|
- The bundled `platform/templates/specialists/agents/database-operator.md` template is symlinked into `$CLAUDE_CONFIG_DIR/agents/database-operator.md` by the installer; without that link `claude --agent database-operator` silently falls back to the admin agent.
|
|
@@ -200,10 +203,11 @@ Three log-line shapes, all keyed on the same recorder `sessionId`, distinguish t
|
|
|
200
203
|
```
|
|
201
204
|
|
|
202
205
|
- `[pty-spawn-tool-inventory]` (Task 178) measures the **framework** layer — the argv built and the MCP servers' published tools. Argv-tools count must be 11; mcp-listed-tools must be ≥ 11.
|
|
203
|
-
- `[recorder-session-init]` (Task 183 — emitted by the JSONL tail in `claude-session-manager`) measures the **CLI/SDK** layer — what the SDK actually exposed to the model. The
|
|
206
|
+
- `[recorder-session-init]` (Task 183 — emitted by the JSONL tail in `claude-session-manager`) measures the **CLI/SDK** layer — what the SDK actually exposed to the model. The `source=` field names the data path; `toolsArgvMissingFromExposed` is the Outcome A fingerprint:
|
|
207
|
+
- `source=system.init` / `command_permissions` / `deferred_tools_delta` ⇒ the SDK wrote an init record to the JSONL and the tail matched one of the three known shapes. The bridge-attached `--remote-control` mode writes `attachment.deferred_tools_delta`; the headless SDK init record (`system.subtype=init`) is what `claude -p --output-format=stream-json` modes write.
|
|
208
|
+
- `source=headless-argv-fallback` ⇒ Task 185 finding (2026-05-20): headless `claude --verbose` PTY mode (which the database-operator recorder uses) does **not** write any tool-list record to the JSONL — none of the three probe shapes ever appears. The tail falls back to the spawner's `--allowed-tools` argv as the exposed set, independently verified by `[pty-spawn-tool-inventory]`'s `exposed=` field. Treat this source identically to a positive JSONL match: the argv set is authoritative for headless spawns.
|
|
204
209
|
- `toolsArgvMissingFromExposed=—` ⇒ all argv tools made it through; Outcome A is ruled out.
|
|
205
|
-
- Any non-`—` value enumerates the names the SDK dropped and is the diagnosis.
|
|
206
|
-
- `error=init-record-not-found probedTypes=system.init,command_permissions,deferred_tools_delta` ⇒ no init-shaped record was found in the JSONL by the time the first assistant turn landed. The miss itself is the observable; iterate the probe list against the live JSONL to learn the new record shape.
|
|
210
|
+
- Any non-`—` value enumerates the names the SDK dropped and is the diagnosis. Only meaningful when `source` is JSONL-derived; under `headless-argv-fallback` the exposed set equals argv by construction, so this field is always `—`.
|
|
207
211
|
- `[recorder-turn]` measures the **model** layer — Haiku's verbatim words (`textHead`) and the tool names it actually called (`toolUseNames`). `toolUseNames=—` paired with `toolsArgvMissingFromExposed=—` is the Outcome B signature: tools present, model abstained.
|
|
208
212
|
|
|
209
213
|
The three lines are emitted into `server.log` via the platform UI's `/api/admin/log-ingest` loopback route, so a single grep against `server.log` answers the question for any past, present, or future recorder spawn:
|
|
@@ -82,10 +82,14 @@ The row feed sits behind `requireAdminSession` like every other admin route, so
|
|
|
82
82
|
|
|
83
83
|
The trade-off is a longer-lived connection per tab: the manager's per-process subscriber count rises with open tabs, and the SSE channel must survive proxy idle timeouts. The manager emits a 25-second keep-alive comment line on every connection (ignored by EventSource consumers, refreshes the proxy clock) and the browser-side store force-closes-and-reconnects on transport errors with exponential backoff capped at 30s.
|
|
84
84
|
|
|
85
|
+
The row payload carries `url: string | null` (Task 189) — the `claude.ai/code/session_<suffix>` URL captured from the `/remote-control` banner. The serialiser reads it from `SessionStore` (not the watcher's `SessionRow`) because URL is in-memory PTY state, not a filesystem property; the value is `null` whenever the spawn is headless (`HEADLESS_ROLES`, Task 171 — `--remote-control` not passed), or before url-capture has fired on a channel-facing spawn (~2 s after spawn), or on archived rows whose `StoredSession` has been removed. When url-capture eventually mutates `StoredSession.url`, the manager pushes a `row-updated` frame to every connected client carrying the fresh URL, so the client's Open-in-new-tab arrow appears in step. The Sidebar gates the arrow on `row.live && row.url !== null` and opens `row.url` directly with no `/meta` round-trip; each click logs `[admin-ui] sidebar-open-in-new-tab outcome=<ok|blocked> sessionId=<8-char>` (`blocked` fires when a popup blocker swallows `window.open`).
|
|
86
|
+
|
|
85
87
|
**Spawn lifecycle: PID-file driven.** Clicking "+ New session" spawns the PTY and waits for Claude Code's PID file to appear at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. The PID file lands at process init (for `entrypoint: cli` spawns) and carries the intrinsic `sessionId`, `bridgeSessionId`, `agent`, and `status` directly. The manager's filesystem watcher reports the create event; the spawn response includes the canonical `sessionId` from that file. URL capture still runs in parallel to populate the operator-facing iframe URL, but it no longer gates readiness. The JSONL transcript is written on the first operator turn (true on 2.1.143 and 2.1.128); the watcher fires a separate event for that, and `/list`, `/meta`, `/log` resolve any of four ids — `sessionId`, `bridgeSessionId`, `bridgeSuffix`, or numeric `pid` — to the same row. The Sidebar POST body carries no `initialMessage` (Task 146): Claude Code's own `tail.aiTitle` is the canonical sidebar row label, and the wrapper does not inject any cold-start stimulus on operator-driven spawns. The wrapper at `platform/ui/server/routes/admin/claude-sessions.ts` is still the single canonical entry point for any programmatic admin spawn-with-prompt — see `admin-session.md` "Spawn-with-initialMessage wrapper" and `internals.md` "Programmatic spawn entry point" — and the turn-recorder loopback path still forwards an `initialMessage`. Resume flows are unaffected (the prior transcript is the stimulus).
|
|
86
88
|
|
|
87
89
|
The sidebar row's displayed name is `tail.aiTitle` verbatim, parsed by `jsonl-enumerator.ts` from the JSONL Claude Code writes. Until Claude Code has written its title, the row label is null and the cell renders empty — no UI-stamped sidecar layer, no 8-char id fallback. When Claude Code later updates its title mid-session, the next `/list` or `/events` tick surfaces the new label. Task 146.
|
|
88
90
|
|
|
91
|
+
Each session row also carries a small muted timestamp crumb under the name showing when the session was last active: "just now", "5m", "3h", "yesterday", a weekday name for 2-6 days back, "20 May" for older dates this year, or "20 May 2025" for prior years. Live rows tick forward on their own without a refresh — every row advances together on a single shared 30-second cadence so two rows with identical names (a fresh session whose `aiTitle` has not landed yet, plus a resumed session whose title also has not landed) are distinguishable at a glance. A row that renders "—" instead of a time is a loud-fail signal: the session manager lost the row's `updatedAt` (the JSONL `mtimeMs` for archived rows, the PID-file `updatedAt`/`startedAt` for live rows). Investigate the server log rather than treating "—" as a normal value. The pure formatter lives at `app/lib/relative-time.ts` and is pinned by `app/lib/__tests__/relative-time.test.ts` (every breakpoint, every '—' input, DST cross 2026-03-29); the shared tick is `app/lib/use-now-tick.ts`; the row-render wiring is pinned by `app/__tests__/Sidebar-timestamp.test.tsx`. Task 187.
|
|
92
|
+
|
|
89
93
|
**Stop vs. delete.** `POST /<id>/stop` sends SIGTERM, leaves the JSONL on disk for audit, and is idempotent against an already-dead row. `DELETE /<id>` removes the JSONL + per-session subdir and returns 409 if the PTY is still alive (stop first). Any unknown id returns 404; nothing returns a silent 204 against an id the manager does not know.
|
|
90
94
|
|
|
91
95
|
**PTY lifecycle contract (Tasks 170 + 176).** A PTY reaches its end via one of two branches: **operator-request** (operator clicks End or the auto-archive Stop hook calls `stopSession`) or **natural-exit** (the claude child exits on its own — operator typed `/quit`, SIGINT in the PTY, crash, network drop on `--remote-control`). Both branches honour a single invariant: the pty master file descriptor is released by an explicit `pty.destroy()` and the SessionStore row is removed before the next `/list` or `/events` tick. Without the explicit destroy, the master fd lingers in node-pty's internal socket until V8 GC finalises the IPty object — non-deterministic and accumulates under load until the kernel pty cap (Linux 3072, macOS 511) refuses new spawns. Without the explicit row removal, the manager shutdown loop SIGTERMs PIDs that already logged `process-exited`, masking the leak only because the manager restarts every few hours. When both branches fire on the same exit (operator clicks End and node-pty's `onExit` fans out the SIGTERM to both listeners), a per-row `fdReleased` flag short-circuits the second branch so `pty.destroy()` runs exactly once on the live socket — without the flag, the second call throws "socket already destroyed" and the operator-request line would falsely log `master-fd=close-failed`. If the first branch's destroy throws and is rescued, the flag stays unset and the second branch retries (defense in depth). Every `kill … pid=<n>` log line carries a `master-fd=closed` suffix (or `master-fd=close-failed err=<msg>` on the rescued throw branch — a graceful degradation so a corner-case socket-state failure cannot turn a logically-successful exit into a 500); the operator-request line additionally identifies `reason=operator-request`, the natural-exit line identifies `reason=process-exited`. Both branches are verified by the `stop-session-fd-release` and `endpoint-stop-delete` integration tests (operator-request live and already-exited cycles + natural-exit cycle + throw-then-retry coordination, Linux kernel-level ptmx fd accounting on each).
|
|
@@ -181,7 +181,15 @@ Restricted fields (`accountId`, `embedding`, `profileVersion`) cannot be set via
|
|
|
181
181
|
|
|
182
182
|
## Schema References
|
|
183
183
|
|
|
184
|
-
Before any structured write, load `references/schema-base.md` via `plugin-read`. This defines property naming rules, required-property groups for documented types, forbidden-property rules, and relationship patterns.
|
|
184
|
+
Before any structured write, load `references/schema-base.md` via `plugin-read`. This defines property naming rules, required-property groups for documented types, forbidden-property rules, and relationship patterns.
|
|
185
|
+
|
|
186
|
+
**Brand declares the default vertical** in `brand.json#vertical` (the schema file basename without `.md`, e.g. `"schema-estate-agent"`, or `null` for non-vertical brands). The validator enforces base + that vertical against every write. If the file named by `brand.json#vertical` is absent from `references/`, the memory server fails to start — a misconfigured brand never validates against the wrong rule set silently.
|
|
187
|
+
|
|
188
|
+
**Every account has exactly one root `:LocalBusiness`.** The installer's `seed-neo4j.sh` MERGEs it at install (and on every re-seed) with the unique key `accountId`, `createdBySource = 'installer'`, a non-null `createdAt`, and `businessType` set to the bare slug derived from `brand.json#vertical` (i.e. `"schema-estate-agent"` becomes `"estate-agent"`, `null` stays null). The MERGE is idempotent: re-runs preserve `createdAt`; `businessType` is coalesced so an operator override on the node wins over the brand default. Failure to write the node aborts the install with `[install-invariant] localbusiness-bootstrap result=failed reason=<text>`. The recorder, validator, and any other writer can rely on the LocalBusiness existing from the first turn of the first install — no first-use bootstrap branch.
|
|
189
|
+
|
|
190
|
+
**A LocalBusiness with its own `businessType` overrides the default** for writes that resolve to it. The validator looks up the account's LocalBusiness on every write; if `businessType` is set (e.g. `"creator"`), the resolved vertical becomes `schema-${businessType}` instead of the brand default. The override applies only to writes anchored under that LocalBusiness.
|
|
191
|
+
|
|
192
|
+
On every write the validator emits `[schema-validator] resolved-vertical=<X|none> source=<brand-default|localbusiness-override> accountId=<id>` so an operator can grep `server.log` to confirm which rules a write ran against without reading source.
|
|
185
193
|
|
|
186
194
|
**Validation surface.** `memory-write` validates labels against `db.labels` ∪ `schema.cypher` declarations — not against this markdown. A label is recognised if it appears in either set. The markdown defines property *shape* (required-property groups, naming rules, forbidden-property rules) for documented labels only; recognised-but-undocumented labels (e.g. `LocalBusiness`, `AdminUser`, `KnowledgeDocument`) accept any property shape and emit `[schema-validator] markdown-undocumented label=<X>` so the doc gap is visible to operators. If `memory-write` rejects a label as unknown, the rejection lists both source sets — the agent can call `maxy-graph-get_neo4j_schema` to refresh its view.
|
|
187
195
|
|
package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-active-vertical.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/resolve-active-vertical.test.ts"],"names":[],"mappings":""}
|
package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolveActiveVertical } from "../resolve-active-vertical.js";
|
|
4
|
+
function mockSession(records, capture) {
|
|
5
|
+
return {
|
|
6
|
+
run: async (cypher, params) => {
|
|
7
|
+
capture?.calls.push({ cypher, params });
|
|
8
|
+
return {
|
|
9
|
+
records: records.map((r) => ({
|
|
10
|
+
get: (key) => r[key],
|
|
11
|
+
})),
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
test("resolveActiveVertical: undefined accountId → brand-default", async () => {
|
|
17
|
+
const session = mockSession([]);
|
|
18
|
+
const result = await resolveActiveVertical({
|
|
19
|
+
session,
|
|
20
|
+
accountId: undefined,
|
|
21
|
+
brandVertical: "schema-estate-agent",
|
|
22
|
+
});
|
|
23
|
+
assert.deepEqual(result, {
|
|
24
|
+
resolvedVertical: "schema-estate-agent",
|
|
25
|
+
source: "brand-default",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
test("resolveActiveVertical: empty-string accountId → brand-default (no query)", async () => {
|
|
29
|
+
const capture = { calls: [] };
|
|
30
|
+
const session = mockSession([], capture);
|
|
31
|
+
const result = await resolveActiveVertical({
|
|
32
|
+
session,
|
|
33
|
+
accountId: " ",
|
|
34
|
+
brandVertical: "schema-estate-agent",
|
|
35
|
+
});
|
|
36
|
+
assert.equal(result.source, "brand-default");
|
|
37
|
+
assert.equal(capture.calls.length, 0);
|
|
38
|
+
});
|
|
39
|
+
test("resolveActiveVertical: no LocalBusiness rows → brand-default", async () => {
|
|
40
|
+
const session = mockSession([]);
|
|
41
|
+
const result = await resolveActiveVertical({
|
|
42
|
+
session,
|
|
43
|
+
accountId: "acct-1",
|
|
44
|
+
brandVertical: "schema-estate-agent",
|
|
45
|
+
});
|
|
46
|
+
assert.equal(result.source, "brand-default");
|
|
47
|
+
assert.equal(result.resolvedVertical, "schema-estate-agent");
|
|
48
|
+
});
|
|
49
|
+
test("resolveActiveVertical: LocalBusiness without businessType → brand-default", async () => {
|
|
50
|
+
const session = mockSession([{ businessType: null }]);
|
|
51
|
+
const result = await resolveActiveVertical({
|
|
52
|
+
session,
|
|
53
|
+
accountId: "acct-1",
|
|
54
|
+
brandVertical: "schema-estate-agent",
|
|
55
|
+
});
|
|
56
|
+
assert.equal(result.source, "brand-default");
|
|
57
|
+
assert.equal(result.resolvedVertical, "schema-estate-agent");
|
|
58
|
+
});
|
|
59
|
+
test("resolveActiveVertical: LocalBusiness with businessType → localbusiness-override", async () => {
|
|
60
|
+
const session = mockSession([{ businessType: "creator" }]);
|
|
61
|
+
const result = await resolveActiveVertical({
|
|
62
|
+
session,
|
|
63
|
+
accountId: "acct-1",
|
|
64
|
+
brandVertical: "schema-estate-agent",
|
|
65
|
+
});
|
|
66
|
+
assert.deepEqual(result, {
|
|
67
|
+
resolvedVertical: "schema-creator",
|
|
68
|
+
source: "localbusiness-override",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
test("resolveActiveVertical: businessType override wins when brandVertical is null", async () => {
|
|
72
|
+
const session = mockSession([{ businessType: "creator" }]);
|
|
73
|
+
const result = await resolveActiveVertical({
|
|
74
|
+
session,
|
|
75
|
+
accountId: "acct-1",
|
|
76
|
+
brandVertical: null,
|
|
77
|
+
});
|
|
78
|
+
assert.deepEqual(result, {
|
|
79
|
+
resolvedVertical: "schema-creator",
|
|
80
|
+
source: "localbusiness-override",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
test("resolveActiveVertical: businessType whitespace-only → brand-default", async () => {
|
|
84
|
+
const session = mockSession([{ businessType: " " }]);
|
|
85
|
+
const result = await resolveActiveVertical({
|
|
86
|
+
session,
|
|
87
|
+
accountId: "acct-1",
|
|
88
|
+
brandVertical: "schema-estate-agent",
|
|
89
|
+
});
|
|
90
|
+
assert.equal(result.source, "brand-default");
|
|
91
|
+
});
|
|
92
|
+
test("resolveActiveVertical: query parameters bind accountId", async () => {
|
|
93
|
+
const capture = { calls: [] };
|
|
94
|
+
const session = mockSession([{ businessType: "creator" }], capture);
|
|
95
|
+
await resolveActiveVertical({
|
|
96
|
+
session,
|
|
97
|
+
accountId: "acct-7",
|
|
98
|
+
brandVertical: null,
|
|
99
|
+
});
|
|
100
|
+
assert.equal(capture.calls.length, 1);
|
|
101
|
+
assert.deepEqual(capture.calls[0].params, { accountId: "acct-7" });
|
|
102
|
+
assert.match(capture.calls[0].cypher, /:LocalBusiness/);
|
|
103
|
+
});
|
|
104
|
+
test("resolveActiveVertical: session.run rejection propagates", async () => {
|
|
105
|
+
const session = {
|
|
106
|
+
run: async () => {
|
|
107
|
+
throw new Error("connection refused");
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
await assert.rejects(resolveActiveVertical({
|
|
111
|
+
session,
|
|
112
|
+
accountId: "acct-1",
|
|
113
|
+
brandVertical: null,
|
|
114
|
+
}), /connection refused/);
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=resolve-active-vertical.test.js.map
|
package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-active-vertical.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/resolve-active-vertical.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAExC,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,SAAS,WAAW,CAClB,OAAuC,EACvC,OAA+D;IAE/D,OAAO;QACL,GAAG,EAAE,KAAK,EAAE,MAAc,EAAE,MAAe,EAAE,EAAE;YAC7C,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YACxC,OAAO;gBACL,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;oBAC3B,GAAG,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;iBAC7B,CAAC,CAAC;aACJ,CAAC;QACJ,CAAC;KACoB,CAAC;AAC1B,CAAC;AAED,IAAI,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;IAC5E,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC;QACzC,OAAO;QACP,SAAS,EAAE,SAAS;QACpB,aAAa,EAAE,qBAAqB;KACrC,CAAC,CAAC;IACH,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE;QACvB,gBAAgB,EAAE,qBAAqB;QACvC,MAAM,EAAE,eAAe;KACxB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;IAC1F,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,EAAgD,EAAE,CAAC;IAC5E,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC;QACzC,OAAO;QACP,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,qBAAqB;KACrC,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACxC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;IAC9E,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC;QACzC,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,qBAAqB;KACrC,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,CAAC;AAC/D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;IAC3F,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC;QACzC,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,qBAAqB;KACrC,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,CAAC;AAC/D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iFAAiF,EAAE,KAAK,IAAI,EAAE;IACjG,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC;QACzC,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,qBAAqB;KACrC,CAAC,CAAC;IACH,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE;QACvB,gBAAgB,EAAE,gBAAgB;QAClC,MAAM,EAAE,wBAAwB;KACjC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;IAC9F,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC;QACzC,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,IAAI;KACpB,CAAC,CAAC;IACH,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE;QACvB,gBAAgB,EAAE,gBAAgB;QAClC,MAAM,EAAE,wBAAwB;KACjC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;IACrF,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC;QACzC,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,qBAAqB;KACrC,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;IACxE,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,EAAgD,EAAE,CAAC;IAC5E,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACpE,MAAM,qBAAqB,CAAC;QAC1B,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,IAAI;KACpB,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACtC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;IACnE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAC1D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;IACzE,MAAM,OAAO,GAAG;QACd,GAAG,EAAE,KAAK,IAAI,EAAE;YACd,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;KACoB,CAAC;IACxB,MAAM,MAAM,CAAC,OAAO,CAClB,qBAAqB,CAAC;QACpB,OAAO;QACP,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,IAAI;KACpB,CAAC,EACF,oBAAoB,CACrB,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
import { loadSchema, splitPipeRow } from "../schema-loader.js";
|
|
7
|
+
function writeBrandJson(content) {
|
|
8
|
+
const dir = mkdtempSync(join(tmpdir(), "schema-loader-brand-"));
|
|
9
|
+
const path = join(dir, "brand.json");
|
|
10
|
+
writeFileSync(path, JSON.stringify(content), "utf-8");
|
|
11
|
+
return path;
|
|
12
|
+
}
|
|
4
13
|
// schema-loader reads from platform/plugins/memory/references/schema-*.md
|
|
5
14
|
// at runtime — these tests assert the contract surface (the renamed field
|
|
6
15
|
// `markdownLabels` plus the loader's invariants) against the actual schema
|
|
@@ -71,6 +80,97 @@ test("splitPipeRow: multiple pipes inside multiple backtick spans are all litera
|
|
|
71
80
|
const cells = splitPipeRow(row);
|
|
72
81
|
assert.deepEqual(cells, ["`a|b`", "`c|d|e`", "f"]);
|
|
73
82
|
});
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// brand.json#vertical resolution (Task 193)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
test("loadSchema: no brand.json discovery → brandVertical=null (dev default)", () => {
|
|
87
|
+
// No explicit path, no env var: matches paths.ts dev-default mode.
|
|
88
|
+
const prev = process.env.MAXY_PLATFORM_ROOT;
|
|
89
|
+
delete process.env.MAXY_PLATFORM_ROOT;
|
|
90
|
+
try {
|
|
91
|
+
const schema = loadSchema();
|
|
92
|
+
assert.equal(schema.brandVertical, null);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (prev !== undefined)
|
|
96
|
+
process.env.MAXY_PLATFORM_ROOT = prev;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
test("loadSchema: brand.json with vertical=null → brandVertical=null", () => {
|
|
100
|
+
const path = writeBrandJson({ vertical: null });
|
|
101
|
+
try {
|
|
102
|
+
const schema = loadSchema({ brandJsonPath: path });
|
|
103
|
+
assert.equal(schema.brandVertical, null);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
rmSync(path, { force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
test("loadSchema: brand.json absent vertical field → brandVertical=null", () => {
|
|
110
|
+
// Backwards compatibility: a brand.json that pre-dates Task 193 has no
|
|
111
|
+
// `vertical` key. The loader must treat that as "no vertical declared",
|
|
112
|
+
// not as an error.
|
|
113
|
+
const path = writeBrandJson({ productName: "Test", configDir: ".test" });
|
|
114
|
+
try {
|
|
115
|
+
const schema = loadSchema({ brandJsonPath: path });
|
|
116
|
+
assert.equal(schema.brandVertical, null);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rmSync(path, { force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
test("loadSchema: brand.json vertical naming existing file → brandVertical set", () => {
|
|
123
|
+
// schema-estate-agent.md is shipped with the plugin; the loader should
|
|
124
|
+
// accept it without throwing and surface the name.
|
|
125
|
+
const path = writeBrandJson({ vertical: "schema-estate-agent" });
|
|
126
|
+
try {
|
|
127
|
+
const schema = loadSchema({ brandJsonPath: path });
|
|
128
|
+
assert.equal(schema.brandVertical, "schema-estate-agent");
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
rmSync(path, { force: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
test("loadSchema: brand.json vertical naming a missing file → loud-fail at boot", () => {
|
|
135
|
+
// Task 193 verification: misconfigured vertical must block boot, with the
|
|
136
|
+
// missing file named in the error.
|
|
137
|
+
const path = writeBrandJson({ vertical: "schema-fake" });
|
|
138
|
+
try {
|
|
139
|
+
assert.throws(() => loadSchema({ brandJsonPath: path }), (err) => {
|
|
140
|
+
assert.match(err.message, /schema-fake\.md/);
|
|
141
|
+
assert.match(err.message, /not present/);
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
rmSync(path, { force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
test("loadSchema: brand.json vertical of wrong type → loud-fail at boot", () => {
|
|
150
|
+
const path = writeBrandJson({ vertical: 42 });
|
|
151
|
+
try {
|
|
152
|
+
assert.throws(() => loadSchema({ brandJsonPath: path }), /brand\.json#vertical.*must be a string or null/);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
rmSync(path, { force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
test("loadSchema: brand.json vertical empty string → loud-fail with explicit message", () => {
|
|
159
|
+
// Operator typo guard: an empty string is misconfiguration, not "no
|
|
160
|
+
// vertical". The error must name the field and suggest the two valid
|
|
161
|
+
// shapes (null or a reference-file name).
|
|
162
|
+
const path = writeBrandJson({ vertical: "" });
|
|
163
|
+
try {
|
|
164
|
+
assert.throws(() => loadSchema({ brandJsonPath: path }), (err) => {
|
|
165
|
+
assert.match(err.message, /empty string/);
|
|
166
|
+
assert.match(err.message, /Use null/);
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
rmSync(path, { force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
74
174
|
test("splitPipeRow: ordinary row with no backticks splits as before", () => {
|
|
75
175
|
const cells = splitPipeRow("| one | two | three |");
|
|
76
176
|
assert.deepEqual(cells, ["one", "two", "three"]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-loader.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/schema-loader.test.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-loader.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/schema-loader.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,12 +12,15 @@ function makeSchema(opts) {
|
|
|
12
12
|
markdownLabels.set(entry.label, {
|
|
13
13
|
label: entry.label,
|
|
14
14
|
required: entry.required,
|
|
15
|
-
sourceFile: entry.sourceFile ?? "schema-
|
|
15
|
+
sourceFile: entry.sourceFile ?? "schema-base.md",
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
const wrongToCorrect = new Map();
|
|
19
|
-
for (const [wrong, correct] of opts.synonyms ?? []) {
|
|
20
|
-
wrongToCorrect.set(wrong, {
|
|
19
|
+
for (const [wrong, correct, sourceFile] of opts.synonyms ?? []) {
|
|
20
|
+
wrongToCorrect.set(wrong, {
|
|
21
|
+
correct,
|
|
22
|
+
sourceFile: sourceFile ?? "schema-base.md",
|
|
23
|
+
});
|
|
21
24
|
}
|
|
22
25
|
const forbiddenByLabel = new Map();
|
|
23
26
|
for (const entry of opts.forbidden ?? []) {
|
|
@@ -28,14 +31,15 @@ function makeSchema(opts) {
|
|
|
28
31
|
}
|
|
29
32
|
perLabel.set(entry.property, {
|
|
30
33
|
rationale: entry.rationale ?? "test rationale",
|
|
31
|
-
sourceFile: "schema-
|
|
34
|
+
sourceFile: entry.sourceFile ?? "schema-base.md",
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
37
|
return {
|
|
35
38
|
markdownLabels,
|
|
36
39
|
wrongToCorrect,
|
|
37
40
|
forbiddenByLabel,
|
|
38
|
-
sourceFiles: ["schema-
|
|
41
|
+
sourceFiles: ["schema-base.md"],
|
|
42
|
+
brandVertical: opts.brandVertical ?? null,
|
|
39
43
|
};
|
|
40
44
|
}
|
|
41
45
|
function makeLiveSource(opts) {
|
|
@@ -445,4 +449,192 @@ test('(893) mode defaults to "create" when omitted; required-props still fires',
|
|
|
445
449
|
accountId: "acct-1",
|
|
446
450
|
}, { schema, liveSource }), /missing required property 'givenName'/);
|
|
447
451
|
});
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
// Vertical scoping (Task 193)
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
test("vertical scoping: base rule applies for every resolved vertical (including null)", () => {
|
|
456
|
+
// schema-base.md rules — synonyms and forbidden — never filter out, even
|
|
457
|
+
// when no brand vertical is declared.
|
|
458
|
+
const schema = makeSchema({
|
|
459
|
+
synonyms: [["firstName", "givenName", "schema-base.md"]],
|
|
460
|
+
});
|
|
461
|
+
const liveSource = makeLiveSource({ live: ["Person"], declared: [] });
|
|
462
|
+
assert.throws(() => validateWrite({
|
|
463
|
+
labels: ["Person"],
|
|
464
|
+
properties: { firstName: "Sam" },
|
|
465
|
+
accountId: "acct-1",
|
|
466
|
+
}, { schema, liveSource }), /firstName.*givenName/);
|
|
467
|
+
});
|
|
468
|
+
test("vertical scoping: vertical synonym applies when its file is the active vertical", () => {
|
|
469
|
+
const schema = makeSchema({
|
|
470
|
+
synonyms: [["roomCount", "numberOfRooms", "schema-estate-agent.md"]],
|
|
471
|
+
});
|
|
472
|
+
const liveSource = makeLiveSource({ live: ["Property"], declared: [] });
|
|
473
|
+
assert.throws(() => validateWrite({
|
|
474
|
+
labels: ["Property"],
|
|
475
|
+
properties: { roomCount: 3 },
|
|
476
|
+
accountId: "acct-1",
|
|
477
|
+
}, {
|
|
478
|
+
schema,
|
|
479
|
+
liveSource,
|
|
480
|
+
}, {
|
|
481
|
+
verticalContext: {
|
|
482
|
+
resolvedVertical: "schema-estate-agent",
|
|
483
|
+
source: "brand-default",
|
|
484
|
+
},
|
|
485
|
+
}), /roomCount.*numberOfRooms/);
|
|
486
|
+
});
|
|
487
|
+
test("vertical scoping: vertical synonym is filtered out when a different vertical is active", () => {
|
|
488
|
+
// Estate-agent synonym must NOT fire when the LocalBusiness has overridden
|
|
489
|
+
// to creator. The doctrine in the task: "the per-LocalBusiness override
|
|
490
|
+
// wins". A write whose property name is wrong only in estate-agent should
|
|
491
|
+
// pass for a creator-scope write.
|
|
492
|
+
const schema = makeSchema({
|
|
493
|
+
synonyms: [["roomCount", "numberOfRooms", "schema-estate-agent.md"]],
|
|
494
|
+
});
|
|
495
|
+
const liveSource = makeLiveSource({ live: ["Property"], declared: [] });
|
|
496
|
+
assert.doesNotThrow(() => validateWrite({
|
|
497
|
+
labels: ["Property"],
|
|
498
|
+
properties: { roomCount: 3 },
|
|
499
|
+
accountId: "acct-1",
|
|
500
|
+
}, { schema, liveSource }, {
|
|
501
|
+
verticalContext: {
|
|
502
|
+
resolvedVertical: "schema-creator",
|
|
503
|
+
source: "localbusiness-override",
|
|
504
|
+
},
|
|
505
|
+
}));
|
|
506
|
+
});
|
|
507
|
+
test("vertical scoping: vertical synonym is filtered out when no vertical is active (null)", () => {
|
|
508
|
+
// Non-vertical install (Maxy default). Only base rules apply.
|
|
509
|
+
const schema = makeSchema({
|
|
510
|
+
synonyms: [["roomCount", "numberOfRooms", "schema-estate-agent.md"]],
|
|
511
|
+
});
|
|
512
|
+
const liveSource = makeLiveSource({ live: ["Property"], declared: [] });
|
|
513
|
+
assert.doesNotThrow(() => validateWrite({
|
|
514
|
+
labels: ["Property"],
|
|
515
|
+
properties: { roomCount: 3 },
|
|
516
|
+
accountId: "acct-1",
|
|
517
|
+
}, { schema, liveSource }));
|
|
518
|
+
});
|
|
519
|
+
test("vertical scoping: forbidden-property rule respects active vertical", () => {
|
|
520
|
+
const schema = makeSchema({
|
|
521
|
+
forbidden: [
|
|
522
|
+
{
|
|
523
|
+
label: "Person",
|
|
524
|
+
property: "agencyNotes",
|
|
525
|
+
rationale: "Applicant-specific",
|
|
526
|
+
sourceFile: "schema-estate-agent.md",
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
});
|
|
530
|
+
const liveSource = makeLiveSource({ live: ["Person"], declared: [] });
|
|
531
|
+
// Active vertical = estate-agent → forbidden fires.
|
|
532
|
+
assert.throws(() => validateWrite({
|
|
533
|
+
labels: ["Person"],
|
|
534
|
+
properties: { agencyNotes: "x" },
|
|
535
|
+
accountId: "acct-1",
|
|
536
|
+
}, { schema, liveSource }, {
|
|
537
|
+
verticalContext: {
|
|
538
|
+
resolvedVertical: "schema-estate-agent",
|
|
539
|
+
source: "brand-default",
|
|
540
|
+
},
|
|
541
|
+
}), /agencyNotes/);
|
|
542
|
+
// Active vertical = creator → estate-agent forbidden rule out of scope.
|
|
543
|
+
assert.doesNotThrow(() => validateWrite({
|
|
544
|
+
labels: ["Person"],
|
|
545
|
+
properties: { agencyNotes: "x" },
|
|
546
|
+
accountId: "acct-1",
|
|
547
|
+
}, { schema, liveSource }, {
|
|
548
|
+
verticalContext: {
|
|
549
|
+
resolvedVertical: "schema-creator",
|
|
550
|
+
source: "localbusiness-override",
|
|
551
|
+
},
|
|
552
|
+
}));
|
|
553
|
+
});
|
|
554
|
+
test("vertical scoping: required-props from out-of-scope vertical are skipped (markdown-undocumented)", () => {
|
|
555
|
+
// Applicant is defined in schema-estate-agent.md. A write on Applicant
|
|
556
|
+
// under an active creator vertical should accept (label is recognised
|
|
557
|
+
// by the live source) but log the out-of-scope-vertical reason.
|
|
558
|
+
const schema = makeSchema({
|
|
559
|
+
documented: [
|
|
560
|
+
{
|
|
561
|
+
label: "Applicant",
|
|
562
|
+
required: [{ kind: "simple", name: "budget" }],
|
|
563
|
+
sourceFile: "schema-estate-agent.md",
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
});
|
|
567
|
+
const liveSource = makeLiveSource({ live: ["Applicant"], declared: [] });
|
|
568
|
+
const { lines, emit } = captureEmits();
|
|
569
|
+
validateWrite({
|
|
570
|
+
labels: ["Applicant"],
|
|
571
|
+
properties: { name: "x" },
|
|
572
|
+
accountId: "acct-1",
|
|
573
|
+
}, { schema, liveSource, emit }, {
|
|
574
|
+
verticalContext: {
|
|
575
|
+
resolvedVertical: "schema-creator",
|
|
576
|
+
source: "localbusiness-override",
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
const accept = lines.find((l) => l.includes("outcome=accepted"));
|
|
580
|
+
assert.ok(accept, `expected accepted log, got ${lines.join(" | ")}`);
|
|
581
|
+
const skip = lines.find((l) => l.includes("markdown-undocumented") &&
|
|
582
|
+
l.includes("reason=out-of-scope-vertical"));
|
|
583
|
+
assert.ok(skip, `expected out-of-scope-vertical log, got ${lines.join(" | ")}`);
|
|
584
|
+
assert.match(skip, /definedIn=schema-estate-agent\.md/);
|
|
585
|
+
assert.match(skip, /active=schema-creator/);
|
|
586
|
+
});
|
|
587
|
+
test("vertical scoping: required-props from active vertical fire", () => {
|
|
588
|
+
const schema = makeSchema({
|
|
589
|
+
documented: [
|
|
590
|
+
{
|
|
591
|
+
label: "Applicant",
|
|
592
|
+
required: [{ kind: "simple", name: "budget" }],
|
|
593
|
+
sourceFile: "schema-estate-agent.md",
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
});
|
|
597
|
+
const liveSource = makeLiveSource({ live: ["Applicant"], declared: [] });
|
|
598
|
+
assert.throws(() => validateWrite({
|
|
599
|
+
labels: ["Applicant"],
|
|
600
|
+
properties: { name: "x" },
|
|
601
|
+
accountId: "acct-1",
|
|
602
|
+
}, { schema, liveSource }, {
|
|
603
|
+
verticalContext: {
|
|
604
|
+
resolvedVertical: "schema-estate-agent",
|
|
605
|
+
source: "brand-default",
|
|
606
|
+
},
|
|
607
|
+
}), /missing required property 'budget'/);
|
|
608
|
+
});
|
|
609
|
+
test("vertical scoping: resolved-vertical log line emits once per write", () => {
|
|
610
|
+
const schema = makeSchema({});
|
|
611
|
+
const liveSource = makeLiveSource({ live: ["Person"], declared: [] });
|
|
612
|
+
const { lines, emit } = captureEmits();
|
|
613
|
+
validateWrite({
|
|
614
|
+
labels: ["Person"],
|
|
615
|
+
properties: {},
|
|
616
|
+
accountId: "acct-99",
|
|
617
|
+
}, { schema, liveSource, emit }, {
|
|
618
|
+
verticalContext: {
|
|
619
|
+
resolvedVertical: "schema-estate-agent",
|
|
620
|
+
source: "localbusiness-override",
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
const verticalLines = lines.filter((l) => l.startsWith("[schema-validator] resolved-vertical="));
|
|
624
|
+
assert.equal(verticalLines.length, 1);
|
|
625
|
+
assert.match(verticalLines[0], /resolved-vertical=schema-estate-agent source=localbusiness-override accountId=acct-99/);
|
|
626
|
+
});
|
|
627
|
+
test("vertical scoping: resolved-vertical=none logged when no context is passed", () => {
|
|
628
|
+
const schema = makeSchema({});
|
|
629
|
+
const liveSource = makeLiveSource({ live: ["Person"], declared: [] });
|
|
630
|
+
const { lines, emit } = captureEmits();
|
|
631
|
+
validateWrite({
|
|
632
|
+
labels: ["Person"],
|
|
633
|
+
properties: {},
|
|
634
|
+
accountId: "acct-1",
|
|
635
|
+
}, { schema, liveSource, emit });
|
|
636
|
+
const verticalLines = lines.filter((l) => l.startsWith("[schema-validator] resolved-vertical="));
|
|
637
|
+
assert.equal(verticalLines.length, 1);
|
|
638
|
+
assert.match(verticalLines[0], /resolved-vertical=none source=brand-default/);
|
|
639
|
+
});
|
|
448
640
|
//# sourceMappingURL=schema-validator.test.js.map
|