@12ui/design 0.2.14 → 0.2.16

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 (108) hide show
  1. package/README.md +21 -1
  2. package/dist/bounded-concurrency.d.ts +13 -0
  3. package/dist/bounded-concurrency.d.ts.map +1 -0
  4. package/dist/bounded-concurrency.js +43 -0
  5. package/dist/bounded-concurrency.js.map +1 -0
  6. package/dist/branch-downloads.d.ts +10 -0
  7. package/dist/branch-downloads.d.ts.map +1 -1
  8. package/dist/branch-downloads.js +20 -0
  9. package/dist/branch-downloads.js.map +1 -1
  10. package/dist/branch-execution.d.ts +7 -0
  11. package/dist/branch-execution.d.ts.map +1 -1
  12. package/dist/branch-execution.js +175 -38
  13. package/dist/branch-execution.js.map +1 -1
  14. package/dist/branch-materialize.d.ts +23 -12
  15. package/dist/branch-materialize.d.ts.map +1 -1
  16. package/dist/branch-materialize.js +55 -38
  17. package/dist/branch-materialize.js.map +1 -1
  18. package/dist/branch-page-conversion.d.ts +109 -5
  19. package/dist/branch-page-conversion.d.ts.map +1 -1
  20. package/dist/branch-page-conversion.js +375 -179
  21. package/dist/branch-page-conversion.js.map +1 -1
  22. package/dist/branch-progress.d.ts +24 -0
  23. package/dist/branch-progress.d.ts.map +1 -1
  24. package/dist/branch-progress.js +35 -6
  25. package/dist/branch-progress.js.map +1 -1
  26. package/dist/branch-run-record.d.ts +14 -0
  27. package/dist/branch-run-record.d.ts.map +1 -1
  28. package/dist/branch-run-record.js.map +1 -1
  29. package/dist/branch-status.d.ts.map +1 -1
  30. package/dist/branch-status.js +3 -0
  31. package/dist/branch-status.js.map +1 -1
  32. package/dist/cli-arguments.d.ts.map +1 -1
  33. package/dist/cli-arguments.js +1 -0
  34. package/dist/cli-arguments.js.map +1 -1
  35. package/dist/cli-branch-command.d.ts +10 -1
  36. package/dist/cli-branch-command.d.ts.map +1 -1
  37. package/dist/cli-branch-command.js +60 -11
  38. package/dist/cli-branch-command.js.map +1 -1
  39. package/dist/cli-capabilities.d.ts +35 -2
  40. package/dist/cli-capabilities.d.ts.map +1 -1
  41. package/dist/cli-capabilities.js +38 -1
  42. package/dist/cli-capabilities.js.map +1 -1
  43. package/dist/cli-draft-command.d.ts +2 -0
  44. package/dist/cli-draft-command.d.ts.map +1 -1
  45. package/dist/cli-draft-command.js +22 -4
  46. package/dist/cli-draft-command.js.map +1 -1
  47. package/dist/cli-package-command.d.ts +13 -0
  48. package/dist/cli-package-command.d.ts.map +1 -1
  49. package/dist/cli-package-command.js +35 -8
  50. package/dist/cli-package-command.js.map +1 -1
  51. package/dist/cli-progress.d.ts +81 -5
  52. package/dist/cli-progress.d.ts.map +1 -1
  53. package/dist/cli-progress.js +108 -5
  54. package/dist/cli-progress.js.map +1 -1
  55. package/dist/cli-skill-command.d.ts.map +1 -1
  56. package/dist/cli-skill-command.js +27 -12
  57. package/dist/cli-skill-command.js.map +1 -1
  58. package/dist/cli.js +20 -10
  59. package/dist/cli.js.map +1 -1
  60. package/dist/conversion-receipt-store.d.ts +75 -0
  61. package/dist/conversion-receipt-store.d.ts.map +1 -0
  62. package/dist/conversion-receipt-store.js +143 -0
  63. package/dist/conversion-receipt-store.js.map +1 -0
  64. package/dist/conversion-receipt.d.ts +223 -0
  65. package/dist/conversion-receipt.d.ts.map +1 -0
  66. package/dist/conversion-receipt.js +352 -0
  67. package/dist/conversion-receipt.js.map +1 -0
  68. package/dist/draft-adoption.d.ts +48 -0
  69. package/dist/draft-adoption.d.ts.map +1 -0
  70. package/dist/draft-adoption.js +237 -0
  71. package/dist/draft-adoption.js.map +1 -0
  72. package/dist/draft-run.d.ts +10 -0
  73. package/dist/draft-run.d.ts.map +1 -1
  74. package/dist/draft-run.js +16 -4
  75. package/dist/draft-run.js.map +1 -1
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/legacy-skill-catalog.d.ts.map +1 -1
  81. package/dist/legacy-skill-catalog.js +65 -0
  82. package/dist/legacy-skill-catalog.js.map +1 -1
  83. package/dist/package-client.d.ts +28 -0
  84. package/dist/package-client.d.ts.map +1 -1
  85. package/dist/package-client.js +21 -1
  86. package/dist/package-client.js.map +1 -1
  87. package/dist/package-source-refusal.d.ts +17 -0
  88. package/dist/package-source-refusal.d.ts.map +1 -0
  89. package/dist/package-source-refusal.js +61 -0
  90. package/dist/package-source-refusal.js.map +1 -0
  91. package/dist/package-submission.d.ts +43 -1
  92. package/dist/package-submission.d.ts.map +1 -1
  93. package/dist/package-submission.js +32 -7
  94. package/dist/package-submission.js.map +1 -1
  95. package/dist/skill-clients.d.ts +39 -0
  96. package/dist/skill-clients.d.ts.map +1 -0
  97. package/dist/skill-clients.js +156 -0
  98. package/dist/skill-clients.js.map +1 -0
  99. package/dist/skill-installer.d.ts +2 -3
  100. package/dist/skill-installer.d.ts.map +1 -1
  101. package/dist/skill-installer.js +9 -101
  102. package/dist/skill-installer.js.map +1 -1
  103. package/package.json +1 -1
  104. package/skills/design/SKILL.md +6 -3
  105. package/skills/design-branch/SKILL.md +6 -5
  106. package/skills/design-convert/SKILL.md +3 -3
  107. package/skills/design-draft/SKILL.md +3 -3
  108. package/skills/design-search/SKILL.md +3 -3
