@yottagraph-app/aether-instructions 1.1.18 → 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.18",
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
@@ -88,9 +88,9 @@ dev (`agents/` on sys.path → absolute import) and Agent Engine runtime
88
88
 
89
89
  Key endpoints:
90
90
  - `GET /elemental/metadata/schema` — entity types and properties
91
- - `POST /elemental/find` — search for entities
91
+ - `POST /elemental/find` — search for entities by expression
92
+ - `POST /entities/search` — search for entities by name (batch, scored)
92
93
  - `POST /elemental/entities/properties` — get entity property values
93
- - `GET /entities/lookup?q=<name>` — look up entity by name
94
94
 
95
95
  Requirements for agents using the Elemental API (add to `requirements.txt`):
96
96
  ```
@@ -352,6 +352,31 @@ for await (const { event, data } of readSSE(res)) {
352
352
  The `done` event always includes the final extracted text, so you don't
353
353
  need to track text deltas yourself.
354
354
 
355
+ ## useAgentChat Gotcha — Vue Reactivity
356
+
357
+ When building a chat UI with `useAgentChat`, do NOT hold a local reference
358
+ to a message object after pushing it into the `messages` array. Vue's
359
+ reactivity only tracks mutations made through the reactive Proxy — writes
360
+ to the original plain object are invisible to the template.
361
+
362
+ ```typescript
363
+ // WRONG — local ref bypasses Vue's reactivity, UI won't update:
364
+ const msg: ChatMessage = { id: '...', role: 'agent', text: '', streaming: true };
365
+ messages.value.push(msg);
366
+ msg.text = 'hello'; // data changes, but Vue doesn't know
367
+ msg.streaming = false; // template still shows typing indicator
368
+
369
+ // CORRECT — access through the reactive array:
370
+ messages.value.push({ id: '...', role: 'agent', text: '', streaming: true });
371
+ const idx = messages.value.length - 1;
372
+ messages.value[idx].text = 'hello'; // Vue detects this
373
+ messages.value[idx].streaming = false; // template re-renders
374
+ ```
375
+
376
+ The `useAgentChat` composable uses the correct pattern internally (via an
377
+ `updateAgent()` helper that writes through the array index). If you build a
378
+ custom chat composable or modify `sendMessage`, follow the same approach.
379
+
355
380
  ## Agent Design Guidelines
356
381
 
357
382
  - Keep agents focused: one agent per domain or task type
@@ -359,3 +384,127 @@ need to track text deltas yourself.
359
384
  - Tool docstrings are critical: they're the LLM's API documentation
360
385
  - Handle errors gracefully in tools: return error messages, don't raise exceptions
361
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
 
@@ -82,15 +84,18 @@ import { useElementalClient } from '@yottagraph-app/elemental-api/client';
82
84
 
83
85
  const client = useElementalClient();
84
86
 
85
- const results = await client.getNEID({ entityName: 'Apple', maxResults: 5 });
86
- const report = await client.getNamedEntityReport(results.neids[0]);
87
87
  const schema = await client.getSchema();
88
+ const report = await client.getNamedEntityReport('00508379502570440213');
89
+ const entities = await client.findEntities({
90
+ expression: JSON.stringify({ type: 'comparison', comparison: { operator: 'string_like', pid: 8, value: 'Apple' } }),
91
+ limit: 5,
92
+ });
88
93
  ```
89
94
 
90
95
  Types are also imported from the client:
91
96
 
92
97
  ```typescript
93
- import type { NamedEntityReport, GetNEIDResponse } from '@yottagraph-app/elemental-api/client';
98
+ import type { NamedEntityReport } from '@yottagraph-app/elemental-api/client';
94
99
  ```
95
100
 
96
101
  ### Client Method Quick Reference
@@ -101,28 +106,28 @@ All methods return data directly and throw on non-2xx responses.
101
106
 
102
107
  | Method | Signature | Purpose |
103
108
  |---|---|---|
104
- | `getNEID` | `(params: { entityName, maxResults?, includeNames? })` | Lookup entity by name |
105
109
  | `findEntities` | `(body: FindEntitiesBody)` | Expression-based search (see `find.md`) |
