@yottagraph-app/aether-instructions 1.1.27 → 1.1.28

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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Copy-paste UI patterns for common pages: entity search, data table, form, chart, master-detail. Read when building new pages or features."
2
+ description: "Copy-paste UI patterns for common pages: data table, form, chart, dialog, master-detail. For Query Server / data-fetching recipes see `cookbook-data`."
3
3
  alwaysApply: false
4
4
  ---
5
5
 
@@ -7,92 +7,7 @@ alwaysApply: false
7
7
 
8
8
  Copy-paste patterns using the project's actual composables and Vuetify components. Adapt to your needs.
9
9
 
10
- ## 1. Entity Search Page
11
-
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.
15
-
16
- ```vue
17
- <template>
18
- <div class="d-flex flex-column fill-height pa-4">
19
- <h1 class="text-h5 mb-4">Entity Search</h1>
20
- <v-text-field
21
- v-model="query"
22
- label="Search entities"
23
- prepend-inner-icon="mdi-magnify"
24
- variant="outlined"
25
- @keyup.enter="search"
26
- :loading="loading"
27
- />
28
- <v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
29
- {{ error }}
30
- </v-alert>
31
- <v-list v-if="results.length" class="mt-4">
32
- <v-list-item
33
- v-for="(neid, i) in results"
34
- :key="neid"
35
- :title="names[i] || neid"
36
- :subtitle="neid"
37
- />
38
- </v-list>
39
- <v-empty-state
40
- v-else-if="searched && !loading"
41
- headline="No results"
42
- icon="mdi-magnify-remove-outline"
43
- />
44
- </div>
45
- </template>
46
-
47
- <script setup lang="ts">
48
- import { useElementalClient } from '@yottagraph-app/elemental-api/client';
49
-
50
- const client = useElementalClient();
51
- const query = ref('');
52
- const results = ref<string[]>([]);
53
- const names = ref<string[]>([]);
54
- const loading = ref(false);
55
- const error = ref<string | null>(null);
56
- const searched = ref(false);
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
-
69
- async function search() {
70
- if (!query.value.trim()) return;
71
- loading.value = true;
72
- error.value = null;
73
- searched.value = true;
74
- try {
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
- },
83
- });
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);
87
- } catch (e: any) {
88
- error.value = e.message || 'Search failed';
89
- results.value = [];
90
- } finally {
91
- loading.value = false;
92
- }
93
- }
94
- </script>
95
- ```
10
+ **Data-fetching recipes** (entity search, news feed, filings, gateway helpers) live in the `cookbook-data` rule.
96
11
 
97
12
  ## 2. Data Table Page
98
13
 
@@ -387,313 +302,3 @@ Two-column layout with selectable list and detail panel.
387
302
  });
388
303
  </script>
