@popoverai/dotrequirements 0.30.1 → 0.32.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.
- package/README.md +26 -9
- package/dist/cli.js +5 -0
- package/dist/commands/create-requirement-document.js +1 -0
- package/dist/commands/diff.js +6 -1
- package/dist/commands/greenfield-discovery.d.ts +10 -0
- package/dist/commands/greenfield-discovery.js +13 -0
- package/dist/commands/sync.js +50 -6
- package/dist/convex.d.ts +1 -0
- package/dist/convex.js +2 -0
- package/dist/requirements/greenfield.d.ts +38 -0
- package/dist/requirements/greenfield.js +182 -0
- package/dist/requirements/style-guide.d.ts +10 -2
- package/dist/requirements/style-guide.js +23 -4
- package/dist/schema/attachment-lists.d.ts +21 -0
- package/dist/schema/attachment-lists.js +22 -0
- package/dist/schema/browser.d.ts +1 -0
- package/dist/schema/browser.js +5 -0
- package/dist/schema/file-writer.d.ts +21 -0
- package/dist/schema/file-writer.js +7 -0
- package/dist/schema/index.d.ts +2 -1
- package/dist/schema/index.js +1 -0
- package/dist/schema/schemas.d.ts +152 -0
- package/dist/schema/schemas.js +18 -0
- package/dist/sync/attachments.d.ts +64 -0
- package/dist/sync/attachments.js +149 -0
- package/dist/sync/compare.js +94 -10
- package/dist/sync/execute.d.ts +11 -0
- package/dist/sync/execute.js +135 -2
- package/dist/sync/publish.d.ts +10 -0
- package/dist/sync/render.js +7 -0
- package/dist/sync/snapshot.js +28 -1
- package/dist/sync/types.d.ts +23 -0
- package/dist/templates/context-file-section.md +2 -1
- package/package.json +5 -4
package/dist/schema/browser.js
CHANGED
|
@@ -25,6 +25,11 @@ ValidationError, validateKey, validateMetadata, validatePrefix, validateRequirem
|
|
|
25
25
|
// Title semantics (pure TypeScript - browser-safe): the document title and
|
|
26
26
|
// the markdown's leading H1 are the same thing (DOC-TITLE-3)
|
|
27
27
|
export { composeMarkdownWithTitle, effectiveTitle, splitLeadingH1, } from "./title-markdown.js";
|
|
28
|
+
// NOTE: parser.ts and resolver.ts are excluded because they use Node.js 'fs' module.
|
|
29
|
+
// Use parser-core.ts functions above for browser/Convex environments.
|
|
30
|
+
// Dependency-free; shared with the Convex mutations so both halves of the
|
|
31
|
+
// attachments compare-and-set use one predicate (see attachment-lists.ts).
|
|
32
|
+
export { attachmentListsEqual } from "./attachment-lists.js";
|
|
28
33
|
// Scenario building (pure TypeScript - browser-safe, used by Convex Node actions)
|
|
29
34
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
30
35
|
//# sourceMappingURL=browser.js.map
|
|
@@ -27,6 +27,23 @@ export type PrefixDirective =
|
|
|
27
27
|
| "keep"
|
|
28
28
|
/** Remove it — the authoritative side has no prefix. */
|
|
29
29
|
| "clear";
|
|
30
|
+
/**
|
|
31
|
+
* What this write does to `document.attachments` (ATTACH-10) — the same
|
|
32
|
+
* three readings the prefix has, for the same reason: an absent cloud list is
|
|
33
|
+
* not self-interpreting.
|
|
34
|
+
*/
|
|
35
|
+
export type AttachmentsDirective =
|
|
36
|
+
/** The cloud has attachments; the file takes the list. */
|
|
37
|
+
{
|
|
38
|
+
set: Array<{
|
|
39
|
+
url: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/** Leave whatever the file says. */
|
|
44
|
+
| "keep"
|
|
45
|
+
/** Remove them — the authoritative side has none. */
|
|
46
|
+
| "clear";
|
|
30
47
|
/**
|
|
31
48
|
* The frontmatter fields sync owns and rewrites on every write
|
|
32
49
|
* (DOC-HEADER-14.1). `pulledAt` is owned too but never passed: it always
|
|
@@ -37,6 +54,10 @@ export interface OwnedFrontmatter {
|
|
|
37
54
|
documentId: string;
|
|
38
55
|
/** Editor hint for new requirement keys — see PrefixDirective. */
|
|
39
56
|
defaultPrefix: PrefixDirective;
|
|
57
|
+
/** The document's attached links — see AttachmentsDirective. Absent means
|
|
58
|
+
* "keep": a caller that doesn't think about attachments must not clear
|
|
59
|
+
* them. */
|
|
60
|
+
attachments?: AttachmentsDirective;
|
|
40
61
|
/** The cloud version this content is at. */
|
|
41
62
|
version: number;
|
|
42
63
|
}
|
|
@@ -43,6 +43,13 @@ export function writeRequirementsFile(filePath, owned, body) {
|
|
|
43
43
|
frontmatter.deleteIn(["document", "defaultPrefix"]);
|
|
44
44
|
else if (owned.defaultPrefix !== "keep")
|
|
45
45
|
frontmatter.setIn(["document", "defaultPrefix"], owned.defaultPrefix.set);
|
|
46
|
+
const attachments = owned.attachments ?? "keep";
|
|
47
|
+
if (attachments === "clear")
|
|
48
|
+
frontmatter.deleteIn(["document", "attachments"]);
|
|
49
|
+
else if (attachments !== "keep")
|
|
50
|
+
frontmatter.setIn(["document", "attachments"],
|
|
51
|
+
// Omit absent titles rather than writing `title: null` into the file.
|
|
52
|
+
attachments.set.map((a) => a.title === undefined ? { url: a.url } : { url: a.url, title: a.title }));
|
|
46
53
|
// SYNC-TITLE-1.2: `document.title` is a recognized-but-retired field — the
|
|
47
54
|
// title lives in the body as its leading H1 — so a rewrite drops it rather
|
|
48
55
|
// than carrying it forward as if it were one of the user's own fields.
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* This module provides types, validation, parsing, and building utilities
|
|
5
5
|
* for the Markdown requirements format.
|
|
6
6
|
*/
|
|
7
|
+
export { attachmentListsEqual } from "./attachment-lists.js";
|
|
7
8
|
export { buildRequirementMarkdown, buildRequirementsFile, buildRequirementsMarkdown, } from "./builder.js";
|
|
8
9
|
export type { ConvexRequirement } from "./conversions.js";
|
|
9
10
|
export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
|
|
10
|
-
export type { OwnedFrontmatter, PrefixDirective } from "./file-writer.js";
|
|
11
|
+
export type { AttachmentsDirective, OwnedFrontmatter, PrefixDirective, } from "./file-writer.js";
|
|
11
12
|
export { writeRequirementsFile } from "./file-writer.js";
|
|
12
13
|
export type { ExtractedRequirementBlock } from "./parser.js";
|
|
13
14
|
export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementsFile, parseRequirementsFromFile, parseRootLine, splitRequirementFenceContent, } from "./parser.js";
|
package/dist/schema/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This module provides types, validation, parsing, and building utilities
|
|
5
5
|
* for the Markdown requirements format.
|
|
6
6
|
*/
|
|
7
|
+
export { attachmentListsEqual } from "./attachment-lists.js";
|
|
7
8
|
// Building
|
|
8
9
|
export { buildRequirementMarkdown, buildRequirementsFile, buildRequirementsMarkdown, } from "./builder.js";
|
|
9
10
|
export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
|
package/dist/schema/schemas.d.ts
CHANGED
|
@@ -86,14 +86,44 @@ export declare const MetadataSchema: z.ZodObject<{
|
|
|
86
86
|
id: z.ZodOptional<z.ZodString>;
|
|
87
87
|
title: z.ZodOptional<z.ZodString>;
|
|
88
88
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
89
|
+
attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
90
|
+
url: z.ZodString;
|
|
91
|
+
title: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, "strip", z.ZodTypeAny, {
|
|
93
|
+
url: string;
|
|
94
|
+
title?: string | undefined;
|
|
95
|
+
}, {
|
|
96
|
+
url: string;
|
|
97
|
+
title?: string | undefined;
|
|
98
|
+
}>, "many">>>, {
|
|
99
|
+
url: string;
|
|
100
|
+
title?: string | undefined;
|
|
101
|
+
}[] | undefined, {
|
|
102
|
+
url: string;
|
|
103
|
+
title?: string | undefined;
|
|
104
|
+
}[] | null | undefined>, {
|
|
105
|
+
url: string;
|
|
106
|
+
title?: string | undefined;
|
|
107
|
+
}[] | undefined, {
|
|
108
|
+
url: string;
|
|
109
|
+
title?: string | undefined;
|
|
110
|
+
}[] | null | undefined>;
|
|
89
111
|
}, "strip", z.ZodTypeAny, {
|
|
90
112
|
id?: string | undefined;
|
|
91
113
|
title?: string | undefined;
|
|
92
114
|
defaultPrefix?: string | undefined;
|
|
115
|
+
attachments?: {
|
|
116
|
+
url: string;
|
|
117
|
+
title?: string | undefined;
|
|
118
|
+
}[] | undefined;
|
|
93
119
|
}, {
|
|
94
120
|
id?: string | undefined;
|
|
95
121
|
title?: string | undefined;
|
|
96
122
|
defaultPrefix?: string | undefined;
|
|
123
|
+
attachments?: {
|
|
124
|
+
url: string;
|
|
125
|
+
title?: string | undefined;
|
|
126
|
+
}[] | null | undefined;
|
|
97
127
|
}>>;
|
|
98
128
|
}, "strip", z.ZodTypeAny, {
|
|
99
129
|
version?: number | undefined;
|
|
@@ -103,6 +133,10 @@ export declare const MetadataSchema: z.ZodObject<{
|
|
|
103
133
|
id?: string | undefined;
|
|
104
134
|
title?: string | undefined;
|
|
105
135
|
defaultPrefix?: string | undefined;
|
|
136
|
+
attachments?: {
|
|
137
|
+
url: string;
|
|
138
|
+
title?: string | undefined;
|
|
139
|
+
}[] | undefined;
|
|
106
140
|
} | undefined;
|
|
107
141
|
}, {
|
|
108
142
|
version?: number | undefined;
|
|
@@ -112,6 +146,10 @@ export declare const MetadataSchema: z.ZodObject<{
|
|
|
112
146
|
id?: string | undefined;
|
|
113
147
|
title?: string | undefined;
|
|
114
148
|
defaultPrefix?: string | undefined;
|
|
149
|
+
attachments?: {
|
|
150
|
+
url: string;
|
|
151
|
+
title?: string | undefined;
|
|
152
|
+
}[] | null | undefined;
|
|
115
153
|
} | undefined;
|
|
116
154
|
}>;
|
|
117
155
|
export type Metadata = z.infer<typeof MetadataSchema>;
|
|
@@ -175,14 +213,44 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
175
213
|
id: z.ZodOptional<z.ZodString>;
|
|
176
214
|
title: z.ZodOptional<z.ZodString>;
|
|
177
215
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
216
|
+
attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
217
|
+
url: z.ZodString;
|
|
218
|
+
title: z.ZodOptional<z.ZodString>;
|
|
219
|
+
}, "strip", z.ZodTypeAny, {
|
|
220
|
+
url: string;
|
|
221
|
+
title?: string | undefined;
|
|
222
|
+
}, {
|
|
223
|
+
url: string;
|
|
224
|
+
title?: string | undefined;
|
|
225
|
+
}>, "many">>>, {
|
|
226
|
+
url: string;
|
|
227
|
+
title?: string | undefined;
|
|
228
|
+
}[] | undefined, {
|
|
229
|
+
url: string;
|
|
230
|
+
title?: string | undefined;
|
|
231
|
+
}[] | null | undefined>, {
|
|
232
|
+
url: string;
|
|
233
|
+
title?: string | undefined;
|
|
234
|
+
}[] | undefined, {
|
|
235
|
+
url: string;
|
|
236
|
+
title?: string | undefined;
|
|
237
|
+
}[] | null | undefined>;
|
|
178
238
|
}, "strip", z.ZodTypeAny, {
|
|
179
239
|
id?: string | undefined;
|
|
180
240
|
title?: string | undefined;
|
|
181
241
|
defaultPrefix?: string | undefined;
|
|
242
|
+
attachments?: {
|
|
243
|
+
url: string;
|
|
244
|
+
title?: string | undefined;
|
|
245
|
+
}[] | undefined;
|
|
182
246
|
}, {
|
|
183
247
|
id?: string | undefined;
|
|
184
248
|
title?: string | undefined;
|
|
185
249
|
defaultPrefix?: string | undefined;
|
|
250
|
+
attachments?: {
|
|
251
|
+
url: string;
|
|
252
|
+
title?: string | undefined;
|
|
253
|
+
}[] | null | undefined;
|
|
186
254
|
}>>;
|
|
187
255
|
}, "strip", z.ZodTypeAny, {
|
|
188
256
|
version?: number | undefined;
|
|
@@ -192,6 +260,10 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
192
260
|
id?: string | undefined;
|
|
193
261
|
title?: string | undefined;
|
|
194
262
|
defaultPrefix?: string | undefined;
|
|
263
|
+
attachments?: {
|
|
264
|
+
url: string;
|
|
265
|
+
title?: string | undefined;
|
|
266
|
+
}[] | undefined;
|
|
195
267
|
} | undefined;
|
|
196
268
|
}, {
|
|
197
269
|
version?: number | undefined;
|
|
@@ -201,6 +273,10 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
201
273
|
id?: string | undefined;
|
|
202
274
|
title?: string | undefined;
|
|
203
275
|
defaultPrefix?: string | undefined;
|
|
276
|
+
attachments?: {
|
|
277
|
+
url: string;
|
|
278
|
+
title?: string | undefined;
|
|
279
|
+
}[] | null | undefined;
|
|
204
280
|
} | undefined;
|
|
205
281
|
}>;
|
|
206
282
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
@@ -212,14 +288,44 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
212
288
|
id: z.ZodOptional<z.ZodString>;
|
|
213
289
|
title: z.ZodOptional<z.ZodString>;
|
|
214
290
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
291
|
+
attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
292
|
+
url: z.ZodString;
|
|
293
|
+
title: z.ZodOptional<z.ZodString>;
|
|
294
|
+
}, "strip", z.ZodTypeAny, {
|
|
295
|
+
url: string;
|
|
296
|
+
title?: string | undefined;
|
|
297
|
+
}, {
|
|
298
|
+
url: string;
|
|
299
|
+
title?: string | undefined;
|
|
300
|
+
}>, "many">>>, {
|
|
301
|
+
url: string;
|
|
302
|
+
title?: string | undefined;
|
|
303
|
+
}[] | undefined, {
|
|
304
|
+
url: string;
|
|
305
|
+
title?: string | undefined;
|
|
306
|
+
}[] | null | undefined>, {
|
|
307
|
+
url: string;
|
|
308
|
+
title?: string | undefined;
|
|
309
|
+
}[] | undefined, {
|
|
310
|
+
url: string;
|
|
311
|
+
title?: string | undefined;
|
|
312
|
+
}[] | null | undefined>;
|
|
215
313
|
}, "strip", z.ZodTypeAny, {
|
|
216
314
|
id?: string | undefined;
|
|
217
315
|
title?: string | undefined;
|
|
218
316
|
defaultPrefix?: string | undefined;
|
|
317
|
+
attachments?: {
|
|
318
|
+
url: string;
|
|
319
|
+
title?: string | undefined;
|
|
320
|
+
}[] | undefined;
|
|
219
321
|
}, {
|
|
220
322
|
id?: string | undefined;
|
|
221
323
|
title?: string | undefined;
|
|
222
324
|
defaultPrefix?: string | undefined;
|
|
325
|
+
attachments?: {
|
|
326
|
+
url: string;
|
|
327
|
+
title?: string | undefined;
|
|
328
|
+
}[] | null | undefined;
|
|
223
329
|
}>>;
|
|
224
330
|
}, "strip", z.ZodTypeAny, {
|
|
225
331
|
version?: number | undefined;
|
|
@@ -229,6 +335,10 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
229
335
|
id?: string | undefined;
|
|
230
336
|
title?: string | undefined;
|
|
231
337
|
defaultPrefix?: string | undefined;
|
|
338
|
+
attachments?: {
|
|
339
|
+
url: string;
|
|
340
|
+
title?: string | undefined;
|
|
341
|
+
}[] | undefined;
|
|
232
342
|
} | undefined;
|
|
233
343
|
}, {
|
|
234
344
|
version?: number | undefined;
|
|
@@ -238,6 +348,10 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
238
348
|
id?: string | undefined;
|
|
239
349
|
title?: string | undefined;
|
|
240
350
|
defaultPrefix?: string | undefined;
|
|
351
|
+
attachments?: {
|
|
352
|
+
url: string;
|
|
353
|
+
title?: string | undefined;
|
|
354
|
+
}[] | null | undefined;
|
|
241
355
|
} | undefined;
|
|
242
356
|
}>;
|
|
243
357
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
@@ -249,14 +363,44 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
249
363
|
id: z.ZodOptional<z.ZodString>;
|
|
250
364
|
title: z.ZodOptional<z.ZodString>;
|
|
251
365
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
366
|
+
attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
367
|
+
url: z.ZodString;
|
|
368
|
+
title: z.ZodOptional<z.ZodString>;
|
|
369
|
+
}, "strip", z.ZodTypeAny, {
|
|
370
|
+
url: string;
|
|
371
|
+
title?: string | undefined;
|
|
372
|
+
}, {
|
|
373
|
+
url: string;
|
|
374
|
+
title?: string | undefined;
|
|
375
|
+
}>, "many">>>, {
|
|
376
|
+
url: string;
|
|
377
|
+
title?: string | undefined;
|
|
378
|
+
}[] | undefined, {
|
|
379
|
+
url: string;
|
|
380
|
+
title?: string | undefined;
|
|
381
|
+
}[] | null | undefined>, {
|
|
382
|
+
url: string;
|
|
383
|
+
title?: string | undefined;
|
|
384
|
+
}[] | undefined, {
|
|
385
|
+
url: string;
|
|
386
|
+
title?: string | undefined;
|
|
387
|
+
}[] | null | undefined>;
|
|
252
388
|
}, "strip", z.ZodTypeAny, {
|
|
253
389
|
id?: string | undefined;
|
|
254
390
|
title?: string | undefined;
|
|
255
391
|
defaultPrefix?: string | undefined;
|
|
392
|
+
attachments?: {
|
|
393
|
+
url: string;
|
|
394
|
+
title?: string | undefined;
|
|
395
|
+
}[] | undefined;
|
|
256
396
|
}, {
|
|
257
397
|
id?: string | undefined;
|
|
258
398
|
title?: string | undefined;
|
|
259
399
|
defaultPrefix?: string | undefined;
|
|
400
|
+
attachments?: {
|
|
401
|
+
url: string;
|
|
402
|
+
title?: string | undefined;
|
|
403
|
+
}[] | null | undefined;
|
|
260
404
|
}>>;
|
|
261
405
|
}, "strip", z.ZodTypeAny, {
|
|
262
406
|
version?: number | undefined;
|
|
@@ -266,6 +410,10 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
266
410
|
id?: string | undefined;
|
|
267
411
|
title?: string | undefined;
|
|
268
412
|
defaultPrefix?: string | undefined;
|
|
413
|
+
attachments?: {
|
|
414
|
+
url: string;
|
|
415
|
+
title?: string | undefined;
|
|
416
|
+
}[] | undefined;
|
|
269
417
|
} | undefined;
|
|
270
418
|
}, {
|
|
271
419
|
version?: number | undefined;
|
|
@@ -275,6 +423,10 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
275
423
|
id?: string | undefined;
|
|
276
424
|
title?: string | undefined;
|
|
277
425
|
defaultPrefix?: string | undefined;
|
|
426
|
+
attachments?: {
|
|
427
|
+
url: string;
|
|
428
|
+
title?: string | undefined;
|
|
429
|
+
}[] | null | undefined;
|
|
278
430
|
} | undefined;
|
|
279
431
|
}>;
|
|
280
432
|
}, z.ZodTypeAny, "passthrough">>;
|
package/dist/schema/schemas.js
CHANGED
|
@@ -147,6 +147,24 @@ export const MetadataSchema = z.object({
|
|
|
147
147
|
id: z.string().optional(), // Cloud document link - filled by push when creating
|
|
148
148
|
title: z.string().optional(), // Legacy fallback — the body's leading H1 is the title
|
|
149
149
|
defaultPrefix: z.string().optional(), // Editor hint for new requirement keys
|
|
150
|
+
// ATTACH-10: links attached to the document (a Miro board, a reference
|
|
151
|
+
// page). Round-trips through sync — the cloud stores them as document
|
|
152
|
+
// metadata, the file carries them here.
|
|
153
|
+
// A bare `attachments:` key parses as YAML null and means the same as
|
|
154
|
+
// an explicit empty list — "this document has none" — which is the
|
|
155
|
+
// natural hand-edit for removing every entry. Absent stays undefined
|
|
156
|
+
// (the file takes no position; see sync/attachments.ts).
|
|
157
|
+
attachments: z
|
|
158
|
+
.array(z.object({
|
|
159
|
+
url: z.string(),
|
|
160
|
+
title: z.string().optional(),
|
|
161
|
+
}))
|
|
162
|
+
.nullable()
|
|
163
|
+
.optional()
|
|
164
|
+
.transform((a) => (a === null ? [] : a))
|
|
165
|
+
.refine((a) => a === undefined || new Set(a.map((x) => x.url)).size === a.length, {
|
|
166
|
+
message: "attachments list the same url more than once — remove the duplicate entry",
|
|
167
|
+
}),
|
|
150
168
|
})
|
|
151
169
|
.optional(),
|
|
152
170
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment resolution, outside the document verdict (ATTACH-10.2).
|
|
3
|
+
*
|
|
4
|
+
* Attachments are a small named-link set, not additive body content: three
|
|
5
|
+
* review rounds showed that routing them through the body's verdict matrix
|
|
6
|
+
* broke a different cell each round — a name difference freezing the whole
|
|
7
|
+
* document, planning a destructive body publish, or --repo-wins wiping
|
|
8
|
+
* attachments a pre-feature file never mentioned. So they resolve here, by
|
|
9
|
+
* their own rule, and the body syncs as if they didn't exist.
|
|
10
|
+
*
|
|
11
|
+
* The rule, per URL:
|
|
12
|
+
* - Additive modes union the two sides. Removals don't propagate (as
|
|
13
|
+
* everywhere in additive sync); a contributes mode only changes the
|
|
14
|
+
* receiving side.
|
|
15
|
+
* - Names: a name beats no name; when both sides name the same URL
|
|
16
|
+
* differently, the REPO wins — it is the hand-authored side, and
|
|
17
|
+
* ATTACH-10.1 promises frontmatter edits propagate (decided 2026-08-20).
|
|
18
|
+
* - Authority modes make that side's list the outcome — except that a file
|
|
19
|
+
* with NO attachments key has no opinion: absence cannot distinguish "the
|
|
20
|
+
* user removed everything" from "this file predates attachments", so it
|
|
21
|
+
* never clears anything, even under --repo-wins. An explicit
|
|
22
|
+
* `attachments: []` (or a bare `attachments:` key) is how a file says
|
|
23
|
+
* "none".
|
|
24
|
+
*/
|
|
25
|
+
import { attachmentListsEqual } from "../schema/attachment-lists.js";
|
|
26
|
+
import type { SyncMode } from "./plan.js";
|
|
27
|
+
import type { Attachment } from "./types.js";
|
|
28
|
+
export { attachmentListsEqual };
|
|
29
|
+
export interface AttachmentsResolution {
|
|
30
|
+
/** The list the cloud should end with. */
|
|
31
|
+
cloudFinal: Attachment[];
|
|
32
|
+
/** The list the file should end with (undefined = leave the file alone). */
|
|
33
|
+
localFinal: Attachment[] | undefined;
|
|
34
|
+
/** The cloud's list differs from cloudFinal — a push is needed. */
|
|
35
|
+
pushNeeded: boolean;
|
|
36
|
+
/** The file's effective list differs from localFinal — a write is needed. */
|
|
37
|
+
writeNeeded: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Does this document need attachment work its body action won't do? The
|
|
41
|
+
* sync command's "already in sync" gate asks this — the resolver, not the
|
|
42
|
+
* raw `differ` flag, because four of five modes deliberately leave one side
|
|
43
|
+
* alone (a no-key file under repo_contributes is a permanent no-op, not
|
|
44
|
+
* pending work).
|
|
45
|
+
*/
|
|
46
|
+
export declare function attachmentSyncNeeded(attachments: {
|
|
47
|
+
local: Attachment[] | undefined;
|
|
48
|
+
cloud: Attachment[];
|
|
49
|
+
} | undefined, mode: SyncMode): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The cloud attachments a sync under this mode removes. Removals ride
|
|
52
|
+
* authority ungated — like requirement blocks dropped from a file's body,
|
|
53
|
+
* and unlike whole documents (SYNC-MODE-4 gates only those); the docs
|
|
54
|
+
* promise attachments never hold up the rest of the spec, and a link is
|
|
55
|
+
* re-pasteable. This names them for the post-run report (ATTACH-10.3.1.0).
|
|
56
|
+
* Only authority can remove (union never shrinks), so this is empty
|
|
57
|
+
* outside --repo-wins.
|
|
58
|
+
*/
|
|
59
|
+
export declare function cloudAttachmentRemovals(attachments: {
|
|
60
|
+
local: Attachment[] | undefined;
|
|
61
|
+
cloud: Attachment[];
|
|
62
|
+
} | undefined, mode: SyncMode): Attachment[];
|
|
63
|
+
export declare function resolveAttachments(local: Attachment[] | undefined, cloud: Attachment[] | undefined, mode: SyncMode): AttachmentsResolution;
|
|
64
|
+
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment resolution, outside the document verdict (ATTACH-10.2).
|
|
3
|
+
*
|
|
4
|
+
* Attachments are a small named-link set, not additive body content: three
|
|
5
|
+
* review rounds showed that routing them through the body's verdict matrix
|
|
6
|
+
* broke a different cell each round — a name difference freezing the whole
|
|
7
|
+
* document, planning a destructive body publish, or --repo-wins wiping
|
|
8
|
+
* attachments a pre-feature file never mentioned. So they resolve here, by
|
|
9
|
+
* their own rule, and the body syncs as if they didn't exist.
|
|
10
|
+
*
|
|
11
|
+
* The rule, per URL:
|
|
12
|
+
* - Additive modes union the two sides. Removals don't propagate (as
|
|
13
|
+
* everywhere in additive sync); a contributes mode only changes the
|
|
14
|
+
* receiving side.
|
|
15
|
+
* - Names: a name beats no name; when both sides name the same URL
|
|
16
|
+
* differently, the REPO wins — it is the hand-authored side, and
|
|
17
|
+
* ATTACH-10.1 promises frontmatter edits propagate (decided 2026-08-20).
|
|
18
|
+
* - Authority modes make that side's list the outcome — except that a file
|
|
19
|
+
* with NO attachments key has no opinion: absence cannot distinguish "the
|
|
20
|
+
* user removed everything" from "this file predates attachments", so it
|
|
21
|
+
* never clears anything, even under --repo-wins. An explicit
|
|
22
|
+
* `attachments: []` (or a bare `attachments:` key) is how a file says
|
|
23
|
+
* "none".
|
|
24
|
+
*/
|
|
25
|
+
import { attachmentListsEqual } from "../schema/attachment-lists.js";
|
|
26
|
+
export { attachmentListsEqual };
|
|
27
|
+
/** One entry per URL — legacy cloud rows can hold duplicates from before
|
|
28
|
+
* the validator, and writing them into frontmatter produces a file our own
|
|
29
|
+
* parser rejects (invalid_file, requirements and all, in every mode). The
|
|
30
|
+
* FIRST TITLED occurrence wins the name: keeping strictly the first entry
|
|
31
|
+
* dropped a later name, contradicting "a name beats no name" below. */
|
|
32
|
+
function dedupeByUrl(list) {
|
|
33
|
+
const byUrl = new Map();
|
|
34
|
+
for (const a of list) {
|
|
35
|
+
const kept = byUrl.get(a.url);
|
|
36
|
+
if (!kept || (kept.title === undefined && a.title !== undefined)) {
|
|
37
|
+
byUrl.set(a.url, kept ? { url: a.url, title: a.title } : a);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [...byUrl.values()];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Union with the name rule: cloud order first (stable for the web), repo-only
|
|
44
|
+
* URLs appended in file order; per shared URL, a name beats no name and the
|
|
45
|
+
* repo's name wins a tie.
|
|
46
|
+
*/
|
|
47
|
+
function merge(local, cloud) {
|
|
48
|
+
const localByUrl = new Map(local.map((a) => [a.url, a]));
|
|
49
|
+
const merged = cloud.map((c) => {
|
|
50
|
+
const l = localByUrl.get(c.url);
|
|
51
|
+
const title = l?.title ?? c.title;
|
|
52
|
+
return title === undefined ? { url: c.url } : { url: c.url, title };
|
|
53
|
+
});
|
|
54
|
+
const cloudUrls = new Set(cloud.map((a) => a.url));
|
|
55
|
+
for (const l of local) {
|
|
56
|
+
if (!cloudUrls.has(l.url))
|
|
57
|
+
merged.push(l);
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Does this document need attachment work its body action won't do? The
|
|
63
|
+
* sync command's "already in sync" gate asks this — the resolver, not the
|
|
64
|
+
* raw `differ` flag, because four of five modes deliberately leave one side
|
|
65
|
+
* alone (a no-key file under repo_contributes is a permanent no-op, not
|
|
66
|
+
* pending work).
|
|
67
|
+
*/
|
|
68
|
+
export function attachmentSyncNeeded(attachments, mode) {
|
|
69
|
+
if (!attachments)
|
|
70
|
+
return false;
|
|
71
|
+
const res = resolveAttachments(attachments.local, attachments.cloud, mode);
|
|
72
|
+
return res.pushNeeded || res.writeNeeded;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The cloud attachments a sync under this mode removes. Removals ride
|
|
76
|
+
* authority ungated — like requirement blocks dropped from a file's body,
|
|
77
|
+
* and unlike whole documents (SYNC-MODE-4 gates only those); the docs
|
|
78
|
+
* promise attachments never hold up the rest of the spec, and a link is
|
|
79
|
+
* re-pasteable. This names them for the post-run report (ATTACH-10.3.1.0).
|
|
80
|
+
* Only authority can remove (union never shrinks), so this is empty
|
|
81
|
+
* outside --repo-wins.
|
|
82
|
+
*/
|
|
83
|
+
export function cloudAttachmentRemovals(attachments, mode) {
|
|
84
|
+
if (!attachments)
|
|
85
|
+
return [];
|
|
86
|
+
const res = resolveAttachments(attachments.local, attachments.cloud, mode);
|
|
87
|
+
return attachments.cloud.filter((c) => !res.cloudFinal.some((f) => f.url === c.url));
|
|
88
|
+
}
|
|
89
|
+
export function resolveAttachments(local, cloud, mode) {
|
|
90
|
+
// The cloud's absence IS "none" — the web shows no attachments. Only the
|
|
91
|
+
// file has the third state. RESOLUTION runs on deduped views (every
|
|
92
|
+
// output must be writable to frontmatter our own parser accepts), but the
|
|
93
|
+
// push/write decisions below compare against the RAW lists — that is what
|
|
94
|
+
// makes a duplicated legacy row repairable: its deduped resolution
|
|
95
|
+
// differs from the raw row, so the corrected list gets pushed.
|
|
96
|
+
const rawCloud = cloud ?? [];
|
|
97
|
+
const rawLocal = local ?? [];
|
|
98
|
+
const effCloud = dedupeByUrl(rawCloud);
|
|
99
|
+
const effLocal = dedupeByUrl(rawLocal);
|
|
100
|
+
let cloudFinal;
|
|
101
|
+
let localFinal;
|
|
102
|
+
switch (mode) {
|
|
103
|
+
case "repo_wins":
|
|
104
|
+
// Normalized like merge's output: an entry read from frontmatter can
|
|
105
|
+
// carry an explicit `title: undefined` key, which only this branch
|
|
106
|
+
// would otherwise pass through to the wire.
|
|
107
|
+
cloudFinal =
|
|
108
|
+
local === undefined
|
|
109
|
+
? effCloud
|
|
110
|
+
: local.map((a) => a.title === undefined
|
|
111
|
+
? { url: a.url }
|
|
112
|
+
: { url: a.url, title: a.title });
|
|
113
|
+
localFinal = undefined; // the file is the authority; leave it alone
|
|
114
|
+
break;
|
|
115
|
+
case "cloud_wins":
|
|
116
|
+
cloudFinal = effCloud;
|
|
117
|
+
localFinal = effCloud;
|
|
118
|
+
break;
|
|
119
|
+
case "repo_contributes":
|
|
120
|
+
cloudFinal = merge(effLocal, effCloud);
|
|
121
|
+
localFinal = undefined;
|
|
122
|
+
break;
|
|
123
|
+
case "cloud_contributes":
|
|
124
|
+
cloudFinal = effCloud;
|
|
125
|
+
localFinal = merge(effLocal, effCloud);
|
|
126
|
+
break;
|
|
127
|
+
default: {
|
|
128
|
+
// bare: both sides converge on the union.
|
|
129
|
+
const merged = merge(effLocal, effCloud);
|
|
130
|
+
cloudFinal = merged;
|
|
131
|
+
localFinal = merged;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// The raw-vs-resolved comparison is what makes a duplicated legacy row
|
|
135
|
+
// repairable — but ONLY modes that legitimately write the cloud may run
|
|
136
|
+
// that repair: under cloud authority the mode's promise is that the repo
|
|
137
|
+
// never overwrites the cloud, and firing a push there rewrote the row
|
|
138
|
+
// unconsented (round 9). Cloud-authority modes compare deduped-to-deduped,
|
|
139
|
+
// which is stable: the duplicated row shows in diff and is cleaned up the
|
|
140
|
+
// next time any pushing mode runs.
|
|
141
|
+
const cloudWriting = mode === "bare" || mode === "repo_wins" || mode === "repo_contributes";
|
|
142
|
+
return {
|
|
143
|
+
cloudFinal,
|
|
144
|
+
localFinal,
|
|
145
|
+
pushNeeded: !attachmentListsEqual(cloudWriting ? rawCloud : effCloud, cloudFinal),
|
|
146
|
+
writeNeeded: localFinal !== undefined && !attachmentListsEqual(rawLocal, localFinal),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=attachments.js.map
|