@rubytech/create-sitedesk-code 0.1.524 → 0.1.525

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  4. package/payload/platform/plugins/memory/PLUGIN.md +10 -0
  5. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
  6. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  7. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
  8. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
  9. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
  10. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
  11. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  15. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
  29. package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  38. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +111 -2
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +38 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +116 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  44. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +13 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  53. package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
  54. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
  55. package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
  56. package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
  57. package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
  58. package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
  59. package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
  60. package/payload/platform/.docs/search-surface-contract.md +0 -58
  61. package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
  62. package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
  63. package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
  64. package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
  65. package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
  66. package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
  67. package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
  68. package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
  69. package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
  70. package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
  71. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
  72. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
  73. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
  74. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
  75. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
  76. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
  77. package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
  78. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
  79. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
  80. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
  81. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
  82. package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
  83. package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
  84. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
  85. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
  86. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
  87. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
  88. package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
  89. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
  90. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
  91. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
  92. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
  93. package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
  94. package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
  95. package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
  96. package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
  97. package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
  98. package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
  99. package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
  100. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
  101. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
  102. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
  103. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
  104. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
  105. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
  106. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
  107. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
  108. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
  109. package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
  110. package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
  111. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
  112. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
  113. package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
  114. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
  115. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
  116. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
  117. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