389
304
  ```
390
-
391
- ## 7. News Feed — Recent Articles with Sentiment
392
-
393
- Fetch recent articles from the knowledge graph. Uses `useElementalSchema()`
394
- for runtime flavor/PID discovery and `buildGatewayUrl()` for gateway access.
395
-
396
- ```vue
397
- <template>
398
- <div class="d-flex flex-column fill-height pa-4">
399
- <h1 class="text-h5 mb-4">Recent News</h1>
400
- <v-alert v-if="error" type="error" variant="tonal" class="mb-4" closable>
401
- {{ error }}
402
- </v-alert>
403
- <v-progress-linear v-if="loading" indeterminate class="mb-4" />
404
- <v-list v-if="articles.length" lines="three">
405
- <v-list-item v-for="a in articles" :key="a.neid">
406
- <template #title>
407
- <span>{{ a.name || a.neid }}</span>
408
- <v-chip
409
- v-if="a.sentiment"
410
- size="x-small"
411
- class="ml-2"
412
- :color="a.sentiment > 0 ? 'success' : a.sentiment < 0 ? 'error' : 'grey'"
413
- >
414
- {{ a.sentiment > 0 ? 'Bullish' : a.sentiment < 0 ? 'Bearish' : 'Neutral' }}
415
- </v-chip>
416
- </template>
417
- <template #subtitle>{{ a.neid }}</template>
418
- </v-list-item>
419
- </v-list>
420
- <v-empty-state
421
- v-else-if="!loading"
422
- headline="No articles found"
423
- icon="mdi-newspaper-variant-outline"
424
- />
425
- </div>
426
- </template>
427
-
428
- <script setup lang="ts">
429
- import { useElementalClient } from '@yottagraph-app/elemental-api/client';
430
- import { padNeid } from '~/utils/elementalHelpers';
431
-
432
- const client = useElementalClient();
433
- const { flavorByName, pidByName, refresh: loadSchema } = useElementalSchema();
434
-
435
- const articles = ref<{ neid: string; name: string; sentiment: number | null }[]>([]);
436
- const loading = ref(false);
437
- const error = ref<string | null>(null);
438
-
439
- onMounted(async () => {
440
- loading.value = true;
441
- try {
442
- await loadSchema();
443
- const articleFid = flavorByName('article');
444
- if (!articleFid) {
445
- error.value = 'Article entity type not found in schema';
446
- return;
447
- }
448
-
449
- const res = await client.findEntities({
450
- expression: JSON.stringify({ type: 'is_type', is_type: { fid: articleFid } }),
451
- limit: 20,
452
- });
453
- const neids: string[] = (res as any).eids ?? [];
454
-
455
- if (!neids.length) {
456
- return;
457
- }
458
-
459
- const namePid = pidByName('name');
460
- const sentimentPid = pidByName('sentiment');
461
- const pids = [namePid, sentimentPid].filter((p): p is number => p !== null);
462
-
463
- const props = await client.getPropertyValues({
464
- eids: JSON.stringify(neids),
465
- pids: JSON.stringify(pids),
466
- });
467
-
468
- const valueMap = new Map<string, Record<number, any>>();
469
- for (const v of (props as any).values ?? []) {
470
- const eid = padNeid(v.eid ?? v.entity_id ?? '');
471
- if (!valueMap.has(eid)) valueMap.set(eid, {});
472
- valueMap.get(eid)![v.pid] = v.value;
473
- }
474
-
475
- articles.value = neids.map((neid) => {
476
- const vals = valueMap.get(neid) ?? {};
477
- return {
478
- neid,
479
- name: namePid ? (vals[namePid] as string) ?? neid : neid,
480
- sentiment: sentimentPid ? (vals[sentimentPid] as number) ?? null : null,
481
- };
482
- });
483
- } catch (e: any) {
484
- error.value = e.message || 'Failed to load articles';
485
- } finally {
486
- loading.value = false;
487
- }
488
- });
489
- </script>
490
- ```
491
-
492
- ## 8. Entity Search with Gateway Helpers
493
-
494
- Simpler version of recipe #1 using the pre-built `searchEntities()` helper.
495
-
496
- ```vue
497
- <template>
498
- <div class="d-flex flex-column fill-height pa-4">
499
- <h1 class="text-h5 mb-4">Entity Search</h1>
500
- <v-text-field
501
- v-model="query"
502
- label="Search entities"
503
- prepend-inner-icon="mdi-magnify"
504
- variant="outlined"
505
- @keyup.enter="search"
506
- :loading="loading"
507
- />
508
- <v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
509
- {{ error }}
510
- </v-alert>
511
- <v-list v-if="results.length" class="mt-4">
512
- <v-list-item
513
- v-for="r in results"
514
- :key="r.neid"
515
- :title="r.name"
516
- :subtitle="r.neid"
517
- />
518
- </v-list>
519
- <v-empty-state
520
- v-else-if="searched && !loading"
521
- headline="No results"
522
- icon="mdi-magnify-remove-outline"
523
- />
524
- </div>
525
- </template>
526
-
527
- <script setup lang="ts">
528
- import { searchEntities } from '~/utils/elementalHelpers';
529
-
530
- const query = ref('');
531
- const results = ref<{ neid: string; name: string }[]>([]);
532
- const loading = ref(false);
533
- const error = ref<string | null>(null);
534
- const searched = ref(false);
535
-
536
- async function search() {
537
- if (!query.value.trim()) return;
538
- loading.value = true;
539
- error.value = null;
540
- searched.value = true;
541
- try {
542
- results.value = await searchEntities(query.value.trim());
543
- } catch (e: any) {
544
- error.value = e.message || 'Search failed';
545
- results.value = [];
546
- } finally {
547
- loading.value = false;
548
- }
549
- }
550
- </script>
551
- ```
552
-
553
- ## 9. Get Filings for a Company
554
-
555
- Fetch Edgar filings (or any relationship-linked documents) for an organization.
556
- Uses `$fetch` for the initial entity search because `POST /entities/search`
557
- is not wrapped by the generated client (same as recipe #1). Filing
558
- properties are then fetched via `useElementalClient()`.
559
-
560
- **Important:** For graph-layer entities (person, organization, location),
561
- use `findEntities` with a `linked` expression. For property-layer entities
562
- (documents, filings, articles), use `getPropertyValues` with the
563
- relationship PID. See the `api` rule for the two-layer architecture.
564
-
565
- ```vue
566
- <template>
567
- <div class="d-flex flex-column fill-height pa-4">
568
- <h1 class="text-h5 mb-4">Company Filings</h1>
569
- <v-text-field
570
- v-model="query"
571
- label="Company name"
572
- prepend-inner-icon="mdi-magnify"
573
- @keyup.enter="search"
574
- :loading="loading"
575
- />
576
- <v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
577
- {{ error }}
578
- </v-alert>
579
- <v-data-table
580
- v-if="filings.length"
581
- :headers="headers"
582
- :items="filings"
583
- :loading="loading"
584
- density="comfortable"
585
- hover
586
- class="mt-4"
587
- />
588
- <v-empty-state
589
- v-else-if="searched && !loading"
590
- headline="No filings found"
591
- icon="mdi-file-document-off"
592
- />
593
- </div>
594
- </template>
595
-
596
- <script setup lang="ts">
597
- import { useElementalClient } from '@yottagraph-app/elemental-api/client';
598
-
599
- const client = useElementalClient();
600
- const query = ref('');
601
- const filings = ref<{ neid: string; name: string }[]>([]);
602
- const loading = ref(false);
603
- const error = ref<string | null>(null);
604
- const searched = ref(false);
605
-
606
- const headers = [
607
- { title: 'NEID', key: 'neid', sortable: true },
608
- { title: 'Name', key: 'name', sortable: true },
609
- ];
610
-
611
- async function getPropertyPidMap(client: ReturnType<typeof useElementalClient>) {
612
- const schemaRes = await client.getSchema();
613
- const properties = schemaRes.schema?.properties ?? (schemaRes as any).properties ?? [];
614
- return new Map(properties.map((p: any) => [p.name, p.pid]));
615
- }
616
-
617
- function getSearchUrl() {
618
- const config = useRuntimeConfig();
619
- const gw = (config.public as any).gatewayUrl as string;
620
- const org = (config.public as any).tenantOrgId as string;
621
- return `${gw}/api/qs/${org}/entities/search`;
622
- }
623
-
624
- function getApiKey() {
625
- return (useRuntimeConfig().public as any).qsApiKey as string;
626
- }
627
-
628
- async function search() {
629
- if (!query.value.trim()) return;
630
- loading.value = true;
631
- error.value = null;
632
- searched.value = true;
633
- try {
634
- const res = await $fetch<any>(getSearchUrl(), {
635
- method: 'POST',
636
- headers: { 'Content-Type': 'application/json', 'X-Api-Key': getApiKey() },
637
- body: {
638
- queries: [{ queryId: 1, query: query.value.trim(), flavors: ['organization'] }],
639
- maxResults: 1,
640
- includeNames: true,
641
- },
642
- });
643
- const matches = res?.results?.[0]?.matches ?? [];
644
- if (!matches.length) {
645
- filings.value = [];
646
- return;
647
- }
648
- const orgNeid = matches[0].neid;
649
-
650
- const pidMap = await getPropertyPidMap(client);
651
- const filedPid = pidMap.get('filed');
652
- if (!filedPid) {
653
- error.value = '"filed" relationship not found in schema';
654
- return;
655
- }
656
-
657
- const propRes = await client.getPropertyValues({
658
- eids: JSON.stringify([orgNeid]),
659
- pids: JSON.stringify([filedPid]),
660
- });
661
-
662
- const docNeids = (propRes.values ?? []).map((v: any) =>
663
- String(v.value).padStart(20, '0'),
664
- );
665
-
666
- function getEntityNameUrl(neid: string) {
667
- const config = useRuntimeConfig();
668
- const gw = (config.public as any).gatewayUrl as string;
669
- const org = (config.public as any).tenantOrgId as string;
670
- return `${gw}/api/qs/${org}/entities/${neid}/name`;
671
- }
672
-
673
- const names = await Promise.all(
674
- docNeids.map(async (neid: string) => {
675
- try {
676
- const res = await $fetch<{ name: string }>(
677
- getEntityNameUrl(neid),
678
- { headers: { 'X-Api-Key': getApiKey() } },
679
- );
680
- return res.name || neid;
681
- } catch {
682
- return neid;
683
- }
684
- }),
685
- );
686
-
687
- filings.value = docNeids.map((neid: string, i: number) => ({
688
- neid,
689
- name: names[i],
690
- }));
691
- } catch (e: any) {
692
- error.value = e.message || 'Failed to load filings';
693
- filings.value = [];
694
- } finally {
695
- loading.value = false;
696
- }
697
- }
698
- </script>
699
- ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: "Elemental API client usage, schema discovery, entity search, API gotchas, MCP servers. Read when building features that fetch or display data from the Query Server."
2
+ description: "Data access via Elemental API: client usage, schema discovery, entity search, API gotchas, MCP servers. Read when building features that fetch or display data from the Query Server."
3
3
  alwaysApply: false
