@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.
Files changed (40) hide show
  1. package/dist/db/syncAppCatalog.mjs +13 -1
  2. package/dist/db/syncAppCatalog.mjs.map +1 -1
  3. package/dist/generated/prisma/internal/class.mjs +4 -4
  4. package/dist/generated/prisma/internal/class.mjs.map +1 -1
  5. package/dist/generated/prisma/internal/prismaNamespace.d.mts +2 -0
  6. package/dist/generated/prisma/internal/prismaNamespace.d.mts.map +1 -1
  7. package/dist/generated/prisma/models/DbResource.d.mts +85 -1
  8. package/dist/generated/prisma/models/DbResource.d.mts.map +1 -1
  9. package/dist/index.d.mts +2 -1
  10. package/dist/index.mjs +2 -1
  11. package/dist/modules/appCatalog/freshness.mjs +5 -3
  12. package/dist/modules/appCatalog/freshness.mjs.map +1 -1
  13. package/dist/modules/appCatalog/service.d.mts +7 -0
  14. package/dist/modules/appCatalog/service.d.mts.map +1 -0
  15. package/dist/modules/appCatalog/service.mjs +20 -2
  16. package/dist/modules/appCatalog/service.mjs.map +1 -1
  17. package/dist/modules/assets/upsertAsset.mjs +12 -1
  18. package/dist/modules/assets/upsertAsset.mjs.map +1 -1
  19. package/dist/server/controller.d.mts +8 -0
  20. package/dist/server/controller.d.mts.map +1 -1
  21. package/dist/server/controller.mjs +7 -1
  22. package/dist/server/controller.mjs.map +1 -1
  23. package/dist/types/common/appCatalogTypes.d.mts +10 -0
  24. package/dist/types/common/appCatalogTypes.d.mts.map +1 -1
  25. package/package.json +7 -7
  26. package/prisma/migrations/20260813000000_add_ai_memory/migration.sql +2 -0
  27. package/prisma/schema.prisma +6 -2
  28. package/src/__tests__/assetMime.test.ts +104 -14
  29. package/src/__tests__/freshness.test.ts +56 -17
  30. package/src/db/syncAppCatalog.ts +25 -0
  31. package/src/generated/prisma/internal/class.ts +4 -4
  32. package/src/generated/prisma/internal/prismaNamespace.ts +2 -0
  33. package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +2 -0
  34. package/src/generated/prisma/models/DbResource.ts +87 -1
  35. package/src/index.ts +1 -0
  36. package/src/modules/appCatalog/freshness.ts +29 -11
  37. package/src/modules/appCatalog/service.ts +35 -4
  38. package/src/modules/assets/upsertAsset.ts +20 -4
  39. package/src/server/controller.ts +11 -0
  40. 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 true, a muted "may be out of date" note.
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`) and when the next check is due (`nextCheckAfter`,
9
- * adaptive backoff). "Stale" means the entry is more than a grace period past
10
- * its due date — i.e. the automated freshness loop has fallen behind for this
11
- * app, so a human reader should treat the info with mild caution.
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 lastCheckedAt ISO string or null — when sources were last verified.
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
- lastCheckedAt: string | null,
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 { lastCheckedAt, isStale }
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
- row.nextCheckAfter ? row.nextCheckAfter.toISOString() : null,
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
- // Reusing the stored binary must not keep a mimeType we no longer derive:
31
- // rows written by an older, wrong derivation would never be corrected.
32
- if (existing.mimeType !== mimeType) {
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 },
@@ -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
  }