@popoverai/dotrequirements 0.30.1 → 0.31.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/dist/commands/diff.js +6 -1
- package/dist/commands/sync.js +50 -6
- package/dist/convex.d.ts +1 -0
- package/dist/convex.js +2 -0
- 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/package.json +3 -3
package/dist/commands/diff.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function diffCommand(scope, options) {
|
|
|
28
28
|
// DIFF-2: any scope argument means document-scope → verbose. No scope means
|
|
29
29
|
// project-scope → verdict list.
|
|
30
30
|
const verbose = scope.length > 0;
|
|
31
|
-
const notInSync = documents.filter((d) => d.verdict !== "in_sync");
|
|
31
|
+
const notInSync = documents.filter((d) => d.verdict !== "in_sync" || d.attachments?.differ);
|
|
32
32
|
// DIFF-1.2: don't list every document when everything is in sync.
|
|
33
33
|
if (notInSync.length === 0 && documents.length > 0) {
|
|
34
34
|
console.log(`✓ Repo and ${brand} cloud are in sync (${documents.length} document(s)).`);
|
|
@@ -49,6 +49,11 @@ export async function diffCommand(scope, options) {
|
|
|
49
49
|
console.log(formatUnitDetail(detail));
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
if (verbose && doc.attachments?.differ) {
|
|
53
|
+
for (const detail of doc.attachments.details) {
|
|
54
|
+
console.log(formatUnitDetail(detail));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
52
57
|
if (doc.verdict === "conflict") {
|
|
53
58
|
console.log(` ${resolutionHint(doc)}`);
|
|
54
59
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import * as readline from "node:readline";
|
|
9
|
+
import { attachmentSyncNeeded } from "../sync/attachments.js";
|
|
9
10
|
import { executePlan } from "../sync/execute.js";
|
|
10
11
|
import { acquireCloudSnapshot, acquireLocalSnapshot, compareSnapshots, duplicateIdAbortMessage, filterByScope, resolutionHint, } from "../sync/index.js";
|
|
11
12
|
import { buildPlan, resolveMode, } from "../sync/plan.js";
|
|
@@ -72,22 +73,52 @@ export async function syncCommand(scope, options) {
|
|
|
72
73
|
console.log(`No document matched scope "${token}".`);
|
|
73
74
|
}
|
|
74
75
|
const plan = buildPlan(documents, mode);
|
|
75
|
-
// SYNC-MODE-4
|
|
76
|
+
// SYNC-MODE-4 gates WHOLE-DOCUMENT deletions only. Content-level removals
|
|
77
|
+
// ride authority ungated — a requirement block dropped from a file is
|
|
78
|
+
// deleted from the cloud by the same run without a prompt, and attachments
|
|
79
|
+
// follow that rule, not the document rule (an attached link is also the
|
|
80
|
+
// most recoverable thing here: re-paste the URL). The docs promise
|
|
81
|
+
// "attachments never hold up the rest of the spec"; a consent gate on
|
|
82
|
+
// their removal held up the entire sync (decided 2026-08-20). What
|
|
83
|
+
// authority removed is NAMED after the run instead — see the
|
|
84
|
+
// attachmentRemovals report below.
|
|
76
85
|
const deletions = plan.filter((p) => p.action === "cloud_delete" || p.action === "local_delete");
|
|
77
86
|
if (deletions.length > 0) {
|
|
87
|
+
// SYNC-MODE-4.0: every deletion is NAMED before anything else happens —
|
|
88
|
+
// in the non-TTY refusal too, or the operator is told three documents
|
|
89
|
+
// will be destroyed with no way to learn which three.
|
|
90
|
+
printDeletionNotice(deletions);
|
|
78
91
|
// SYNC-MODE-4.3: a non-interactive run cannot consent — refuse rather than
|
|
79
92
|
// hanging on a prompt that will never answer (or worse, deleting silently).
|
|
80
93
|
if (!options.yes && !process.stdin.isTTY) {
|
|
81
|
-
throw new Error(`This sync would delete ${deletions.length} document(s), and there is no terminal
|
|
82
|
-
`Re-run with --yes to consent to the deletions listed
|
|
94
|
+
throw new Error(`This sync would delete ${deletions.length} document(s), and there is no terminal ` +
|
|
95
|
+
`to confirm on. Re-run with --yes to consent to the deletions listed above.`);
|
|
83
96
|
}
|
|
84
|
-
if (!(await confirmDeletions(
|
|
97
|
+
if (!(await confirmDeletions(options.yes))) {
|
|
85
98
|
console.log("Sync cancelled.");
|
|
86
99
|
return "cancelled";
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
|
-
|
|
102
|
+
// ATTACH-10.4: a difference this mode deliberately leaves alone (a no-key
|
|
103
|
+
// file under repo authority, a duplicated legacy cloud row under cloud
|
|
104
|
+
// authority) would otherwise leave `dotreq diff --exit-code` red with
|
|
105
|
+
// nothing in this mode ever clearing it — say so and name the gesture
|
|
106
|
+
// that repairs it, on EVERY run that leaves one behind, not only when the
|
|
107
|
+
// sync had nothing else to do (the round-1 release review's finding).
|
|
108
|
+
const leftAlone = plan.filter((p) => p.doc.attachments?.differ &&
|
|
109
|
+
!attachmentSyncNeeded(p.doc.attachments, mode));
|
|
110
|
+
const printLeftAlone = () => {
|
|
111
|
+
if (leftAlone.length > 0) {
|
|
112
|
+
console.log(` (${leftAlone.length} document(s) have attachment differences this mode leaves alone — a repo-writing sync, e.g. plain \`dotreq sync\`, reconciles them.)`);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// ATTACH-10.2: attachments resolve outside the body's verdict, so "every
|
|
116
|
+
// body action is skip" is not "nothing to do" — the resolver, per mode,
|
|
117
|
+
// is what knows whether attachment work remains. Without this, an
|
|
118
|
+
// attachment-only difference reported by diff would never clear.
|
|
119
|
+
if (plan.every((p) => p.action === "skip" && !attachmentSyncNeeded(p.doc.attachments, mode))) {
|
|
90
120
|
console.log(`✓ Repo and ${brand} cloud are already in sync.`);
|
|
121
|
+
printLeftAlone();
|
|
91
122
|
return "clean";
|
|
92
123
|
}
|
|
93
124
|
console.log(`\nSyncing with ${brand} cloud...`);
|
|
@@ -103,6 +134,17 @@ export async function syncCommand(scope, options) {
|
|
|
103
134
|
console.log(` Written locally: ${outcome.localWritten}`);
|
|
104
135
|
if (outcome.localDeleted)
|
|
105
136
|
console.log(` Deleted locally: ${outcome.localDeleted}`);
|
|
137
|
+
if (outcome.attachmentPushes)
|
|
138
|
+
console.log(` Attachments updated in cloud: ${outcome.attachmentPushes}`);
|
|
139
|
+
if (outcome.attachmentWrites)
|
|
140
|
+
console.log(` Attachments updated locally: ${outcome.attachmentWrites}`);
|
|
141
|
+
// ATTACH-10.3.1.0: authority removals are a trace, not a consent gate —
|
|
142
|
+
// named after the push that actually performed them, like renames, so the
|
|
143
|
+
// report never claims a removal a failed push left in place.
|
|
144
|
+
for (const r of outcome.attachmentRemovals) {
|
|
145
|
+
console.log(` removed attached link on "${r.name}": ${r.title ?? r.url}`);
|
|
146
|
+
}
|
|
147
|
+
printLeftAlone();
|
|
106
148
|
for (const conflict of outcome.conflicts) {
|
|
107
149
|
console.log(` ! conflict: ${path.basename(conflict.filePath ?? conflict.title)} — ${resolutionHint(conflict)}`);
|
|
108
150
|
}
|
|
@@ -178,7 +220,7 @@ export async function syncCommand(scope, options) {
|
|
|
178
220
|
}
|
|
179
221
|
return hadTrouble ? "trouble" : "clean";
|
|
180
222
|
}
|
|
181
|
-
|
|
223
|
+
function printDeletionNotice(deletions) {
|
|
182
224
|
const cloudDeletes = deletions.filter((d) => d.action === "cloud_delete");
|
|
183
225
|
const localDeletes = deletions.filter((d) => d.action === "local_delete");
|
|
184
226
|
console.log("\nThis will PERMANENTLY delete:");
|
|
@@ -188,6 +230,8 @@ async function confirmDeletions(deletions, skip) {
|
|
|
188
230
|
for (const d of localDeletes) {
|
|
189
231
|
console.log(` from disk: ${d.doc.filePath ? path.basename(d.doc.filePath) : d.doc.title}`);
|
|
190
232
|
}
|
|
233
|
+
}
|
|
234
|
+
async function confirmDeletions(skip) {
|
|
191
235
|
if (skip) {
|
|
192
236
|
console.log("(--yes: proceeding without confirmation)");
|
|
193
237
|
return true;
|
package/dist/convex.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare const api: {
|
|
|
25
25
|
create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
26
26
|
update: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
27
27
|
deleteForCli: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
28
|
+
setAttachmentsForSync: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
28
29
|
};
|
|
29
30
|
saveWithRequirements: {
|
|
30
31
|
saveWithRequirements: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
package/dist/convex.js
CHANGED
|
@@ -30,6 +30,8 @@ export const api = {
|
|
|
30
30
|
update: mutation("documents/mutations:update"),
|
|
31
31
|
// SYNC-MODE-3: delete a cloud document from the CLI (repo-wins)
|
|
32
32
|
deleteForCli: mutation("documents/mutations:deleteForCli"),
|
|
33
|
+
// ATTACH-10.2: attachment-only sync — a metadata patch, never a publish
|
|
34
|
+
setAttachmentsForSync: mutation("documents/mutations:setAttachmentsForSync"),
|
|
33
35
|
},
|
|
34
36
|
// SYNC-ARCH-1: Save document with requirements derivation
|
|
35
37
|
saveWithRequirements: {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MULTISET equality for attachment lists: order never matters, names and
|
|
3
|
+
* duplicates do. The predecessor was a Map probe — asymmetric
|
|
4
|
+
* (eq([A,B],[A,A]) was true) — which silently disarmed the compare-and-set
|
|
5
|
+
* on duplicated legacy rows.
|
|
6
|
+
*
|
|
7
|
+
* Dependency-free and in schema/ so BOTH halves of the compare-and-set import
|
|
8
|
+
* the same function (the CLI resolver via sync/attachments, the Convex
|
|
9
|
+
* mutations via lib/attachments — the same cross-package path testCoverage
|
|
10
|
+
* already uses): two byte-identical copies would drift the moment one is
|
|
11
|
+
* tightened, and a drifted pair reports "attachments changed since the sync
|
|
12
|
+
* read them" on every run with nothing moving.
|
|
13
|
+
*/
|
|
14
|
+
export declare function attachmentListsEqual(a: Array<{
|
|
15
|
+
url: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
}>, b: Array<{
|
|
18
|
+
url: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
}>): boolean;
|
|
21
|
+
//# sourceMappingURL=attachment-lists.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MULTISET equality for attachment lists: order never matters, names and
|
|
3
|
+
* duplicates do. The predecessor was a Map probe — asymmetric
|
|
4
|
+
* (eq([A,B],[A,A]) was true) — which silently disarmed the compare-and-set
|
|
5
|
+
* on duplicated legacy rows.
|
|
6
|
+
*
|
|
7
|
+
* Dependency-free and in schema/ so BOTH halves of the compare-and-set import
|
|
8
|
+
* the same function (the CLI resolver via sync/attachments, the Convex
|
|
9
|
+
* mutations via lib/attachments — the same cross-package path testCoverage
|
|
10
|
+
* already uses): two byte-identical copies would drift the moment one is
|
|
11
|
+
* tightened, and a drifted pair reports "attachments changed since the sync
|
|
12
|
+
* read them" on every run with nothing moving.
|
|
13
|
+
*/
|
|
14
|
+
export function attachmentListsEqual(a, b) {
|
|
15
|
+
if (a.length !== b.length)
|
|
16
|
+
return false;
|
|
17
|
+
const key = (x) => `${x.url}\u0000${x.title ?? ""}`;
|
|
18
|
+
const as = a.map(key).sort();
|
|
19
|
+
const bs = b.map(key).sort();
|
|
20
|
+
return as.every((v, i) => v === bs[i]);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=attachment-lists.js.map
|
package/dist/schema/browser.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { DELIMITER_PATTERN, type ExtractedRequirementBlock, extractRequirementBl
|
|
|
10
10
|
export type { Metadata, ParsedCriterion, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
|
|
11
11
|
export { buildRequirementKey, canonicalRequirementKey, MetadataSchema, normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PATTERN, REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema, ValidationError, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
12
12
|
export { composeMarkdownWithTitle, effectiveTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
|
|
13
|
+
export { attachmentListsEqual } from "./attachment-lists.js";
|
|
13
14
|
export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioStep, } from "./scenario.js";
|
|
14
15
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
15
16
|
//# sourceMappingURL=browser.d.ts.map
|
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
|