4
4
  ---
5
- # Elemental API The Platform Data Source
5
+ # Data — Elemental API (platform data source)
6
6
 
7
7
  **This app is built on the Lovelace platform.** The Query Server is the
8
8
  primary data source — use it first for any data needs (entities, news,
@@ -379,7 +379,7 @@ const res = await client.getPropertyValues({
379
379
  const docNeids = (res.values ?? []).map((v) => String(v.value).padStart(20, '0'));
380
380
  ```
381
381
 
382
- See the **cookbook** rule for a full "Get filings for a company" recipe.
382
+ See the **cookbook-data** rule for a full "Get filings for a company" recipe.
383
383
 
384
384
  ### Expression language pitfalls
385
385
 
@@ -425,7 +425,7 @@ source-specific schemas.
425
425
  `getPropertyValues()` with the relationship PID. Values are entity IDs
426
426
  that must be zero-padded to 20 characters.
427
427
 
428
- See the **cookbook** rule (recipe #7) for a full example.
428
+ See the **cookbook-data** rule (news feed recipe) for a full example.
429
429
 
430
430
  ## Error Handling
431
431
 
@@ -27,7 +27,7 @@ Example:
27
27
 
28
28
  ```bash
29
29
  # Copy the rule you want to customize
30
- cp .cursor/rules/api.mdc .cursor/rules/api_custom.mdc
30
+ cp .cursor/rules/data.mdc .cursor/rules/data_custom.mdc
31
31
 
32
32
  # Edit your copy — it won't be affected by instruction updates
33
33
  ```
@@ -35,7 +35,7 @@ cp .cursor/rules/api.mdc .cursor/rules/api_custom.mdc
35
35
  ## How It Works
36
36
 
37
37
  - `.cursor/.aether-instructions-manifest` lists every file installed by the
38
- package (one relative path per line, e.g. `rules/api.mdc`)
38
+ package (one relative path per line, e.g. `rules/data.mdc`)
39
39
  - `/update_instructions` deletes manifest entries, extracts fresh files from
40
40
  the latest package, and writes a new manifest
41
41
  - Files not in the manifest are never touched
@@ -0,0 +1,54 @@
1
+ ---
2
+ description: "Calling the Elemental API from Nitro server routes via the Portal Gateway. Read when proxying Query Server calls server-side."
3
+ alwaysApply: false
4
+ globs: server/**
5
+ ---
6
+
7
+ # Server routes: Elemental API (Query Server)
8
+
9
+ Server routes can call the Elemental API through the Portal Gateway proxy,
10
+ just like client-side code does. The gateway URL, tenant org ID, and API key
11
+ are available via `useRuntimeConfig()`.
12
+
13
+ **NEVER use `readFileSync('broadchurch.yaml')` in server routes.** The YAML
14
+ file is read at build time by `nuxt.config.ts` and its values flow into
15
+ `runtimeConfig`. Nitro serverless functions (Vercel) don't bundle arbitrary
16
+ project files — `readFileSync` will crash with ENOENT in production even
17
+ though it works locally.
18
+
19
+ ```typescript
20
+ export default defineEventHandler(async (event) => {
21
+ const { public: config } = useRuntimeConfig();
22
+
23
+ const gatewayUrl = config.gatewayUrl; // Portal Gateway base URL
24
+ const orgId = config.tenantOrgId; // Tenant org ID (path segment)
25
+ const apiKey = config.qsApiKey; // API key for X-Api-Key header
26
+
27
+ if (!gatewayUrl || !orgId) {
28
+ throw createError({ statusCode: 503, statusMessage: 'Gateway not configured' });
29
+ }
30
+
31
+ const res = await $fetch(`${gatewayUrl}/api/qs/${orgId}/entities/search`, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ ...(apiKey && { 'X-Api-Key': apiKey }),
36
+ },
37
+ body: { queries: [{ queryId: 1, query: 'Microsoft' }], maxResults: 5 },
38
+ });
39
+
40
+ return res;
41
+ });
42
+ ```
43
+
44
+ Available runtime config keys (all under `runtimeConfig.public`):
45
+
46
+ | Key | Source | Purpose |
47
+ |---|---|---|
48
+ | `gatewayUrl` | `broadchurch.yaml` → `gateway.url` | Portal Gateway base URL |
49
+ | `tenantOrgId` | `broadchurch.yaml` → `tenant.org_id` | Tenant ID for API path |
50
+ | `qsApiKey` | `broadchurch.yaml` → `gateway.qs_api_key` | API key sent as `X-Api-Key` |
51
+ | `queryServerAddress` | `broadchurch.yaml` → `query_server.url` | Direct QS URL (prefer gateway) |
52
+
53
+ Build the request URL as `{gatewayUrl}/api/qs/{tenantOrgId}/{endpoint}`.
54
+ See the `data` rule for endpoint reference and response shapes.
package/rules/server.mdc CHANGED
@@ -158,54 +158,8 @@ 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.
161
+ For **Query Server / Elemental API** calls from server routes (gateway URL,
162
+ `X-Api-Key`, request shapes), see the `server-data` rule.
209
163
 
210
164
  ## Neon Postgres: Handle Missing Tables in GET Routes
211
165
 
@@ -49,7 +49,7 @@ import { myHelper } from '~/utils/myHelper';
49
49
 
50
50
  ### `Type 'X' is not assignable to type 'Y'`
51
51
 
52
- Usually an API response shape mismatch. Common case: `getSchema()` nests data under `response.schema` but TypeScript types suggest top-level access. See the `api` rule's API Gotchas section.
52
+ Usually an API response shape mismatch. Common case: `getSchema()` nests data under `response.schema` but TypeScript types suggest top-level access. See the `data` rule's API Gotchas section.
53
53
 
54
54
  ### `SyntaxError` or blank page with "missing export"
55
55