@rubytech/create-maxy-code 0.1.485 → 0.1.486
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/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +124 -0
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +107 -0
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +205 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts +4 -1
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +109 -1
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/caption-coverage.test.ts +117 -0
- package/payload/platform/lib/graph-style/src/index.ts +81 -1
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh +159 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +18 -6
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +141 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-pre.sh +66 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +33 -1
- package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +3 -3
- package/payload/platform/plugins/cloudflare/PLUGIN.md +2 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +23 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +49 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +57 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +44 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +67 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +42 -3
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +10 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +19 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +19 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +8 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +28 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +195 -6
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +193 -10
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +4 -3
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +5 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js +50 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js +51 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js +89 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +28 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts +22 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js +55 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +32 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +75 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js +49 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +7 -1
- package/payload/platform/plugins/memory/references/schema-construction.md +18 -12
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +11 -6
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +75 -6
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +111 -22
- package/payload/platform/scripts/lib/provision-account-dir.sh +8 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +39 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +299 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +15 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/account-schema/SCHEMA.md +17 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-XGNRSM57.js → chunk-HN4HFTXE.js} +6 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{admin-rScqRq-R.js → admin-BoVCP4Mw.js} +1 -1
- package/payload/server/public/assets/{data-TOWuhMFb.js → data-CtOxa94P.js} +1 -1
- package/payload/server/public/assets/{graph-labels-Axa04w8A.js → graph-labels-C8-sIKuv.js} +1 -1
- package/payload/server/public/assets/{graph-BEGs_P2m.js → graph-mJ9ky5dc.js} +1 -1
- package/payload/server/public/assets/{page-6CZ8R57f.js → page-Cv0qUwMG.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +3 -3
- package/payload/server/server.js +84 -1
package/package.json
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Graph Caption Resolver Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make every top-level operator-entry graph label resolve to a human-readable caption instead of its bare type, and add a build-time gate that fails when a new styled label ships without a caption decision.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add per-label caption branches to `pickShortLabel` and `pickDisplayName` in `platform/lib/graph-style/src/index.ts` for the first-class labels that currently fall through to `return primaryLabel`. Add a `caption-coverage.test.ts` that reconciles every `GRAPH_LABEL_COLOURS` key against a `CAPTION_COVERAGE` fixture, so registering a styled colour without deciding its caption is a red build.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, vitest 4 (run via `platform/ui/node_modules/.bin/vitest`), no new dependencies.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Do not touch `GRAPH_LABEL_COLOURS` or `LABEL_ICONS` — every label already has both.
|
|
14
|
+
- `pickShortLabel` truncates to 24 chars (`slice(0, 24) + '…'`); `pickDisplayName` returns the untruncated value. New branches follow that per-function convention.
|
|
15
|
+
- Caption source is a scalar property already written on the node itself; never an id that points at another node, never a composed value that needs a second node.
|
|
16
|
+
- The UI parity test `platform/ui/app/graph/__tests__/display-helpers.test.ts` imports graph-style's **built** `dist/index.js`, so graph-style must be rebuilt (`tsc -p tsconfig.json`) before that test is rerun.
|
|
17
|
+
- `SHAPE_BY_LABEL` assigns shapes by key order in `GRAPH_LABEL_COLOURS`; do not reorder or insert keys.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Label classification (the crux)
|
|
22
|
+
|
|
23
|
+
The resolver captions a node when: (1) its primary label has an explicit branch, or (2) the node carries one of the generic-fallback keys `name, title, summary, givenName, subject, text`, or (3) it is the `Conversation` special-case. Otherwise it returns the bare type.
|
|
24
|
+
|
|
25
|
+
**Already captioned by an existing branch** (unchanged): `ToolCall, WorkflowRun, WorkflowStep, Person, Agent, Job, Quote, InboundInvoice, WhatsAppGroup, CashEntry, Message`, plus the `Conversation` `sessionId` special-case.
|
|
26
|
+
|
|
27
|
+
**Already captioned by the generic fallback** (they declare a fallback key): `LocalBusiness, Service, Organization, Customer, Site, Asset, Part, PpmContract, DigitalDocument, Question, FAQPage, DefinedTerm, Task, Event, ImageObject` (via `name`); `CreativeWork, ConversationArchive, Objective, KeyResult, Decision, Source` (via `title`); `Note` (via `text`).
|
|
28
|
+
|
|
29
|
+
**First-class fall-throughs that need a NEW branch** (top-level operator-entry per schema, no fallback key, no existing branch):
|
|
30
|
+
|
|
31
|
+
| Label | Caption field(s) | Schema evidence |
|
|
32
|
+
|-------|------------------|-----------------|
|
|
33
|
+
| `Property` | `address` → `slug` | schema-estate-agent top-level; required `slug`, `address` |
|
|
34
|
+
| `Listing` | `displayName` → `slug` | schema-estate-agent top-level; required `slug`, `displayName` |
|
|
35
|
+
| `Viewing` | `date` | schema-estate-agent top-level; local scalar `date` |
|
|
36
|
+
| `Offer` | `status` then `price` composed as `status £price` when both present, else whichever is present | schema-estate-agent top-level; local scalars `status`, `price` |
|
|
37
|
+
| `Visit` | `purpose` → `status` | schema-construction top-level; local scalars `purpose`, `status` |
|
|
38
|
+
| `PurchaseOrder` | `poNumber` | schema-construction top-level; natural key `poNumber` |
|
|
39
|
+
| `Invoice` | `confirmationNumber` | schema-base first-class financial; `confirmationNumber` (mirror of the `InboundInvoice` branch's identity) |
|
|
40
|
+
| `Risk` | `statement` | schema-knowledge-work; local `statement` |
|
|
41
|
+
| `Finding` | `statement` | schema-knowledge-work; local `statement` |
|
|
42
|
+
| `Hypothesis` | `statement` | schema-knowledge-work; local `statement` |
|
|
43
|
+
|
|
44
|
+
**Grey allowlist — render as bare type by design** (child/specification/platform-plumbing labels, not first-class viewer entities): `PriceSpecification, OpeningHoursSpecification, Section, Chunk, Review, Preference, UserProfile, AdminUser, AccessGrant, StepResult`.
|
|
45
|
+
|
|
46
|
+
**Empirically finalised during implementation** (their bucket depends on their actual write shape, decided by building a synthetic node from the label's real properties and observing `pickShortLabel`): `KnowledgeDocument, Project, Workflow, Email, EmailAccount, UserMessage, AssistantMessage, AdminConversation, PublicConversation`. Rule: if the label declares a fallback key (`name/title/summary/subject/text`) it goes in the captioned bucket with that field as `expected`; otherwise it is grey. Every one of these must land in exactly one bucket before Task 2 passes — none may be left undeclared.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Task 1: Caption-coverage gate (Part B) — written first, RED
|
|
51
|
+
|
|
52
|
+
**Files:**
|
|
53
|
+
- Create: `platform/lib/graph-style/src/__tests__/caption-coverage.test.ts`
|
|
54
|
+
|
|
55
|
+
**Interfaces:**
|
|
56
|
+
- Consumes: `GRAPH_LABEL_COLOURS`, `pickShortLabel` from `../index`.
|
|
57
|
+
- Produces: nothing consumed by later tasks; it is the verifier for Task 2.
|
|
58
|
+
|
|
59
|
+
- [ ] **Step 1: Write the coverage test with the full fixture**
|
|
60
|
+
|
|
61
|
+
The test owns `CAPTION_COVERAGE: Record<string, { node: GraphNodeLike; expected: string } | { fallbackGrey: true }>`, one entry per key of `GRAPH_LABEL_COLOURS`. Captioned entries carry a synthetic node built from the label's identity fields and the `expected` caption. Grey entries are `{ fallbackGrey: true }`. Seed from the classification table above.
|
|
62
|
+
|
|
63
|
+
Assertions:
|
|
64
|
+
1. **Completeness.** `Object.keys(GRAPH_LABEL_COLOURS)` and `Object.keys(CAPTION_COVERAGE)` are set-equal — every styled label has an entry, and no entry names an unstyled label.
|
|
65
|
+
2. **Correctness.** For each non-grey entry, `pickShortLabel(entry.node) === entry.expected` and `pickShortLabel(entry.node) !== label`.
|
|
66
|
+
|
|
67
|
+
- [ ] **Step 2: Run it — expect RED**
|
|
68
|
+
|
|
69
|
+
Run: `cd platform/lib/graph-style && ./node_modules/.bin/vitest run src/__tests__/caption-coverage.test.ts`
|
|
70
|
+
Expected: FAIL — the correctness assertion fails for `Property, Listing, Viewing, Offer, Visit, PurchaseOrder, Invoice, Risk, Finding, Hypothesis` (each still returns its bare type).
|
|
71
|
+
|
|
72
|
+
- [ ] **Step 3: Commit the RED test**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git add platform/lib/graph-style/src/__tests__/caption-coverage.test.ts
|
|
76
|
+
git commit -m "test(graph-style): caption-coverage gate (red)"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Task 2: Caption branches (Part A) — turn it GREEN
|
|
80
|
+
|
|
81
|
+
**Files:**
|
|
82
|
+
- Modify: `platform/lib/graph-style/src/index.ts` — `pickShortLabel` (before the generic-fallback loop, ~line 494) and `pickDisplayName` (before its generic-fallback loop, ~line 560).
|
|
83
|
+
|
|
84
|
+
**Interfaces:**
|
|
85
|
+
- Consumes: the fixture from Task 1 as its pass condition.
|
|
86
|
+
- Produces: caption branches for the ten fall-through labels; both functions keep their existing signatures.
|
|
87
|
+
|
|
88
|
+
- [ ] **Step 1: Add the ten branches to `pickShortLabel`**
|
|
89
|
+
|
|
90
|
+
Each branch reads the label's caption field(s) as `string`, returns the 24-char-truncated value when non-empty, and otherwise falls through to the next fallback. `Offer` composes `status` and `£price`. Placed in the existing `else if` chain before the `for (const k of [...])` loop.
|
|
91
|
+
|
|
92
|
+
- [ ] **Step 2: Add the same ten branches to `pickDisplayName`, untruncated**
|
|
93
|
+
|
|
94
|
+
Identical field logic, returning the full value (no `slice`), matching that function's convention.
|
|
95
|
+
|
|
96
|
+
- [ ] **Step 3: Finalise the empirical bucket for the nine ambiguous labels**
|
|
97
|
+
|
|
98
|
+
For `KnowledgeDocument, Project, Workflow, Email, EmailAccount, UserMessage, AssistantMessage, AdminConversation, PublicConversation`, build a synthetic node from the label's real write shape, run `pickShortLabel`, and place each in the captioned or grey bucket of `CAPTION_COVERAGE` per the rule. Add a branch only if the label is a first-class entity that falls through (none expected; if one appears, it joins Part A).
|
|
99
|
+
|
|
100
|
+
- [ ] **Step 4: Run the coverage test — expect GREEN**
|
|
101
|
+
|
|
102
|
+
Run: `cd platform/lib/graph-style && ./node_modules/.bin/vitest run`
|
|
103
|
+
Expected: PASS — all graph-style tests including `caption-coverage.test.ts`.
|
|
104
|
+
|
|
105
|
+
- [ ] **Step 5: Rebuild dist and run the UI parity test — expect GREEN**
|
|
106
|
+
|
|
107
|
+
Run: `cd platform/lib/graph-style && ./node_modules/.bin/tsc -p tsconfig.json && cd ../../ui && ./node_modules/.bin/vitest run app/graph/__tests__/display-helpers.test.ts`
|
|
108
|
+
Expected: PASS (77 tests) — parity between the canvas renderer and the live page holds.
|
|
109
|
+
|
|
110
|
+
- [ ] **Step 6: Commit Part A**
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git add platform/lib/graph-style/src/index.ts platform/lib/graph-style/src/__tests__/caption-coverage.test.ts
|
|
114
|
+
git commit -m "fix(graph-style): caption branches for top-level fall-through labels"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Self-review
|
|
120
|
+
|
|
121
|
+
- **Spec coverage.** Task file Part A (Property/Listing branches) → Task 2 Steps 1-2, expanded to all fall-throughs per the approved scope decision. Task file Part B (coverage gate) → Task 1. Verification section (reproduction check, parity, display-helpers) → Task 2 Steps 4-5.
|
|
122
|
+
- **Scope deviation from the task file.** The task named only Property and Listing for Part A; the coverage seed surfaced eight more first-class fall-throughs (`Viewing, Offer, Visit, PurchaseOrder, Invoice, Risk, Finding, Hypothesis`), and the operator chose to branch all of them now. This is a deliberate, approved expansion, recorded in Plan Conformity at land time.
|
|
123
|
+
- **Correction to the task file.** The task listed `Note` as a grey example; `Note` declares `text`, a generic-fallback key, so it captions via `text` and is in the captioned bucket, not grey.
|
|
124
|
+
- **No placeholders.** The nine empirically-finalised labels have an explicit decision rule and a hard requirement to land in one bucket; they are not left "TBD".
|
package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Task 1877 — Bash schema enforcement + standing reconcile — implementation plan
|
|
2
|
+
|
|
3
|
+
Closes the Bash channel gap in account-dir schema enforcement. Two layers, per
|
|
4
|
+
the approved task decision. No agent-tool narrowing, no command-string parsing,
|
|
5
|
+
no auto-repair.
|
|
6
|
+
|
|
7
|
+
## Layer A — immediate Bash stray detection (two hooks)
|
|
8
|
+
|
|
9
|
+
Both live in `platform/plugins/admin/hooks/`, beside `fs-schema-guard.sh`. Both
|
|
10
|
+
key off account-root state (`$PWD` is the account dir for these hooks), never off
|
|
11
|
+
the command string.
|
|
12
|
+
|
|
13
|
+
### `fs-schema-guard-bash-pre.sh` (PreToolUse, matcher `Bash`)
|
|
14
|
+
- Read stdin envelope, extract `tool_name` and `session_id` (python3, matching
|
|
15
|
+
`fs-schema-guard.sh`'s `field()` helper and the session-id extraction in
|
|
16
|
+
`askuserquestion-channel-carrier-gate.sh`).
|
|
17
|
+
- Non-`Bash` tool: `exit 0`.
|
|
18
|
+
- Resolve `TMP_DIR` with the exact `TMPDIR:-TMP:-TEMP:-/tmp` order and trailing-
|
|
19
|
+
slash strip used by `askuserquestion-channel-carrier-gate.sh`; sanitise the
|
|
20
|
+
session id `[^A-Za-z0-9_-]` to `_`.
|
|
21
|
+
- List the account root's immediate top-level entry names (one per line, python3
|
|
22
|
+
`os.listdir`) and write to `$TMP_DIR/maxy-fsguard-snapshot-<sid>.txt`.
|
|
23
|
+
- Fail open on every error (no session id, no python3, unlistable dir, unwritable
|
|
24
|
+
tmp): `exit 0`, take no snapshot. A snapshot that cannot be taken must not
|
|
25
|
+
block the command.
|
|
26
|
+
|
|
27
|
+
### `fs-schema-guard-bash-post.sh` (PostToolUse, matcher `Bash`)
|
|
28
|
+
- Same envelope parse; non-`Bash`: `exit 0`.
|
|
29
|
+
- Read the pre-snapshot for this session; missing snapshot: `exit 0` (fail open).
|
|
30
|
+
- Parse `allowed-top-level` from `$PWD/SCHEMA.md` with the same awk extraction
|
|
31
|
+
`fs-schema-guard.sh` uses; empty/absent allowed set: `exit 0` (fail open — an
|
|
32
|
+
un-seeded legacy account must not have every Bash call blocked).
|
|
33
|
+
- Re-list the account root now. Strays = entries **present now AND absent from
|
|
34
|
+
the pre-snapshot AND not in the allowed set**.
|
|
35
|
+
- No strays: `exit 0` silent.
|
|
36
|
+
- For each stray, log `[fs-guard-bash] stray path=<name> reason=top-level` to
|
|
37
|
+
stderr; then `exit 2` with a schema-citing message telling the agent to
|
|
38
|
+
relocate through the `data-manager` specialist. No filesystem mutation
|
|
39
|
+
(PostToolUse runs after the command; auto-repair is rejected).
|
|
40
|
+
|
|
41
|
+
## Layer B — standing reconcile
|
|
42
|
+
|
|
43
|
+
### `platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`
|
|
44
|
+
Shape mirrors `rootless-client-audit.ts`: pure formatter (unit-tested) + impure
|
|
45
|
+
collector + `create…Reconcile` start/stop factory with an unref'd `setInterval`.
|
|
46
|
+
Purely filesystem: no Neo4j, no graph writes, no deletes.
|
|
47
|
+
|
|
48
|
+
Per account under the accounts root, per run:
|
|
49
|
+
- **Stray top-level:** any root entry not in that account's `allowed-top-level`
|
|
50
|
+
set (parsed from its `SCHEMA.md`). Accounts with no/empty allowed set are
|
|
51
|
+
skipped (fail open, like the hook).
|
|
52
|
+
- **Over-deep:** under the depth-checked buckets only, mirroring
|
|
53
|
+
`fs-schema-guard.sh`'s `case` statement exactly — `projects`/`contacts` flat to
|
|
54
|
+
3 segments, `documents` to 3; every other (tool-owned) bucket is not walked.
|
|
55
|
+
This is the reuse of the guard's own encoding the task calls for, not a new
|
|
56
|
+
list.
|
|
57
|
+
- **Quarantine, never repair:** move each stray top-level entry into the
|
|
58
|
+
account's `.quarantine/` with a `manifest.jsonl` line recording original path +
|
|
59
|
+
reason + timestamp. Over-deep files are counted only (not moved and not
|
|
60
|
+
manifest-logged): moving a file out of a flat bucket would orphan it from its
|
|
61
|
+
entity folder; re-filing is `data-manager`'s job.
|
|
62
|
+
- **Control-plane safety belt:** the reconcile never quarantines the platform
|
|
63
|
+
control-plane entries (`.git`, `.claude`, `.quarantine`, `SCHEMA.md`,
|
|
64
|
+
`account.json`, `AGENTS.md`) regardless of the account's allowed set, so a
|
|
65
|
+
stale or hand-edited SCHEMA.md cannot drive it to move the account's git root,
|
|
66
|
+
hook settings, or identity file. This is a deliberate deviation from the task's
|
|
67
|
+
strict "any entry not in allowed" wording, chosen because a 5-minute auto-move
|
|
68
|
+
loop that can brick an account on a stale schema is an unacceptable foot-gun.
|
|
69
|
+
- Emit one line per run: `[fs-reconcile] stray-top-level=<n> over-deep=<m>
|
|
70
|
+
quarantined=<q>`.
|
|
71
|
+
|
|
72
|
+
Timestamps come from an injected clock (the module never calls `Date.now()` at
|
|
73
|
+
import; the factory passes `() => Date.now()`), so the pure formatter and
|
|
74
|
+
collector stay deterministic under test.
|
|
75
|
+
|
|
76
|
+
### Wiring
|
|
77
|
+
- `index.ts`: construct `createAccountDirSchemaReconcile({ accountsRoot:
|
|
78
|
+
dirname(config.accountDir), logger: log, intervalMs: 300_000 })` beside the
|
|
79
|
+
sibling audits and call `.start()`.
|
|
80
|
+
|
|
81
|
+
## Registration + seeding
|
|
82
|
+
- `provision-account-dir.sh`: add `fs-schema-guard-bash-pre.sh` to the existing
|
|
83
|
+
PreToolUse `Bash` matcher (after `archive-ingest-surface-gate.sh`); add a new
|
|
84
|
+
PostToolUse `Bash` matcher carrying `fs-schema-guard-bash-post.sh`.
|
|
85
|
+
- `templates/account-schema/SCHEMA.md`: add `.quarantine` to the
|
|
86
|
+
`allowed-top-level` block so the reconcile's own move never creates a stray.
|
|
87
|
+
|
|
88
|
+
## Tests
|
|
89
|
+
- `platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh` — pre
|
|
90
|
+
writes snapshot; post blocks a whim folder with the log line; allowed
|
|
91
|
+
top-level and nested-allowed produce no block; no-new-entry is silent; a
|
|
92
|
+
pre-existing stray is not re-flagged; fail-open on missing snapshot / empty
|
|
93
|
+
allowed set.
|
|
94
|
+
- `platform/services/claude-session-manager/src/__tests__/account-dir-schema-reconcile.test.ts`
|
|
95
|
+
— formatter output; planted stray moved to `.quarantine/` with manifest;
|
|
96
|
+
allowed dir and tool-owned deep files untouched; over-deep counted not moved;
|
|
97
|
+
clean account logs zeros and moves nothing.
|
|
98
|
+
|
|
99
|
+
## Verification surface
|
|
100
|
+
Build (`tsc`), lint, the new + existing `fs-schema-guard` tests, the manager
|
|
101
|
+
vitest suite. Installer publish awaits explicit operator command (sprint gating).
|
|
102
|
+
|
|
103
|
+
## Out of scope (per task)
|
|
104
|
+
Graph-reconcile/broken-refs passes (original 1092 graph half); auto-re-filing
|
|
105
|
+
quarantined content; agent-tool narrowing; command-string parsing; `chattr`
|
|
106
|
+
immutability; one-time cleanup of the existing backlog (a `data-manager`
|
|
107
|
+
dispatch).
|
package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Task 1887 — Reconcile `allowed-top-level` to the ontology Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make `merge()` in `account-schema-owned-dirs.py` rebuild the account's `allowed-top-level` list each run from `fixed-seed ∪ plugin-owned ∪ ontology-buckets`, pruning a stale entry only when its on-disk dir is empty or absent and keeping-and-logging it when it holds files.
|
|
6
|
+
|
|
7
|
+
**Architecture:** `merge()` currently appends owned+domain dirs into the fenced `allowed-top-level` block and never removes. Replace the append with a reconcile: keep every current entry that is still in the target set, keep a stale entry that holds files (logged as `keptPopulated`), drop a stale entry that is empty/absent (logged as `removed`), then append newly-projected owned/domain dirs as before. A hardcoded fixed-seed set shields base template dirs from pruning.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3 (stdlib only), bash test harness (`__tests__/*.test.sh`).
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Fixed seed set (prune-protection allowlist, verbatim from `platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md:65-89`): `projects contacts documents url-get output generated extracted uploads agents specialists sites public cache secrets state logs tmp SCHEMA.md account.json AGENTS.md .claude .git .quarantine`. This is 23 entries and includes `url-get` (the task body's inline list dropped it; the operator confirmed the plan-doc list governs).
|
|
14
|
+
- The seed set is prune-protection only. `merge()` never *adds* a missing seed entry; seed membership only prevents an entry already in the list from being flagged stale.
|
|
15
|
+
- "Holds files" = at least one regular file exists anywhere under `account_dir/<entry>` (recursive), or `<entry>` is a non-empty regular file. Absent path, empty dir, or dir whose whole subtree contains no files → prunable. Strict reading: never silently drop a path that carries any file content.
|
|
16
|
+
- Summary line format (single line, extends the existing one): `brand=<b> added=<…|none> domainBuckets=<…|none> removed=<…|none> keptPopulated=<…|none>`.
|
|
17
|
+
- Idempotency is mandatory: two consecutive `merge()` runs produce a byte-identical `SCHEMA.md`.
|
|
18
|
+
- Out of scope: `account-dir-schema-reconcile.ts` (1877), `fs-schema-guard.sh`, `schema-exposed-dirs.mjs`, and disposition of existing populated strays. Do not touch them.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: Reconcile in `merge()` + extended log
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Modify: `platform/scripts/lib/account-schema-owned-dirs.py` (add `FIXED_SEED`, add `_dir_holds_files`, rewrite the block-mutation part of `merge()`, extend the summary `print`)
|
|
26
|
+
- Test: `platform/scripts/__tests__/account-schema-owned-dirs.test.sh` (append new cases)
|
|
27
|
+
|
|
28
|
+
**Interfaces:**
|
|
29
|
+
- Consumes: existing `resolve(project_dir) -> [{dir, description, plugin}]`, `resolve_domain_buckets(project_dir) -> [{dir, label}]`, `_allowed_block(text) -> (entries, start, end)`, `_read_brand`, `_strip_region`.
|
|
30
|
+
- Produces: `FIXED_SEED` (frozenset of 23 str), `_dir_holds_files(account_dir, name) -> bool`, and a `merge()` whose summary line carries `removed=` and `keptPopulated=`.
|
|
31
|
+
|
|
32
|
+
- [ ] **Step 1: Write the failing tests**
|
|
33
|
+
|
|
34
|
+
Append to `platform/scripts/__tests__/account-schema-owned-dirs.test.sh`, before the final `echo "PASS=$PASS FAIL=$FAIL"` line. Helper to inject a stray line inside the fenced block:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# insert_stray <schema_path> <name>: add <name> as the first entry inside the
|
|
38
|
+
# allowed-top-level fence (simulates an account whose list carries a bucket no
|
|
39
|
+
# longer in the ontology).
|
|
40
|
+
insert_stray() {
|
|
41
|
+
awk -v n="$2" '{print} /^```allowed-top-level$/{print n}' "$1" > "$1.tmp" && mv "$1.tmp" "$1"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# --- prune: stale bucket, absent on disk -> removed from list + logged ---
|
|
45
|
+
A_PRUNE="$(make_vertical_tree '["sitedesk"]' 'schema-construction')"
|
|
46
|
+
A_P="$(make_account "$A_PRUNE")"; export PROJECT_DIR="$A_PRUNE/platform"
|
|
47
|
+
insert_stray "$A_P/SCHEMA.md" "inbound-invoices"
|
|
48
|
+
OUT_P="$(merge_owned_dirs_into_schema "$A_P")"
|
|
49
|
+
ALLOWED_P="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_P/SCHEMA.md")"
|
|
50
|
+
assert_nogrep "inbound-invoices" "$ALLOWED_P" "prune-empty-removed-from-list"
|
|
51
|
+
assert_grep "removed=inbound-invoices" "$OUT_P" "prune-empty-logged"
|
|
52
|
+
|
|
53
|
+
# --- keep: stale bucket that holds files -> retained + keptPopulated ---
|
|
54
|
+
A_KEEP="$(make_vertical_tree '["sitedesk"]' 'schema-construction')"
|
|
55
|
+
A_K="$(make_account "$A_KEEP")"; export PROJECT_DIR="$A_KEEP/platform"
|
|
56
|
+
insert_stray "$A_K/SCHEMA.md" "inbound-invoices"
|
|
57
|
+
mkdir -p "$A_K/inbound-invoices"; echo "stamp" > "$A_K/inbound-invoices/stamp_invoice.py"
|
|
58
|
+
OUT_K="$(merge_owned_dirs_into_schema "$A_K")"
|
|
59
|
+
ALLOWED_K="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_K/SCHEMA.md")"
|
|
60
|
+
assert_grep "inbound-invoices" "$ALLOWED_K" "keep-populated-retained"
|
|
61
|
+
assert_grep "keptPopulated=inbound-invoices" "$OUT_K" "keep-populated-logged"
|
|
62
|
+
assert_nogrep "removed=inbound-invoices" "$OUT_K" "keep-populated-not-in-removed"
|
|
63
|
+
|
|
64
|
+
# --- fixed-seed survival: empty base dir is never pruned ---
|
|
65
|
+
A_SEED="$(make_tree '[]')"; A_S="$(make_account "$A_SEED")"; export PROJECT_DIR="$A_SEED/platform"
|
|
66
|
+
OUT_S="$(merge_owned_dirs_into_schema "$A_S")"
|
|
67
|
+
ALLOWED_S="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_S/SCHEMA.md")"
|
|
68
|
+
for d in documents url-get .git output; do
|
|
69
|
+
assert_grep "$d" "$ALLOWED_S" "seed-survives-$d"
|
|
70
|
+
done
|
|
71
|
+
assert_grep "removed=none" "$OUT_S" "seed-nothing-removed"
|
|
72
|
+
|
|
73
|
+
# --- idempotency after a prune: second run byte-identical ---
|
|
74
|
+
B_P="$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')"
|
|
75
|
+
merge_owned_dirs_into_schema "$A_P" >/dev/null
|
|
76
|
+
assert_eq "$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')" "$B_P" "prune-idempotent"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- [ ] **Step 2: Run the suite to verify the new cases fail**
|
|
80
|
+
|
|
81
|
+
Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
|
|
82
|
+
Expected: FAIL — `removed=`/`keptPopulated=` absent from output; `inbound-invoices` still in `ALLOWED_P`.
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 3: Add `FIXED_SEED` and `_dir_holds_files`**
|
|
85
|
+
|
|
86
|
+
Insert after the `ONT_END` constant (line 25) region:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
FIXED_SEED = frozenset({
|
|
90
|
+
"projects", "contacts", "documents", "url-get", "output", "generated",
|
|
91
|
+
"extracted", "uploads", "agents", "specialists", "sites", "public",
|
|
92
|
+
"cache", "secrets", "state", "logs", "tmp", "SCHEMA.md", "account.json",
|
|
93
|
+
"AGENTS.md", ".claude", ".git", ".quarantine",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _dir_holds_files(account_dir, name):
|
|
98
|
+
"""True iff account_dir/<name> carries file content: a non-empty regular
|
|
99
|
+
file, or a directory whose subtree contains at least one regular file.
|
|
100
|
+
Absent path, empty dir, or a dir tree with no files -> False (prunable)."""
|
|
101
|
+
path = os.path.join(account_dir, name)
|
|
102
|
+
if not os.path.exists(path):
|
|
103
|
+
return False
|
|
104
|
+
if os.path.isfile(path):
|
|
105
|
+
return os.path.getsize(path) > 0
|
|
106
|
+
for _root, _dirs, files in os.walk(path):
|
|
107
|
+
if files:
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- [ ] **Step 4: Rewrite the block-mutation section of `merge()`**
|
|
113
|
+
|
|
114
|
+
Replace the current block (lines 256-270, the `if entries is not None:` … `text = "\n".join(lines)` section) with:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
removed = []
|
|
118
|
+
kept_populated = []
|
|
119
|
+
if entries is not None:
|
|
120
|
+
present = set(entries)
|
|
121
|
+
owned_dirs = {e["dir"] for e in owned}
|
|
122
|
+
domain_dirs = {b["dir"] for b in domain}
|
|
123
|
+
target = FIXED_SEED | owned_dirs | domain_dirs
|
|
124
|
+
# Reconcile existing entries: keep in-target; keep populated strays
|
|
125
|
+
# (logged); drop empty/absent strays (logged).
|
|
126
|
+
new_entries = []
|
|
127
|
+
for entry in entries:
|
|
128
|
+
if entry in target:
|
|
129
|
+
new_entries.append(entry)
|
|
130
|
+
elif _dir_holds_files(account_dir, entry):
|
|
131
|
+
new_entries.append(entry)
|
|
132
|
+
kept_populated.append(entry)
|
|
133
|
+
else:
|
|
134
|
+
removed.append(entry)
|
|
135
|
+
# Append newly-projected owned then domain dirs not already present.
|
|
136
|
+
insert = []
|
|
137
|
+
for e in owned:
|
|
138
|
+
if e["dir"] not in present and e["dir"] not in insert:
|
|
139
|
+
insert.append(e["dir"])
|
|
140
|
+
added.append(e["dir"])
|
|
141
|
+
for b in domain:
|
|
142
|
+
if b["dir"] not in present and b["dir"] not in insert:
|
|
143
|
+
insert.append(b["dir"])
|
|
144
|
+
added_domain.append(b["dir"])
|
|
145
|
+
new_entries.extend(insert)
|
|
146
|
+
lines = text.split("\n")
|
|
147
|
+
lines[start + 1:end] = new_entries
|
|
148
|
+
text = "\n".join(lines)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 5: Extend the summary `print`**
|
|
152
|
+
|
|
153
|
+
Replace the final `print(...)` of `merge()` (lines 299-300) with:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
print(f"brand={brand} added={','.join(added) if added else 'none'} "
|
|
157
|
+
f"domainBuckets={','.join(added_domain) if added_domain else 'none'} "
|
|
158
|
+
f"removed={','.join(removed) if removed else 'none'} "
|
|
159
|
+
f"keptPopulated={','.join(kept_populated) if kept_populated else 'none'}")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Also update the early-return `print` (line 249, the no-SCHEMA.md branch) to keep the log shape uniform:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
print(f"brand={brand} added=none domainBuckets=none removed=none keptPopulated=none")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- [ ] **Step 6: Run the full suite to verify all pass**
|
|
169
|
+
|
|
170
|
+
Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
|
|
171
|
+
Expected: `PASS=<n> FAIL=0` (n = prior 38 + new assertions).
|
|
172
|
+
|
|
173
|
+
- [ ] **Step 7: Commit**
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git add platform/scripts/lib/account-schema-owned-dirs.py platform/scripts/__tests__/account-schema-owned-dirs.test.sh
|
|
177
|
+
git commit -m "feat(admin): reconcile account allowed-top-level to ontology, empty-only prune (Task 1887)"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### Task 2: Document the projection + empty-only prune rule
|
|
183
|
+
|
|
184
|
+
**Files:**
|
|
185
|
+
- Modify: `.docs/data-portal-folder-index.md` (add a short section stating the reconcile rule)
|
|
186
|
+
|
|
187
|
+
- [ ] **Step 1: Add the rule to the living reference**
|
|
188
|
+
|
|
189
|
+
Append a section after the "Three details that fail silently" block explaining: `allowed-top-level` is reconciled each `merge()` to `fixed-seed ∪ plugin-owned ∪ ontology-buckets`; a stale entry (in the list, not in that union) is pruned only when its on-disk dir is empty or absent, and kept-and-logged (`keptPopulated=`) when it holds files, so the write-guard list tracks the ontology without ever orphaning live data into the 1877 quarantine reconcile.
|
|
190
|
+
|
|
191
|
+
- [ ] **Step 2: Commit**
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git add .docs/data-portal-folder-index.md
|
|
195
|
+
git commit -m "docs: account allowed-top-level projection + empty-only prune rule (Task 1887)"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Self-Review
|
|
201
|
+
|
|
202
|
+
- **Spec coverage:** `merge()` reconcile (Task 1 Step 4), empty-only prune (Step 3 `_dir_holds_files` + Step 4), fixed-seed survival (Step 3 `FIXED_SEED` in `target`), logging `removed`/`keptPopulated` (Step 5), tests for prune-empty / keep-populated / seed-survival / idempotency (Step 1), `.docs` update (Task 2). New-ontology-bucket-add and collision cases stay covered by the existing untouched assertions.
|
|
203
|
+
- **Placeholder scan:** none — every step carries the literal code or command.
|
|
204
|
+
- **Type consistency:** `FIXED_SEED` frozenset of str; `_dir_holds_files(account_dir, name)->bool`; `removed`/`kept_populated` lists of str consumed only by the summary `print`. `owned`/`domain` element shapes match `resolve`/`resolve_domain_buckets` (`e["dir"]`, `b["dir"]`).
|
|
205
|
+
- **Regression boundary:** `lines[start+1:end] = new_entries` replaces only the fence body; the wholesale regeneration of the two descriptive regions (lines 272-296) is unchanged, so the em-dash/hyphen separator contract and portal exposure are untouched.
|
|
@@ -90,7 +90,10 @@ export declare function formatMessageTimestamp(iso: string): string | null;
|
|
|
90
90
|
* Person givenName+familyName compose, Agent displayName/slug, Job
|
|
91
91
|
* client+#jobId compose, Quote ref, InboundInvoice
|
|
92
92
|
* supplier+#confirmationNumber compose, WhatsAppGroup clientName,
|
|
93
|
-
* Message role+timestamp
|
|
93
|
+
* Message role+timestamp, Property address/slug, Listing displayName/slug,
|
|
94
|
+
* Viewing date, Offer status+price, Visit purpose/status, PurchaseOrder
|
|
95
|
+
* poNumber, Invoice confirmationNumber, Risk/Finding/Hypothesis statement),
|
|
96
|
+
* then a generic
|
|
94
97
|
* fallthrough across curated keys
|
|
95
98
|
* (name, title, summary, givenName, subject, text) — :ConversationArchive
|
|
96
99
|
* rows (Task 397) carry a stable `title` written by the writer so they pick
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAqItD,CAAA;AAED,eAAO,MAAM,qBAAqB,YAAY,CAAA;AAE9C,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAE7C,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAOzE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAiBD,eAAO,MAAM,qBAAqB,sFAQxB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,eAAO,MAAM,oBAAoB,EAAE,cAAsB,CAAA;AAUzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAE3D;AA8ED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY7D;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CAU/E;AASD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAKD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI7D;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIjE;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAqItD,CAAA;AAED,eAAO,MAAM,qBAAqB,YAAY,CAAA;AAE9C,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAE7C,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAOzE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAiBD,eAAO,MAAM,qBAAqB,sFAQxB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,eAAO,MAAM,oBAAoB,EAAE,cAAsB,CAAA;AAUzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAE3D;AA8ED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY7D;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CAU/E;AASD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAKD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI7D;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIjE;AA2BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAqH1D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAoF3D"}
|
|
@@ -356,13 +356,37 @@ function formatMessageTimestamp(iso) {
|
|
|
356
356
|
return null;
|
|
357
357
|
return m[1];
|
|
358
358
|
}
|
|
359
|
+
// Estate-agent caption sources. :Property and :Listing carry no `name`; their
|
|
360
|
+
// identity lives in address/displayName with slug as the always-present natural
|
|
361
|
+
// key. Shared by pickShortLabel (truncated) and pickDisplayName (untruncated).
|
|
362
|
+
function estateAddress(props) {
|
|
363
|
+
const address = typeof props.address === 'string' ? props.address : '';
|
|
364
|
+
const slug = typeof props.slug === 'string' ? props.slug : '';
|
|
365
|
+
return address.length > 0 ? address : slug;
|
|
366
|
+
}
|
|
367
|
+
function estateListingName(props) {
|
|
368
|
+
const displayName = typeof props.displayName === 'string' ? props.displayName : '';
|
|
369
|
+
const slug = typeof props.slug === 'string' ? props.slug : '';
|
|
370
|
+
return displayName.length > 0 ? displayName : slug;
|
|
371
|
+
}
|
|
372
|
+
// :Offer carries no single human label; compose status with the offered price.
|
|
373
|
+
function offerCaption(props) {
|
|
374
|
+
const status = typeof props.status === 'string' ? props.status : '';
|
|
375
|
+
const price = typeof props.price === 'number' ? `£${props.price}`
|
|
376
|
+
: typeof props.price === 'string' && props.price.length > 0 ? `£${props.price}`
|
|
377
|
+
: '';
|
|
378
|
+
return [status, price].filter((s) => s.length > 0).join(' ');
|
|
379
|
+
}
|
|
359
380
|
/**
|
|
360
381
|
* 24-char canvas label for a node. Label precedence per label family
|
|
361
382
|
* (ToolCall.toolName, WorkflowRun startedAt formatted, WorkflowStep.label,
|
|
362
383
|
* Person givenName+familyName compose, Agent displayName/slug, Job
|
|
363
384
|
* client+#jobId compose, Quote ref, InboundInvoice
|
|
364
385
|
* supplier+#confirmationNumber compose, WhatsAppGroup clientName,
|
|
365
|
-
* Message role+timestamp
|
|
386
|
+
* Message role+timestamp, Property address/slug, Listing displayName/slug,
|
|
387
|
+
* Viewing date, Offer status+price, Visit purpose/status, PurchaseOrder
|
|
388
|
+
* poNumber, Invoice confirmationNumber, Risk/Finding/Hypothesis statement),
|
|
389
|
+
* then a generic
|
|
366
390
|
* fallthrough across curated keys
|
|
367
391
|
* (name, title, summary, givenName, subject, text) — :ConversationArchive
|
|
368
392
|
* rows (Task 397) carry a stable `title` written by the writer so they pick
|
|
@@ -461,6 +485,48 @@ function pickShortLabel(node) {
|
|
|
461
485
|
return `${roleShort} ${ts}`;
|
|
462
486
|
}
|
|
463
487
|
}
|
|
488
|
+
else if (primaryLabel === 'Property') {
|
|
489
|
+
const v = estateAddress(props);
|
|
490
|
+
if (v.length > 0)
|
|
491
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
492
|
+
}
|
|
493
|
+
else if (primaryLabel === 'Listing') {
|
|
494
|
+
const v = estateListingName(props);
|
|
495
|
+
if (v.length > 0)
|
|
496
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
497
|
+
}
|
|
498
|
+
else if (primaryLabel === 'Viewing') {
|
|
499
|
+
const v = typeof props.date === 'string' ? props.date : '';
|
|
500
|
+
if (v.length > 0)
|
|
501
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
502
|
+
}
|
|
503
|
+
else if (primaryLabel === 'Offer') {
|
|
504
|
+
const v = offerCaption(props);
|
|
505
|
+
if (v.length > 0)
|
|
506
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
507
|
+
}
|
|
508
|
+
else if (primaryLabel === 'Visit') {
|
|
509
|
+
const purpose = typeof props.purpose === 'string' ? props.purpose : '';
|
|
510
|
+
const status = typeof props.status === 'string' ? props.status : '';
|
|
511
|
+
const v = purpose.length > 0 ? purpose : status;
|
|
512
|
+
if (v.length > 0)
|
|
513
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
514
|
+
}
|
|
515
|
+
else if (primaryLabel === 'PurchaseOrder') {
|
|
516
|
+
const v = typeof props.poNumber === 'string' ? props.poNumber : '';
|
|
517
|
+
if (v.length > 0)
|
|
518
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
519
|
+
}
|
|
520
|
+
else if (primaryLabel === 'Invoice') {
|
|
521
|
+
const v = typeof props.confirmationNumber === 'string' ? props.confirmationNumber : '';
|
|
522
|
+
if (v.length > 0)
|
|
523
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
524
|
+
}
|
|
525
|
+
else if (primaryLabel === 'Risk' || primaryLabel === 'Finding' || primaryLabel === 'Hypothesis') {
|
|
526
|
+
const v = typeof props.statement === 'string' ? props.statement : '';
|
|
527
|
+
if (v.length > 0)
|
|
528
|
+
return v.length > 24 ? v.slice(0, 24) + '…' : v;
|
|
529
|
+
}
|
|
464
530
|
for (const k of ['name', 'title', 'summary', 'givenName', 'subject', 'text']) {
|
|
465
531
|
const v = props[k];
|
|
466
532
|
if (typeof v === 'string' && v.length > 0) {
|
|
@@ -546,6 +612,48 @@ function pickDisplayName(node) {
|
|
|
546
612
|
if (desc.length > 0)
|
|
547
613
|
return desc;
|
|
548
614
|
}
|
|
615
|
+
else if (primaryLabel === 'Property') {
|
|
616
|
+
const v = estateAddress(props);
|
|
617
|
+
if (v.length > 0)
|
|
618
|
+
return v;
|
|
619
|
+
}
|
|
620
|
+
else if (primaryLabel === 'Listing') {
|
|
621
|
+
const v = estateListingName(props);
|
|
622
|
+
if (v.length > 0)
|
|
623
|
+
return v;
|
|
624
|
+
}
|
|
625
|
+
else if (primaryLabel === 'Viewing') {
|
|
626
|
+
const v = typeof props.date === 'string' ? props.date : '';
|
|
627
|
+
if (v.length > 0)
|
|
628
|
+
return v;
|
|
629
|
+
}
|
|
630
|
+
else if (primaryLabel === 'Offer') {
|
|
631
|
+
const v = offerCaption(props);
|
|
632
|
+
if (v.length > 0)
|
|
633
|
+
return v;
|
|
634
|
+
}
|
|
635
|
+
else if (primaryLabel === 'Visit') {
|
|
636
|
+
const purpose = typeof props.purpose === 'string' ? props.purpose : '';
|
|
637
|
+
const status = typeof props.status === 'string' ? props.status : '';
|
|
638
|
+
const v = purpose.length > 0 ? purpose : status;
|
|
639
|
+
if (v.length > 0)
|
|
640
|
+
return v;
|
|
641
|
+
}
|
|
642
|
+
else if (primaryLabel === 'PurchaseOrder') {
|
|
643
|
+
const v = typeof props.poNumber === 'string' ? props.poNumber : '';
|
|
644
|
+
if (v.length > 0)
|
|
645
|
+
return v;
|
|
646
|
+
}
|
|
647
|
+
else if (primaryLabel === 'Invoice') {
|
|
648
|
+
const v = typeof props.confirmationNumber === 'string' ? props.confirmationNumber : '';
|
|
649
|
+
if (v.length > 0)
|
|
650
|
+
return v;
|
|
651
|
+
}
|
|
652
|
+
else if (primaryLabel === 'Risk' || primaryLabel === 'Finding' || primaryLabel === 'Hypothesis') {
|
|
653
|
+
const v = typeof props.statement === 'string' ? props.statement : '';
|
|
654
|
+
if (v.length > 0)
|
|
655
|
+
return v;
|
|
656
|
+
}
|
|
549
657
|
for (const k of ['name', 'title', 'summary', 'givenName', 'subject', 'text']) {
|
|
550
658
|
const v = props[k];
|
|
551
659
|
if (typeof v === 'string' && v.length > 0)
|