@produtype/core 0.75.0 → 0.77.0

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectGame = detectGame;
4
4
  const detectContext_1 = require("./detectContext");
5
5
  const textSearch_1 = require("../utils/textSearch");
6
+ const localStores_1 = require("./localStores");
6
7
  const absenceEvidence_1 = require("./absenceEvidence");
7
8
  /**
8
9
  * Whether this project is a game.
@@ -151,20 +152,42 @@ async function detectStatePersistence(ctx) {
151
152
  }
152
153
  // A database is the durable half. Read from the stack rather than re-detected, so
153
154
  // this agrees with what the report says the data layer is.
154
- const durable = ctx.files.all.some((file) => /(^|\/)(schema\.prisma|.*\.sql)$/i.test(file))
155
+ const serverStore = ctx.files.all.some((file) => /(^|\/)(schema\.prisma|.*\.sql)$/i.test(file))
155
156
  || Boolean(ctx.packageJson?.dependencies?.pg)
156
157
  || Boolean(ctx.packageJson?.dependencies?.mongoose)
157
158
  || Boolean(ctx.packageJson?.dependencies?.['better-sqlite3']);
158
- if (durable)
159
+ /**
160
+ * On a device, the local store is the durable one.
161
+ *
162
+ * This whole check was written for a game in a browser, where `localStorage` is the
163
+ * partial case because a cleared cache takes the save with it. That reasoning does
164
+ * not carry to a phone: Core Data and SQLite survive the application being killed,
165
+ * the device restarting and the person coming back a week later — they are the
166
+ * durable half there, not the fragile one.
167
+ *
168
+ * It is required of the mobile-app profile, and all four real applications measured
169
+ * came back `partial` on it while keeping everything they own on disk. The failure
170
+ * this should catch on a phone is different and still caught: state held only in
171
+ * memory, written nowhere, gone when the operating system reclaims the process.
172
+ */
173
+ const deviceStore = await (0, localStores_1.readLocalStores)(ctx, 3);
174
+ const onDevice = deviceStore.dependencies.length > 0 || deviceStore.uses.length > 0;
175
+ const durable = serverStore || onDevice;
176
+ if (serverStore)
159
177
  evidence.push({ type: 'note', value: 'a durable store is present' });
178
+ for (const dep of deviceStore.dependencies)
179
+ evidence.push({ type: 'dependency', value: dep });
180
+ for (const use of deviceStore.uses) {
181
+ evidence.push({ type: 'snippet', value: use.snippet, file: use.file, line: use.line });
182
+ }
160
183
  const saves = clientOnly.length > 0 || saveRoutes.length > 0;
161
184
  return {
162
185
  key: 'app.stateDurability',
163
186
  present: saves || durable,
164
187
  // Client storage alone is the partial case: it saves, until it does not.
165
188
  complete: durable,
166
- evidence: (0, absenceEvidence_1.evidenceOrSearch)(evidence, 'anywhere progress is written down', ['localStorage', 'sessionStorage', 'IndexedDB', 'a save route', 'schema.prisma', 'a .sql file', 'pg', 'mongoose', 'better-sqlite3']),
167
- details: { durable, clientStorage: clientOnly.length > 0, saveRoutines: saveRoutes.length },
189
+ evidence: (0, absenceEvidence_1.evidenceOrSearch)(evidence, 'anywhere progress is written down', ['localStorage', 'sessionStorage', 'IndexedDB', 'a save route', 'schema.prisma', 'a .sql file', 'pg', 'mongoose', 'better-sqlite3', 'Core Data', 'SQLiteOpenHelper', 'Room', 'sqflite']),
190
+ details: { durable, serverStore, onDevice, clientStorage: clientOnly.length > 0, saveRoutines: saveRoutes.length },
168
191
  };
169
192
  }