@@ -0,0 +1,223 @@
1
+ import type { ConversionOperation } from './conversion-client.js';
2
+ import type { ConversionOutput } from './conversion-outputs.js';
3
+ import { type ConversionReceiptKey } from './conversion-receipt-store.js';
4
+ import type { PackageSubmissionResult } from './package-submission.js';
5
+ /**
6
+ * The single owner of "an image's conversion was already bought, and here is
7
+ * the proof".
8
+ *
9
+ * A branch run's approved design is viewport 1 of its first page, and the
10
+ * common flow converts that same design on its own first
11
+ * (`12ui convert <winner>.png --output layerdoc --export html`), so without a
12
+ * receipt every branch run buys an image-to-LayerDoc conversion of bytes the
13
+ * account has already paid to convert. The package accepts a finished
14
+ * conversion for a viewport (`source_conversion_id`), and the server proves the
15
+ * claim against the referenced conversion's admitted evidence before it skips
16
+ * the upload — so a false claim is a failed page, not a cheap win.
17
+ *
18
+ * This is therefore a receipt of a proved fact, not a cache: it records the
19
+ * digest of the exact bytes a succeeded conversion was admitted with. A later
20
+ * run reuses it only when the bytes it is about to submit hash identically.
21
+ * Absent proof it converts fresh.
22
+ *
23
+ * The records live in the per-user store (`conversion-receipt-store.ts`), keyed
24
+ * by that digest and the model, because the fact is about bytes and not about
25
+ * where a copy of them happens to sit. `12ui convert`, `12ui convert package`,
26
+ * and `12ui branch execute --convert` write them, `12ui branch execute
27
+ * --convert` reads them, and only this module decides what counts as proof.
28
+ *
29
+ * SUNSET — legacy sidecars. Receipts shipped in 0.2.15 as
30
+ * `<image>.conversion.json` written beside the input image. Those files are
31
+ * still READ beside a winner image for one release cycle and migrated into the
32
+ * store the first time they are seen, so an upgrade never re-buys a conversion
33
+ * a 0.2.15 convert already paid for. Nothing writes them any more. Delete the
34
+ * legacy read path — `LEGACY_CONVERSION_RECEIPT_SUFFIX`,
35
+ * `legacyConversionReceiptPath`, `readLegacyConversionReceipts`, and the
36
+ * `legacyImagePaths` argument of `invalidateConversionReceipts` — one release
37
+ * after 0.2.16; by then every receipt a 0.2.15 convert wrote has long outlived
38
+ * the ~24h conversion it names.
39
+ */
40
+ export declare const LEGACY_CONVERSION_RECEIPT_SUFFIX = ".conversion.json";
41
+ export type { ConversionReceiptKey } from './conversion-receipt-store.js';
42
+ export { resolveConversionReceiptStore } from './conversion-receipt-store.js';
43
+ /**
44
+ * The output an attachable conversion must have produced. A package converts
45
+ * every viewport to a LayerDoc and buys the page export separately, so the
46
+ * server (`PACKAGE_VIEWPORT_CHILD_OUTPUT`) refuses any attached conversion
47
+ * whose own output is `html` or any other rendered document.
48
+ */
49
+ export declare const REUSABLE_CONVERSION_OUTPUT: ConversionOutput;
50
+ /**
51
+ * How much life the referenced conversion must have left. The server refuses an
52
+ * attachment expiring within this window (`PACKAGE_SOURCE_MINIMUM_REMAINING_MS`)
53
+ * because composition reads the source run at the END of a package drive, and
54
+ * that refusal fails the page after its screens are already paid for.
55
+ * Conversions live 24h while a stored receipt sits in the user's home
56
+ * indefinitely, so an expiry that is not checked here is a page failure waiting
57
+ * for day two.
58
+ */
59
+ export declare const CONVERSION_REUSE_MINIMUM_REMAINING_MS: number;
60
+ export type ConversionReceipt = {
61
+ version: 1;
62
+ /** Digest of the EXACT bytes the conversion was admitted with. */
63
+ sha256: string;
64
+ conversionId: string;
65
+ /** A reused conversion must have run the model the package declares. */
66
+ model: string;
67
+ /**
68
+ * The operation that bought it: a branch run id, a package id, or for
69
+ * `12ui convert` the conversion's own id. An operation never reuses its own
70
+ * receipt — a branch package key is derived per run and page, so attaching a
71
+ * reuse on a resume would change the request body under a key the server
72
+ * already settled, which is `409 idempotency_conflict` on a page that was
73
+ * already paid for.
74
+ */
75
+ boughtBy: string;
76
+ /** When the referenced conversion stops being attachable at all. */
77
+ expiresAt: string;
78
+ createdAt: string;
79
+ };
80
+ export type ConversionReceiptWrite = {
81
+ /** Where the record landed, or null when nothing was written. */
82
+ path: string | null;
83
+ failure?: {
84
+ path: string;
85
+ detail: string;
86
+ };
87
+ };
88
+ /** Where 0.2.15 wrote a receipt. Read for one release cycle, never written. */
89
+ export declare const legacyConversionReceiptPath: (imagePath: string) => string;
90
+ export declare const conversionReceiptKey: (receipt: ConversionReceipt) => ConversionReceiptKey;
91
+ /**
92
+ * The stored record for exactly these bytes and this model, or nothing.
93
+ *
94
+ * An expired record is removed as it is read: it is the one expired record this
95
+ * invocation is certain about, and leaving it would have the next run read it
96
+ * again to reach the same conclusion.
97
+ */
98
+ export declare const readStoredConversionReceipt: (key: ConversionReceiptKey, options?: {
99
+ store?: string;
100
+ now?: Date;
101
+ }) => Promise<ConversionReceipt | undefined>;
102
+ /**
103
+ * Record a proved conversion. A write that fails is not a command failure: the
104
+ * receipt is an optimisation, and its absence only costs one conversion.
105
+ * Callers that can say so report the failure; the rest ignore it.
106
+ */
107
+ export declare const recordConversionReceipt: (receipt: ConversionReceipt, options?: {
108
+ store?: string;
109
+ now?: Date;
110
+ }) => Promise<ConversionReceiptWrite>;
111
+ /**
112
+ * The 0.2.15 sidecars beside these images, carried forward.
113
+ *
114
+ * Every one that is still live is copied into the store the moment it is seen,
115
+ * so the upgrade costs nothing and the next run finds the proof by digest no
116
+ * matter which copy of the image it starts from. An existing store record wins:
117
+ * it was written by a later command about the same bytes, and overwriting it
118
+ * with the sidecar would replace a fresher expiry with an older one. Migration
119
+ * is best-effort — the receipt is still honoured this run either way.
120
+ */
121
+ export declare const readLegacyConversionReceipts: (imagePaths: readonly string[], options?: {
122
+ store?: string;
123
+ now?: Date;
124
+ }) => Promise<ConversionReceipt[]>;
125
+ /**
126
+ * The proof gate. Every condition is a fact about the bytes in hand, never an
127
+ * assumption about what an earlier command probably did.
128
+ */
129
+ export declare const reusableConversionId: (args: {
130
+ receipts: readonly ConversionReceipt[];
131
+ /** Digest of the bytes this package is about to submit for the winner. */
132
+ sha256: string;
133
+ model: string;
134
+ /** The operation about to submit: its own receipt is never proof for it. */
135
+ boughtBy: string;
136
+ now?: Date;
137
+ }) => string | undefined;
138
+ /**
139
+ * The gate, over the store and over whatever the caller already holds.
140
+ *
141
+ * The store is consulted first because it is the record every command writes;
142
+ * the in-hand receipts are the 0.2.15 sidecars this run read beside the winner,
143
+ * which stay proof for one release cycle even if their migration failed.
144
+ */
145
+ export declare const findReusableConversionId: (args: {
146
+ sha256: string;
147
+ model: string;
148
+ boughtBy: string;
149
+ receipts?: readonly ConversionReceipt[];
150
+ store?: string;
151
+ now?: Date;
152
+ }) => Promise<string | undefined>;
153
+ /**
154
+ * Withdraw a receipt the SERVER disproved, so the next run does not repeat the
155
+ * dance the last one already paid a retry for.
156
+ *
157
+ * The bar is deliberately narrow: only an explicit refusal of the attachment at
158
+ * admission — the server having read the referenced conversion's durable
159
+ * evidence and rejected the claim — is proof that this receipt is no longer
160
+ * proof of anything. A network error, a timeout, or any other transient failure
161
+ * says nothing about the referenced conversion, and deleting on one of those
162
+ * would throw away a live receipt and buy its conversion again for nothing.
163
+ *
164
+ * Only records naming the disproven conversion are withdrawn: a newer record
165
+ * under the same key describes different work and is untouched. Removal is
166
+ * best-effort, because a receipt that cannot be deleted only costs the next run
167
+ * one refused attachment and one retry — the same price this run paid.
168
+ */
169
+ export declare const invalidateConversionReceipts: (args: {
170
+ conversionId: string;
171
+ /** The keys the disproven claim could have come from. */
172
+ keys?: readonly ConversionReceiptKey[];
173
+ /** Sunset: 0.2.15 sidecars beside these images, if they name it. */
174
+ legacyImagePaths?: readonly string[];
175
+ store?: string;
176
+ }) => Promise<string[]>;
177
+ /**
178
+ * What `12ui convert` leaves behind for the branch run that follows it.
179
+ *
180
+ * Only a succeeded image-to-LayerDoc conversion can stand in for a package
181
+ * viewport, so anything else records nothing at all: a receipt for an `html`
182
+ * conversion would be a claim the server refuses, and the refusal costs a whole
183
+ * page. The digest is of the bytes actually submitted, which is what `--width`
184
+ * normalization changes and what the server admitted the run with.
185
+ */
186
+ export declare const recordConvertedImageReceipt: (args: {
187
+ /** The EXACT bytes submitted, after any `--width` normalization. */
188
+ bytes: Uint8Array;
189
+ operation: Pick<ConversionOperation, "id" | "status" | "output" | "model" | "expires_at" | "input_kind">;
190
+ store?: string;
191
+ warn?: (message: string) => void;
192
+ now?: Date;
193
+ }) => Promise<ConversionReceipt | undefined>;
194
+ /**
195
+ * What `12ui convert package` leaves behind: one receipt per viewport whose own
196
+ * child conversion the package bought.
197
+ *
198
+ * A package converts each viewport to a LayerDoc and composes the page from
199
+ * those children, so every viewport of a submitted page is exactly the artifact
200
+ * a later package can attach instead of buying — the same fact the branch path
201
+ * records for the winner, for every screenshot the user named.
202
+ *
203
+ * A viewport that ATTACHED a conversion bought nothing, so it records nothing.
204
+ * The expiry reported is the package's own, which its children cannot outlive
205
+ * backwards, and the model is the one the server says it ran; without either
206
+ * fact nothing here proves a conversion is still attachable, so nothing is
207
+ * recorded and the caller is told once.
208
+ */
209
+ export declare const recordPackageConversionReceipts: (args: {
210
+ pages: readonly {
211
+ id: string;
212
+ viewports: readonly {
213
+ id: string;
214
+ bytes: Uint8Array;
215
+ sourceConversionId?: string;
216
+ }[];
217
+ }[];
218
+ result: Pick<PackageSubmissionResult, "packageId" | "model" | "expiresAt" | "pages">;
219
+ store?: string;
220
+ warn?: (message: string) => void;
221
+ now?: Date;
222
+ }) => Promise<ConversionReceipt[]>;
223
+ //# sourceMappingURL=conversion-receipt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversion-receipt.d.ts","sourceRoot":"","sources":["../src/conversion-receipt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,352 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, rm } from 'node:fs/promises';
3
+ import { conversionReceiptRecordPath, pruneConversionReceiptRecords, readConversionReceiptRecord, removeConversionReceiptRecord, resolveConversionReceiptStore, writeConversionReceiptRecord, } from './conversion-receipt-store.js';
4
+ /**
5
+ * The single owner of "an image's conversion was already bought, and here is
6
+ * the proof".
7
+ *
8
+ * A branch run's approved design is viewport 1 of its first page, and the
9
+ * common flow converts that same design on its own first
10
+ * (`12ui convert <winner>.png --output layerdoc --export html`), so without a
11
+ * receipt every branch run buys an image-to-LayerDoc conversion of bytes the
12
+ * account has already paid to convert. The package accepts a finished
13
+ * conversion for a viewport (`source_conversion_id`), and the server proves the
14
+ * claim against the referenced conversion's admitted evidence before it skips
15
+ * the upload — so a false claim is a failed page, not a cheap win.
16
+ *
17
+ * This is therefore a receipt of a proved fact, not a cache: it records the
18
+ * digest of the exact bytes a succeeded conversion was admitted with. A later
19
+ * run reuses it only when the bytes it is about to submit hash identically.
20
+ * Absent proof it converts fresh.
21
+ *
22
+ * The records live in the per-user store (`conversion-receipt-store.ts`), keyed
23
+ * by that digest and the model, because the fact is about bytes and not about
24
+ * where a copy of them happens to sit. `12ui convert`, `12ui convert package`,
25
+ * and `12ui branch execute --convert` write them, `12ui branch execute
26
+ * --convert` reads them, and only this module decides what counts as proof.
27
+ *
28
+ * SUNSET — legacy sidecars. Receipts shipped in 0.2.15 as
29
+ * `<image>.conversion.json` written beside the input image. Those files are
30
+ * still READ beside a winner image for one release cycle and migrated into the
31
+ * store the first time they are seen, so an upgrade never re-buys a conversion
32
+ * a 0.2.15 convert already paid for. Nothing writes them any more. Delete the
33
+ * legacy read path — `LEGACY_CONVERSION_RECEIPT_SUFFIX`,
34
+ * `legacyConversionReceiptPath`, `readLegacyConversionReceipts`, and the
35
+ * `legacyImagePaths` argument of `invalidateConversionReceipts` — one release
36
+ * after 0.2.16; by then every receipt a 0.2.15 convert wrote has long outlived
37
+ * the ~24h conversion it names.
38
+ */
39
+ export const LEGACY_CONVERSION_RECEIPT_SUFFIX = '.conversion.json';
40
+ export { resolveConversionReceiptStore } from './conversion-receipt-store.js';
41
+ /**
42
+ * The output an attachable conversion must have produced. A package converts
43
+ * every viewport to a LayerDoc and buys the page export separately, so the
44
+ * server (`PACKAGE_VIEWPORT_CHILD_OUTPUT`) refuses any attached conversion
45
+ * whose own output is `html` or any other rendered document.
46
+ */
47
+ export const REUSABLE_CONVERSION_OUTPUT = 'layerdoc';
48
+ /**
49
+ * How much life the referenced conversion must have left. The server refuses an
50
+ * attachment expiring within this window (`PACKAGE_SOURCE_MINIMUM_REMAINING_MS`)
51
+ * because composition reads the source run at the END of a package drive, and
52
+ * that refusal fails the page after its screens are already paid for.
53
+ * Conversions live 24h while a stored receipt sits in the user's home
54
+ * indefinitely, so an expiry that is not checked here is a page failure waiting
55
+ * for day two.
56
+ */
57
+ export const CONVERSION_REUSE_MINIMUM_REMAINING_MS = 60 * 60_000;
58
+ /** Where 0.2.15 wrote a receipt. Read for one release cycle, never written. */
59
+ export const legacyConversionReceiptPath = (imagePath) => (`${imagePath}${LEGACY_CONVERSION_RECEIPT_SUFFIX}`);
60
+ const isReceipt = (value) => {
61
+ if (!value || typeof value !== 'object')
62
+ return false;
63
+ const record = value;
64
+ return record.version === 1
65
+ && typeof record.sha256 === 'string'
66
+ && /^[a-f0-9]{64}$/u.test(record.sha256)
67
+ && typeof record.conversionId === 'string'
68
+ && record.conversionId.length > 0
69
+ && typeof record.model === 'string'
70
+ && record.model.length > 0
71
+ && typeof record.boughtBy === 'string'
72
+ && record.boughtBy.length > 0
73
+ && typeof record.expiresAt === 'string'
74
+ && Number.isFinite(Date.parse(record.expiresAt));
75
+ };
76
+ export const conversionReceiptKey = (receipt) => ({
77
+ sha256: receipt.sha256,
78
+ model: receipt.model,
79
+ });
80
+ const storeOf = (store) => store ?? resolveConversionReceiptStore();
81
+ const expiredReceipt = (receipt, now) => (Date.parse(receipt.expiresAt) <= now.getTime());
82
+ /**
83
+ * Expiry hygiene, paid for by the invocations that already write to the store.
84
+ *
85
+ * Bounded on purpose: a user who converts every day accumulates a record per
86
+ * distinct image, and a full sweep on a hot path would make the cheapest
87
+ * command the one that reads the most files. A rotating sample covers the whole
88
+ * store across invocations, and a record that survives one pass costs nothing
89
+ * but its own inode until the next.
90
+ */
91
+ const pruneExpired = async (store, now) => (await pruneConversionReceiptRecords({
92
+ store,
93
+ expired: (record) => isReceipt(record) && expiredReceipt(record, now),
94
+ }));
95
+ /**
96
+ * The stored record for exactly these bytes and this model, or nothing.
97
+ *
98
+ * An expired record is removed as it is read: it is the one expired record this
99
+ * invocation is certain about, and leaving it would have the next run read it
100
+ * again to reach the same conclusion.
101
+ */
102
+ export const readStoredConversionReceipt = async (key, options = {}) => {
103
+ const store = storeOf(options.store);
104
+ const record = await readConversionReceiptRecord(store, key);
105
+ if (!isReceipt(record))
106
+ return undefined;
107
+ if (expiredReceipt(record, options.now ?? new Date())) {
108
+ await removeConversionReceiptRecord(store, key).catch(() => null);
109
+ return undefined;
110
+ }
111
+ return record;
112
+ };
113
+ /**
114
+ * Record a proved conversion. A write that fails is not a command failure: the
115
+ * receipt is an optimisation, and its absence only costs one conversion.
116
+ * Callers that can say so report the failure; the rest ignore it.
117
+ */
118
+ export const recordConversionReceipt = async (receipt, options = {}) => {
119
+ const store = storeOf(options.store);
120
+ // Never leave behind something that cannot be read back as proof: a record
121
+ // that fails the gate is indistinguishable from a corrupt file later.
122
+ if (!isReceipt(receipt)) {
123
+ return {
124
+ path: null,
125
+ failure: {
126
+ path: store,
127
+ detail: 'the conversion did not report the digest, id, model, and expiry a receipt proves',
128
+ },
129
+ };
130
+ }
131
+ const key = conversionReceiptKey(receipt);
132
+ try {
133
+ const written = await writeConversionReceiptRecord(store, key, receipt);
134
+ await pruneExpired(store, options.now ?? new Date());
135
+ return { path: written };
136
+ }
137
+ catch (error) {
138
+ return {
139
+ path: null,
140
+ failure: {
141
+ path: conversionReceiptRecordPath(store, key),
142
+ detail: error instanceof Error ? error.message : String(error),
143
+ },
144
+ };
145
+ }
146
+ };
147
+ const readLegacyConversionReceipt = async (imagePath) => {
148
+ try {
149
+ const parsed = JSON.parse(await readFile(legacyConversionReceiptPath(imagePath), 'utf8'));
150
+ return isReceipt(parsed) ? parsed : undefined;
151
+ }
152
+ catch {
153
+ return undefined;
154
+ }
155
+ };
156
+ /**
157
+ * The 0.2.15 sidecars beside these images, carried forward.
158
+ *
159
+ * Every one that is still live is copied into the store the moment it is seen,
160
+ * so the upgrade costs nothing and the next run finds the proof by digest no
161
+ * matter which copy of the image it starts from. An existing store record wins:
162
+ * it was written by a later command about the same bytes, and overwriting it
163
+ * with the sidecar would replace a fresher expiry with an older one. Migration
164
+ * is best-effort — the receipt is still honoured this run either way.
165
+ */
166
+ export const readLegacyConversionReceipts = async (imagePaths, options = {}) => {
167
+ const now = options.now ?? new Date();
168
+ const found = await Promise.all(imagePaths.map(readLegacyConversionReceipt));
169
+ const receipts = found.filter((receipt) => (receipt !== undefined && !expiredReceipt(receipt, now)));
170
+ const store = storeOf(options.store);
171
+ const migrated = new Set();
172
+ for (const receipt of receipts) {
173
+ const key = conversionReceiptKey(receipt);
174
+ const name = `${key.sha256}/${key.model}`;
175
+ if (migrated.has(name))
176
+ continue;
177
+ migrated.add(name);
178
+ const existing = await readStoredConversionReceipt(key, { store, now });
179
+ if (existing)
180
+ continue;
181
+ await writeConversionReceiptRecord(store, key, receipt).catch(() => null);
182
+ }
183
+ return receipts;
184
+ };
185
+ /**
186
+ * The proof gate. Every condition is a fact about the bytes in hand, never an
187
+ * assumption about what an earlier command probably did.
188
+ */
189
+ export const reusableConversionId = (args) => {
190
+ const deadline = (args.now ?? new Date()).getTime() + CONVERSION_REUSE_MINIMUM_REMAINING_MS;
191
+ return args.receipts.find((receipt) => (receipt.sha256 === args.sha256
192
+ && receipt.model === args.model
193
+ && receipt.boughtBy !== args.boughtBy
194
+ && Date.parse(receipt.expiresAt) > deadline))?.conversionId;
195
+ };
196
+ /**
197
+ * The gate, over the store and over whatever the caller already holds.
198
+ *
199
+ * The store is consulted first because it is the record every command writes;
200
+ * the in-hand receipts are the 0.2.15 sidecars this run read beside the winner,
201
+ * which stay proof for one release cycle even if their migration failed.
202
+ */
203
+ export const findReusableConversionId = async (args) => {
204
+ const now = args.now ?? new Date();
205
+ const stored = await readStoredConversionReceipt({ sha256: args.sha256, model: args.model }, { ...(args.store ? { store: args.store } : {}), now });
206
+ return reusableConversionId({
207
+ receipts: [...(stored ? [stored] : []), ...(args.receipts ?? [])],
208
+ sha256: args.sha256,
209
+ model: args.model,
210
+ boughtBy: args.boughtBy,
211
+ now,
212
+ });
213
+ };
214
+ /**
215
+ * Withdraw a receipt the SERVER disproved, so the next run does not repeat the
216
+ * dance the last one already paid a retry for.
217
+ *
218
+ * The bar is deliberately narrow: only an explicit refusal of the attachment at
219
+ * admission — the server having read the referenced conversion's durable
220
+ * evidence and rejected the claim — is proof that this receipt is no longer
221
+ * proof of anything. A network error, a timeout, or any other transient failure
222
+ * says nothing about the referenced conversion, and deleting on one of those
223
+ * would throw away a live receipt and buy its conversion again for nothing.
224
+ *
225
+ * Only records naming the disproven conversion are withdrawn: a newer record
226
+ * under the same key describes different work and is untouched. Removal is
227
+ * best-effort, because a receipt that cannot be deleted only costs the next run
228
+ * one refused attachment and one retry — the same price this run paid.
229
+ */
230
+ export const invalidateConversionReceipts = async (args) => {
231
+ const store = storeOf(args.store);
232
+ const fromStore = await Promise.all((args.keys ?? []).map(async (key) => {
233
+ const record = await readConversionReceiptRecord(store, key);
234
+ if (!isReceipt(record) || record.conversionId !== args.conversionId)
235
+ return null;
236
+ return await removeConversionReceiptRecord(store, key);
237
+ }));
238
+ const fromSidecars = await Promise.all((args.legacyImagePaths ?? []).map(async (imagePath) => {
239
+ const receipt = await readLegacyConversionReceipt(imagePath);
240
+ if (receipt?.conversionId !== args.conversionId)
241
+ return null;
242
+ const receiptPath = legacyConversionReceiptPath(imagePath);
243
+ try {
244
+ await rm(receiptPath, { force: true });
245
+ return receiptPath;
246
+ }
247
+ catch {
248
+ return null;
249
+ }
250
+ }));
251
+ return [...fromStore, ...fromSidecars].filter((entry) => entry !== null);
252
+ };
253
+ /**
254
+ * What `12ui convert` leaves behind for the branch run that follows it.
255
+ *
256
+ * Only a succeeded image-to-LayerDoc conversion can stand in for a package
257
+ * viewport, so anything else records nothing at all: a receipt for an `html`
258
+ * conversion would be a claim the server refuses, and the refusal costs a whole
259
+ * page. The digest is of the bytes actually submitted, which is what `--width`
260
+ * normalization changes and what the server admitted the run with.
261
+ */
262
+ export const recordConvertedImageReceipt = async (args) => {
263
+ const { operation } = args;
264
+ if (operation.status !== 'succeeded'
265
+ || operation.output !== REUSABLE_CONVERSION_OUTPUT
266
+ // The caller submitted image bytes; an absent input_kind (a legacy cached
267
+ // payload) does not contradict that, while a declared `layerdoc` does.
268
+ || operation.input_kind === 'layerdoc') {
269
+ return undefined;
270
+ }
271
+ const now = args.now ?? new Date();
272
+ const receipt = {
273
+ version: 1,
274
+ sha256: createHash('sha256').update(args.bytes).digest('hex'),
275
+ conversionId: operation.id,
276
+ model: operation.model,
277
+ boughtBy: operation.id,
278
+ expiresAt: operation.expires_at,
279
+ createdAt: now.toISOString(),
280
+ };
281
+ const write = await recordConversionReceipt(receipt, {
282
+ ...(args.store ? { store: args.store } : {}),
283
+ now,
284
+ });
285
+ if (!write.failure)
286
+ return receipt;
287
+ args.warn?.(`12ui convert: could not record the conversion receipt in ${write.failure.path}`
288
+ + ` (${write.failure.detail}). The conversion is unaffected; a later`
289
+ + ' `12ui branch execute` from this image will buy its conversion again.');
290
+ return undefined;
291
+ };
292
+ /**
293
+ * What `12ui convert package` leaves behind: one receipt per viewport whose own
294
+ * child conversion the package bought.
295
+ *
296
+ * A package converts each viewport to a LayerDoc and composes the page from
297
+ * those children, so every viewport of a submitted page is exactly the artifact
298
+ * a later package can attach instead of buying — the same fact the branch path
299
+ * records for the winner, for every screenshot the user named.
300
+ *
301
+ * A viewport that ATTACHED a conversion bought nothing, so it records nothing.
302
+ * The expiry reported is the package's own, which its children cannot outlive
303
+ * backwards, and the model is the one the server says it ran; without either
304
+ * fact nothing here proves a conversion is still attachable, so nothing is
305
+ * recorded and the caller is told once.
306
+ */
307
+ export const recordPackageConversionReceipts = async (args) => {
308
+ const { result } = args;
309
+ if (!result.model || !result.expiresAt) {
310
+ args.warn?.(`12ui convert package: package ${result.packageId} answered without a`
311
+ + ` ${result.model ? 'viewport expiry' : 'model'}, so nothing here proves its viewport`
312
+ + ' conversions are still attachable. They were bought and the pages are unaffected; a'
313
+ + ' later run from the same screenshots will buy them again.');
314
+ return [];
315
+ }
316
+ const now = args.now ?? new Date();
317
+ const submitted = new Map(args.pages.map((page) => [page.id, page]));
318
+ const recorded = [];
319
+ for (const page of result.pages) {
320
+ for (const viewport of page.viewports ?? []) {
321
+ if (viewport.status !== 'succeeded' || !viewport.conversionId)
322
+ continue;
323
+ const source = submitted.get(page.id)?.viewports
324
+ .find((entry) => entry.id === viewport.id);
325
+ // Attached work was already proved by the receipt that named it.
326
+ if (!source || source.sourceConversionId)
327
+ continue;
328
+ const receipt = {
329
+ version: 1,
330
+ sha256: createHash('sha256').update(source.bytes).digest('hex'),
331
+ conversionId: viewport.conversionId,
332
+ model: result.model,
333
+ boughtBy: result.packageId,
334
+ expiresAt: result.expiresAt,
335
+ createdAt: now.toISOString(),
336
+ };
337
+ const write = await recordConversionReceipt(receipt, {
338
+ ...(args.store ? { store: args.store } : {}),
339
+ now,
340
+ });
341
+ if (write.failure) {
342
+ args.warn?.(`12ui convert package: could not record the conversion receipt for ${page.id}/`
343
+ + `${viewport.id} in ${write.failure.path} (${write.failure.detail}). The package is`
344
+ + ' unaffected; a later run from this screenshot will buy its conversion again.');
345
+ continue;
346
+ }
347
+ recorded.push(receipt);
348
+ }
349
+ }
350
+ return recorded;
351
+ };
352
+ //# sourceMappingURL=conversion-receipt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversion-receipt.js","sourceRoot":"","sources":["../src/conversion-receipt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { type DraftPaths } from './draft-workspace.js';
2
+ /**
3
+ * Which directory a `12ui draft` run continues in.
4
+ *
5
+ * MEASURED overnight 2026-08-10: a draft whose corpus search died left
6
+ * `…-CPcNkH` holding nothing but `references/inspire-attempt.json`, and the
7
+ * retry opened a NEW same-slug sibling `…-hOnLBB`. Two same-slug directories
8
+ * then held genuinely different candidates, so every tool that resolves a run
9
+ * by slug glob (`ls -dt …`) silently switched between them. The derived corpus
10
+ * key already stops the retry from buying the search twice, so this is a
11
+ * tooling-confusion hazard rather than a cost one — and the fix is to continue
12
+ * the stranded directory instead of stranding it.
13
+ *
14
+ * Adoption is decided on the DERIVED corpus key, never on the slug: two
15
+ * different design problems whose first four words match share a slug, and
16
+ * they must keep separate directories.
17
+ */
18
+ export declare const DRAFT_CLAIM_FILENAME = "draft-claim.json";
19
+ /**
20
+ * The owner marker. It is created O_EXCL the moment a run directory is created
21
+ * or adopted, so exactly one process can ever own a directory, and it records
22
+ * the derived corpus key so a run that died BEFORE writing its corpus attempt
23
+ * record is still adoptable on its key.
24
+ */
25
+ export type DraftRunClaim = {
26
+ version: 1;
27
+ corpusKey: string;
28
+ pid: number;
29
+ hostname: string;
30
+ claimedAt: string;
31
+ };
32
+ export type AcquiredDraftRun = {
33
+ paths: DraftPaths;
34
+ /** True when this run continues an incomplete previous directory in place. */
35
+ adopted: boolean;
36
+ };
37
+ /**
38
+ * The run directory `12ui draft` continues in: an incomplete previous run for
39
+ * the same derived corpus key, adopted in place, or a fresh unique sibling.
40
+ */
41
+ export declare const acquireDraftRunDirectory: (args: {
42
+ query: string;
43
+ corpusKey: string;
44
+ /** Overridden only by tests; production always uses the system temp root. */
45
+ root?: string;
46
+ now?: () => Date;
47
+ }) => Promise<AcquiredDraftRun>;
48
+ //# sourceMappingURL=draft-adoption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-adoption.d.ts","sourceRoot":"","sources":["../src/draft-adoption.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,oBAAoB,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,CAAC,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,8EAA8E;IAC9E,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAkNF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GAAU,MAAM;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB,KAAG,OAAO,CAAC,gBAAgB,CAiB3B,CAAC"}