106
110
  | `getNamedEntityReport` | `(neid: string)` | Entity details (name, aliases, type) |
107
111
  | `getEntityDetails` | `(neid: string)` | Alias for entity reports |
108
112
 
113
+ > **Entity search**: Use `findEntities()` with `string_like` on the name PID
114
+ > for name-based searches, or call `POST /entities/search` directly via
115
+ > `$fetch` for batch name resolution with scored ranking (this endpoint is
116
+ > not wrapped by the generated client).
117
+
109
118
  **Properties and schema:**
110
119
 
111
120
  | Method | Signature | Purpose |
112
121
  |---|---|---|
113
122
  | `getSchema` | `()` | All entity types (flavors) and properties (PIDs) |
114
- | `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) |
115
124
  | `summarizeProperty` | `(pid: number)` | Summary stats for a property |
116
125
 
117
126
  **Relationships and graph:**
118
127
 
119
128
  | Method | Signature | Purpose |
120
129
  |---|---|---|
121
- | `getLinkedEntities` | `(neid, params?: { entity_type?, link_type? })` | Linked entities (person/org/location only) |
122
- | `getLinks` | `(sourceNeid, targetNeid, params?)` | Links between two specific entities |
123
- | `getLinkCounts` | `(sourceNeid, targetNeid)` | Link counts between entities |
124
- | `getNeighborhood` | `(centerNeid, params?)` | Neighboring entities |
125
- | `getGraphLayout` | `(centerNeid, params?)` | Graph layout for visualization |
130
+ | `findEntities` | `(body: { expression, limit? })` | Find linked entities via `linked` expression (see `find.md`) |
126
131
 
127
132
  **Other:**
128
133
 
@@ -205,43 +210,82 @@ const type = res.report?.type ?? (res as any).type;
205
210
  The `(res as any).name` fallback handles the case where the client is
206
211
  eventually fixed to unwrap the response.
207
212
 
213
+ ### Relationship property values need zero-padding to form valid NEIDs
214
+
215
+ Relationship properties (`data_nindex`) return linked entity IDs as raw
216
+ numbers (e.g. `4926132345040704022`). These must be **zero-padded to 20
217
+ characters** to form valid NEIDs. This is easy to miss and causes silent
218
+ failures — `getNamedEntityReport` returns a 404, `getPropertyValues`
219
+ returns empty results.
220
+
221
+ ```typescript
222
+ // WRONG — raw value is NOT a valid NEID:
223
+ const filingId = res.values[0].value; // "4926132345040704022" (19 chars)
224
+
225
+ // CORRECT — always pad to 20 characters:
226
+ const filingNeid = String(res.values[0].value).padStart(20, '0'); // "04926132345040704022"
227
+ ```
228
+
208
229
  > **WARNING -- `getPropertyValues()` takes JSON-stringified arrays**: The `eids`
209
230
  > and `pids` parameters must be JSON-encoded strings, NOT native arrays. The
210
231
  > TypeScript type is `string`, not `string[]`. Passing a raw array will silently
211
232
  > return no data.
212
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
+
213
240
  ```typescript
241
+ // WRONG — PIDs are numbers, not strings:
214
242
  const values = await client.getPropertyValues({
215
243
  eids: JSON.stringify(['00416400910670863867']),
216
- pids: JSON.stringify(['name', 'country', 'industry']),
244
+ pids: JSON.stringify(['name', 'country', 'industry']), // FAILS
245
+ });
246
+
247
+ // CORRECT — use numeric PIDs from getSchema():
248
+ const values = await client.getPropertyValues({
249
+ eids: JSON.stringify(['00416400910670863867']),
250
+ pids: JSON.stringify([8, 313]), // 8=name, 313=country (from schema)
217
251
  });
