@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.
- package/commands/build_my_app.md +3 -3
- package/commands/update_instructions.md +23 -0
- package/package.json +3 -2
- package/rules/aether.mdc +2 -2
- package/rules/agents-data.mdc +72 -0
- package/rules/agents.mdc +2 -54
- package/rules/architecture.mdc +9 -10
- package/rules/cookbook-data.mdc +405 -0
- package/rules/cookbook.mdc +2 -397
- package/rules/{api.mdc → data.mdc} +4 -4
- package/rules/instructions_warning.mdc +2 -2
- package/rules/server-data.mdc +54 -0
- package/rules/server.mdc +2 -48
- package/rules/something-broke.mdc +1 -1
- package/variants/mcp-only/commands/build_my_app.md +146 -0
- package/variants/mcp-only/rules/agents-data.mdc +45 -0
- package/variants/mcp-only/rules/cookbook-data.mdc +50 -0
- package/variants/mcp-only/rules/data.mdc +55 -0
- package/variants/mcp-only/rules/server-data.mdc +41 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Data-fetching UI recipes: entity search, news feed, filings, gateway helpers. Read with the `data` rule when building pages that use the Query Server or platform data."
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Data-fetching cookbook
|
|
7
|
+
|
|
8
|
+
Copy-paste patterns that call the Elemental API, gateway, or helpers. For platform API details see the `data` rule. Pure UI patterns (tables, forms, charts) are in the `cookbook` rule.
|
|
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 `data` 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
|
+
```
|
|
96
|
+
|
|
97
|
+
## 2. News Feed — Recent Articles with Sentiment
|
|
98
|
+
|
|
99
|
+
Fetch recent articles from the knowledge graph. Uses `useElementalSchema()`
|
|
100
|
+
for runtime flavor/PID discovery and `buildGatewayUrl()` for gateway access.
|
|
101
|
+
|
|
102
|
+
```vue
|
|
103
|
+
<template>
|
|
104
|
+
<div class="d-flex flex-column fill-height pa-4">
|
|
105
|
+
<h1 class="text-h5 mb-4">Recent News</h1>
|
|
106
|
+
<v-alert v-if="error" type="error" variant="tonal" class="mb-4" closable>
|
|
107
|
+
{{ error }}
|
|
108
|
+
</v-alert>
|
|
109
|
+
<v-progress-linear v-if="loading" indeterminate class="mb-4" />
|
|
110
|
+
<v-list v-if="articles.length" lines="three">
|
|
111
|
+
<v-list-item v-for="a in articles" :key="a.neid">
|
|
112
|
+
<template #title>
|
|
113
|
+
<span>{{ a.name || a.neid }}</span>
|
|
114
|
+
<v-chip
|
|
115
|
+
v-if="a.sentiment"
|
|
116
|
+
size="x-small"
|
|
117
|
+
class="ml-2"
|
|
118
|
+
:color="a.sentiment > 0 ? 'success' : a.sentiment < 0 ? 'error' : 'grey'"
|
|
119
|
+
>
|
|
120
|
+
{{ a.sentiment > 0 ? 'Bullish' : a.sentiment < 0 ? 'Bearish' : 'Neutral' }}
|
|
121
|
+
</v-chip>
|
|
122
|
+
</template>
|
|
123
|
+
<template #subtitle>{{ a.neid }}</template>
|
|
124
|
+
</v-list-item>
|
|
125
|
+
</v-list>
|
|
126
|
+
<v-empty-state
|
|
127
|
+
v-else-if="!loading"
|
|
128
|
+
headline="No articles found"
|
|
129
|
+
icon="mdi-newspaper-variant-outline"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<script setup lang="ts">
|
|
135
|
+
import { useElementalClient } from '@yottagraph-app/elemental-api/client';
|
|
136
|
+
import { padNeid } from '~/utils/elementalHelpers';
|
|
137
|
+
|
|
138
|
+
const client = useElementalClient();
|
|
139
|
+
const { flavorByName, pidByName, refresh: loadSchema } = useElementalSchema();
|
|
140
|
+
|
|
141
|
+
const articles = ref<{ neid: string; name: string; sentiment: number | null }[]>([]);
|
|
142
|
+
const loading = ref(false);
|
|
143
|
+
const error = ref<string | null>(null);
|
|
144
|
+
|
|
145
|
+
onMounted(async () => {
|
|
146
|
+
loading.value = true;
|
|
147
|
+
try {
|
|
148
|
+
await loadSchema();
|
|
149
|
+
const articleFid = flavorByName('article');
|
|
150
|
+
if (!articleFid) {
|
|
151
|
+
error.value = 'Article entity type not found in schema';
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const res = await client.findEntities({
|
|
156
|
+
expression: JSON.stringify({ type: 'is_type', is_type: { fid: articleFid } }),
|
|
157
|
+
limit: 20,
|
|
158
|
+
});
|
|
159
|
+
const neids: string[] = (res as any).eids ?? [];
|
|
160
|
+
|
|
161
|
+
if (!neids.length) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const namePid = pidByName('name');
|
|
166
|
+
const sentimentPid = pidByName('sentiment');
|
|
167
|
+
const pids = [namePid, sentimentPid].filter((p): p is number => p !== null);
|
|
168
|
+
|
|
169
|
+
const props = await client.getPropertyValues({
|
|
170
|
+
eids: JSON.stringify(neids),
|
|
171
|
+
pids: JSON.stringify(pids),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const valueMap = new Map<string, Record<number, any>>();
|
|
175
|
+
for (const v of (props as any).values ?? []) {
|
|
176
|
+
const eid = padNeid(v.eid ?? v.entity_id ?? '');
|
|
177
|
+
if (!valueMap.has(eid)) valueMap.set(eid, {});
|
|
178
|
+
valueMap.get(eid)![v.pid] = v.value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
articles.value = neids.map((neid) => {
|
|
182
|
+
const vals = valueMap.get(neid) ?? {};
|
|
183
|
+
return {
|
|
184
|
+
neid,
|
|
185
|
+
name: namePid ? (vals[namePid] as string) ?? neid : neid,
|
|
186
|
+
sentiment: sentimentPid ? (vals[sentimentPid] as number) ?? null : null,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
error.value = e.message || 'Failed to load articles';
|
|
191
|
+
} finally {
|
|
192
|
+
loading.value = false;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
</script>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 3. Entity Search with Gateway Helpers
|
|
199
|
+
|
|
200
|
+
Simpler version of recipe #1 using the pre-built `searchEntities()` helper.
|
|
201
|
+
|
|
202
|
+
```vue
|
|
203
|
+
<template>
|
|
204
|
+
<div class="d-flex flex-column fill-height pa-4">
|
|
205
|
+
<h1 class="text-h5 mb-4">Entity Search</h1>
|
|
206
|
+
<v-text-field
|
|
207
|
+
v-model="query"
|
|
208
|
+
label="Search entities"
|
|
209
|
+
prepend-inner-icon="mdi-magnify"
|
|
210
|
+
variant="outlined"
|
|
211
|
+
@keyup.enter="search"
|
|
212
|
+
:loading="loading"
|
|
213
|
+
/>
|
|
214
|
+
<v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
|
|
215
|
+
{{ error }}
|
|
216
|
+
</v-alert>
|
|
217
|
+
<v-list v-if="results.length" class="mt-4">
|
|
218
|
+
<v-list-item
|
|
219
|
+
v-for="r in results"
|
|
220
|
+
:key="r.neid"
|
|
221
|
+
:title="r.name"
|
|
222
|
+
:subtitle="r.neid"
|
|
223
|
+
/>
|
|
224
|
+
</v-list>
|
|
225
|
+
<v-empty-state
|
|
226
|
+
v-else-if="searched && !loading"
|
|
227
|
+
headline="No results"
|
|
228
|
+
icon="mdi-magnify-remove-outline"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
</template>
|
|
232
|
+
|
|
233
|
+
<script setup lang="ts">
|
|
234
|
+
import { searchEntities } from '~/utils/elementalHelpers';
|
|
235
|
+
|
|
236
|
+
const query = ref('');
|
|
237
|
+
const results = ref<{ neid: string; name: string }[]>([]);
|
|
238
|
+
const loading = ref(false);
|
|
239
|
+
const error = ref<string | null>(null);
|
|
240
|
+
const searched = ref(false);
|
|
241
|
+
|
|
242
|
+
async function search() {
|
|
243
|
+
if (!query.value.trim()) return;
|
|
244
|
+
loading.value = true;
|
|
245
|
+
error.value = null;
|
|
246
|
+
searched.value = true;
|
|
247
|
+
try {
|
|
248
|
+
results.value = await searchEntities(query.value.trim());
|
|
249
|
+
} catch (e: any) {
|
|
250
|
+
error.value = e.message || 'Search failed';
|
|
251
|
+
results.value = [];
|
|
252
|
+
} finally {
|
|
253
|
+
loading.value = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
</script>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## 4. Get Filings for a Company
|
|
260
|
+
|
|
261
|
+
Fetch Edgar filings (or any relationship-linked documents) for an organization.
|
|
262
|
+
Uses `$fetch` for the initial entity search because `POST /entities/search`
|
|
263
|
+
is not wrapped by the generated client (same as recipe #1). Filing
|
|
264
|
+
properties are then fetched via `useElementalClient()`.
|
|
265
|
+
|
|
266
|
+
**Important:** For graph-layer entities (person, organization, location),
|
|
267
|
+
use `findEntities` with a `linked` expression. For property-layer entities
|
|
268
|
+
(documents, filings, articles), use `getPropertyValues` with the
|
|
269
|
+
relationship PID. See the `data` rule for the two-layer architecture.
|
|
270
|
+
|
|
271
|
+
```vue
|
|
272
|
+
<template>
|
|
273
|
+
<div class="d-flex flex-column fill-height pa-4">
|
|
274
|
+
<h1 class="text-h5 mb-4">Company Filings</h1>
|
|
275
|
+
<v-text-field
|
|
276
|
+
v-model="query"
|
|
277
|
+
label="Company name"
|
|
278
|
+
prepend-inner-icon="mdi-magnify"
|
|
279
|
+
@keyup.enter="search"
|
|
280
|
+
:loading="loading"
|
|
281
|
+
/>
|
|
282
|
+
<v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
|
|
283
|
+
{{ error }}
|
|
284
|
+
</v-alert>
|
|
285
|
+
<v-data-table
|
|
286
|
+
v-if="filings.length"
|
|
287
|
+
:headers="headers"
|
|
288
|
+
:items="filings"
|
|
289
|
+
:loading="loading"
|
|
290
|
+
density="comfortable"
|
|
291
|
+
hover
|
|
292
|
+
class="mt-4"
|
|
293
|
+
/>
|
|
294
|
+
<v-empty-state
|
|
295
|
+
v-else-if="searched && !loading"
|
|
296
|
+
headline="No filings found"
|
|
297
|
+
icon="mdi-file-document-off"
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
</template>
|
|
301
|
+
|
|
302
|
+
<script setup lang="ts">
|
|
303
|
+
import { useElementalClient } from '@yottagraph-app/elemental-api/client';
|
|
304
|
+
|
|
305
|
+
const client = useElementalClient();
|
|
306
|
+
const query = ref('');
|
|
307
|
+
const filings = ref<{ neid: string; name: string }[]>([]);
|
|
308
|
+
const loading = ref(false);
|
|
309
|
+
const error = ref<string | null>(null);
|
|
310
|
+
const searched = ref(false);
|
|
311
|
+
|
|
312
|
+
const headers = [
|
|
313
|
+
{ title: 'NEID', key: 'neid', sortable: true },
|
|
314
|
+
{ title: 'Name', key: 'name', sortable: true },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
async function getPropertyPidMap(client: ReturnType<typeof useElementalClient>) {
|
|
318
|
+
const schemaRes = await client.getSchema();
|
|
319
|
+
const properties = schemaRes.schema?.properties ?? (schemaRes as any).properties ?? [];
|
|
320
|
+
return new Map(properties.map((p: any) => [p.name, p.pid]));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getSearchUrl() {
|
|
324
|
+
const config = useRuntimeConfig();
|
|
325
|
+
const gw = (config.public as any).gatewayUrl as string;
|
|
326
|
+
const org = (config.public as any).tenantOrgId as string;
|
|
327
|
+
return `${gw}/api/qs/${org}/entities/search`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getApiKey() {
|
|
331
|
+
return (useRuntimeConfig().public as any).qsApiKey as string;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function search() {
|
|
335
|
+
if (!query.value.trim()) return;
|
|
336
|
+
loading.value = true;
|
|
337
|
+
error.value = null;
|
|
338
|
+
searched.value = true;
|
|
339
|
+
try {
|
|
340
|
+
const res = await $fetch<any>(getSearchUrl(), {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: { 'Content-Type': 'application/json', 'X-Api-Key': getApiKey() },
|
|
343
|
+
body: {
|
|
344
|
+
queries: [{ queryId: 1, query: query.value.trim(), flavors: ['organization'] }],
|
|
345
|
+
maxResults: 1,
|
|
346
|
+
includeNames: true,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
const matches = res?.results?.[0]?.matches ?? [];
|
|
350
|
+
if (!matches.length) {
|
|
351
|
+
filings.value = [];
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const orgNeid = matches[0].neid;
|
|
355
|
+
|
|
356
|
+
const pidMap = await getPropertyPidMap(client);
|
|
357
|
+
const filedPid = pidMap.get('filed');
|
|
358
|
+
if (!filedPid) {
|
|
359
|
+
error.value = '"filed" relationship not found in schema';
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const propRes = await client.getPropertyValues({
|
|
364
|
+
eids: JSON.stringify([orgNeid]),
|
|
365
|
+
pids: JSON.stringify([filedPid]),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const docNeids = (propRes.values ?? []).map((v: any) =>
|
|
369
|
+
String(v.value).padStart(20, '0'),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
function getEntityNameUrl(neid: string) {
|
|
373
|
+
const config = useRuntimeConfig();
|
|
374
|
+
const gw = (config.public as any).gatewayUrl as string;
|
|
375
|
+
const org = (config.public as any).tenantOrgId as string;
|
|
376
|
+
return `${gw}/api/qs/${org}/entities/${neid}/name`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const names = await Promise.all(
|
|
380
|
+
docNeids.map(async (neid: string) => {
|
|
381
|
+
try {
|
|
382
|
+
const res = await $fetch<{ name: string }>(
|
|
383
|
+
getEntityNameUrl(neid),
|
|
384
|
+
{ headers: { 'X-Api-Key': getApiKey() } },
|
|
385
|
+
);
|
|
386
|
+
return res.name || neid;
|
|
387
|
+
} catch {
|
|
388
|
+
return neid;
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
filings.value = docNeids.map((neid: string, i: number) => ({
|
|
394
|
+
neid,
|
|
395
|
+
name: names[i],
|
|
396
|
+
}));
|
|
397
|
+
} catch (e: any) {
|
|
398
|
+
error.value = e.message || 'Failed to load filings';
|
|
399
|
+
filings.value = [];
|
|
400
|
+
} finally {
|
|
401
|
+
loading.value = false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
</script>
|
|
405
|
+
```
|