170
193
  /**
@@ -4,6 +4,7 @@ exports.detectMobile = detectMobile;
4
4
  const detectContext_1 = require("./detectContext");
5
5
  const readTextFileSafe_1 = require("../utils/readTextFileSafe");
6
6
  const textSearch_1 = require("../utils/textSearch");
7
+ const localStores_1 = require("./localStores");
7
8
  const absenceEvidence_1 = require("./absenceEvidence");
8
9
  /**
9
10
  * Whether this is an application that ships to somebody else's phone, and what that
@@ -71,37 +72,6 @@ const SECURE_STORAGE_SWIFT = [
71
72
  'square/valet',
72
73
  'valet',
73
74
  ];
74
- /** Local databases, by platform, that let an app open without a network. */
75
- const OFFLINE_GRADLE = ['androidx.room', 'io.realm', 'io.objectbox', 'com.squareup.sqldelight', 'app.cash.sqldelight'];
76
- const OFFLINE_SWIFT = [
77
- 'groue/grdb.swift',
78
- 'grdb.swift',
79
- 'stephencelis/sqlite.swift',
80
- 'sqlite.swift',
81
- 'realm/realm-swift',
82
- 'realm/realm-cocoa',
83
- 'realmswift',
84
- ];
85
- /**
86
- * Local storage the platform itself provides, which no dependency list will hold.
87
- *
88
- * Every entry in the lists above is a third-party database, and the two most widely
89
- * used stores on these platforms ship with the operating system: Core Data on Apple's,
90
- * `SQLiteDatabase` on Android's. Measured on two real applications whose whole purpose
91
- * is working without a network — WordPress-iOS keeps its posts in Core Data,
92
- * thunderbird-android keeps its mail in SQLite — and both were told at `high` that they
93
- * store nothing locally.
94
- *
95
- * These are types the platform defines, not names an author picked: `NSManagedObjectContext`
96
- * belongs to Core Data and appears nowhere else, and `getWritableDatabase()` is the one
97
- * way an Android application opens its own database.
98
- */
99
- const PLATFORM_LOCAL_STORES = [
100
- /NSPersistentContainer|NSManagedObjectContext|NSPersistentStoreCoordinator/,
101
- /ModelContainer\s*\(|@Model\b/,
102
- /SQLiteOpenHelper|getWritableDatabase\s*\(|getReadableDatabase\s*\(/,
103
- /android\.database\.sqlite\.SQLiteDatabase/,
104
- ];
105
75
  /** Dependencies that put a secret somewhere the operating system protects. */
106
76
  const SECURE_STORAGE_DEPS = [
107
77
  'flutter_secure_storage',
@@ -111,22 +81,6 @@ const SECURE_STORAGE_DEPS = [
111
81
  '@capacitor/preferences',
112
82
  'capacitor-secure-storage-plugin',
113
83
  ];
114
- /** Dependencies whose whole purpose is that the app works with no network. */
115
- const OFFLINE_DEPS = [
116
- 'sqflite',
117
- 'drift',
118
- 'hive',
119
- 'isar',
120
- 'objectbox',
121
- 'realm',
122
- 'watermelondb',
123
- '@nozbe/watermelondb',
124
- 'react-native-mmkv',
125
- '@react-native-async-storage/async-storage',
126
- 'redux-persist',
127
- '@tanstack/query-persist-client-core',
128
- 'powersync',
129
- ];
130
84
  /**
131
85
  * Paths that contain a platform marker without being a project.
132
86
  *
@@ -323,19 +277,15 @@ async function detectMobile(ctx) {
323
277
  line: match.line,
324
278
  })),
325
279
  ];
326
- const offlineDeps = [
327
- ...(0, detectContext_1.hasAnyDep)(ctx, OFFLINE_DEPS),
328
- ...(0, detectContext_1.hasAnyDartDep)(ctx, OFFLINE_DEPS),
329
- ...(0, detectContext_1.hasAnyGradleDep)(ctx, OFFLINE_GRADLE),
330
- ...(0, detectContext_1.hasAnySwiftDep)(ctx, OFFLINE_SWIFT),
331
- ];
280
+ const localStores = await (0, localStores_1.readLocalStores)(ctx);
281
+ const offlineDeps = localStores.dependencies;
332
282
  /**
333
283
  * A local database is the strong signal; knowing the network dropped is the weak
334
284
  * one. Both are recorded, because an app that stores nothing but tells the user the
335
285
  * connection is gone is a different thing from one that shows a spinner forever.
336
286
  */
337
287
  const connectivityChecks = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/Connectivity\(\)/, /connectivity_plus/, /NetInfo\./, /navigator\.onLine/, /NWPathMonitor/, /isReachable/], 3);
338
- const platformStores = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, PLATFORM_LOCAL_STORES, 3);
288
+ const platformStores = localStores.uses;
339
289
  const offlineEvidence = [
340
290
  ...offlineDeps.map((dep) => ({ type: 'dependency', value: dep })),
341
291
  ...platformStores.map((match) => ({
@@ -404,6 +354,17 @@ async function detectMobile(ctx) {
404
354
  {
405
355
  key: 'mobile.privacyDeclaration',
406
356
  present: privacyFiles.length > 0,
357
+ /**
358
+ * On Android the declaration is not in the repository, and never was.
359
+ *
360
+ * Apple has required `PrivacyInfo.xcprivacy` in the bundle since 2024, so an iOS
361
+ * project either has the file or has not made the declaration. Play's data-safety
362
+ * form is filled in the console: there is no committed artifact to find, and no
363
+ * convention that puts one in the tree. thunderbird-android was told it was
364
+ * missing a file it has no way to have — a question this analyzer cannot ask
365
+ * rather than an answer of no.
366
+ */
367
+ unanswered: privacyFiles.length === 0 && !platforms.includes('ios'),
407
368
  evidence: (0, absenceEvidence_1.evidenceOrSearch)(privacyFiles.map((file) => ({ type: 'file', value: file, file })), 'the privacy declaration the stores require', ['PrivacyInfo.xcprivacy', 'a data-safety declaration', 'privacy_policy', 'PRIVACY.md']),
408
369
  },
409
370
  ];
@@ -0,0 +1,40 @@
1
+ import type { DetectContext } from './detectContext';
2
+ /**
3
+ * Where an application on somebody else's device writes things down.
4
+ *
5
+ * One list, read by the two capabilities that both ask this question and used to
6
+ * answer it differently: `mobile.offline` — can this open with no network — and
7
+ * `app.state-durability` — does the person's work survive. A second copy would drift,
8
+ * and it already had: durability was written for a game in a browser and knew only
9
+ * `localStorage` and four npm packages, so every native application measured came back
10
+ * `partial` while keeping its data in Core Data or SQLite.
11
+ */
12
+ /** Local databases declared as dependencies, by ecosystem. */
13
+ export declare const LOCAL_STORE_GRADLE: string[];
14
+ export declare const LOCAL_STORE_SWIFT: string[];
15
+ export declare const LOCAL_STORE_DEPS: string[];
16
+ /**
17
+ * Storage the platform itself provides, which no dependency list will hold.
18
+ *
19
+ * Every entry above is a third-party database, and the two most widely used stores on
20
+ * these platforms ship with the operating system: Core Data on Apple's,
21
+ * `SQLiteDatabase` on Android's. Measured on two applications whose whole purpose is
22
+ * working without a network — WordPress-iOS keeps its posts in Core Data,
23
+ * thunderbird-android keeps its mail in SQLite — and both were told they store nothing
24
+ * locally.
25
+ *
26
+ * Types the platform defines, not names an author picked: `NSManagedObjectContext`
27
+ * belongs to Core Data and appears nowhere else, and `getWritableDatabase()` is the one
28
+ * way an Android application opens its own database.
29
+ */
30
+ export declare const PLATFORM_LOCAL_STORES: RegExp[];
31
+ export interface LocalStoreReading {
32
+ dependencies: string[];
33
+ uses: Array<{
34
+ file: string;
35
+ line: number;
36
+ snippet: string;
37
+ }>;
38
+ }
39
+ /** Both halves of the question, so a caller can cite whichever it found. */
40
+ export declare function readLocalStores(ctx: DetectContext, limit?: number): Promise<LocalStoreReading>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLATFORM_LOCAL_STORES = exports.LOCAL_STORE_DEPS = exports.LOCAL_STORE_SWIFT = exports.LOCAL_STORE_GRADLE = void 0;
4
+ exports.readLocalStores = readLocalStores;
5
+ const detectContext_1 = require("./detectContext");
6
+ const textSearch_1 = require("../utils/textSearch");
7
+ /**
8
+ * Where an application on somebody else's device writes things down.
9
+ *
10
+ * One list, read by the two capabilities that both ask this question and used to
11
+ * answer it differently: `mobile.offline` — can this open with no network — and
12
+ * `app.state-durability` — does the person's work survive. A second copy would drift,
13
+ * and it already had: durability was written for a game in a browser and knew only
14
+ * `localStorage` and four npm packages, so every native application measured came back
15
+ * `partial` while keeping its data in Core Data or SQLite.
16
+ */
17
+ /** Local databases declared as dependencies, by ecosystem. */
18
+ exports.LOCAL_STORE_GRADLE = ['androidx.room', 'io.realm', 'io.objectbox', 'com.squareup.sqldelight', 'app.cash.sqldelight', 'androidx.datastore'];
19
+ exports.LOCAL_STORE_SWIFT = [
20
+ 'groue/grdb.swift',
21
+ 'grdb.swift',
22
+ 'stephencelis/sqlite.swift',
23
+ 'sqlite.swift',
24
+ 'realm/realm-swift',
25
+ 'realm/realm-cocoa',
26
+ 'realmswift',
27
+ ];
28
+ exports.LOCAL_STORE_DEPS = [
29
+ 'sqflite',
30
+ 'drift',
31
+ 'hive',
32
+ 'isar',
33
+ 'objectbox',
34
+ 'realm',
35
+ 'watermelondb',
36
+ '@nozbe/watermelondb',
37
+ 'react-native-mmkv',
38
+ '@react-native-async-storage/async-storage',
39
+ 'redux-persist',
40
+ '@tanstack/query-persist-client-core',
41
+ 'powersync',
42
+ ];
43
+ /**
44
+ * Storage the platform itself provides, which no dependency list will hold.
45
+ *
46
+ * Every entry above is a third-party database, and the two most widely used stores on
47
+ * these platforms ship with the operating system: Core Data on Apple's,
48
+ * `SQLiteDatabase` on Android's. Measured on two applications whose whole purpose is
49
+ * working without a network — WordPress-iOS keeps its posts in Core Data,
50
+ * thunderbird-android keeps its mail in SQLite — and both were told they store nothing
51
+ * locally.
52
+ *
53
+ * Types the platform defines, not names an author picked: `NSManagedObjectContext`
54
+ * belongs to Core Data and appears nowhere else, and `getWritableDatabase()` is the one
55
+ * way an Android application opens its own database.
56
+ */
57
+ exports.PLATFORM_LOCAL_STORES = [
58
+ /NSPersistentContainer|NSManagedObjectContext|NSPersistentStoreCoordinator/,
59
+ /ModelContainer\s*\(|@Model\b/,
60
+ /SQLiteOpenHelper|getWritableDatabase\s*\(|getReadableDatabase\s*\(/,
61
+ /android\.database\.sqlite\.SQLiteDatabase/,
62
+ ];
63
+ /** Both halves of the question, so a caller can cite whichever it found. */
64
+ async function readLocalStores(ctx, limit = 3) {
65
+ return {
66
+ dependencies: [
67
+ ...(0, detectContext_1.hasAnyDep)(ctx, exports.LOCAL_STORE_DEPS),
68
+ ...(0, detectContext_1.hasAnyDartDep)(ctx, exports.LOCAL_STORE_DEPS),
69
+ ...(0, detectContext_1.hasAnyGradleDep)(ctx, exports.LOCAL_STORE_GRADLE),
70
+ ...(0, detectContext_1.hasAnySwiftDep)(ctx, exports.LOCAL_STORE_SWIFT),
71
+ ],
72
+ uses: await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, exports.PLATFORM_LOCAL_STORES, limit),
73
+ };
74
+ }
@@ -224,6 +224,16 @@ function deriveStatus(analysis, capability) {
224
224
  return 'present';
225
225
  if (matches.some((m) => m.present))
226
226
  return 'partial';
227
+ /**
228
+ * Nothing found, and at least one detector says it could not look.
229
+ *
230
+ * The specific rules in `rules.ts` grew this in 0.74.0 one claim at a time; this
231
+ * is the same rule for every capability that reaches the generic path. A finding
232
+ * is only ever weakened by it: `present` and `partial` are decided above, so
233
+ * blindness can turn `missing` into `unknown` and nothing else.
234
+ */
235
+ if (matches.some((m) => m.unanswered))
236
+ return 'unknown';
227
237
  return 'missing';
228
238
  }
229
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.75.0",
3
+ "version": "0.77.0",
4
4
  "description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
5
5
  "license": "MIT",
6
6
  "bin": {