218
252
  ```
219
253
 
220
- ### `getLinkedEntities` only supports graph node types
221
-
222
- `getLinkedEntities(neid, { entity_type: ['document'] })` will fail at
223
- runtime with _"entity_type document not a valid graph node type"_. The
224
- `/entities/{neid}/linked` endpoint only supports three entity types:
225
- **person**, **organization**, **location**. Documents, filings, articles,
226
- financial instruments, events, and all other types are excluded — even
227
- though the schema shows relationships like `filed` connecting organizations
228
- to documents.
229
-
230
- **Why?** The knowledge graph has two layers. The **graph layer** models
231
- first-class entities (people, organizations, locations) as nodes with
232
- edges between them this is what `getLinkedEntities` traverses. The
233
- **property layer** attaches everything else (documents, filings, financial
234
- instruments, events) as property values on graph nodes. Documents aren't
235
- "lesser" entities — they're stored differently because they're associated
236
- with specific graph nodes rather than standing independently in the graph.
237
- Understanding this distinction helps you generalize: if a target entity
238
- type doesn't appear in the `getLinkedEntities` response, it's a property-
239
- layer entity and you need `getPropertyValues` with the relationship PID.
240
-
241
- To traverse relationships to non-graph-node types, use `getPropertyValues`
242
- with the relationship PID instead. Relationship properties (`data_nindex`)
243
- return linked entity IDs as values. Zero-pad the returned IDs to 20
244
- characters to form valid NEIDs.
254
+ ### Traversing relationships: graph-layer vs property-layer entities
255
+
256
+ The knowledge graph has two layers:
257
+
258
+ - **Graph layer** people, organizations, and locations are first-class
259
+ nodes with edges between them. Use `findEntities()` with a `linked`
260
+ expression to traverse these (see `find.md`).
261
+ - **Property layer** documents, filings, articles, financial instruments,
262
+ events, and all other types are attached as property values on graph
263
+ nodes. Use `getPropertyValues()` with the relationship PID to traverse
264
+ these.
265
+
266
+ If you need to find people linked to an organization, use `findEntities`
267
+ with a `linked` expression:
268
+
269
+ ```typescript
270
+ const res = await client.findEntities({
271
+ expression: JSON.stringify({
272
+ type: 'linked',
273
+ linked: {
274
+ to_entity: orgNeid,
275
+ distance: 1,
276
+ pids: [isOfficerPid, isDirectorPid, worksAtPid],
277
+ direction: 'incoming',
278
+ },
279
+ }),
280
+ limit: 50,
281
+ });
282
+ const personNeids = (res as any).eids ?? [];
283
+ ```
284
+
285
+ For non-graph-node types (filings, documents, etc.), use `getPropertyValues`
286
+ with the relationship PID. Relationship properties (`data_nindex`) return
287
+ linked entity IDs as values. Zero-pad the returned IDs to 20 characters
288
+ to form valid NEIDs.
245
289
 
246
290
  ```typescript
247
291
  const pidMap = await getPropertyPidMap(client);
