scenri 0.9.4 → 0.10.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/{chunk-3ZRMKVBB.js → chunk-WORM6GB3.js} +128 -3
  3. package/dist/{cli-MQO66C5O.js → cli-EXASA5BK.js} +3 -3
  4. package/dist/index.js +2 -2
  5. package/dist/serve.js +2156 -336
  6. package/dist/{src-3ITTV2UE.js → src-4N4TS3HO.js} +3 -3
  7. package/package.json +1 -1
  8. package/studio-dist/assets/facial-2-BmVnDzjJ.webp +0 -0
  9. package/studio-dist/assets/facial-3-DZtKCJBc.webp +0 -0
  10. package/studio-dist/assets/facial-4-D3bBk4Y-.webp +0 -0
  11. package/studio-dist/assets/facial-5-BZUOhBlt.webp +0 -0
  12. package/studio-dist/assets/facial-6-BjNlybGd.webp +0 -0
  13. package/studio-dist/assets/freckles-1-EZ0x3-vK.webp +0 -0
  14. package/studio-dist/assets/freckles-2-DuHEq26I.webp +0 -0
  15. package/studio-dist/assets/freckles-3-WGP6r0Fh.webp +0 -0
  16. package/studio-dist/assets/glasses-1-DPlo67j6.webp +0 -0
  17. package/studio-dist/assets/glasses-2-mRgaHYWV.webp +0 -0
  18. package/studio-dist/assets/glasses-3-DuTs0m8y.webp +0 -0
  19. package/studio-dist/assets/glasses-4-DGjoOjFS.webp +0 -0
  20. package/studio-dist/assets/glasses-5-DoEVdOaY.webp +0 -0
  21. package/studio-dist/assets/glasses-6-CcUVUrfz.webp +0 -0
  22. package/studio-dist/assets/{index-BGts89lc.css → index-CVbs0aDq.css} +1 -1
  23. package/studio-dist/assets/index-CyQ6cSKd.js +107 -0
  24. package/studio-dist/assets/length-1-androgynous-DiFL-HbJ.webp +0 -0
  25. package/studio-dist/assets/length-1-man-OI1u_7yA.webp +0 -0
  26. package/studio-dist/assets/length-1-woman-BbYkzjnG.webp +0 -0
  27. package/studio-dist/assets/length-2-androgynous-zBgi1XzH.webp +0 -0
  28. package/studio-dist/assets/length-2-man-Df3fVDUO.webp +0 -0
  29. package/studio-dist/assets/length-2-woman-BP1NSRcI.webp +0 -0
  30. package/studio-dist/assets/length-3-androgynous-BN_SLD6d.webp +0 -0
  31. package/studio-dist/assets/length-3-man-Dk_3OkKz.webp +0 -0
  32. package/studio-dist/assets/length-3-woman-BFV6b4If.webp +0 -0
  33. package/studio-dist/assets/length-4-androgynous-BblLWRi-.webp +0 -0
  34. package/studio-dist/assets/length-4-man-Cszq08t7.webp +0 -0
  35. package/studio-dist/assets/length-4-woman-B8gzWrbX.webp +0 -0
  36. package/studio-dist/assets/length-5-androgynous-DwxP_mgy.webp +0 -0
  37. package/studio-dist/assets/length-5-man-Yt6YRiWE.webp +0 -0
  38. package/studio-dist/assets/length-5-woman-BTz0YJMm.webp +0 -0
  39. package/studio-dist/assets/length-6-androgynous-BD7OoH6P.webp +0 -0
  40. package/studio-dist/assets/length-6-man-oceHSSPF.webp +0 -0
  41. package/studio-dist/assets/length-6-woman-BvX37wCz.webp +0 -0
  42. package/studio-dist/assets/makeup-1-CvGOSJO_.webp +0 -0
  43. package/studio-dist/assets/makeup-3-yoncgA8G.webp +0 -0
  44. package/studio-dist/assets/makeup-4-Buulx0wj.webp +0 -0
  45. package/studio-dist/assets/makeup-5-5Y_Qpn53.webp +0 -0
  46. package/studio-dist/assets/makeup-6-CSQm_m8e.webp +0 -0
  47. package/studio-dist/assets/prosthetic-1-C-bbKoKE.webp +0 -0
  48. package/studio-dist/assets/prosthetic-2-DCGXnKzb.webp +0 -0
  49. package/studio-dist/assets/prosthetic-3-BW_gdjlG.webp +0 -0
  50. package/studio-dist/assets/prosthetic-4-ec_5-7Tb.webp +0 -0
  51. package/studio-dist/assets/prosthetic-5-res5u7gj.webp +0 -0
  52. package/studio-dist/assets/prosthetic-6-0AvaEGMf.webp +0 -0
  53. package/studio-dist/assets/tattoo-3-33-3gd4C.webp +0 -0
  54. package/studio-dist/assets/tattoo-4-BW22hxdb.webp +0 -0
  55. package/studio-dist/assets/tattoo-5-BWJwjBfm.webp +0 -0
  56. package/studio-dist/index.html +2 -2
  57. package/studio-dist/assets/index-uiX1mW43.js +0 -104
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/tonygorb/Scenri/compare/v0.9.4...v0.10.0) (2026-09-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * presenter creation as a conversation, with a view and an editor ([39e1a36](https://github.com/tonygorb/Scenri/commit/39e1a3633ab27712a628508fd5c1db51c3bcfb51))
9
+ * **presenter:** the studio arrives the way everything else you make here does ([7fee9b7](https://github.com/tonygorb/Scenri/commit/7fee9b77160475490c5e4dc39dd19725f7e2ba53))
10
+ * **studio:** show an import where you are, and fill the wall as it lands ([667d8ac](https://github.com/tonygorb/Scenri/commit/667d8acd913818400b5ea7fee0502439e06fc0e9))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **brand:** teach the brand scraper the manners the crawler already had ([b62f4da](https://github.com/tonygorb/Scenri/commit/b62f4da7d929dc9a3fdbdba5c4840e5bba3c9c84))
16
+ * **catalog:** a card asks for a thumbnail, not the picture it will import ([2a91200](https://github.com/tonygorb/Scenri/commit/2a91200a96cc240e7b0e144ece6670747efccee3))
17
+ * **catalog:** capture the site's own icon, and open the import from its dock ([cb76556](https://github.com/tonygorb/Scenri/commit/cb76556c1b85bd001deffbc9e1f3f35595bca679))
18
+ * **catalog:** count a catalogue the budget can actually reach ([8ba5d85](https://github.com/tonygorb/Scenri/commit/8ba5d85111e46c6c4a0afb7c48c9b972c067a168))
19
+ * **catalog:** count the pictures a run owes, not the ones it has looked at ([7913f21](https://github.com/tonygorb/Scenri/commit/7913f21949ea58823f0eab1a19bd175a84ae376c))
20
+ * **catalog:** count what a person is missing, in the unit they asked in ([e763b93](https://github.com/tonygorb/Scenri/commit/e763b93cdf0cfce12a84fe6a5e703e55bc7db973))
21
+ * **catalog:** read a guarded endpoint as guarded, not as a busy one ([1643170](https://github.com/tonygorb/Scenri/commit/1643170dcb93c80cc06d5be565e85d6aa7dfdc9a))
22
+ * **catalog:** reliable website import — accurate counts, guarded endpoints, live progress ([37658f5](https://github.com/tonygorb/Scenri/commit/37658f53a846d42bc87b7fc522c68c589aef20a6))
23
+ * **cli:** a revision does not carry a picture of the face it replaced ([072a353](https://github.com/tonygorb/Scenri/commit/072a353f8c43328df3e8b066a1f78637398c5394))
24
+ * **cli:** putting a picture back is a swap, not a decision to build the next view ([12d1787](https://github.com/tonygorb/Scenri/commit/12d17879e711a8275e34f27879030f5082226439))
25
+ * **composer:** a chip for a presenter who is gone says so ([58de149](https://github.com/tonygorb/Scenri/commit/58de1497623ab4b512eca3a3ac7e77d73a067f24))
26
+ * **core:** a draft becoming a saved presenter is one write ([970c0dd](https://github.com/tonygorb/Scenri/commit/970c0dd2c1a571ba524a719bf05c4b06d653f454))
27
+ * **presenter:** "Describe someone instead" describes someone ([b115d2e](https://github.com/tonygorb/Scenri/commit/b115d2e70a1e0642b4350d74c1149ba3804539b8))
28
+ * **presenter:** a record from before the studio opens, reads right, and can be drawn ([24f6ae1](https://github.com/tonygorb/Scenri/commit/24f6ae1dfaa3a92148c9caad2c915965eb702755))
29
+ * **presenter:** a reload is not a way of holding state ([3f7231d](https://github.com/tonygorb/Scenri/commit/3f7231dd6d5e51ae71ac46dbf58dcd2dc6df77fa))
30
+ * **presenter:** a view drawn from a face that moved lands stale, not current ([ce7d5fe](https://github.com/tonygorb/Scenri/commit/ce7d5fe6b40c93173a937095902089803352fe4b))
31
+ * **presenter:** an answer is installed only for the draft it was asked about ([c62ae1a](https://github.com/tonygorb/Scenri/commit/c62ae1ac9a261d319295f30e76f113bec1a6f845))
32
+ * **presenter:** arriving at a draft that is drawing reads its conversation ([edec9e3](https://github.com/tonygorb/Scenri/commit/edec9e35d213a0e6db30f78792cd1cffd098daff))
33
+ * **presenter:** Create presenter is a new presenter, every time ([4188c42](https://github.com/tonygorb/Scenri/commit/4188c4230865981366fb3ccccfe4c55ea40788b3))
34
+ * **presenter:** deleting a presenter reaches every surface, and takes its session with it ([73ceea2](https://github.com/tonygorb/Scenri/commit/73ceea292f3c124aeb85903dc0b40677e1cdf4c7))
35
+ * **presenter:** opening a draft does not ask a question it is about to withdraw ([4e4c6a4](https://github.com/tonygorb/Scenri/commit/4e4c6a427797f721cdb936474f3d7778a4de3a6b))
36
+ * **presenter:** the close on a phone head sits on the screen, and in the middle of the bar ([54634bd](https://github.com/tonygorb/Scenri/commit/54634bdcd5a050ad3cabc032bef67a68d22e001f))
37
+ * **presenter:** the editor gets the latch the creation flow already has ([56119c7](https://github.com/tonygorb/Scenri/commit/56119c7731ab71d69806a99944531b33235eedf3))
38
+ * **presenter:** the picture is simply there, and a picture put back brings its view up ([8665646](https://github.com/tonygorb/Scenri/commit/866564696cab338829c77dee9851bb65a69f7cef))
39
+ * **presenter:** the question over a picture put back says what happened to it ([d678a96](https://github.com/tonygorb/Scenri/commit/d678a969aa92260d0d02f6dcb615b78eb477dd80))
40
+ * **presenter:** the stage is the shape of the picture, so a bad draw is not a crop ([52f6bde](https://github.com/tonygorb/Scenri/commit/52f6bde7b6dfbade987b1e24324315c62a950a48))
41
+ * **presenter:** throwing an unfinished person away asks first ([9226470](https://github.com/tonygorb/Scenri/commit/9226470fe7cb0001695699f446d5e4c2f16ad579))
42
+ * **presenter:** without an engine, the photo is the face rather than a dead end ([8ddae49](https://github.com/tonygorb/Scenri/commit/8ddae49a74a191e126024d65340ee89ab78d84c9))
43
+ * **studio:** a discarded draft hands its place on, instead of dropping focus ([5180237](https://github.com/tonygorb/Scenri/commit/5180237bcbf18d2d76eff0cbe64f6644084ad071))
44
+ * **studio:** a kit you walk away from is not a brand you made ([03b3613](https://github.com/tonygorb/Scenri/commit/03b36139699e665669e75d7722c170930c8ec87b))
45
+ * **studio:** close the silent paths through onboarding and import ([441d310](https://github.com/tonygorb/Scenri/commit/441d310ecdd6e30ab63f117c19507036bff06513))
46
+ * **studio:** count one thing in the task row, and say what a stop kept ([0aba7e5](https://github.com/tonygorb/Scenri/commit/0aba7e5ee42b1a05b9a4635a2557c3a0bca2a5c3))
47
+ * **studio:** offer the products a listing found, not only the ones read ([4ea36ef](https://github.com/tonygorb/Scenri/commit/4ea36ef0890a854e19375a865e2c8190e97b78c7))
48
+ * **studio:** one press is one act, for Enter and for a confirm ([d43a8c7](https://github.com/tonygorb/Scenri/commit/d43a8c7f99a083f2d8d559d02475f89e03952464))
49
+ * **studio:** say what was kept, not only what went wrong ([70f1855](https://github.com/tonygorb/Scenri/commit/70f1855ce4945d72f56bf43ece8080b5050cafa5))
50
+ * **studio:** say when the look for a shop did not finish ([be0e2f6](https://github.com/tonygorb/Scenri/commit/be0e2f600673f3f1c8f8aef36d80b7cba0430212))
51
+ * **studio:** Start over keeps drawn work, and lets go of a draft with none ([f5d1328](https://github.com/tonygorb/Scenri/commit/f5d132824d030ab604cf52a97f71c5a73118464f))
52
+ * **studio:** the import pill wears the brand's mark and a clock ([4b78b1a](https://github.com/tonygorb/Scenri/commit/4b78b1a6e93780e5389ce2990af929c3dc515638))
53
+ * **studio:** the presenter studio says what it will spend, like the other two flows ([3bb039c](https://github.com/tonygorb/Scenri/commit/3bb039cf22c5950fd5226f22b93405477a216a4c))
54
+ * **test:** keep the private repo out of the battery drivers' defaults ([cd0f674](https://github.com/tonygorb/Scenri/commit/cd0f674d4f7b98490bf70c840ef8c9292c5f8f53))
55
+ * **test:** read the reference image's basename, not its POSIX split ([442cc07](https://github.com/tonygorb/Scenri/commit/442cc07bdedb02c62a695b9c22ac090caee2d890))
56
+ * **test:** the analyzer's imagePaths assertion had the same split('/') bug ([7877088](https://github.com/tonygorb/Scenri/commit/787708802c94f46be158c4f3e93c147e0973ee97))
57
+
58
+
59
+ ### Performance Improvements
60
+
61
+ * **catalog:** ask the listing for a chosen set, not a page each ([cfe9a3c](https://github.com/tonygorb/Scenri/commit/cfe9a3c3aa7411ae1e8fe6d5002a2f1b4397de53))
62
+ * **catalog:** keep the catalogue the store already handed over ([9630378](https://github.com/tonygorb/Scenri/commit/9630378ed2e076f6cb9b7ce50dde28c68e615548))
63
+
3
64
  ## [0.9.4](https://github.com/tonygorb/Scenri/compare/v0.9.3...v0.9.4) (2026-09-14)
4
65
 
5
66
 
@@ -2,7 +2,7 @@ import { homedir } from 'os';
2
2
  import { join } from 'path';
3
3
  import Database from 'better-sqlite3';
4
4
  import { randomUUID, createHash } from 'crypto';
5
- import { mkdirSync, existsSync, chmodSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'fs';
5
+ import { mkdirSync, existsSync, chmodSync, rmSync, readFileSync, writeFileSync, readdirSync } from 'fs';
6
6
 
7
7
  // ../core/src/index.ts
8
8
 
@@ -173,6 +173,14 @@ CREATE TABLE IF NOT EXISTS import_jobs (
173
173
  finished_at TEXT
174
174
  );
175
175
  CREATE INDEX IF NOT EXISTS idx_import_jobs_brand ON import_jobs(brand_id);
176
+ CREATE TABLE IF NOT EXISTS presenter_drafts (
177
+ id TEXT PRIMARY KEY,
178
+ brand_id TEXT NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
179
+ json TEXT NOT NULL,
180
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
181
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
182
+ );
183
+ CREATE INDEX IF NOT EXISTS idx_presenter_drafts_brand ON presenter_drafts(brand_id);
176
184
  `;
177
185
  function widenNodeStatusCheck(db) {
178
186
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='nodes'").get();
@@ -577,6 +585,12 @@ function createImageStore(homeDir) {
577
585
  },
578
586
  has(hash) {
579
587
  return /^[a-f0-9]{32}$/.test(hash) && EXTS.some((ext) => existsSync(fileFor(hash, ext)));
588
+ },
589
+ remove(hash) {
590
+ const file = this.pathFor(hash);
591
+ if (!existsSync(file)) return false;
592
+ rmSync(file, { force: true });
593
+ return true;
580
594
  }
581
595
  };
582
596
  }
@@ -674,6 +688,9 @@ function uniqueSetSlug(db, brandId, name, id) {
674
688
  return firstFree(slugifyWithId(name, id, "set"), (c) => !!stmt.get(brandId, c, id));
675
689
  }
676
690
  var SET_NAME_SEP = String.fromCharCode(31);
691
+ function rowToPresenterDraft(r) {
692
+ return { id: r.id, brandId: r.brand_id, json: JSON.parse(r.json), createdAt: r.created_at, updatedAt: r.updated_at };
693
+ }
677
694
  function rowToSet(r) {
678
695
  return {
679
696
  id: r.id,
@@ -850,6 +867,21 @@ function createStore(db) {
850
867
  );
851
868
  return this.getBrand(id);
852
869
  },
870
+ /**
871
+ * A draft becoming a saved asset: the document and the row move together.
872
+ *
873
+ * These were two statements with nothing holding them. A crash between
874
+ * them left the presenter in the brand AND its draft row alive, so the
875
+ * wall showed a finished person and an unfinished one, and saving that
876
+ * draft again appended a second copy of the same person.
877
+ */
878
+ updateBrandAndDropPresenterDraft(id, json, draftId) {
879
+ return db.transaction(() => {
880
+ const row = this.updateBrand(id, json);
881
+ this.deletePresenterDraft(draftId);
882
+ return row;
883
+ })();
884
+ },
853
885
  deleteBrand(id) {
854
886
  db.prepare("DELETE FROM brands WHERE id=?").run(id);
855
887
  },
@@ -972,6 +1004,42 @@ function createStore(db) {
972
1004
  membersOf(setId) {
973
1005
  return db.prepare("SELECT node_id FROM set_nodes WHERE set_id=? ORDER BY added_at, node_id").all(setId).map((r) => r.node_id);
974
1006
  },
1007
+ // presenter drafts
1008
+ /** Upsert: the same id replaces the json and moves updated_at forward; created_at stays. */
1009
+ putPresenterDraft(row) {
1010
+ db.prepare(
1011
+ // Millisecond stamps, the nodes table's own format: a draft is touched
1012
+ // on every step, and two touches inside one second must still order.
1013
+ `INSERT INTO presenter_drafts (id, brand_id, json, created_at, updated_at)
1014
+ VALUES (?,?,?, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'))
1015
+ ON CONFLICT(id) DO UPDATE SET json=excluded.json, updated_at=strftime('%Y-%m-%d %H:%M:%f','now')`
1016
+ ).run(row.id, row.brandId, JSON.stringify(row.json));
1017
+ return this.getPresenterDraft(row.id);
1018
+ },
1019
+ getPresenterDraft(id) {
1020
+ const r = db.prepare("SELECT * FROM presenter_drafts WHERE id=?").get(id);
1021
+ return r ? rowToPresenterDraft(r) : null;
1022
+ },
1023
+ /** Most recently touched first, the same order sets keep. */
1024
+ listPresenterDrafts(brandId) {
1025
+ return db.prepare("SELECT * FROM presenter_drafts WHERE brand_id=? ORDER BY updated_at DESC, created_at DESC, id").all(brandId).map(rowToPresenterDraft);
1026
+ },
1027
+ deletePresenterDraft(id) {
1028
+ db.prepare("DELETE FROM presenter_drafts WHERE id=?").run(id);
1029
+ },
1030
+ /**
1031
+ * Whether any row still points at a stored image: a shot's images, a
1032
+ * shot's brief (source image, attachments), an imported catalog image, or
1033
+ * another presenter draft. The store is content-addressed, so the same
1034
+ * photo uploaded twice is one file; whoever removes an image asks this
1035
+ * first, and the brand documents are the caller's half of the answer.
1036
+ */
1037
+ imageReferenced(hash) {
1038
+ if (!/^[a-f0-9]{32}$/.test(hash)) return false;
1039
+ const like = `%${hash}%`;
1040
+ const hit = (sql, ...args) => !!db.prepare(sql).get(...args);
1041
+ return hit("SELECT 1 FROM nodes WHERE images LIKE ? LIMIT 1", `%"${hash}"%`) || hit("SELECT 1 FROM nodes WHERE brief LIKE ? LIMIT 1", like) || hit("SELECT 1 FROM catalog_images WHERE asset_ref LIKE ? LIMIT 1", like) || hit("SELECT 1 FROM presenter_drafts WHERE json LIKE ? LIMIT 1", like);
1042
+ },
975
1043
  // nodes / version tree
976
1044
  addNode(input) {
977
1045
  if (input.parentId) {
@@ -1711,6 +1779,24 @@ function productImportMethods(db) {
1711
1779
  WHERE product_id=? AND source_url=?`
1712
1780
  ).run(assetRef, meta?.width ?? null, meta?.height ?? null, productId, sourceUrl);
1713
1781
  },
1782
+ /**
1783
+ * How many pictures are still owed, for a progress that can be trusted.
1784
+ *
1785
+ * The importer used to add up the rounds it had fetched and call that the
1786
+ * total, so the total was really "pictures discovered so far": it grew by
1787
+ * sixty every round, and the fraction fell back every time it did. A run
1788
+ * went 60/60, then 64/120, then 122/172 - a bar sliding backwards twice
1789
+ * while nothing had gone wrong. Counting what is left is one query and it
1790
+ * is the actual answer.
1791
+ */
1792
+ countImagesNeedingAssets(brandId, imagesPerProduct) {
1793
+ const row = db.prepare(
1794
+ `SELECT COUNT(*) AS n FROM catalog_images i
1795
+ JOIN catalog_products p ON p.id = i.product_id
1796
+ WHERE p.brand_id=? AND (i.asset_ref IS NULL OR i.asset_ref='') AND i.position < ?`
1797
+ ).get(brandId, imagesPerProduct);
1798
+ return row?.n ?? 0;
1799
+ },
1714
1800
  listImagesNeedingAssets(brandId, limit = 500) {
1715
1801
  return db.prepare(
1716
1802
  `SELECT i.* FROM catalog_images i
@@ -1730,6 +1816,38 @@ function productImportMethods(db) {
1730
1816
  excluded: !!r.excluded
1731
1817
  }));
1732
1818
  },
1819
+ /**
1820
+ * Hide pictures that turn up on product after product.
1821
+ *
1822
+ * A photograph of a thing belongs to that thing. A picture the crawl found
1823
+ * on eighteen different product pages is the shop talking, not the
1824
+ * product: a promotion flash, a collection sticker, a delivery badge.
1825
+ * Measured on a real storefront, three addresses out of 265 were shared
1826
+ * past this threshold and all three were merchandising furniture, while
1827
+ * every genuine packshot appeared once.
1828
+ *
1829
+ * Names nothing and knows nothing about any platform, which is the point:
1830
+ * the badges that caused this carry no word a filter could match, and the
1831
+ * next store's will be different words.
1832
+ *
1833
+ * Excluded rather than deleted, the same state the picture chooser uses,
1834
+ * so anything wrongly caught is still there to be put back.
1835
+ */
1836
+ excludeSharedImages(sourceId, minProducts = 3) {
1837
+ const r = db.prepare(
1838
+ `UPDATE catalog_images SET excluded=1
1839
+ WHERE excluded=0
1840
+ AND product_id IN (SELECT id FROM catalog_products WHERE source_id=?)
1841
+ AND source_url IN (
1842
+ SELECT ci.source_url FROM catalog_images ci
1843
+ JOIN catalog_products cp ON cp.id = ci.product_id
1844
+ WHERE cp.source_id=?
1845
+ GROUP BY ci.source_url
1846
+ HAVING COUNT(DISTINCT ci.product_id) >= ?
1847
+ )`
1848
+ ).run(sourceId, sourceId, minProducts);
1849
+ return r.changes;
1850
+ },
1733
1851
  markMissingUnavailable(sourceId, seenExternalKeys) {
1734
1852
  if (!seenExternalKeys.length) {
1735
1853
  const r2 = db.prepare(
@@ -1952,6 +2070,12 @@ function createCatalogStore(db) {
1952
2070
  var REFERENCE_ROLE_DIRECTIVE = {
1953
2071
  product: "the exact product \u2014 preserve its label, shape, colors and design faithfully; do not redesign it",
1954
2072
  character: "the exact person \u2014 match their face, facial structure, skin, hair and build exactly; their clothing, pose and background are capture context, not styling to reproduce",
2073
+ // One thing in the picture, and nothing else in it. A person uploads a pair
2074
+ // of glasses they like, or the exact tattoo, and every other role would have
2075
+ // handed the model a second face to blend with the first: `character` says
2076
+ // "this is who they are", `style` takes the mood of the whole frame. This
2077
+ // says take the object and leave the rest of the photograph behind.
2078
+ detail: "a reference for one detail only \u2014 match the design, shape, proportions, colours and finish of the thing itself, exactly as drawn or made. Take nothing else from this image: not the identity of anyone in it, not their face, skin, hair, body or build, not their clothing, not the background, not the lighting. The person in the picture being drawn is somebody else entirely",
1955
2079
  brand: "the brand's own mark \u2014 if the direction calls for the mark to appear, reproduce it exactly as drawn, same colours, letterforms and proportions, every character down to the smallest secondary lettering, in its original script and reading direction, never translated, transliterated or re-spelled; otherwise take only its colour and treatment, and never its subject, geometry or composition",
1956
2080
  // Only a figure-led scene attaches one of these now, so this says what that
1957
2081
  // case actually needs. It used to read "environment and light only - take no
@@ -1976,6 +2100,7 @@ var REFERENCE_ROLE_DIRECTIVE = {
1976
2100
  var EDIT_REFERENCE_ROLE_DIRECTIVE = {
1977
2101
  product: "the exact product: keep or restore its label, shape and design faithfully",
1978
2102
  character: "the exact person: keep their face, facial structure, skin, hair and build faithfully; take no clothing, pose or background from this reference, and keep the source image's existing outfit unless the instruction changes it",
2103
+ detail: "one detail to match: its design, shape, colours and finish only. Take no identity, skin, hair, body, clothing or background from it",
1979
2104
  brand: "the brand's own mark: reproduce it exactly as drawn wherever it appears \u2014 every character down to the smallest secondary lettering, in its original script and reading direction \u2014 never redrawn, re-lettered, translated or transliterated",
1980
2105
  scene: "a reference for environment, light and treatment only \u2014 take no identity from any person in it",
1981
2106
  composition: "a reference for framing and pose only",
@@ -2032,5 +2157,5 @@ function createCore(homeDir = defaultHome()) {
2032
2157
  }
2033
2158
 
2034
2159
  export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug };
2035
- //# sourceMappingURL=chunk-3ZRMKVBB.js.map
2036
- //# sourceMappingURL=chunk-3ZRMKVBB.js.map
2160
+ //# sourceMappingURL=chunk-WORM6GB3.js.map
2161
+ //# sourceMappingURL=chunk-WORM6GB3.js.map
@@ -463,7 +463,7 @@ async function addOrRepair(ownEntry, say, over) {
463
463
  }
464
464
  async function rememberDecline(home) {
465
465
  try {
466
- const { createCore } = await import('./src-3ITTV2UE.js');
466
+ const { createCore } = await import('./src-4N4TS3HO.js');
467
467
  const core = createCore(home);
468
468
  try {
469
469
  core.store.setSetting("desktop.prompt", "declined");
@@ -530,5 +530,5 @@ function tail(path, bytes = 4096) {
530
530
  }
531
531
 
532
532
  export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand };
533
- //# sourceMappingURL=cli-MQO66C5O.js.map
534
- //# sourceMappingURL=cli-MQO66C5O.js.map
533
+ //# sourceMappingURL=cli-EXASA5BK.js.map
534
+ //# sourceMappingURL=cli-EXASA5BK.js.map
package/dist/index.js CHANGED
@@ -86,12 +86,12 @@ try {
86
86
  break;
87
87
  }
88
88
  case "desktop": {
89
- const { runDesktopCommand } = await import('./cli-MQO66C5O.js');
89
+ const { runDesktopCommand } = await import('./cli-EXASA5BK.js');
90
90
  process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
91
91
  break;
92
92
  }
93
93
  case "open": {
94
- const { runOpenCommand } = await import('./cli-MQO66C5O.js');
94
+ const { runOpenCommand } = await import('./cli-EXASA5BK.js');
95
95
  process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
96
96
  break;
97
97
  }