@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,458 +0,0 @@
1
- # Graph top-level label allowlist ontology single-source — Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Make the `/graph` filter-popover chip allowlist (and the two other server routes that read the same set) derive from each vertical's `## Top-level node types` schema table instead of the hardcoded `FILTER_TOP_LEVEL_LABELS`, so a top-level label added to a vertical schema surfaces as a chip with no TypeScript edit, while preserving today's exact chip visibility.
6
-
7
- **Architecture:** A new server-only module `platform/ui/server/lib/top-level-labels.ts` reads every `platform/plugins/memory/references/schema-*.md`, extracts the `Neo4j Label` column of each file's `## Top-level node types` table, and unions those with an explicit `STATIC_TOP_LEVEL_LABELS` const (base-schema infra plus verticals lacking a top-level section). The three server routes (`graph-labels-in-graph`, `graph-subgraph`, `graph-default-view`) call `getTopLevelLabelAllowlist()` instead of importing `FILTER_TOP_LEVEL_LABELS`. Count-gating in the cypher is unchanged.
8
-
9
- **Tech Stack:** TypeScript (ES modules), Node `fs` (`readdirSync`/`readFileSync`), Vitest, Hono routes.
10
-
11
- ## Global Constraints
12
-
13
- - Derived union MUST deep-equal today's `FILTER_TOP_LEVEL_LABELS` (48 members) against the real references dir — the byte-identical acceptance baseline.
14
- - No `schema:` namespace filter: every row of a `## Top-level node types` table contributes its `Neo4j Label`, so `WhatsAppGroup` (`cdm:Channel`) stays a chip.
15
- - The `Neo4j Label` column is resolved by header name, never by index (construction table has 5 columns, estate-agent 4).
16
- - The new module is server-only (reads files). `FILTER_TOP_LEVEL_LABELS` stays in `app/lib/graph-labels.ts` for the client `searchHelpers.ts`.
17
- - `getTopLevelLabelAllowlist()` returns a `ReadonlySet<string>` so `.has()` and spread work at every call site.
18
- - Observability line on computation: `op=top-level-labels source=ontology count=<n> files=<...> derived=<d> static=<s>`, written to stderr via `console.error`.
19
- - No em-dashes in any shipped log string. No task numbers in operator-visible strings or log lines.
20
- - Path resolution mirrors `server/index.ts`: `process.env.MAXY_PLATFORM_ROOT ?? join(__dirname, '..')`, then `plugins/memory/references`.
21
-
22
- ---
23
-
24
- ### Task 1: The derivation module and its regression test
25
-
26
- **Files:**
27
- - Create: `platform/ui/server/lib/top-level-labels.ts`
28
- - Test: `platform/ui/server/lib/top-level-labels.test.ts`
29
-
30
- **Interfaces:**
31
- - Produces: `getTopLevelLabelAllowlist(opts?: { referencesDir?: string }): ReadonlySet<string>` and `STATIC_TOP_LEVEL_LABELS: ReadonlySet<string>`.
32
- - Consumes: nothing from earlier tasks. Reads `FILTER_TOP_LEVEL_LABELS` from `../../app/lib/graph-labels` in the test only.
33
-
34
- - [ ] **Step 1: Write the failing test**
35
-
36
- ```ts
37
- // platform/ui/server/lib/top-level-labels.test.ts
38
- import { describe, it, expect } from 'vitest'
39
- import { resolve } from 'node:path'
40
- import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs'
41
- import { tmpdir } from 'node:os'
42
- import { getTopLevelLabelAllowlist } from './top-level-labels'
43
- import { FILTER_TOP_LEVEL_LABELS } from '../../app/lib/graph-labels'
44
-
45
- // The real shipped references directory (four hops up from this test file:
46
- // server/lib -> server -> ui -> platform -> plugins/memory/references).
47
- const REAL_REFERENCES = resolve(
48
- import.meta.dirname,
49
- '../../../plugins/memory/references',
50
- )
51
-
52
- describe('getTopLevelLabelAllowlist', () => {
53
- it('deep-equals FILTER_TOP_LEVEL_LABELS against the real references dir', () => {
54
- const derived = getTopLevelLabelAllowlist({ referencesDir: REAL_REFERENCES })
55
- expect([...derived].sort()).toEqual([...FILTER_TOP_LEVEL_LABELS].sort())
56
- })
57
-
58
- it('surfaces a new top-level row with no TypeScript edit', () => {
59
- const dir = mkdtempSync(resolve(tmpdir(), 'tll-'))
60
- writeFileSync(
61
- resolve(dir, 'schema-widgets.md'),
62
- [
63
- '# Widgets',
64
- '',
65
- '## Top-level node types (operator-entry, natural key)',
66
- '',
67
- '| Entity | Neo4j Label | Schema.org Type | Required Properties |',
68
- '|--------|-------------|-----------------|---------------------|',
69
- '| Widget | `Widget` | `schema:Product` | `accountId`, `sku` |',
70
- '',
71
- ].join('\n'),
72
- )
73
- const derived = getTopLevelLabelAllowlist({ referencesDir: dir })
74
- expect(derived.has('Widget')).toBe(true)
75
- })
76
-
77
- it('includes a cdm:-namespaced top-level row (no namespace filter)', () => {
78
- const dir = mkdtempSync(resolve(tmpdir(), 'tll-'))
79
- writeFileSync(
80
- resolve(dir, 'schema-transport.md'),
81
- [
82
- '## Top-level node types',
83
- '',
84
- '| Entity | Neo4j Label | Schema.org Type | Required Properties |',
85
- '|--------|-------------|-----------------|---------------------|',
86
- '| Channel | `SomeChannel` | `cdm:Channel` | `accountId`, `id` |',
87
- '',
88
- ].join('\n'),
89
- )
90
- const derived = getTopLevelLabelAllowlist({ referencesDir: dir })
91
- expect(derived.has('SomeChannel')).toBe(true)
92
- })
93
-
94
- it('ignores tables that are not under a Top-level node types heading', () => {
95
- const dir = mkdtempSync(resolve(tmpdir(), 'tll-'))
96
- writeFileSync(
97
- resolve(dir, 'schema-child.md'),
98
- [
99
- '## Child node types (reached via parent neighbourhood)',
100
- '',
101
- '| Entity | Neo4j Label | Schema.org Type | Required Properties |',
102
- '|--------|-------------|-----------------|---------------------|',
103
- '| Line | `QuoteLine` | `schema:Thing` | `accountId`, `id` |',
104
- '',
105
- ].join('\n'),
106
- )
107
- const derived = getTopLevelLabelAllowlist({ referencesDir: dir })
108
- // Only the STATIC base labels survive; QuoteLine is a child, not top-level.
109
- expect(derived.has('QuoteLine')).toBe(false)
110
- expect(derived.has('Person')).toBe(true)
111
- })
112
- })
113
- ```
114
-
115
- - [ ] **Step 2: Run test to verify it fails**
116
-
117
- Run: `cd platform/ui && npx vitest run server/lib/top-level-labels.test.ts`
118
- Expected: FAIL — `getTopLevelLabelAllowlist` is not exported / module not found.
119
-
120
- - [ ] **Step 3: Write minimal implementation**
121
-
122
- ```ts
123
- // platform/ui/server/lib/top-level-labels.ts
124
- /**
125
- * Server-side derivation of the /graph top-level chip allowlist from the
126
- * per-vertical ontology.
127
- *
128
- * The allowlist is the union of two sources:
129
- * 1. The `Neo4j Label` column of every `## Top-level node types` table in
130
- * `plugins/memory/references/schema-*.md`. Today only schema-construction
131
- * and schema-estate-agent carry that section; more arrive as verticals
132
- * gain one.
133
- * 2. `STATIC_TOP_LEVEL_LABELS` — base-schema infra (schema-base.md has no
134
- * top-level section; its Node Types table mixes in child types) plus any
135
- * vertical whose schema has no top-level section yet.
136
- *
137
- * No namespace filter: every row contributes its label, so transport labels
138
- * like WhatsAppGroup (cdm:Channel) stay chips. This differs from the account
139
- * file-schema generator, which excludes non-schema: rows because a channel
140
- * has no file bucket. A channel is still a legitimate operator-entry graph node.
141
- */
142
- import { readdirSync, readFileSync } from 'node:fs'
143
- import { join, resolve } from 'node:path'
144
-
145
- /**
146
- * Top-level labels not (yet) sourced from a vertical `## Top-level node types`
147
- * section. This set shrinks as verticals gain a top-level section; each such
148
- * change moves its labels from here into the derived union.
149
- */
150
- export const STATIC_TOP_LEVEL_LABELS: ReadonlySet<string> = Object.freeze(
151
- new Set([
152
- // Base-schema infra (schema-base.md has no top-level section).
153
- 'LocalBusiness',
154
- 'Service',
155
- 'PriceSpecification',
156
- 'OpeningHoursSpecification',
157
- 'Organization',
158
- 'Person',
159
- 'UserProfile',
160
- 'Preference',
161
- 'AdminUser',
162
- 'AccessGrant',
163
- 'KnowledgeDocument',
164
- 'DigitalDocument',
165
- 'CreativeWork',
166
- 'Question',
167
- 'FAQPage',
168
- 'DefinedTerm',
169
- 'Review',
170
- 'ImageObject',
171
- 'Invoice',
172
- 'Task',
173
- 'Project',
174
- 'Event',
175
- 'Workflow',
176
- 'Email',
177
- 'EmailAccount',
178
- 'Agent',
179
- // Knowledge-work vertical: schema-knowledge-work.md has no top-level
180
- // section yet.
181
- 'Objective',
182
- 'KeyResult',
183
- 'Decision',
184
- 'Risk',
185
- 'Source',
186
- 'Finding',
187
- 'Hypothesis',
188
- ]),
189
- ) as ReadonlySet<string>
190
-
191
- /**
192
- * Split a pipe-delimited markdown row into trimmed cells, honouring backtick
193
- * code-spans so a `|` inside `` `A|B` `` does not split the row. Strips the
194
- * optional leading/trailing pipes.
195
- */
196
- function splitPipeRow(row: string): string[] {
197
- let trimmed = row.trim()
198
- if (trimmed.startsWith('|')) trimmed = trimmed.slice(1)
199
- if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1)
200
- const cells: string[] = []
201
- let inBacktick = false
202
- let start = 0
203
- for (let i = 0; i < trimmed.length; i++) {
204
- const ch = trimmed[i]
205
- if (ch === '`') inBacktick = !inBacktick
206
- else if (ch === '|' && !inBacktick) {
207
- cells.push(trimmed.slice(start, i).trim())
208
- start = i + 1
209
- }
210
- }
211
- cells.push(trimmed.slice(start).trim())
212
- return cells
213
- }
214
-
215
- const stripBackticks = (s: string): string => s.replace(/`/g, '').trim()
216
-
217
- /**
218
- * Extract the `Neo4j Label` column of the first pipe-table beneath a
219
- * `## Top-level node types` heading. Returns [] when the file has no such
220
- * section. The heading is matched by case-insensitive prefix so the
221
- * `(operator-entry, natural key)` suffix is tolerated; parsing stops at the
222
- * next `## ` heading.
223
- */
224
- function parseTopLevelLabels(content: string): string[] {
225
- const lines = content.split(/\r?\n/)
226
- let i = 0
227
- // Find the heading.
228
- while (i < lines.length) {
229
- const l = lines[i]
230
- if (l.startsWith('## ') && l.toLowerCase().includes('top-level node types')) break
231
- i++
232
- }
233
- if (i >= lines.length) return []
234
- i++
235
- // Find the header row of the first table before the next `## `.
236
- for (; i < lines.length - 1; i++) {
237
- const line = lines[i]
238
- if (line.startsWith('## ')) return []
239
- if (!line.includes('|')) continue
240
- const sep = lines[i + 1]
241
- if (!/^\s*\|?[\s:|-]+\|?\s*$/.test(sep) || !sep.includes('-')) continue
242
- const headers = splitPipeRow(line)
243
- const labelCol = headers.findIndex((h) => h.toLowerCase() === 'neo4j label')
244
- if (labelCol === -1) return []
245
- const out: string[] = []
246
- let j = i + 2
247
- for (; j < lines.length; j++) {
248
- const row = lines[j]
249
- if (!row.trim() || !row.includes('|')) break
250
- const cells = splitPipeRow(row)
251
- const label = stripBackticks(cells[labelCol] ?? '')
252
- if (label) out.push(label)
253
- }
254
- return out
255
- }
256
- return []
257
- }
258
-
259
- function resolveReferencesDir(): string {
260
- // Production: MAXY_PLATFORM_ROOT is `<install>/platform`, and the schema
261
- // references live at `<platform>/plugins/memory/references` (same root
262
- // schema-loader.ts resolves brand.json against).
263
- if (process.env.MAXY_PLATFORM_ROOT) {
264
- return join(process.env.MAXY_PLATFORM_ROOT, 'plugins', 'memory', 'references')
265
- }
266
- // Dev fallback (env unset, unbundled): three hops up from this source file,
267
- // server/lib -> server -> ui -> platform, then plugins/memory/references.
268
- return resolve(import.meta.dirname, '../../../plugins/memory/references')
269
- }
270
-
271
- let cached: ReadonlySet<string> | null = null
272
-
273
- /**
274
- * The /graph top-level chip allowlist, derived from the ontology. Cached at
275
- * module scope because schema files are static per install. An injected
276
- * `referencesDir` (tests) bypasses the cache and does not populate it.
277
- */
278
- export function getTopLevelLabelAllowlist(
279
- opts: { referencesDir?: string } = {},
280
- ): ReadonlySet<string> {
281
- if (!opts.referencesDir && cached) return cached
282
-
283
- const referencesDir = opts.referencesDir ?? resolveReferencesDir()
284
- const fileNames = readdirSync(referencesDir)
285
- .filter((f) => f.startsWith('schema-') && f.endsWith('.md'))
286
- .sort()
287
-
288
- const derived = new Set<string>()
289
- const contributingFiles: string[] = []
290
- for (const fileName of fileNames) {
291
- const content = readFileSync(join(referencesDir, fileName), 'utf-8')
292
- const labels = parseTopLevelLabels(content)
293
- if (labels.length > 0) contributingFiles.push(fileName)
294
- for (const label of labels) derived.add(label)
295
- }
296
-
297
- const derivedCount = derived.size
298
- for (const label of STATIC_TOP_LEVEL_LABELS) derived.add(label)
299
-
300
- const result = Object.freeze(derived) as ReadonlySet<string>
301
- process.stderr.write(
302
- `[graph-page] op=top-level-labels source=ontology count=${derived.size} ` +
303
- `files=${contributingFiles.join('|') || 'none'} derived=${derivedCount} ` +
304
- `static=${STATIC_TOP_LEVEL_LABELS.size}\n`,
305
- )
306
-
307
- if (!opts.referencesDir) cached = result
308
- return result
309
- }
310
- ```
311
-
312
- - [ ] **Step 4: Run test to verify it passes**
313
-
314
- Run: `cd platform/ui && npx vitest run server/lib/top-level-labels.test.ts`
315
- Expected: PASS — all four tests green. If the deep-equal test fails, the diff names the drifted label(s); reconcile `STATIC_TOP_LEVEL_LABELS` against `FILTER_TOP_LEVEL_LABELS` (do not weaken the assertion).
316
-
317
- - [ ] **Step 5: Commit**
318
-
319
- ```bash
320
- git add platform/ui/server/lib/top-level-labels.ts platform/ui/server/lib/top-level-labels.test.ts
321
- git commit -m "feat: derive /graph top-level chip allowlist from vertical ontology (1624)"
322
- ```
323
-
324
- ---
325
-
326
- ### Task 2: Point the three server routes at the derived allowlist
327
-
328
- **Files:**
329
- - Modify: `platform/ui/server/routes/admin/graph-labels-in-graph.ts`
330
- - Modify: `platform/ui/server/routes/admin/graph-subgraph.ts`
331
- - Modify: `platform/ui/server/routes/admin/graph-default-view.ts`
332
-
333
- **Interfaces:**
334
- - Consumes: `getTopLevelLabelAllowlist(): ReadonlySet<string>` from Task 1.
335
- - Produces: nothing new; behaviour of the three routes is unchanged because the derived set deep-equals `FILTER_TOP_LEVEL_LABELS`.
336
-
337
- - [ ] **Step 1: Confirm the existing route tests pass before the swap (baseline)**
338
-
339
- Run: `cd platform/ui && npx vitest run app/graph/__tests__/labels-in-graph.test.ts server/routes/__tests__/graph-default-topLevel.test.ts`
340
- Expected: PASS. This is the regression baseline the swap must preserve.
341
-
342
- - [ ] **Step 2: Swap the popover route**
343
-
344
- In `graph-labels-in-graph.ts`, change the import block
345
-
346
- ```ts
347
- import {
348
- FILTER_TOP_LEVEL_LABELS,
349
- AGENT_ACTION_LABELS,
350
- } from '../../../app/lib/graph-labels'
351
- ```
352
-
353
- to
354
-
355
- ```ts
356
- import { AGENT_ACTION_LABELS } from '../../../app/lib/graph-labels'
357
- import { getTopLevelLabelAllowlist } from '../../lib/top-level-labels'
358
- ```
359
-
360
- and change the allowlist construction (currently `new Set<string>(FILTER_TOP_LEVEL_LABELS)`)
361
-
362
- ```ts
363
- const allowedSet = new Set<string>(getTopLevelLabelAllowlist())
364
- if (includeAgentActions) {
365
- for (const label of AGENT_ACTION_LABELS) allowedSet.add(label)
366
- }
367
- ```
368
-
369
- - [ ] **Step 3: Swap the default-view fetch route**
370
-
371
- In `graph-subgraph.ts`, remove `FILTER_TOP_LEVEL_LABELS` from the `../../../app/lib/graph-labels` import (keep `HIDDEN_BY_DEFAULT_LABELS` and any others it imports), add `import { getTopLevelLabelAllowlist } from '../../lib/top-level-labels'`, and change line ~280
372
-
373
- ```ts
374
- const labels = [...getTopLevelLabelAllowlist()].filter((l) => !HIDDEN_BY_DEFAULT_LABELS.has(l))
375
- ```
376
-
377
- - [ ] **Step 4: Swap the eligibility route**
378
-
379
- In `graph-default-view.ts`, replace
380
-
381
- ```ts
382
- import { FILTER_TOP_LEVEL_LABELS } from '../../../app/lib/graph-labels'
383
- ```
384
-
385
- with
386
-
387
- ```ts
388
- import { getTopLevelLabelAllowlist } from '../../lib/top-level-labels'
389
- ```
390
-
391
- and change the eligibility check (line ~158)
392
-
393
- ```ts
394
- if (!getTopLevelLabelAllowlist().has(lbl)) {
395
- ```
396
-
397
- Update the adjacent `reason` string to drop the parenthetical `(FILTER_TOP_LEVEL_LABELS)` naming so no internal set name leaks; keep the operator-facing text: `label "${lbl}" is not eligible for default view — outside the chip allowlist`.
398
-
399
- - [ ] **Step 5: Run the route tests to verify no behaviour change**
400
-
401
- Run: `cd platform/ui && npx vitest run app/graph/__tests__/labels-in-graph.test.ts server/routes/__tests__/graph-default-topLevel.test.ts`
402
- Expected: PASS — the derived set equals `FILTER_TOP_LEVEL_LABELS`, so every assertion still holds.
403
-
404
- - [ ] **Step 6: Typecheck the UI package**
405
-
406
- Run: `cd platform/ui && npx tsc --noEmit`
407
- Expected: no errors (no unused-import errors for the removed `FILTER_TOP_LEVEL_LABELS`).
408
-
409
- - [ ] **Step 7: Commit**
410
-
411
- ```bash
412
- git add platform/ui/server/routes/admin/graph-labels-in-graph.ts platform/ui/server/routes/admin/graph-subgraph.ts platform/ui/server/routes/admin/graph-default-view.ts
413
- git commit -m "refactor: three /graph server routes read the ontology-derived allowlist (1624)"
414
- ```
415
-
416
- ---
417
-
418
- ### Task 3: Update the graph-labels doc comment and file the client follow-up
419
-
420
- **Files:**
421
- - Modify: `platform/ui/app/lib/graph-labels.ts` (comment only — the set stays for the client)
422
- - Create: `.tasks/NNNN-graph-searchhelpers-derive-top-level-from-ontology.md` (number assigned at land time from LANES)
423
-
424
- **Interfaces:**
425
- - Consumes: nothing. Documentation and task-tracking only.
426
-
427
- - [ ] **Step 1: Annotate `FILTER_TOP_LEVEL_LABELS` as the client-only mirror**
428
-
429
- Add a sentence to the `FILTER_TOP_LEVEL_LABELS` doc block in `graph-labels.ts` recording that the server routes now derive this set from the vertical ontology (`server/lib/top-level-labels.ts`), and that this constant remains only for the client `searchHelpers.ts`, which cannot read files. Do not change the set members.
430
-
431
- - [ ] **Step 2: Write the follow-up task file**
432
-
433
- Create the task file describing: plumb the ontology-derived allowlist to the client `app/data/searchHelpers.ts` so its `/data` search chip pre-filter stops reading the static `FILTER_TOP_LEVEL_LABELS`, closing the last second source. In scope: deliver the derived set to the client (via the `graph-labels-in-graph` response the page already fetches, or an equivalent). Out of scope: the server routes (done here). Include Testing and Observability sections per the `.tasks` format.
434
-
435
- - [ ] **Step 3: Commit**
436
-
437
- ```bash
438
- git add platform/ui/app/lib/graph-labels.ts .tasks/NNNN-graph-searchhelpers-derive-top-level-from-ontology.md
439
- git commit -m "docs: mark FILTER_TOP_LEVEL_LABELS client-only; file searchHelpers follow-up (1624)"
440
- ```
441
-
442
- ---
443
-
444
- ## Self-Review
445
-
446
- **Spec coverage:**
447
- - New module + derivation → Task 1. ✅
448
- - Three server routes switch → Task 2. ✅
449
- - Byte-identical baseline → Task 1 Step 1 deep-equal test + Task 2 route tests. ✅
450
- - No namespace filter (WhatsAppGroup) → Task 1 Step 1 cdm test + the real-dir deep-equal (which contains WhatsAppGroup). ✅
451
- - Base list explicit → `STATIC_TOP_LEVEL_LABELS` in Task 1. ✅
452
- - Observability line → Task 1 Step 3 `process.stderr.write`. ✅
453
- - Client follow-up filed → Task 3. ✅
454
- - Count-gating unchanged → cypher untouched in Task 2; asserted by the pre-existing route tests. ✅
455
-
456
- **Placeholder scan:** The only deferred value is the follow-up task number (`NNNN`), assigned from LANES at archive time per sprint protocol — not a code placeholder.
457
-
458
- **Type consistency:** `getTopLevelLabelAllowlist(opts?: { referencesDir?: string }): ReadonlySet<string>` is used identically in Task 1 (definition), Task 1 test, and all three Task 2 call sites. `STATIC_TOP_LEVEL_LABELS` is a `ReadonlySet<string>`. Call sites use `.has()` and spread, both valid on `ReadonlySet`.