@@ -255,64 +299,43 @@ const docNeids = (res.values ?? []).map((v) => String(v.value).padStart(20, '0')
255
299
 
256
300
  See the **cookbook** rule for a full "Get filings for a company" recipe.
257
301
 
258
- ### `getNEID()` vs `findEntities()` for entity search
259
-
260
- - **`client.getNEID()`** -- simple single-entity lookup by name
261
- (`GET /entities/lookup`). Best for resolving one company/person name.
262
- - **`client.findEntities()`** -- expression-based search
263
- (`POST /elemental/find`). Best for filtered searches (by type, property,
264
- relationship). See `find.md` for the expression language.
265
-
266
- ## Common Entity Relationships
267
-
268
- The knowledge graph connects entities through relationship properties
269
- (`data_nindex` PIDs). These are the most common patterns:
302
+ ### Entity Search
270
303
 
271
- | From | PID | To | How to traverse |
272
- |---|---|---|---|
273
- | Organization | `filed` | Document (filings) | `getPropertyValues` with the `filed` PID on the org's NEID |
274
- | Organization | `employs` | Person | `getLinkedEntities` (person is a graph node type) |
275
- | Person | `employed_by` | Organization | `getLinkedEntities` (organization is a graph node type) |
276
- | Organization | `headquartered_in` | Location | `getLinkedEntities` (location is a graph node type) |
277
- | Any entity | `related_to` | Any entity | `getLinkedEntities` for person/org/location; `getPropertyValues` for others |
304
+ Use `client.findEntities()` (`POST /elemental/find`) for entity search.
305
+ It supports filtering by type, property value, and relationship via the
306
+ expression language (see `find.md`). For name-based lookups, use
307
+ `string_like` on the name property (PID 8).
278
308
 
279
- **Key constraint:** `getLinkedEntities` only works for three target types:
280
- **person**, **organization**, **location**. For documents, filings,
281
- articles, financial instruments, and events, use `getPropertyValues` with
282
- the relationship PID. See the cookbook rule (recipe #7) for a full filings
283
- example.
309
+ For batch name resolution with scored ranking, call `POST /entities/search`
310
+ directly via `$fetch` (not on the generated client). See the
311
+ **elemental-api skill** (`entities.md`) for request/response shapes.
284
312
 
285
- **Traversal pattern for non-graph-node types:**
313
+ ## Traversing Relationships
286
314
 
287
- ```typescript
288
- // 1. Get the PID for the relationship
289
- const schema = await client.getSchema();
290
- const pids = schema.schema?.properties ?? [];
291
- const filedPid = pids.find((p: any) => p.name === 'filed')?.pid;
315
+ Relationships between entities are discoverable via the schema — use
316
+ `getSchema()` to find relationship properties (`data_nindex` type) and
317
+ their PIDs. Do NOT hardcode relationship names or PIDs; they can change
318
+ as the knowledge graph evolves. See the **data-model skill** for
319
+ source-specific schemas.
292
320
 
293
- // 2. Get linked entity IDs via getPropertyValues
294
- const res = await client.getPropertyValues({
295
- eids: JSON.stringify([orgNeid]),
296
- pids: JSON.stringify([filedPid]),
297
- });
321
+ **Two traversal methods:**
298
322
 
299
- // 3. Pad IDs to 20 chars to form valid NEIDs
300
- const docNeids = (res.values ?? []).map((v: any) => String(v.value).padStart(20, '0'));
323
+ - **Graph-layer entities** (person, organization, location): Use
324
+ `findEntities()` with a `linked` expression. See `find.md`.
325
+ - **Property-layer entities** (documents, filings, articles, etc.): Use
326
+ `getPropertyValues()` with the relationship PID. Values are entity IDs
327
+ that must be zero-padded to 20 characters.
301
328
 
302
- // 4. Get details for each linked entity (response is nested under .report)
303
- const reports = await Promise.all(
304
- docNeids.map(async (neid: string) => {
305
- const r = await client.getNamedEntityReport(neid);
306
- return r.report ?? r;
307
- }),
308
- );
309
- ```
329
+ See the **cookbook** rule (recipe #7) for a full example.
310
330
 
311
331
  ## Error Handling
312
332
 
313
333
  ```typescript
314
334
  try {
315
- const data = await client.getNEID({ entityName: '...' });
335
+ const data = await client.findEntities({
336
+ expression: JSON.stringify({ type: 'comparison', comparison: { operator: 'string_like', pid: 8, value: 'Apple' } }),
337
+ limit: 5,
338
+ });
316
339
  } catch (error) {
317
340
  console.error('API Error:', error);
318
341
  showError('Failed to load data. Please try again.');
@@ -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>
@@ -53,19 +55,35 @@ Search for entities by name and display results.
53
55
  const error = ref<string | null>(null);
54
56
  const searched = ref(false);
55
57
 
58
+ function getSearchUrl() {
59
+ const config = useRuntimeConfig();
60
+ const gw = (config.public as any).gatewayUrl as string;
61
+ const org = (config.public as any).tenantOrgId as string;
62
+ return `${gw}/api/qs/${org}/entities/search`;
63
+ }
64
+
65
+ function getApiKey() {
66
+ return (useRuntimeConfig().public as any).qsApiKey as string;
67
+ }
68
+
56
69
  async function search() {
57
70
  if (!query.value.trim()) return;
58
71
  loading.value = true;
59
72
  error.value = null;
60
73
  searched.value = true;
61
74
  try {
62
- const res = await client.getNEID({
63
- entityName: query.value.trim(),
64
- maxResults: 10,
65
- includeNames: true,
75
+ const res = await $fetch<any>(getSearchUrl(), {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json', 'X-Api-Key': getApiKey() },
78
+ body: {
79
+ queries: [{ queryId: 1, query: query.value.trim() }],
80
+ maxResults: 10,
81
+ includeNames: true,
82
+ },
66
83
  });
67
- results.value = res.neids || [];
68
- names.value = res.names || [];
84
+ const matches = res?.results?.[0]?.matches ?? [];
85
+ results.value = matches.map((m: any) => m.neid);
86
+ names.value = matches.map((m: any) => m.name || m.neid);
69
87
  } catch (e: any) {
70
88
  error.value = e.message || 'Search failed';
71
89
  results.value = [];
@@ -373,10 +391,14 @@ Two-column layout with selectable list and detail panel.
373
391
  ## 7. Get Filings for a Company
374
392
 
375
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()`.
376
397
 
377
- **Important:** `getLinkedEntities` only supports graph node types (person,
378
- organization, location). Documents, filings, articles, and other types are
379
- NOT supported use `getPropertyValues` with the relationship PID instead.
398
+ **Important:** For graph-layer entities (person, organization, location),
399
+ use `findEntities` with a `linked` expression. For property-layer entities
400
+ (documents, filings, articles), use `getPropertyValues` with the
401
+ relationship PID. See the `api` rule for the two-layer architecture.
380
402
 
381
403
  ```vue
382
404
  <template>
@@ -430,22 +452,38 @@ NOT supported — use `getPropertyValues` with the relationship PID instead.
430
452
  return new Map(properties.map((p: any) => [p.name, p.pid]));
431
453
  }
432
454
 
455
+ function getSearchUrl() {
456
+ const config = useRuntimeConfig();
457
+ const gw = (config.public as any).gatewayUrl as string;
458
+ const org = (config.public as any).tenantOrgId as string;
459
+ return `${gw}/api/qs/${org}/entities/search`;
460
+ }
461
+
462
+ function getApiKey() {
463
+ return (useRuntimeConfig().public as any).qsApiKey as string;
464
+ }
465
+
433
466
  async function search() {
434
467
  if (!query.value.trim()) return;
435
468
  loading.value = true;
436
469
  error.value = null;
437
470
  searched.value = true;
438
471
  try {
439
- const lookup = await client.getNEID({
440
- entityName: query.value.trim(),
441
- maxResults: 1,
442
- includeNames: true,
472
+ const res = await $fetch<any>(getSearchUrl(), {
473
+ method: 'POST',
474
+ headers: { 'Content-Type': 'application/json', 'X-Api-Key': getApiKey() },
475
+ body: {
476
+ queries: [{ queryId: 1, query: query.value.trim(), flavors: ['organization'] }],
477
+ maxResults: 1,
478
+ includeNames: true,
479
+ },
443
480
  });
444
- if (!lookup.neids?.length) {
481
+ const matches = res?.results?.[0]?.matches ?? [];
482
+ if (!matches.length) {
445
483
  filings.value = [];
446
484
  return;
447
485
  }
448
- const orgNeid = lookup.neids[0];
486
+ const orgNeid = matches[0].neid;
449
487
 
450
488
  const pidMap = await getPropertyPidMap(client);
451
489
  const filedPid = pidMap.get('filed');
@@ -454,12 +492,12 @@ NOT supported — use `getPropertyValues` with the relationship PID instead.
454
492
  return;
455
493
  }
456
494
 
457
- const res = await client.getPropertyValues({
495
+ const propRes = await client.getPropertyValues({
458
496
  eids: JSON.stringify([orgNeid]),
459
497
  pids: JSON.stringify([filedPid]),
460
498
  });
461
499
 
462
- const docNeids = (res.values ?? []).map((v: any) =>
500
+ const docNeids = (propRes.values ?? []).map((v: any) =>
463
501
  String(v.value).padStart(20, '0'),
464
502
  );
465
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.