@@ -1,116 +0,0 @@
1
- # Task 610 — Webchat follower 202 cold-start retry — design
2
-
3
- **Date:** 2026-06-02
4
- **Task:** `maxy-code/.tasks/610-webchat-follower-202-cold-start-race-never-detects-end-turn.md`
5
- **Status:** Approved, pre-implementation
6
-
7
- ## Problem
8
-
9
- Every public webchat turn returns "Sorry, something went wrong" even when the agent
10
- completes successfully. Proven live on the realagent-code Pi: session `2a2a6239`
11
- completed its turn (assistant `text` greeting, `stop_reason:"end_turn"`) at T+52s, yet
12
- `dispatchOnce` returned `reject reason=turn-timeout` at the 120s `WEBCHAT_TURN_TIMEOUT_MS`.
13
-
14
- Root cause, confirmed in code: the JSONL follower's first fetch of
15
- `GET /:sessionId/log?follow=1` hits the pre-first-JSONL window of a cold-spawned PTY.
16
- The manager returns `202 {pending:true}` (`http-server.ts:1468-1471`) when the PTY is
17
- live but no JSONL exists yet. The follower guard is `if (!res.ok || !res.body)`
18
- (`follower.ts:46`). **`202` satisfies `res.ok`** (200–299), so the follower falls
19
- through, reads the pending JSON body as one non-event line (`type` undefined → skipped),
20
- reads stream `done`, `break`s, runs `finally → onClose()`, and **never retries**. No
21
- `follower-error`, no `fanOut`, no `outbound` — silent death. Public sessions idle-reap,
22
- so every greeting is the first turn of a fresh spawn → every turn hits the race → every
23
- turn times out.
24
-
25
- ## Decision
26
-
27
- Fix the follower, not the manager. The follower retries on `202` with a bounded poll
28
- loop until it gets a `200` stream or the give-up window elapses. The manager's `202`
29
- branch is left unchanged — `202 {pending:true}` is the correct signal; the follower's
30
- handling of it was the defect. This matches the task's observability spec, which is
31
- entirely follower-centric (`follower-connect` / `follower-retry` / `follower-open` /
32
- `follower-give-up`), and keeps the change in one file with a unit-testable fetch seam.
33
-
34
- ## The change — `follower.ts`
35
-
36
- Wrap the single `fetch` in a bounded retry loop. Per attempt:
37
-
38
- - `fetch(managerLogFollowUrl)`, then log `follower-connect sessionId=… status=<code>`.
39
- - **`status === 202`**: log `follower-retry sessionId=… attempt=N reason=pending`,
40
- cancel the pending response body, check `abort.signal.aborted` (return cleanly via
41
- `finally → onClose` if aborted), check elapsed against the give-up window. If exceeded:
42
- log `follower-give-up sessionId=… reason=pending-timeout attempts=N`, call
43
- `onError('follow-pending-timeout')` + `onClose()`, return. Otherwise `sleep(interval)`
44
- and retry.
45
- - **`200` + body**: log `follower-open sessionId=…`, break into the existing
46
- read/parse/accumulate/fan-out loop, unchanged.
47
- - **any other non-ok (404/410/5xx)**: unchanged — `onError('follow-status-<code>')` +
48
- `onClose()`. Session-gone respawn is handled by `writeInput`'s 410/404 path in
49
- `bridge.ts`, not the follower.
50
-
51
- Abort during a retry sleep ends the loop with no `onError` (just `onClose` via the outer
52
- `finally`), matching today's abort-during-stream behaviour. The retry loop sits inside the
53
- existing `try/finally`, so `onClose` fires exactly once on every exit path.
54
-
55
- ## Config
56
-
57
- Two env vars, mirroring `CHANNEL_PTY_IDLE_MS` in `bridge.ts`:
58
-
59
- - `CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS` — give-up window, default `120_000`. Covers
60
- claude cold-boot and matches the webchat 120s turn window. If the follower gives up
61
- before the JSONL appears, the turn times out anyway, so the bound need not exceed the
62
- turn window.
63
- - `CHANNEL_PTY_FOLLOWER_RETRY_MS` — poll interval between `202` retries, default `1_000`.
64
-
65
- Both read inside `follower.ts` at loop entry.
66
-
67
- ## No behaviour change elsewhere
68
-
69
- - **Warm sessions**: first fetch returns `200`, logs `follower-connect status=200` +
70
- `follower-open`, streams as today. The four existing follower tests cover this path
71
- unchanged.
72
- - **whatsapp / email**: same shared follower. They only see new behaviour if they ever
73
- cold-start into the `202` window — the same bug, fixed once. No channel-specific code.
74
- - **Manager**: untouched. `202`/`200`/`404`/`410` contract is unchanged.
75
-
76
- ## Tests (ephemeral)
77
-
78
- In `__tests__/follower.test.ts`, with a small `helpers.ts` extension to script a follow
79
- response sequence (`202` then `200`-stream) per sessionId:
80
-
81
- 1. **Repro**: first follow fetch returns `202 {pending:true}`, second returns a `200`
82
- stream carrying an `end_turn`+text record → subscriber receives the text. Fails today
83
- (silent close, no fan-out).
84
- 2. **Give-up**: follow keeps returning `202` past `CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS`
85
- (set small in the test) → `onError` + `onClose` fire, no fan-out.
86
- 3. **Regression**: the four existing follower tests pass unchanged (warm 200 path,
87
- user-event reset, malformed-line skip, multi-subscriber fan-out, abort→onClose).
88
-
89
- Tests are run to prove the fix during the sprint and discarded unless they belong in the
90
- existing committed suite — this suite is already tracked, so the new cases stay.
91
-
92
- ## Observability (per task spec)
93
-
94
- - Success signal: `follower-open` + `outbound bytes>0` per webchat turn; the manager's
95
- `log-follow-open` for that sessionId now appears (absent today for every timed-out turn).
96
- - Failure signal: `reject reason=turn-timeout` with no preceding `follower-open` =
97
- follower never connected. With the fix, a persistent cold session emits
98
- `follower-give-up` instead of dying silently.
99
-
100
- ## Out of scope (separate tasks)
101
-
102
- - Task 609 — fail-open public `--allowed-tools` / `memory-search` strip. Independent.
103
- - WhatsApp/email turn-timeout tuning — no change beyond incidental shared-follower coverage.
104
- - Session reuse / idle-reap tuning (why each turn cold-spawns) — separate concern.
105
- - Manager-side connection hold — explicitly rejected in favour of the follower retry.
106
-
107
- ## Touches
108
-
109
- - `platform/ui/app/lib/channel-pty-bridge/follower.ts`
110
- - `platform/ui/app/lib/channel-pty-bridge/__tests__/follower.test.ts`
111
- - `platform/ui/app/lib/channel-pty-bridge/__tests__/helpers.ts`
112
- - `.docs/` + `platform/plugins/docs/references/` (webchat turn lifecycle + new env vars)
113
- - `.tasks/LANES.md`, archive task file
114
-
115
- Installer publish required — `platform/ui/**` ships in the bundled `create-realagent-code` /
116
- `create-maxy-code` payload.
@@ -1,170 +0,0 @@
1
- # Account filesystem schema: seed it, and enforce file↔graph stewardship — design
2
-
3
- Source task: `.tasks/1091-account-filesystem-schema-enforcement.md`. Backstop
4
- (out of scope here): `.tasks/1092-account-dir-standing-reconcile-and-quarantine.md`.
5
-
6
- ## Problem (recap)
7
-
8
- The account data dir (`data/accounts/<id>/`) should be a consistent projection of
9
- the graph ontology, but that doctrine lives only inside the `data-manager`
10
- specialist the main agent often routes around, nothing seeds a canonical file
11
- layout, and nothing stops an agent inventing folders. The observed failure: an
12
- agent invented `projects/<name>/deploy|sources|design|archive` with no ontology
13
- basis, justified as "there was no canonical schema to copy from."
14
-
15
- ## Resolved gating decision
16
-
17
- `provision_account_dir` runs **unprivileged** on the Pi target — at install and
18
- at runtime when the `account_create` admin tool calls it via `execFileSync`
19
- inside the `systemctl --user` brand service. `chattr +i` needs root
20
- (`CAP_LINUX_IMMUTABLE`). So the task's Decision C1 (OS-immutable top level)
21
- **cannot take effect on the real target**.
22
-
23
- **Decision (operator, 2026-06-23): drop C1 entirely.** No `chattr`/`chflags`
24
- code ships. The PreToolUse Write-hook becomes the sole tool-layer hard guard;
25
- top-level strays created via bash fall to the Task 1092 reconcile. This also
26
- removes the chattr observability line and the upgrade/uninstall-unlock risk from
27
- the task.
28
-
29
- ## What ships
30
-
31
- Three layers, honest about hard-enforceable vs cooperative.
32
-
33
- ### A. Seeded schema (one standard, every account)
34
-
35
- `provision_account_dir` (the function both `setup-account.sh` and the
36
- `account_create` lifecycle tool call) gains an idempotent seed step:
37
-
38
- - **Create the operator-data skeleton:** `projects/`, `contacts/`, `documents/`.
39
- - **Create the tool-owned skeleton:** `url-get/`, `output/`, `generated/`,
40
- `extracted/`, `uploads/` (`agents/`, `specialists/` already created earlier in
41
- the function).
42
- - **Copy `SCHEMA.md`** to the account root from a shipped template
43
- (`platform/templates/account-schema/SCHEMA.md`). The template is the **single
44
- source of truth** for the allowed top-level set (see below) and the flatness
45
- rule.
46
- - **Log** `[acct-schema] seeded dirs=<n>` once per provision. Idempotent: a
47
- re-provision over an existing account re-creates nothing and overwrites
48
- `SCHEMA.md` with the shipped version (Rubytech-controlled, like `IDENTITY.md`).
49
-
50
- Operator-data buckets are **flat**: files live directly under one entity folder
51
- (`projects/<name>/file`, `contacts/<name>/file`) or, for `documents/`, directly
52
- in the bucket or one document-folder deep. Flatness is what makes
53
- `projects/<name>/deploy/` a machine-detectable violation.
54
-
55
- ### B. `DATA_STEWARDSHIP` directive (cooperative layer)
56
-
57
- A `DATA_STEWARDSHIP` const beside `PLATFORM_BOUNDARY` in
58
- `platform/services/claude-session-manager/src/system-prompt.ts`, rendered as a
59
- `<data-stewardship>` section in `renderAppendBlock` next to `<platform-boundary>`
60
- — joined into `--append-system-prompt` on **every spawn, every role**. It states:
61
- the account-dir layout is fixed by the seeded `SCHEMA.md`; never author new
62
- top-level folders or nested subtrees under an entity folder; route
63
- reorganisation/moves through `data-manager` and non-trivial graph writes through
64
- `database-operator`; the existing carve-out (a one-line update against an
65
- in-context, unambiguously-classified node) stays direct.
66
-
67
- `runSystemPromptSelfTest` is extended to assert `<data-stewardship>` /
68
- `</data-stewardship>` appear in the rendered block.
69
-
70
- ### C. PreToolUse Write-hook (hard guard, no bash removal)
71
-
72
- A new hook `platform/plugins/admin/hooks/fs-schema-guard.sh`, seeded into the
73
- account `.claude/settings.json` as a PreToolUse hook on `Write`, `Edit`, and
74
- `NotebookEdit` (added alongside the existing `archive-ingest-surface-gate.sh`
75
- entries on those three matchers).
76
-
77
- Behaviour:
78
-
79
- 1. Read the stdin JSON envelope. Fail **closed** on terminal stdin (no envelope).
80
- 2. Extract the target path: `tool_input.file_path` for Write/Edit,
81
- `tool_input.notebook_path` for NotebookEdit (via python3, matching the
82
- existing `extract_tool_input_field` pattern — never jq, never command parsing).
83
- 3. Resolve the path against the account dir. If the resolved path is **outside**
84
- the account data dir, **allow** (exit 0) — this hook governs the account-dir
85
- schema only; platform-source writes are PLATFORM_BOUNDARY's concern.
86
- 4. Compute the path relative to the account dir. Let `seg0` be its first segment.
87
- 5. **Top-level check:** if `seg0` is a single path segment (a top-level file or
88
- dir) not in the allowed top-level set → **block** (exit 2),
89
- `reason=top-level`.
90
- 6. **Over-deep check:** if `seg0 ∈ {projects, contacts}` and the path has more
91
- than `bucket/entity/file` depth (i.e. a directory component beyond the single
92
- entity folder), or `seg0 == documents` and the path exceeds
93
- `documents/folder/file` → **block** (exit 2), `reason=over-deep`.
94
- 7. Tool-owned dirs (everything in the allowed set except the three operator-data
95
- buckets) pass at **any** depth.
96
- 8. Otherwise allow (exit 0).
97
- 9. Every block logs `[fs-guard] blocked path=<rel> reason=<top-level|over-deep>`
98
- to stderr (operator-greppable). No task numbers / internal refs in any
99
- operator-visible string. The exit-2 stderr message cites the schema in plain
100
- terms.
101
-
102
- **Allowed top-level set — single source.** The set lives once, as a fenced
103
- ` ```allowed-top-level ` block in `platform/templates/account-schema/SCHEMA.md`
104
- (newline-delimited). The hook parses that fenced block from the **account's**
105
- seeded `SCHEMA.md` at runtime (deterministic structured data, not prose control
106
- flow). Shipped contents:
107
-
108
- ```
109
- projects
110
- contacts
111
- documents
112
- url-get
113
- output
114
- generated
115
- extracted
116
- uploads
117
- agents
118
- specialists
119
- sites
120
- public
121
- cache
122
- secrets
123
- state
124
- logs
125
- tmp
126
- SCHEMA.md
127
- account.json
128
- AGENTS.md
129
- .claude
130
- .git
131
- .quarantine
132
- ```
133
-
134
- A unit test asserts the hook's parse of the template equals this documented set,
135
- so the human doc and the enforcement never drift. Adding a future plugin
136
- top-level dir is a one-line edit to this block.
137
-
138
- ## Out of scope (unchanged from task)
139
-
140
- - The standing reconcile / quarantine — Task 1092 (named as the backstop here,
141
- not built).
142
- - Removing or narrowing the main agent's `Bash`/`Write`/`Edit` tools — rejected.
143
- - Auto-repairing drift — `data-manager` fixes on dispatch.
144
- - Per-account schema variants.
145
- - Any change to the graph schema or the `database-operator` write path.
146
- - `chattr`/`chflags` OS-immutability (Decision C1) — dropped per the gating
147
- decision above.
148
-
149
- ## Verification
150
-
151
- - **Seed:** a fresh `provision_account_dir` run produces the documented skeleton
152
- and `SCHEMA.md`; the seeded allowed-top-level set matches the template exactly.
153
- Re-provision is idempotent.
154
- - **Write-hook:** a `Write` to `projects/Acme/deploy/sources/a.txt` blocks
155
- (`over-deep`); `projects/Acme/a.txt` and `documents/a.pdf` allow; a write into
156
- `output/` or `generated/` at any depth allows; a write to a novel top-level dir
157
- `whim/x` blocks (`top-level`); a write outside the account dir allows.
158
- - **Directive:** `runSystemPromptSelfTest` asserts the `<data-stewardship>` tag.
159
- - **No tool regression:** the main agent retains `Bash`/`Write`/`Edit`; existing
160
- hook + spawn + specialist tests pass; `platform/ui` (if touched) builds/lints
161
- clean. The new hook does not interfere with the existing PreToolUse gate on the
162
- same matchers (both run; either can block).
163
- - **Device:** after publish + upgrade, a real account shows the seeded skeleton,
164
- `SCHEMA.md`, and a blocked whim-write in the logs.
165
-
166
- ## Observability
167
-
168
- - `provision_account_dir` logs `[acct-schema] seeded dirs=<n>`.
169
- - The Write-hook logs `[fs-guard] blocked path=<rel> reason=<top-level|over-deep>`
170
- per block.
@@ -1,90 +0,0 @@
1
- # Task 1557 — preferences an operator states inside a client's sub-account attach to the owner, not the operator
2
-
3
- **Date:** 2026-07-11
4
- **Status:** Approved design, pending plan
5
- **Scope:** `platform/plugins/memory/mcp/src/` (identity resolution), `platform/plugins/memory/skills/conversational-memory/SKILL.md`, `platform/plugins/memory/PLUGIN.md`, tests.
6
-
7
- ## The problem, in one line
8
-
9
- A house operator working inside a client's sub-account states a preference on the client's behalf. It attaches to the operator, not the client's owner. The operator wants a way to say "this belongs to them, not me."
10
-
11
- ## What the evidence established (measure-first, now measured)
12
-
13
- The attribution is decided in `resolveProfileIdentity` (`platform/plugins/memory/mcp/src/index.ts:153`). Two facts, both confirmed at the code layer, fix the shape of the solution:
14
-
15
- 1. **Operating a sub-account re-scopes the whole session into it.** The sub-account picker POSTs `/api/admin/session/switch-account`, which calls `createAdminSession(target.accountId, …, operatorUserId)` (`platform/ui/server/routes/admin/session.ts:341-371`). The rc-spawn that follows boots the memory MCP with `ACCOUNT_ID` = the **sub-account**, `USER_ID` = the **house operator**, and **no** `HOUSE_ADMIN_SCOPE` (`buildRcChildEnv`, and `houseAdminScopeFor` returns null for a non-house account). So the preference write is an ordinary own-account write, keyed to the operator's `USER_ID`, landing on `:UserProfile{accountId:sub, userId:operator}` — never the owner.
16
-
17
- 2. **The tool-layer `targetAccountId` lever is inert in this session.** `resolveEffectiveAccountCore` rejects any `targetAccountId` with `cross-account-denied` when `houseScope` is null (`platform/lib/account-scope/src/index.ts:49`). A sub-account operator session has no house scope, so the agent cannot set `targetAccountId`. **Teaching the skill to pass `targetAccountId` would do nothing** — it would be rejected. This is the branch the task pre-authorised: MCP `src/` changes "only if the operator-surface `targetAccountId` wiring proves absent — measure first." It is absent.
18
-
19
- 3. **The owner is a distinct, single, resolvable identity.** Each sub-account is seeded with exactly one `:AdminUser{accountId, role:'owner'}` carrying a **fresh per-account `userId`** (`platform/plugins/admin/mcp/src/tools/account-lifecycle.ts:256,279`), deliberately never the operator's. `resolveOwnerUserId(accountId)` already resolves it (`platform/plugins/memory/mcp/src/lib/resolve-owner-userid.ts`).
20
-
21
- 4. **`AdminUser` nodes are keyed on `userId` alone; `accountId` is a home-account stamp.** `neo4j-store.ts:627-630` MERGEs `:AdminUser {userId}` and sets `accountId` only `ON CREATE`. Each admin therefore has one global `AdminUser` node whose `accountId` is their home account. This makes "is the caller an admin of the account they are operating?" answerable with one query, with the home account fixed at the node's first creation.
22
-
23
- ## The fix
24
-
25
- A preference can only ever attach to an account's owner — that is the only "designated person" the memory model supports, confirmed with the operator. So "attribute this to the designated person" resolves to "attribute this to the owner of the account being operated." The switch that decides it lives in one place: `resolveProfileIdentity`.
26
-
27
- ### Behaviour
28
-
29
- In `resolveProfileIdentity`, the own-account branch (`crossAccount === false`) gains an operator-on-behalf case:
30
-
31
- - Resolve the caller's home account: the `accountId` on their `:AdminUser {userId: callerUserId}` node.
32
- - **Caller is operating their home account** (home `accountId` == the session's resolved `accountId`): attribute to `callerUserId`. Unchanged — this is the owner-is-speaker path and the genuine second-admin path.
33
- - **Caller is a foreign operator** (home `accountId` != the session's resolved `accountId`, i.e. a house operator working inside a client's sub-account): resolve the session account's owner via `resolveOwnerUserId`.
34
- - Owner exists → attribute the preference to the **owner**, and bootstrap the owner's `UserProfile`/`Person` the same way the cross-account path already does. Emit `[xacct] op=identity tool=… mode=operator-on-behalf target=<acct8> caller=<caller8> resolved-userId=<owner8>`.
35
- - No owner on record → **loud reject** (identical message class to the cross-account no-owner rejection). Never a silent fall-through to the operator's own profile.
36
- - Caller `userId` absent → existing "requires an authenticated admin session with userId" reject, unchanged.
37
-
38
- The cross-account branch (`crossAccount === true`, a house session that set `targetAccountId`) is unchanged.
39
-
40
- ### Discriminator — why it is correct and regression-safe
41
-
42
- The condition "caller's `AdminUser.accountId` != the session's `accountId`" is equivalent to "no `:AdminUser {accountId: <session>, userId: <caller>}` exists," because there is exactly one `AdminUser` node per `userId`. Case-by-case:
43
-
44
- | Caller | Session account | Home `accountId` | Result |
45
- |---|---|---|---|
46
- | Owner (home = own account) | own | own | attribute self — unchanged |
47
- | Second admin of an account | that account | that account | attribute self — no regression |
48
- | House operator | a client sub-account | house | attribute the sub-account **owner** — the fix |
49
- | House operator | the house account | house | attribute self — unchanged |
50
-
51
- A sub-account subgraph never contains an `AdminUser` node for the operator (their node carries `accountId=house`), so the foreign-operator branch fires for exactly the reported case and no other.
52
-
53
- ### Bootstrap generalisation
54
-
55
- Today the handlers set `bootstrapProfile`/`bootstrapPerson` `= crossAccount`. After the fix, a foreign-operator own-account write also attributes to an owner whose `UserProfile` may not yet exist, so the bootstrap flags must be true whenever the resolved identity is a **foreign owner**, not only when `crossAccount`. `resolveProfileIdentity` will return whether the attribution is to a foreign owner, and the three handlers derive the bootstrap flags from that rather than from `crossAccount` directly. This keeps a single meaning: "we resolved to an owner who is not the caller → bootstrap their profile if absent."
56
-
57
- ### Skill
58
-
59
- `conversational-memory/SKILL.md` currently says "the owner" throughout and assumes the person conversing **is** the owner. It gains the operator-on-behalf case: when the session is a house operator working inside a client's account, preferences captured there belong to that client (the account's owner), not to the operator; the operator's own preferences live on their own (house) session. "What does this owner prefer?" reads the owner's list. The skill also states the loud refusal when the client account has no owner on record, pointing at the rootless-account remedy. No new tool argument is introduced — attribution is decided by who is driving the session, deterministically, so the skill describes the model rather than instructing a per-call lever.
60
-
61
- ### Docs
62
-
63
- `PLUGIN.md`'s preference section records the operator/owner attribution model: in a client sub-account session, preference reads and writes resolve to the account's owner, and a no-owner account rejects loudly. This sits alongside the existing cross-account `targetAccountId` note (lines 189-193), which documents the house-session path; the two paths resolve to the same owner identity.
64
-
65
- ## What success looks like
66
-
67
- - A preference an operator states while operating a client's sub-account is stored as a `:Preference` attributed to that sub-account's **owner** `userId`, not the operator's.
68
- - "What does the owner prefer?" on the sub-account returns the owner's preferences, not the operator's.
69
- - A sub-account with no seeded owner rejects the write out loud, never silently attributing to the operator.
70
- - The own-account owner-is-speaker path and the genuine second-admin path are byte-for-byte unchanged.
71
-
72
- ## Testing
73
-
74
- Unit tests over `resolveProfileIdentity` / the profile handlers:
75
-
76
- - **Operator-on-behalf write** — caller whose `AdminUser.accountId` != the session account resolves the session account's owner `userId`; the resolved identity is the owner and the bootstrap flag is set.
77
- - **Own-account owner-is-speaker** — caller whose `AdminUser.accountId` == the session account keys to the caller `userId`; bootstrap flag is off. Unchanged.
78
- - **Second admin of the account** — caller with a home `AdminUser.accountId` equal to the session account attributes to self, proving multi-admin accounts do not mis-redirect.
79
- - **Foreign operator, no owner** — session account has no owner `AdminUser`; the write is rejected loudly, never caller-attributed.
80
- - **Cross-account (`targetAccountId`) path** — unchanged: still resolves the target owner, still rejects a no-owner target.
81
-
82
- The attribution is already logged (`[xacct] op=identity … resolved-userId=`, `[preference-write] op=… accountId=`), so on-device the before/after is visible from the journal without new instrumentation. The runtime reproduce on the SiteDesk device is a confirmation of the already-established code path, not a design input; this Mac cannot read that device's session log, so the design rests on the code evidence above.
83
-
84
- ## Out of scope
85
-
86
- - The cross-account `targetAccountId` / `resolveOwnerUserId` mechanism (Task 1461) and owner seeding (Task 1359) — consumed, not re-implemented.
87
- - How the sub-account picker switches/re-scopes sessions (Task 1311) — untouched; the fix needs no spawn-env change.
88
- - Attribution to an arbitrary named individual who is not an account owner — a much larger storage/schema change, explicitly excluded.
89
- - Backfill of pre-1359 sub-accounts created without an owner on record — a separate one-time seed, never a gate relaxation. If needed, filed as its own task.
90
- - Non-preference profile fields and the `personFields` owner-`Person` bootstrap beyond their sharing the same resolved-owner identity path.
@@ -1,178 +0,0 @@
1
- # Account file-schema as a real projection of the domain graph ontology — design
2
-
3
- Source task: `.tasks/1622-account-file-schema-does-not-mirror-the-domain-graph-ontology.md`.
4
- Prior design (the mechanism this extends): `2026-06-23-account-filesystem-schema-design.md`.
5
-
6
- ## Problem
7
-
8
- The account data directory is specified to be a projection of the graph ontology,
9
- but the allowed top-level set is a hardcoded generic base
10
- (`platform/templates/account-schema/SCHEMA.md`: `projects`, `contacts`,
11
- `documents` plus tool-owned dirs). Domain entities that the businessType ontology
12
- declares top-level are never added. On SiteDesk the ontology
13
- (`platform/plugins/memory/references/schema-construction.md`) makes `:Job` the hub
14
- with `:Customer`/`:Site`/`:Quote`/`:Visit`/`:Asset` as first-class operator-entry
15
- node types, and the sidebar's SiteDesk primary container is `:Job`, yet the
16
- account `SCHEMA.md` lists no `jobs` bucket. So a produced quote is filed inside the
17
- pricing plugin's private `quoting/jobs/<jobId>/` workspace, disconnected from the
18
- `:Job`/`:Quote` graph nodes and invisible to the Jobs view.
19
-
20
- A second, internal inconsistency exists inside the sitedesk-job plugin: the
21
- `valuation` skill's `references/job-folder.md` already documents the job folder as
22
- the top-level `$ACCOUNT_DIR/jobs/{jobId}/`, a path the file-schema does not permit,
23
- so valuation's own filing is admissible only by accident. Making `jobs/` a real
24
- derived bucket fixes both the quotation gap and this latent valuation gap.
25
-
26
- ## The single source (resolves the task's "Depends on")
27
-
28
- `platform/plugins/memory/mcp/src/lib/schema-loader.ts` already machine-parses the
29
- schema markdown entity tables (each recognised by a `Neo4j Label` +
30
- `Required Properties` header pair). `brand.json#vertical` already names which
31
- `schema-<vertical>.md` applies (SiteDesk = `schema-construction`). Therefore
32
- `schema-<vertical>.md` is the single authoritative ontology, and the file-schema
33
- generator becomes a second reader of the same file. The graph loader itself does
34
- not change: it already reads these files. "The ontology" is one list, read by both
35
- the loader (for validation) and the generator (for top-level bucket derivation).
36
-
37
- `graph-labels.ts` `FILTER_TOP_LEVEL_LABELS` (the `/graph` UI chip allowlist) stays
38
- a separate concern this sprint. Single-sourcing it from the same per-vertical
39
- source is filed as a follow-up, not built here.
40
-
41
- ## What ships
42
-
43
- ### A. File-schema derives domain buckets from the vertical ontology
44
-
45
- **Convention for "top-level".** A label is a derived operator-entity bucket iff it
46
- appears as a row in the entity table under the heading
47
- `## Top-level node types (operator-entry, natural key)` in `schema-<vertical>.md`
48
- **and** its `Schema.org Type` column is a `schema:`-namespaced type. The
49
- `schema:`-namespace test deterministically excludes transport labels
50
- (`WhatsAppGroup` carries `cdm:Channel`) with no hardcoded skip-list. A vertical
51
- whose schema file carries no such heading contributes no domain buckets (generic
52
- base only) and is surfaced by the reconcile check (below).
53
-
54
- **Label to directory name.** kebab-case-plural of the label: camelCase word
55
- boundaries split on case, joined with `-`, lowercased, with a trailing `s`
56
- appended (or `y` becomes `ies`). Examples: `Job` to `jobs`, `Customer` to
57
- `customers`, `Quote` to `quotes`, `Asset` to `assets`, `Visit` to `visits`,
58
- `Part` to `parts`, `PurchaseOrder` to `purchase-orders`, `PpmContract` to
59
- `ppm-contracts`, `InboundInvoice` to `inbound-invoices`. One collision
60
- special-case: `Site` maps to `customer-sites`, so the tool-owned `sites/`
61
- published-tree bucket is untouched. The special-case is a single explicit entry in
62
- a `LABEL_DIR_OVERRIDES` map keyed by label; every other label uses the mechanical
63
- rule.
64
-
65
- **Generator.** `platform/scripts/lib/account-schema-owned-dirs.py` gains a
66
- `resolve_domain_buckets(project_dir)` beside the existing `resolve()` (plugin
67
- owned-dirs). It reads `brand.json#vertical`, opens
68
- `<project_dir>/plugins/memory/references/<vertical>.md`, extracts the top-level
69
- labels per the convention above, maps each to a dir, and returns
70
- `[{dir, label}, ...]` in table order. `merge()` unions these dirs into the
71
- `allowed-top-level` fenced block (same insertion path as owned-dirs) and writes a
72
- documented region between `<!-- ontology-buckets:start -->` and
73
- `<!-- ontology-buckets:end -->` markers, listing each bucket and its source label.
74
- The region is regenerated wholesale on every merge (idempotent), exactly like the
75
- existing plugin-owned region.
76
-
77
- **Provision.** No new call site. `merge_owned_dirs_into_schema` (in
78
- `account-schema-owned-dirs.sh`, already called by `provision-account-dir.sh` and
79
- `setup-account.sh`) now emits domain buckets as well as owned-dirs. Empty domain
80
- directories are **not** pre-created on provision. A fresh account has no jobs yet;
81
- the directory is created on first write, the same as any tool-owned dir. Only the
82
- allowed set and the `SCHEMA.md` region change at provision time.
83
-
84
- **Guard.** `platform/plugins/admin/hooks/fs-schema-guard.sh` needs no logic change.
85
- It already reads the account's `SCHEMA.md` allowed block and admits any first
86
- segment in that set at any depth except the three flat operator-data buckets
87
- (`projects`, `contacts`, `documents`). A domain bucket such as `jobs/` is therefore
88
- admitted at any depth, which is correct for the `jobs/<jobId>/Quotations/...`
89
- subtree the ontology's own filesystem projection describes. The derived buckets
90
- reach the guard through the regenerated allowed block.
91
-
92
- ### B. Quotation reifies the Job/Quote/QuoteLine chain and files under the Job
93
-
94
- With `jobs/` a real bucket, the quotation apply-path is brought to parity with
95
- `valuation`'s job-setup (which already creates/updates `:Job` and `:Quote` and
96
- seeds `:QuoteLine`s on the quote). After the engine prices the job and
97
- `quote-render`'s fail-closed gates pass, the apply-path:
98
-
99
- 1. **Reifies** `:Job-[:HAS_QUOTE]->:Quote-[:HAS_LINE]->:QuoteLine`, idempotent on
100
- the natural keys the construction ontology defines (`:Job` on
101
- `(accountId, jobId)`, `:Quote` on `(accountId, ref)`, `:QuoteLine` on
102
- `(accountId, jobId, trade, description)`), via the same graph-write path
103
- `valuation` uses. The exact write surface (memory tools vs the
104
- `database-operator` specialist) is pinned in the plan by reading how
105
- `valuation`'s job-setup writes today, so the two paths stay identical.
106
- 2. **Files** the produced quote documents under the Job bucket
107
- (`jobs/<jobId>/Quotations/...`, the operator-facing location valuation's
108
- `job-folder.md` already documents), not orphaned in `quoting/jobs/<jobId>/`.
109
- The `quoting/` engine workspace (`engine.mjs`, `method.json`, `render.mjs`, and
110
- per-job figures) stays tool-owned. What changes is `quote-render`'s
111
- filing-scope for the operator-facing result and the graph binding.
112
- 3. **Emits** `[quote-render] op=quote-reify jobId=<id> quoteRef=<ref> lines=<n>` on
113
- the graph write, a post-condition breadcrumb. A quote that renders documents but
114
- writes no `:Quote` node is then immediately visible in the log.
115
-
116
- The schema-free pricing-method capture (item catalogue, rates, roll-up) is
117
- untouched. Only the output's placement and its entity binding are in scope.
118
-
119
- ### C. estate-agent schema reorganisation (proves derivation is not hardcoded)
120
-
121
- `schema-estate-agent.md` currently holds one mixed `## Additional Node Types` table
122
- (hub `Property` plus its children `Listing`, `ImageObject`, `Viewing`, `Applicant`,
123
- `Offer`). It is reorganised into a `## Top-level node types (operator-entry,
124
- natural key)` table and a `## Child node types` table, with no row duplication, so
125
- `schema-loader` sees the same union of labels it sees today (it already parses
126
- construction's two-table shape). Top-level for estate-agent: `Property` (the
127
- obligatory hub) plus the operator-entry entities the vertical's prose names
128
- (`Listing`, `Viewing`, `Offer`). `ImageObject` is a child of `Listing` (reached
129
- through its parent, as construction places `QuoteLine`); `Applicant` is a Person
130
- extension (base `:Person` plus applicant properties, the same additional-label
131
- pattern construction uses for `Engineer`/`Supplier`), so neither is a top-level
132
- bucket. The exact top-level/child split for estate-agent is finalised during
133
- implementation by reading the whole file, but the two `schema:`-namespaced sets
134
- above are the intent. The result: a construction account derives `jobs`/`customers`/...
135
- and an estate-agent account derives `properties`/`listings`/`viewings`/`offers`
136
- from the same generator, proving the set is derived, not hardcoded.
137
-
138
- ## Testing
139
-
140
- - `platform/scripts/__tests__/account-schema-owned-dirs.test.sh`: for a
141
- construction brand, the merged allowed set contains `jobs`, `customers`,
142
- `customer-sites`, `assets`, `visits`, `quotes` (and the rest), and does **not**
143
- collide with the tool-owned `sites`. For an estate-agent brand it contains
144
- `properties`, `listings`, `viewings`, `offers` and none of SiteDesk's buckets. A
145
- vertical with no top-level heading yields no domain buckets (generic base
146
- intact). `reconcile` reports every top-level label and whether its bucket is
147
- present.
148
- - `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`: a write under
149
- `jobs/<jobId>/Quotations/q.pdf` is admitted (domain bucket, any depth); an
150
- undeclared top-level segment is still blocked; the three flat buckets keep their
151
- over-deep block.
152
- - Cardinal repro (device-side, per the Pi-signal verification step): producing a
153
- quote files the documents under the Job and the graph shows
154
- `:Job-[:HAS_QUOTE]->:Quote-[:HAS_LINE]->:QuoteLine`; the quote appears in the
155
- sidebar Jobs view. What is unit-testable locally is the derivation, the guard
156
- admission, and the presence of the reify call and its breadcrumb.
157
-
158
- ## Observability
159
-
160
- - `account-schema-owned-dirs.py reconcile` is extended into a standing check that
161
- reports, per account, every top-level ontology entity **missing** a file-schema
162
- bucket, so a re-divergence surfaces without waiting for a mis-filed artifact.
163
- - `[quote-render] op=quote-reify jobId= quoteRef= lines=` on quote production
164
- confirms the graph write occurred.
165
-
166
- ## Out of scope (each a filed follow-up, no prose-only deferral)
167
-
168
- - Normalising the remaining vertical schema files (`schema-retail`,
169
- `schema-logistics`, `schema-hospitality`, `schema-food-beverage`,
170
- `schema-professional-services`, `schema-trades`, `schema-creator`,
171
- `schema-knowledge-work`) to carry the top-level heading. Until done they fall
172
- back to the generic base and the reconcile check flags them. New task.
173
- - Single-sourcing `graph-labels.ts` `FILTER_TOP_LEVEL_LABELS` from the same
174
- per-vertical ontology source. New task.
175
- - Backfilling existing `quoting/jobs/<jobId>/` figures folders for
176
- already-produced quotes (the source task already defers this).
177
- - Any change to the graph schema, the `schema-loader` validation path, or the
178
- `fs-schema-guard` decision logic beyond consuming the derived set.