@igstack/app-catalog-backend-core 0.16.0 → 0.17.1-alpha-20260821043201
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/dist/db/syncAppCatalog.mjs +13 -1
- package/dist/db/syncAppCatalog.mjs.map +1 -1
- package/dist/generated/prisma/internal/class.mjs +4 -4
- package/dist/generated/prisma/internal/class.mjs.map +1 -1
- package/dist/generated/prisma/internal/prismaNamespace.d.mts +2 -0
- package/dist/generated/prisma/internal/prismaNamespace.d.mts.map +1 -1
- package/dist/generated/prisma/models/DbResource.d.mts +85 -1
- package/dist/generated/prisma/models/DbResource.d.mts.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/modules/appCatalog/freshness.mjs +5 -3
- package/dist/modules/appCatalog/freshness.mjs.map +1 -1
- package/dist/modules/appCatalog/service.d.mts +7 -0
- package/dist/modules/appCatalog/service.d.mts.map +1 -0
- package/dist/modules/appCatalog/service.mjs +20 -2
- package/dist/modules/appCatalog/service.mjs.map +1 -1
- package/dist/modules/assets/upsertAsset.mjs +12 -1
- package/dist/modules/assets/upsertAsset.mjs.map +1 -1
- package/dist/server/controller.d.mts +8 -0
- package/dist/server/controller.d.mts.map +1 -1
- package/dist/server/controller.mjs +7 -1
- package/dist/server/controller.mjs.map +1 -1
- package/dist/types/common/appCatalogTypes.d.mts +10 -0
- package/dist/types/common/appCatalogTypes.d.mts.map +1 -1
- package/package.json +7 -7
- package/prisma/migrations/20260813000000_add_ai_memory/migration.sql +2 -0
- package/prisma/schema.prisma +6 -2
- package/src/__tests__/assetMime.test.ts +104 -14
- package/src/__tests__/freshness.test.ts +56 -17
- package/src/db/syncAppCatalog.ts +25 -0
- package/src/generated/prisma/internal/class.ts +4 -4
- package/src/generated/prisma/internal/prismaNamespace.ts +2 -0
- package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +2 -0
- package/src/generated/prisma/models/DbResource.ts +87 -1
- package/src/index.ts +1 -0
- package/src/modules/appCatalog/freshness.ts +29 -11
- package/src/modules/appCatalog/service.ts +35 -4
- package/src/modules/assets/upsertAsset.ts +20 -4
- package/src/server/controller.ts +11 -0
- package/src/types/common/appCatalogTypes.ts +10 -0
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* Freshness semantic for a catalog resource.
|
|
3
3
|
*
|
|
4
4
|
* The backend owns this calculation so the frontend stays dumb: it just renders
|
|
5
|
-
* `lastCheckedAt` and, when `isStale` is
|
|
5
|
+
* `lastContentChangeAt` (falling back to `lastCheckedAt`) and, when `isStale` is
|
|
6
|
+
* true, a muted "may be out of date" note.
|
|
6
7
|
*
|
|
7
8
|
* Data source: the autoupdate scan records, per app, when its sources were last
|
|
8
|
-
* verified (`lastCheckedAt`)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* verified (`lastCheckedAt`), when their content last actually changed
|
|
10
|
+
* (`lastContentChangeAt`), and when the next check is due (`nextCheckAfter`,
|
|
11
|
+
* adaptive backoff). The two "last" dates are deliberately distinct — a source
|
|
12
|
+
* can be re-read many times without changing, so "checked" is not "updated".
|
|
13
|
+
* "Stale" means the entry is more than a grace period past its due date — i.e.
|
|
14
|
+
* the automated freshness loop has fallen behind for this app, so a human reader
|
|
15
|
+
* should treat the info with mild caution.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import type { Freshness } from '../../types/common/appCatalogTypes'
|
|
@@ -18,21 +22,31 @@ export type { Freshness }
|
|
|
18
22
|
/** A check is "stale" once it is this far past its due date (ms). Default: 7 days. */
|
|
19
23
|
export const STALE_GRACE_MS = 7 * 24 * 60 * 60 * 1000
|
|
20
24
|
|
|
25
|
+
/** The stored dates a resource's freshness is computed from (ISO strings). */
|
|
26
|
+
export interface FreshnessInput {
|
|
27
|
+
/** When the sources were last verified — only means the schedule advanced. */
|
|
28
|
+
lastCheckedAt?: string | null
|
|
29
|
+
/** When the next check is due (adaptive backoff). */
|
|
30
|
+
nextCheckAfter?: string | null
|
|
31
|
+
/** When the source content last actually changed. Null on pre-existing rows. */
|
|
32
|
+
lastContentChangeAt?: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
/**
|
|
22
36
|
* Compute the freshness semantic from the stored dates.
|
|
23
37
|
*
|
|
24
|
-
* @param
|
|
25
|
-
* @param nextCheckAfter ISO string or null — when the next check is due.
|
|
38
|
+
* @param dates the resource's stored freshness timestamps.
|
|
26
39
|
* @param now current time (defaults to Date.now()); injectable for tests.
|
|
27
40
|
*/
|
|
28
41
|
export function computeFreshness(
|
|
29
|
-
|
|
30
|
-
nextCheckAfter: string | null,
|
|
42
|
+
dates: FreshnessInput,
|
|
31
43
|
now: number = Date.now(),
|
|
32
44
|
): Freshness {
|
|
45
|
+
const { lastCheckedAt, nextCheckAfter, lastContentChangeAt } = dates
|
|
46
|
+
|
|
33
47
|
// Never checked → no line is shown at all (frontend hides null lastCheckedAt).
|
|
34
48
|
if (!lastCheckedAt) {
|
|
35
|
-
return { lastCheckedAt: null, isStale: false }
|
|
49
|
+
return { lastCheckedAt: null, lastContentChangeAt: null, isStale: false }
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
let isStale = false
|
|
@@ -43,5 +57,9 @@ export function computeFreshness(
|
|
|
43
57
|
}
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
lastCheckedAt,
|
|
62
|
+
lastContentChangeAt: lastContentChangeAt ?? null,
|
|
63
|
+
isStale,
|
|
64
|
+
}
|
|
47
65
|
}
|
|
@@ -153,15 +153,21 @@ function rowToResource(row: ResourceRowWithSourceRefs): Resource {
|
|
|
153
153
|
? undefined
|
|
154
154
|
: (row.deprecated as unknown as Resource['deprecated'])
|
|
155
155
|
const aiPrompt = row.aiPrompt == null ? undefined : row.aiPrompt
|
|
156
|
+
const aiMemory = row.aiMemory == null ? undefined : row.aiMemory
|
|
156
157
|
const urlIssues = (row.urlIssues as unknown as string[] | null)?.length
|
|
157
158
|
? (row.urlIssues as unknown as string[])
|
|
158
159
|
: undefined
|
|
159
160
|
const tiers =
|
|
160
161
|
row.tiers == null ? undefined : (row.tiers as unknown as Resource['tiers'])
|
|
161
|
-
const freshness = computeFreshness(
|
|
162
|
-
row.lastCheckedAt ? row.lastCheckedAt.toISOString() : null,
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
const freshness = computeFreshness({
|
|
163
|
+
lastCheckedAt: row.lastCheckedAt ? row.lastCheckedAt.toISOString() : null,
|
|
164
|
+
nextCheckAfter: row.nextCheckAfter
|
|
165
|
+
? row.nextCheckAfter.toISOString()
|
|
166
|
+
: null,
|
|
167
|
+
lastContentChangeAt: row.lastContentChangeAt
|
|
168
|
+
? row.lastContentChangeAt.toISOString()
|
|
169
|
+
: null,
|
|
170
|
+
})
|
|
165
171
|
|
|
166
172
|
return {
|
|
167
173
|
id: row.id,
|
|
@@ -181,6 +187,7 @@ function rowToResource(row: ResourceRowWithSourceRefs): Resource {
|
|
|
181
187
|
sources,
|
|
182
188
|
deprecated,
|
|
183
189
|
aiPrompt,
|
|
190
|
+
aiMemory,
|
|
184
191
|
urlIssues,
|
|
185
192
|
tiers,
|
|
186
193
|
// Fields from former SubResource
|
|
@@ -295,6 +302,30 @@ export async function updateApp(input: UpdateAppInput): Promise<Resource> {
|
|
|
295
302
|
return rowToResource(updated)
|
|
296
303
|
}
|
|
297
304
|
|
|
305
|
+
export interface UpdateAiMemoryInput {
|
|
306
|
+
/** Resource id (DB id, not slug) */
|
|
307
|
+
id: string
|
|
308
|
+
/** New value for aiMemory. Pass null to clear. */
|
|
309
|
+
aiMemory: string | null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Updates the AI-owned scratchpad for a resource.
|
|
314
|
+
* This mutation is intentionally separate from updateApp so AI agents can
|
|
315
|
+
* write knowledge without a full MR cycle. It does NOT touch aiPrompt.
|
|
316
|
+
*/
|
|
317
|
+
export async function updateAiMemory(
|
|
318
|
+
input: UpdateAiMemoryInput,
|
|
319
|
+
): Promise<Resource> {
|
|
320
|
+
const prisma = getDbClient()
|
|
321
|
+
const updated = await prisma.dbResource.update({
|
|
322
|
+
where: { id: input.id },
|
|
323
|
+
data: { aiMemory: input.aiMemory },
|
|
324
|
+
include: { sourceRefs: true },
|
|
325
|
+
})
|
|
326
|
+
return rowToResource(updated)
|
|
327
|
+
}
|
|
328
|
+
|
|
298
329
|
export function deriveCategories(resources: Resource[]): AppCategory[] {
|
|
299
330
|
const tagSet = new Set<string>()
|
|
300
331
|
for (const resource of resources) {
|
|
@@ -21,15 +21,31 @@ export async function upsertAsset({
|
|
|
21
21
|
originalFilename,
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
// If an asset with the same checksum already exists, reuse it instead of storing duplicate binary.
|
|
25
24
|
const existing = await prisma.dbAsset.findUnique({
|
|
26
25
|
where: { name },
|
|
27
26
|
})
|
|
28
27
|
|
|
29
28
|
if (existing) {
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
29
|
+
// The checksum is the source of truth for whether the underlying bytes
|
|
30
|
+
// changed. Reusing the stored binary on a checksum match must not keep a
|
|
31
|
+
// mimeType we no longer derive: rows written by an older, wrong
|
|
32
|
+
// derivation would never be corrected otherwise. A checksum mismatch
|
|
33
|
+
// means the file on disk was replaced with different content (e.g. an
|
|
34
|
+
// SVG swapped for a PNG) — the whole row must be rewritten, not just
|
|
35
|
+
// mimeType, or the new bytes are silently discarded forever.
|
|
36
|
+
if (existing.checksum !== checksum) {
|
|
37
|
+
await prisma.dbAsset.update({
|
|
38
|
+
where: { id: existing.id },
|
|
39
|
+
data: {
|
|
40
|
+
content: new Uint8Array(buffer),
|
|
41
|
+
checksum,
|
|
42
|
+
fileSize,
|
|
43
|
+
width,
|
|
44
|
+
height,
|
|
45
|
+
mimeType,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
} else if (existing.mimeType !== mimeType) {
|
|
33
49
|
await prisma.dbAsset.update({
|
|
34
50
|
where: { id: existing.id },
|
|
35
51
|
data: { mimeType },
|
package/src/server/controller.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAppCatalogData,
|
|
3
|
+
updateAiMemory as updateAiMemoryService,
|
|
3
4
|
updateApp as updateAppService,
|
|
4
5
|
} from '../modules/appCatalog/service'
|
|
5
6
|
import type { AppCatalogData } from '../types'
|
|
@@ -53,6 +54,16 @@ export function createTrpcRouter(
|
|
|
53
54
|
.mutation(async ({ input }): Promise<Resource> => {
|
|
54
55
|
return updateAppService(input)
|
|
55
56
|
}),
|
|
57
|
+
updateAiMemory: publicProcedure
|
|
58
|
+
.input(
|
|
59
|
+
z.object({
|
|
60
|
+
id: z.string(),
|
|
61
|
+
aiMemory: z.string().nullable(),
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
.mutation(async ({ input }): Promise<Resource> => {
|
|
65
|
+
return updateAiMemoryService(input)
|
|
66
|
+
}),
|
|
56
67
|
}),
|
|
57
68
|
|
|
58
69
|
// Auth routes (requires auth instance)
|
|
@@ -46,6 +46,13 @@ export interface SourceReference {
|
|
|
46
46
|
export interface Freshness {
|
|
47
47
|
/** ISO-8601 timestamp the app's sources were last verified, or null if never. */
|
|
48
48
|
lastCheckedAt: string | null
|
|
49
|
+
/**
|
|
50
|
+
* ISO-8601 timestamp the source content last actually CHANGED — what the UI
|
|
51
|
+
* shows as "Updated". Distinct from `lastCheckedAt`: a source can be re-read
|
|
52
|
+
* repeatedly without changing. Null for entries scanned before this was
|
|
53
|
+
* tracked, in which case the frontend falls back to `lastCheckedAt`.
|
|
54
|
+
*/
|
|
55
|
+
lastContentChangeAt: string | null
|
|
49
56
|
/** True when the entry is overdue for a re-check by more than the grace period. */
|
|
50
57
|
isStale: boolean
|
|
51
58
|
}
|
|
@@ -82,6 +89,8 @@ export interface Resource {
|
|
|
82
89
|
}
|
|
83
90
|
/** Agent-facing prompt guiding how to maintain this app's data */
|
|
84
91
|
aiPrompt?: string
|
|
92
|
+
/** AI-owned scratchpad: strategies, gotchas, scripts, retros accumulated per app */
|
|
93
|
+
aiMemory?: string
|
|
85
94
|
/** URL health issues detected by automated scanning */
|
|
86
95
|
urlIssues?: string[]
|
|
87
96
|
/** Optional tier variants (e.g., prod/dev) with per-tier URLs and access */
|
|
@@ -118,6 +127,7 @@ export interface Resource {
|
|
|
118
127
|
*/
|
|
119
128
|
lastCheckedAt?: string | null
|
|
120
129
|
nextCheckAfter?: string | null
|
|
130
|
+
lastContentChangeAt?: string | null
|
|
121
131
|
/** ISO-8601 timestamp of when this entry was first created in the catalog DB. */
|
|
122
132
|
createdAt?: string
|
|
123
133
|
}
|