@taodigital/app-brain 0.0.0-stage → 1.2.1
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/README.md +13 -2
- package/dist/archetype-classification.d.ts +54 -0
- package/dist/archetype-classification.js +95 -0
- package/dist/cloud-storage.d.ts +106 -0
- package/dist/cloud-storage.js +539 -0
- package/dist/email.d.ts +21 -0
- package/dist/email.js +234 -0
- package/dist/grounding.d.ts +30 -0
- package/dist/grounding.js +138 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +37 -0
- package/dist/nav-graph.d.ts +42 -0
- package/dist/nav-graph.js +248 -0
- package/dist/notifications.d.ts +26 -0
- package/dist/notifications.js +105 -0
- package/dist/orphan-classification.d.ts +32 -0
- package/dist/orphan-classification.js +40 -0
- package/dist/repository.d.ts +3185 -0
- package/dist/repository.js +5927 -0
- package/dist/run-notifications.d.ts +16 -0
- package/dist/run-notifications.js +244 -0
- package/dist/skill-file-kinds.d.ts +5 -0
- package/dist/skill-file-kinds.js +50 -0
- package/dist/token-ledger.d.ts +23 -0
- package/dist/token-ledger.js +178 -0
- package/dist/traceability.d.ts +213 -0
- package/dist/traceability.js +339 -0
- package/package.json +24 -4
- package/src/archetype-classification.test.ts +91 -0
- package/src/archetype-classification.ts +132 -0
- package/src/cloud-storage.test.ts +83 -0
- package/src/cloud-storage.ts +627 -0
- package/src/email.ts +268 -0
- package/src/grounding.ts +172 -0
- package/src/index.ts +18 -0
- package/src/nav-graph.ts +295 -0
- package/src/notifications.ts +156 -0
- package/src/orphan-classification.test.ts +61 -0
- package/src/orphan-classification.ts +65 -0
- package/src/orphan-screens.test.ts +88 -0
- package/src/repository.ts +7397 -0
- package/src/run-notifications.ts +281 -0
- package/src/skill-file-kinds.ts +51 -0
- package/src/token-ledger.ts +209 -0
- package/src/traceability.test.ts +75 -0
- package/src/traceability.ts +477 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen-archetype classification + KAG journey-pair suggestion — pure logic,
|
|
3
|
+
* no I/O, unit-testable in isolation (same shape as orphan-classification.ts).
|
|
4
|
+
*
|
|
5
|
+
* Rule-based rather than LLM-based on purpose: this only needs to look at
|
|
6
|
+
* data already captured during crawl (Element.role/expectedData, Screen.url/
|
|
7
|
+
* name), so it works with zero LLM provider configured and costs nothing to
|
|
8
|
+
* run on every project. It is deliberately coarse — good enough to rank
|
|
9
|
+
* "which screen pairs are worth generating a KAG journey test for" so a user
|
|
10
|
+
* doesn't have to already know both screen ids, not a general-purpose UI
|
|
11
|
+
* classifier.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ScreenArchetype = 'LOGIN' | 'SIGNUP' | 'SEARCH' | 'CHECKOUT' | 'FORM' | 'GENERIC';
|
|
15
|
+
|
|
16
|
+
export interface ClassifiableElement {
|
|
17
|
+
role: string | null;
|
|
18
|
+
expectedData: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ClassifiableScreen {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
url: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ArchetypeResult {
|
|
28
|
+
archetype: ScreenArchetype;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LOGIN_URL_RE = /login|signin|sign-in/i;
|
|
33
|
+
const SIGNUP_URL_RE = /signup|sign-up|register|onboard/i;
|
|
34
|
+
const CHECKOUT_URL_RE = /checkout|cart|basket/i;
|
|
35
|
+
// Element.role values observed at crawl time (crawler.ts) are lowercase HTML
|
|
36
|
+
// input types or tag/aria names — see that file for exactly how they're set.
|
|
37
|
+
const FORM_FIELD_ROLES = new Set(['text', 'email', 'tel', 'number', 'date', 'textarea', 'select']);
|
|
38
|
+
|
|
39
|
+
/** Classifies a single screen from its URL/name and the elements crawled on it.
|
|
40
|
+
* Checked in priority order — the first matching, most specific signal wins,
|
|
41
|
+
* since e.g. a checkout page often also has a form field or two. */
|
|
42
|
+
export function classifyScreenArchetype(
|
|
43
|
+
screen: ClassifiableScreen,
|
|
44
|
+
elements: ClassifiableElement[],
|
|
45
|
+
): ArchetypeResult {
|
|
46
|
+
const hasPassword = elements.some((e) => e.role === 'password' || e.expectedData === 'password');
|
|
47
|
+
const hasEmail = elements.some((e) => e.role === 'email' || e.expectedData === 'valid_email');
|
|
48
|
+
const hasSearch = elements.some((e) => e.role === 'search' || e.expectedData === 'search_term');
|
|
49
|
+
const urlOrName = `${screen.url ?? ''} ${screen.name ?? ''}`;
|
|
50
|
+
|
|
51
|
+
if (CHECKOUT_URL_RE.test(urlOrName)) {
|
|
52
|
+
return { archetype: 'CHECKOUT', reason: "URL/name matches checkout/cart/basket" };
|
|
53
|
+
}
|
|
54
|
+
if (SIGNUP_URL_RE.test(urlOrName) && hasEmail && hasPassword) {
|
|
55
|
+
return { archetype: 'SIGNUP', reason: 'URL/name matches signup/register, with email + password fields present' };
|
|
56
|
+
}
|
|
57
|
+
if (hasPassword) {
|
|
58
|
+
return { archetype: 'LOGIN', reason: 'Has a password-type input element' };
|
|
59
|
+
}
|
|
60
|
+
if (LOGIN_URL_RE.test(urlOrName)) {
|
|
61
|
+
return { archetype: 'LOGIN', reason: 'URL/name matches login/signin' };
|
|
62
|
+
}
|
|
63
|
+
if (hasSearch) {
|
|
64
|
+
return { archetype: 'SEARCH', reason: 'Has a search-type input element' };
|
|
65
|
+
}
|
|
66
|
+
if (elements.some((e) => e.role && FORM_FIELD_ROLES.has(e.role))) {
|
|
67
|
+
return { archetype: 'FORM', reason: 'Has one or more data-entry form fields' };
|
|
68
|
+
}
|
|
69
|
+
return { archetype: 'GENERIC', reason: 'No distinguishing signals found' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ClassifiedScreen {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
archetype: ScreenArchetype;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface JourneyCandidate {
|
|
79
|
+
startScreenId: string;
|
|
80
|
+
startScreenName: string;
|
|
81
|
+
startArchetype: ScreenArchetype;
|
|
82
|
+
endScreenId: string;
|
|
83
|
+
endScreenName: string;
|
|
84
|
+
endArchetype: ScreenArchetype;
|
|
85
|
+
label: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Archetype pairs worth generating a cross-screen journey test for, ranked
|
|
89
|
+
* by how commonly they represent a real user flow. Order matters — earlier
|
|
90
|
+
* entries are suggested before later ones when a project has many candidate
|
|
91
|
+
* pairs. GENERIC is deliberately excluded from both sides: with no
|
|
92
|
+
* distinguishing signal, pairing it with anything is a guess, not a
|
|
93
|
+
* suggestion. */
|
|
94
|
+
const INTERESTING_JOURNEYS: Array<{ from: ScreenArchetype; to: ScreenArchetype; label: string }> = [
|
|
95
|
+
{ from: 'LOGIN', to: 'CHECKOUT', label: 'Login → Checkout' },
|
|
96
|
+
{ from: 'SIGNUP', to: 'LOGIN', label: 'Signup → Login' },
|
|
97
|
+
{ from: 'SEARCH', to: 'CHECKOUT', label: 'Search → Checkout' },
|
|
98
|
+
{ from: 'LOGIN', to: 'FORM', label: 'Login → Form submission' },
|
|
99
|
+
{ from: 'SEARCH', to: 'FORM', label: 'Search → Form submission' },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Finds every (start, end) screen pair whose archetypes match one of the
|
|
104
|
+
* interesting-journey rules. Pure and graph-agnostic on purpose — this does
|
|
105
|
+
* NOT check whether a KAG path actually connects the two screens (that
|
|
106
|
+
* requires a DB-backed BFS, see AppBrain.suggestJourneys in repository.ts,
|
|
107
|
+
* which calls this first to get a small, ranked candidate list and only then
|
|
108
|
+
* pays for a graph traversal per candidate instead of checking every
|
|
109
|
+
* archetype pair against every other screen).
|
|
110
|
+
*/
|
|
111
|
+
export function matchInterestingJourneyPairs(screens: ClassifiedScreen[]): JourneyCandidate[] {
|
|
112
|
+
const candidates: JourneyCandidate[] = [];
|
|
113
|
+
for (const rule of INTERESTING_JOURNEYS) {
|
|
114
|
+
const starts = screens.filter((s) => s.archetype === rule.from);
|
|
115
|
+
const ends = screens.filter((s) => s.archetype === rule.to);
|
|
116
|
+
for (const start of starts) {
|
|
117
|
+
for (const end of ends) {
|
|
118
|
+
if (start.id === end.id) continue;
|
|
119
|
+
candidates.push({
|
|
120
|
+
startScreenId: start.id,
|
|
121
|
+
startScreenName: start.name,
|
|
122
|
+
startArchetype: start.archetype,
|
|
123
|
+
endScreenId: end.id,
|
|
124
|
+
endScreenName: end.name,
|
|
125
|
+
endArchetype: end.archetype,
|
|
126
|
+
label: rule.label,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return candidates;
|
|
132
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { invalidateSystemConfigCache } from '@taodigital/core';
|
|
5
|
+
import { prisma, prismaAdmin } from '@taodigital/database';
|
|
6
|
+
import { getTenantStorageConfig } from './cloud-storage.js';
|
|
7
|
+
|
|
8
|
+
// Run without AWS_S3_BUCKET/etc set (see packages/core's test:platform-storage script
|
|
9
|
+
// for the platform-fallback-is-active case) — so getTenantStorageConfig()'s fallback
|
|
10
|
+
// to a platform default is expected to resolve to null here, exercising the
|
|
11
|
+
// "nothing configured at all" path deliberately.
|
|
12
|
+
|
|
13
|
+
function id(prefix: string) {
|
|
14
|
+
return `${prefix}_${crypto.randomUUID()}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function makeTenant() {
|
|
18
|
+
const tenantId = id('tenant');
|
|
19
|
+
await prisma.$transaction(async (tx) => {
|
|
20
|
+
await tx.$executeRawUnsafe(`SELECT set_config('app.current_tenant', $1, true)`, tenantId);
|
|
21
|
+
await tx.tenant.create({ data: { id: tenantId, name: 'Cloud Storage Test Tenant', slug: tenantId, isolation: 'SHARED', region: 'US' } });
|
|
22
|
+
});
|
|
23
|
+
return tenantId;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('getTenantStorageConfig: returns null when tenant has no connector and no platform default is configured', async () => {
|
|
27
|
+
const rows = await prismaAdmin.systemConfig.findMany({ where: { category: 'storage' } });
|
|
28
|
+
await prismaAdmin.systemConfig.deleteMany({ where: { category: 'storage' } });
|
|
29
|
+
await prismaAdmin.systemConfig.createMany({
|
|
30
|
+
data: [
|
|
31
|
+
{ key: 'storage.provider', category: 'storage', value: '', sensitive: false },
|
|
32
|
+
{ key: 'storage.bucket', category: 'storage', value: '', sensitive: false },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
invalidateSystemConfigCache('storage');
|
|
36
|
+
try {
|
|
37
|
+
const tenantId = await makeTenant();
|
|
38
|
+
const cfg = await getTenantStorageConfig(tenantId);
|
|
39
|
+
assert.equal(cfg, null);
|
|
40
|
+
} finally {
|
|
41
|
+
await prismaAdmin.systemConfig.deleteMany({ where: { category: 'storage' } });
|
|
42
|
+
if (rows.length) {
|
|
43
|
+
await prismaAdmin.systemConfig.createMany({
|
|
44
|
+
data: rows.map(({ key, value, category, sensitive, updatedByAdminId }) => ({
|
|
45
|
+
key,
|
|
46
|
+
value,
|
|
47
|
+
category,
|
|
48
|
+
sensitive,
|
|
49
|
+
updatedByAdminId,
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
invalidateSystemConfigCache('storage');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('getTenantStorageConfig: returns the tenant-specific connector when one is configured', async () => {
|
|
58
|
+
const tenantId = await makeTenant();
|
|
59
|
+
const { encrypt } = await import('@taodigital/core');
|
|
60
|
+
await prisma.$transaction(async (tx) => {
|
|
61
|
+
await tx.$executeRawUnsafe(`SELECT set_config('app.current_tenant', $1, true)`, tenantId);
|
|
62
|
+
await tx.tenantSetting.create({
|
|
63
|
+
data: {
|
|
64
|
+
id: id('tsetting'),
|
|
65
|
+
tenantId,
|
|
66
|
+
category: 'STORAGE',
|
|
67
|
+
config: {
|
|
68
|
+
provider: 'S3',
|
|
69
|
+
bucket: 'tenant-own-bucket',
|
|
70
|
+
region: 'eu-west-1',
|
|
71
|
+
accessKeyId: 'AKIATENANT',
|
|
72
|
+
secretAccessKey_enc: encrypt('tenant-secret'),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const cfg = await getTenantStorageConfig(tenantId);
|
|
79
|
+
assert.ok(cfg);
|
|
80
|
+
assert.equal(cfg!.bucket, 'tenant-own-bucket');
|
|
81
|
+
assert.equal(cfg!.region, 'eu-west-1');
|
|
82
|
+
assert.equal(cfg!.secretAccessKey, 'tenant-secret');
|
|
83
|
+
});
|