@yottagraph-app/aether-instructions 1.1.19 → 1.1.20

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.
@@ -62,12 +62,15 @@ MCP tools — the app can still be built using the Elemental API client
62
62
 
63
63
  ## Step 3: Understand the Environment
64
64
 
65
- First, ensure dependencies are installed (skill docs and types aren't available without this):
65
+ First, ensure dependencies are installed (types aren't available without this):
66
66
 
67
67
  ```bash
68
68
  test -d node_modules || npm install
69
69
  ```
70
70
 
71
+ Skills in `.cursor/skills/` are populated during project init (`node init-project.js`).
72
+ If that directory is empty after `npm install`, run `node init-project.js` to install them.
73
+
71
74
  Then read these files to understand what's available:
72
75
 
73
76
  1. `DESIGN.md` -- project vision and current status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottagraph-app/aether-instructions",
3
- "version": "1.1.19",
3
+ "version": "1.1.20",
4
4
  "description": "Cursor rules, commands, and skills for Aether development",
5
5
  "files": [
6
6
  "rules",
package/rules/aether.mdc CHANGED
@@ -18,4 +18,4 @@ alwaysApply: true
18
18
 
19
19
  **First action for a new project:** Run `/build_my_app`.
20
20
 
21
- **Task-specific rules:** `architecture` (project structure, navigation, server routes, agents, MCP), `api` (Elemental API client, schema discovery, gotchas, optional MCP servers), `design` (DESIGN.md workflow, feature docs), `ui` (page templates, layout patterns), `cookbook` (copy-paste UI patterns), `pref` (KV preferences), `branding` (colors, fonts), `server` (Nitro routes, Neon Postgres), `something-broke` (error recovery, build failures).
21
+ **Task-specific rules:** `architecture` (project structure, navigation, server routes, agents, MCP), `api` (Elemental API client, schema discovery, gotchas, optional MCP servers), `design` (DESIGN.md workflow, feature docs), `ui` (page templates, layout patterns), `cookbook` (copy-paste UI patterns), `pref` (KV preferences), `branding` (colors, fonts), `server` (Nitro routes, Neon Postgres, server-side Elemental API), `something-broke` (error recovery, build failures).
package/rules/agents.mdc CHANGED
@@ -384,3 +384,127 @@ custom chat composable or modify `sendMessage`, follow the same approach.
384
384
  - Tool docstrings are critical: they're the LLM's API documentation
385
385
  - Handle errors gracefully in tools: return error messages, don't raise exceptions
386
386
  - Use `get_schema` as a discovery tool so the agent can learn about entity types at runtime
387
+
388
+ ## Agent Tool Design
389
+
390
+ LLMs are better at parsing prose than nested JSON. The difference between
391
+ a tool that works reliably and one that confuses the agent often comes down
392
+ to how the tool formats its output. Follow these principles:
393
+
394
+ ### Return formatted strings, not raw JSON
395
+
396
+ This applies to ADK agent tools, where the LLM reads tool output directly.
397
+ MCP server tools follow different conventions (structured dicts/lists) — see
398
+ the `mcp-servers` rule.
399
+
400
+ The agent's LLM will try to interpret whatever the tool returns. Raw API
401
+ responses with nested dicts, numeric IDs, and arrays of unlabeled values
402
+ create unnecessary interpretation burden.
403
+
404
+ ```python
405
+ # BAD — raw API response overwhelms the LLM:
406
+ def lookup_entity(name: str) -> dict:
407
+ resp = elemental_client.get(f"/entities/lookup?entityName={name}&maxResults=5")
408
+ resp.raise_for_status()
409
+ return resp.json() # {"results": [{"neid": "00416400910670863867", ...}]}
410
+
411
+ # GOOD — formatted string the LLM can immediately use:
412
+ def lookup_entity(name: str) -> str:
413
+ """Look up an entity by name. Returns entity name, ID, and type."""
414
+ try:
415
+ resp = elemental_client.get(f"/entities/lookup?entityName={name}&maxResults=5")
416
+ resp.raise_for_status()
417
+ data = resp.json()
418
+ results = data.get("results", [])
419
+ if not results:
420
+ return f"No entities found matching '{name}'."
421
+ lines = []
422
+ for r in results[:5]:
423
+ lines.append(f"- {r.get('name', 'Unknown')} (ID: {r.get('neid', '?')}, Type: {r.get('type', '?')})")
424
+ return f"Found {len(results)} result(s) for '{name}':\n" + "\n".join(lines)
425
+ except Exception as e:
426
+ return f"Error looking up '{name}': {e}"
427
+ ```
428
+
429
+ ### Catch all exceptions — never let errors propagate
430
+
431
+ `raise_for_status()` without a try/catch will crash the tool and produce an
432
+ opaque error in the agent's event stream. Always catch exceptions and return
433
+ a descriptive error string.
434
+
435
+ ```python
436
+ # BAD — unhandled exception kills the tool call:
437
+ def get_data(neid: str) -> dict:
438
+ resp = elemental_client.post("/elemental/entities/properties", data={...})
439
+ resp.raise_for_status()
440
+ return resp.json()
441
+
442
+ # GOOD — agent gets a useful error message it can relay to the user:
443
+ def get_data(neid: str) -> str:
444
+ """Get properties for an entity by its NEID."""
445
+ try:
446
+ resp = elemental_client.post("/elemental/entities/properties", data={...})
447
+ resp.raise_for_status()
448
+ # ... format results ...
449
+ return formatted_output
450
+ except Exception as e:
451
+ return f"Error fetching data for {neid}: {e}"
452
+ ```
453
+
454
+ ### Pre-resolve known entities for focused agents
455
+
456
+ If your agent tracks specific companies, people, or assets, hardcode their
457
+ NEIDs instead of requiring a lookup step. This eliminates a fragile
458
+ search-then-resolve flow.
459
+
460
+ ```python
461
+ TRACKED_COMPANIES = {
462
+ "Apple": "00416400910670863867",
463
+ "Tesla": "00508379502570440213",
464
+ "Microsoft": "00112855504880632635",
465
+ }
466
+
467
+ def get_company_info(company_name: str) -> str:
468
+ """Get current information about a tracked company.
469
+ Available companies: Apple, Tesla, Microsoft."""
470
+ neid = TRACKED_COMPANIES.get(company_name)
471
+ if not neid:
472
+ return f"Unknown company '{company_name}'. Available: {', '.join(TRACKED_COMPANIES)}"
473
+ # ... fetch and format properties using the known NEID ...
474
+ ```
475
+
476
+ ### Combine multi-step API flows into single tools
477
+
478
+ The fewer tools the agent needs to chain together, the more reliably it
479
+ operates. If every query requires lookup → get properties → format, combine
480
+ those steps into a single tool.
481
+
482
+ ```python
483
+ # BAD — agent must chain 3 tools correctly:
484
+ # 1. lookup_entity("Apple") → get NEID
485
+ # 2. get_schema() → find property PIDs
486
+ # 3. get_property_values(neid, pids) → parse values array
487
+
488
+ # GOOD — one tool does the full flow:
489
+ def get_company_summary(name: str) -> str:
490
+ """Get a summary of a company including name, country, and industry."""
491
+ try:
492
+ neid = resolve_entity(name)
493
+ if not neid:
494
+ return f"Could not find entity '{name}'."
495
+ props = fetch_properties(neid, [NAME_PID, COUNTRY_PID, INDUSTRY_PID])
496
+ return format_company_summary(neid, props)
497
+ except Exception as e:
498
+ return f"Error getting summary for '{name}': {e}"
499
+ ```
500
+
501
+ ### Two agent archetypes
502
+
503
+ **General explorer** — uses `get_schema` + `find_entities` +
504
+ `get_property_values` with runtime discovery. Good for open-ended
505
+ exploration but requires more tool calls and is more error-prone.
506
+
507
+ **Focused domain agent** — pre-resolves entity IDs, hardcodes relevant PIDs,
508
+ returns formatted strings. Fewer tools, more reliable, better for production
509
+ use cases. **Prefer this pattern** unless the agent genuinely needs to
510
+ explore arbitrary entity types.
package/rules/api.mdc CHANGED
@@ -28,9 +28,11 @@ For Lovelace **entity types, properties, relationships, and per-source schemas**
28
28
 
29
29
  ## Test Before You Build
30
30
 
31
- **Before writing app code that calls the Query Server, test the actual API
32
- call first and verify the response shape.** This avoids wasted iteration from incorrect
33
- assumptions about response shapes.
31
+ **ALWAYS test API calls via curl before writing code that depends on them.**
32
+ This is not optional the Elemental API has response shapes that differ from
33
+ what the TypeScript types suggest, and assumptions about nesting, property
34
+ formats, and field names will be wrong without testing. This applies doubly
35
+ to server-side code, where you can't inspect responses in the browser console.
34
36
 
35
37
  ### How to test
36
38
 
@@ -118,7 +120,7 @@ All methods return data directly and throw on non-2xx responses.
118
120
  | Method | Signature | Purpose |
119
121
  |---|---|---|
120
122
  | `getSchema` | `()` | All entity types (flavors) and properties (PIDs) |
121
- | `getPropertyValues` | `(body: { eids: string, pids: string })` | Property values (eids/pids are JSON-stringified arrays!) |
123
+ | `getPropertyValues` | `(body: { eids: string, pids: string })` | Property values (eids: JSON array of NEID strings; pids: JSON array of numeric PIDs) |
122
124
  | `summarizeProperty` | `(pid: number)` | Summary stats for a property |
123
125
 
124
126
  **Relationships and graph:**
@@ -229,10 +231,23 @@ const filingNeid = String(res.values[0].value).padStart(20, '0'); // "0492613234
229
231
  > TypeScript type is `string`, not `string[]`. Passing a raw array will silently
230
232
  > return no data.
231
233
 
234
+ > **WARNING -- PIDs are numeric IDs, not string names.** Property IDs (PIDs)
235
+ > are integers, not human-readable names. `pids: JSON.stringify(['name'])`
236
+ > will fail — use `pids: JSON.stringify([8])` (where 8 is the PID for "name"
237
+ > from `getSchema()`). Always call `getSchema()` first to discover the
238
+ > numeric PID for each property.
239
+
232
240
  ```typescript
241
+ // WRONG — PIDs are numbers, not strings:
242
+ const values = await client.getPropertyValues({
243
+ eids: JSON.stringify(['00416400910670863867']),
244
+ pids: JSON.stringify(['name', 'country', 'industry']), // FAILS
245
+ });
246
+
247
+ // CORRECT — use numeric PIDs from getSchema():
233
248
  const values = await client.getPropertyValues({
234
249
  eids: JSON.stringify(['00416400910670863867']),
235
- pids: JSON.stringify(['name', 'country', 'industry']),
250
+ pids: JSON.stringify([8, 313]), // 8=name, 313=country (from schema)
236
251
  });
237
252
  ```
238
253
 
@@ -99,21 +99,26 @@ Only add navigation if the app genuinely needs multiple views. Choose the patter
99
99
 
100
100
  ## New Page Checklist
101
101
 
102
- - [ ] Created a design doc in `design/` (copy `design/feature_template.md`)
103
102
  - [ ] Page in `pages/` with `<script setup lang="ts">`
104
103
  - [ ] Uses Vuetify components and the project's dark theme
105
104
  - [ ] Updated `DESIGN.md` with current status
105
+ - [ ] (Optional) Created a design doc in `design/` for complex features
106
106
 
107
- ## Server Routes
107
+ Design docs in `design/` are most useful for incremental feature work where
108
+ you need to plan, track decisions, and coordinate across multiple changes.
109
+ For initial app builds via `/build_my_app`, skip the per-page design docs —
110
+ they add friction without value when building the whole app at once. Start
111
+ using them later when adding features to an established app.
108
112
 
109
- Nitro server routes live in `server/api/` and deploy with the app to Vercel. They're auto-registered by Nuxt:
113
+ ## Server Routes
110
114
 
111
- ```
112
- server/api/my-data/fetch.get.ts → GET /api/my-data/fetch
113
- server/api/my-data/submit.post.ts → POST /api/my-data/submit
114
- ```
115
+ Nitro server routes live in `server/api/` and deploy with the app to Vercel.
116
+ Use server routes when you need to proxy external APIs (avoid CORS), call
117
+ the Elemental API server-side, or keep secrets off the client. Call them
118
+ from client code with `$fetch('/api/my-data/fetch')`.
115
119
 
116
- Call them from client code with `$fetch('/api/my-data/fetch')`. See the `server` cursor rule for patterns. Use server routes when you need to proxy external APIs (avoid CORS) or keep secrets off the client.
120
+ See the `server` rule for file-routing conventions, Neon Postgres patterns,
121
+ and server-side Elemental API access via `useRuntimeConfig()`.
117
122
 
118
123
  ## Beyond the UI: Agents, MCP Servers, and Server Routes
119
124
 
@@ -9,7 +9,9 @@ Copy-paste patterns using the project's actual composables and Vuetify component
9
9
 
10
10
  ## 1. Entity Search Page
11
11
 
12
- Search for entities by name and display results.
12
+ Search for entities by name and display results. Uses `$fetch` directly
13
+ because `POST /entities/search` (batch name resolution with scored ranking)
14
+ is not wrapped by the generated `useElementalClient()` — see the `api` rule.
13
15
 
14
16
  ```vue
15
17
  <template>
@@ -389,6 +391,9 @@ Two-column layout with selectable list and detail panel.
389
391
  ## 7. Get Filings for a Company
390
392
 
391
393
  Fetch Edgar filings (or any relationship-linked documents) for an organization.
394
+ Uses `$fetch` for the initial entity search because `POST /entities/search`
395
+ is not wrapped by the generated client (same as recipe #1). Filing
396
+ properties are then fetched via `useElementalClient()`.
392
397
 
393
398
  **Important:** For graph-layer entities (person, organization, location),
394
399
  use `findEntities` with a `linked` expression. For property-layer entities
@@ -487,12 +492,12 @@ relationship PID. See the `api` rule for the two-layer architecture.
487
492
  return;
488
493
  }
489
494
 
490
- const res = await client.getPropertyValues({
495
+ const propRes = await client.getPropertyValues({
491
496
  eids: JSON.stringify([orgNeid]),
492
497
  pids: JSON.stringify([filedPid]),
493
498
  });
494
499
 
495
- const docNeids = (res.values ?? []).map((v: any) =>
500
+ const docNeids = (propRes.values ?? []).map((v: any) =>
496
501
  String(v.value).padStart(20, '0'),
497
502
  );
498
503
 
package/rules/design.mdc CHANGED
@@ -40,6 +40,8 @@ Feature docs are used as working documents to help a user and agent collaborate
40
40
 
41
41
  When a user starts working on implementing a change, if they are using a feature doc, read the relevant feature doc before starting work. Then update the feature doc with every change you make. Be sure to create checklists as you plan work, and check off the checklists as the work is completed. Document design choices and open questions.
42
42
 
43
- If the user is implementing a change that has no feature doc, encourage them to work with you to create one before starting work. A new feature doc should be created by copying `design/feature_template.md` to a new file in `design`, giving it an appropriate name, and editing it from there.
43
+ If the user is implementing a change that has no feature doc, encourage them to work with you to create one before starting work. A new feature doc should be created by copying `design/feature_template.md` to a new file in `design`, giving it an appropriate name, and editing it from there.
44
+
45
+ **Exception for initial app builds:** When building a new app from scratch via `/build_my_app`, skip per-page feature docs. They add overhead during greenfield builds where the entire app is being created at once. Start using feature docs once the initial app is built and you're iterating on individual features.
44
46
 
45
47
  It is a good practice to close out one feature doc and start a new one as the focus of the work shifts. It is acceptable to be working with multiple feature docs at once, if the user is working on multiple unconnected or loosely connected features. The sections of the feature doc are flexible and you should add or remove sections to fit the needs of the project.
@@ -102,7 +102,7 @@ Once deployed, agents can connect to your MCP server. Add the Cloud Run URL to y
102
102
 
103
103
  - One server per data domain or external service
104
104
  - Keep tools focused and well-documented
105
- - Return structured data (dicts/lists), not raw strings
105
+ - Return structured data (dicts/lists), not raw strings — MCP handles serialization via the protocol. (This differs from ADK agent tools, which should return formatted strings because the LLM reads tool output directly — see the `agents` rule.)
106
106
  - Handle errors by returning descriptive error messages in the response
107
107
  - Use environment variables for API keys and configuration (never hardcode secrets)
108
108
  - For secrets, use GCP Secret Manager and access at runtime
package/rules/pref.mdc CHANGED
@@ -46,13 +46,25 @@ The backing store auto-initializes on first use — no need to call
46
46
 
47
47
  ## Local Development
48
48
 
49
- KV credentials are only available in deployed builds (Vercel auto-injects
50
- them at runtime). In local dev, `getRedis()` returns `null` and KV routes
51
- return `undefined` for reads and silently skip writes. `Pref<T>` still
52
- works with its default value but won't persist across page refreshes.
49
+ KV credentials (`KV_REST_API_URL`, `KV_REST_API_TOKEN`) are only available
50
+ in deployed builds (Vercel auto-injects them at runtime). In local dev,
51
+ `getRedis()` returns `null` and KV routes return `undefined` for reads and
52
+ silently skip writes. `Pref<T>` still works with its default value but
53
+ won't persist across page refreshes.
53
54
 
54
55
  This is expected — push to `main` and test persistence on the deployed build.
55
56
 
57
+ For local-only persistence, use `localStorage` as a lightweight alternative:
58
+
59
+ ```typescript
60
+ const saved = localStorage.getItem('watchlist');
61
+ const watchlist = ref<string[]>(saved ? JSON.parse(saved) : []);
62
+ watch(watchlist, (val) => localStorage.setItem('watchlist', JSON.stringify(val)), { deep: true });
63
+ ```
64
+
65
+ Use `Pref<T>` for production persistence and `localStorage` for local-only
66
+ development when KV isn't available.
67
+
56
68
  **Auth dependency:** All `/api/kv/*` routes call `unsealCookie(event)` to
57
69
  identify the user. In dev mode (no `NUXT_PUBLIC_AUTH0_CLIENT_SECRET` set),
58
70
  this is bypassed automatically using `NUXT_PUBLIC_USER_NAME`. If you set an
@@ -107,22 +119,6 @@ function useMyFeaturePrefs() {
107
119
  - **Key format**: `prefs:users:{userId}:apps:{appId}:settings:general`
108
120
  (doc-style paths converted to colon-separated Redis keys)
109
121
 
110
- ## Local Development Without KV
111
-
112
- When KV credentials (`KV_REST_API_URL`, `KV_REST_API_TOKEN`) aren't configured,
113
- `Pref<T>` still works with its default value but won't persist across page
114
- refreshes. For local dev, use `localStorage` directly as a lightweight
115
- alternative:
116
-
117
- ```typescript
118
- const saved = localStorage.getItem('watchlist');
119
- const watchlist = ref<string[]>(saved ? JSON.parse(saved) : []);
120
- watch(watchlist, (val) => localStorage.setItem('watchlist', JSON.stringify(val)), { deep: true });
121
- ```
122
-
123
- Use `Pref<T>` for production persistence and `localStorage` for local-only
124
- development when KV isn't available.
125
-
126
122
  ## Scope Guidance
127
123
 
128
124
  | App-specific | Global |
package/rules/server.mdc CHANGED
@@ -158,6 +158,110 @@ export function getDb(): NeonQueryFunction | null {
158
158
  }
159
159
  ```
160
160
 
161
+ ## Calling the Elemental API from Server Routes
162
+
163
+ Server routes can call the Elemental API through the Portal Gateway proxy,
164
+ just like client-side code does. The gateway URL, tenant org ID, and API key
165
+ are available via `useRuntimeConfig()`.
166
+
167
+ **NEVER use `readFileSync('broadchurch.yaml')` in server routes.** The YAML
168
+ file is read at build time by `nuxt.config.ts` and its values flow into
169
+ `runtimeConfig`. Nitro serverless functions (Vercel) don't bundle arbitrary
170
+ project files — `readFileSync` will crash with ENOENT in production even
171
+ though it works locally.
172
+
173
+ ```typescript
174
+ export default defineEventHandler(async (event) => {
175
+ const { public: config } = useRuntimeConfig();
176
+
177
+ const gatewayUrl = config.gatewayUrl; // Portal Gateway base URL
178
+ const orgId = config.tenantOrgId; // Tenant org ID (path segment)
179
+ const apiKey = config.qsApiKey; // API key for X-Api-Key header
180
+
181
+ if (!gatewayUrl || !orgId) {
182
+ throw createError({ statusCode: 503, statusMessage: 'Gateway not configured' });
183
+ }
184
+
185
+ const res = await $fetch(`${gatewayUrl}/api/qs/${orgId}/entities/search`, {
186
+ method: 'POST',
187
+ headers: {
188
+ 'Content-Type': 'application/json',
189
+ ...(apiKey && { 'X-Api-Key': apiKey }),
190
+ },
191
+ body: { queries: [{ queryId: 1, query: 'Microsoft' }], maxResults: 5 },
192
+ });
193
+
194
+ return res;
195
+ });
196
+ ```
197
+
198
+ Available runtime config keys (all under `runtimeConfig.public`):
199
+
200
+ | Key | Source | Purpose |
201
+ |---|---|---|
202
+ | `gatewayUrl` | `broadchurch.yaml` → `gateway.url` | Portal Gateway base URL |
203
+ | `tenantOrgId` | `broadchurch.yaml` → `tenant.org_id` | Tenant ID for API path |
204
+ | `qsApiKey` | `broadchurch.yaml` → `gateway.qs_api_key` | API key sent as `X-Api-Key` |
205
+ | `queryServerAddress` | `broadchurch.yaml` → `query_server.url` | Direct QS URL (prefer gateway) |
206
+
207
+ Build the request URL as `{gatewayUrl}/api/qs/{tenantOrgId}/{endpoint}`.
208
+ See the `api` rule for endpoint reference and response shapes.
209
+
210
+ ## Neon Postgres: Handle Missing Tables in GET Routes
211
+
212
+ Tables created by POST/setup routes won't exist on a fresh deployment.
213
+ **Every GET route that queries a table must handle the case where the table
214
+ doesn't exist yet.** Without this, fresh deploys will 500 on every page load
215
+ until the setup route runs.
216
+
217
+ ```typescript
218
+ export default defineEventHandler(async () => {
219
+ const sql = getDb();
220
+ if (!sql) throw createError({ statusCode: 503, statusMessage: 'Database not configured' });
221
+
222
+ try {
223
+ const rows = await sql`SELECT * FROM companies ORDER BY updated_at DESC`;
224
+ return rows;
225
+ } catch (err: any) {
226
+ if (err.message?.includes('does not exist')) {
227
+ return [];
228
+ }
229
+ throw err;
230
+ }
231
+ });
232
+ ```
233
+
234
+ Alternatively, ensure tables exist before querying by calling
235
+ `CREATE TABLE IF NOT EXISTS` at the top of each GET route, or by calling a
236
+ shared setup function:
237
+
238
+ ```typescript
239
+ // server/utils/ensure-tables.ts
240
+ import { getDb } from '~/server/utils/neon';
241
+
242
+ let _initialized = false;
243
+
244
+ export async function ensureTables() {
245
+ if (_initialized) return;
246
+ const sql = getDb();
247
+ if (!sql) return;
248
+
249
+ await sql`CREATE TABLE IF NOT EXISTS companies (
250
+ id SERIAL PRIMARY KEY,
251
+ neid TEXT UNIQUE NOT NULL,
252
+ name TEXT NOT NULL,
253
+ data JSONB DEFAULT '{}',
254
+ updated_at TIMESTAMPTZ DEFAULT NOW()
255
+ )`;
256
+
257
+ _initialized = true;
258
+ }
259
+ ```
260
+
261
+ Then call `await ensureTables()` at the start of any route that reads the
262
+ table. The `_initialized` flag makes it a no-op after the first call within
263
+ the same serverless invocation.
264
+
161
265
  ## Key Differences from Client-Side Code
162
266
 
163
267
  - Server routes run on the server (Node.js), not in the browser