@popoverai/dotrequirements 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/cli.js +13 -1
  2. package/dist/cloud.d.ts +5 -1
  3. package/dist/cloud.js +6 -2
  4. package/dist/commands/board/index.d.ts +13 -0
  5. package/dist/commands/board/index.js +96 -0
  6. package/dist/commands/branch-merge.d.ts +78 -0
  7. package/dist/commands/branch-merge.js +129 -0
  8. package/dist/commands/diff.js +15 -5
  9. package/dist/commands/mockup/index.d.ts +11 -0
  10. package/dist/commands/mockup/index.js +82 -0
  11. package/dist/commands/scores/index.d.ts +50 -0
  12. package/dist/commands/scores/index.js +190 -0
  13. package/dist/commands/solution/ask.d.ts +9 -0
  14. package/dist/commands/solution/ask.js +27 -0
  15. package/dist/commands/solution/author.d.ts +19 -0
  16. package/dist/commands/solution/author.js +202 -0
  17. package/dist/commands/solution/branch.d.ts +103 -0
  18. package/dist/commands/solution/branch.js +341 -0
  19. package/dist/commands/solution/brief.d.ts +81 -0
  20. package/dist/commands/solution/brief.js +47 -0
  21. package/dist/commands/solution/checkout.d.ts +27 -0
  22. package/dist/commands/solution/checkout.js +66 -0
  23. package/dist/commands/solution/client.d.ts +16 -0
  24. package/dist/commands/solution/client.js +46 -0
  25. package/dist/commands/solution/contract.d.ts +9 -0
  26. package/dist/commands/solution/contract.js +23 -0
  27. package/dist/commands/solution/create.d.ts +14 -0
  28. package/dist/commands/solution/create.js +52 -0
  29. package/dist/commands/solution/demo-script.d.ts +28 -0
  30. package/dist/commands/solution/demo-script.js +43 -0
  31. package/dist/commands/solution/image.d.ts +60 -0
  32. package/dist/commands/solution/image.js +148 -0
  33. package/dist/commands/solution/index.d.ts +12 -0
  34. package/dist/commands/solution/index.js +281 -0
  35. package/dist/commands/solution/look-up.d.ts +9 -0
  36. package/dist/commands/solution/look-up.js +69 -0
  37. package/dist/commands/solution/next.d.ts +12 -0
  38. package/dist/commands/solution/next.js +48 -0
  39. package/dist/commands/solution/plan-file.d.ts +22 -0
  40. package/dist/commands/solution/plan-file.js +33 -0
  41. package/dist/commands/solution/preview-screenshot.d.ts +21 -0
  42. package/dist/commands/solution/preview-screenshot.js +56 -0
  43. package/dist/commands/solution/pull-request.d.ts +25 -0
  44. package/dist/commands/solution/pull-request.js +117 -0
  45. package/dist/commands/solution/pushing.d.ts +66 -0
  46. package/dist/commands/solution/pushing.js +185 -0
  47. package/dist/commands/solution/rehearse.d.ts +10 -0
  48. package/dist/commands/solution/rehearse.js +24 -0
  49. package/dist/commands/solution/request.d.ts +3 -0
  50. package/dist/commands/solution/request.js +13 -0
  51. package/dist/commands/solution/review.d.ts +27 -0
  52. package/dist/commands/solution/review.js +104 -0
  53. package/dist/commands/solution/run-file.d.ts +42 -0
  54. package/dist/commands/solution/run-file.js +63 -0
  55. package/dist/commands/solution/share.d.ts +62 -0
  56. package/dist/commands/solution/share.js +346 -0
  57. package/dist/commands/solution/stall.d.ts +14 -0
  58. package/dist/commands/solution/stall.js +41 -0
  59. package/dist/commands/solution/start.d.ts +11 -0
  60. package/dist/commands/solution/start.js +20 -0
  61. package/dist/commands/solution/status.d.ts +39 -0
  62. package/dist/commands/solution/status.js +145 -0
  63. package/dist/commands/solution/summary.d.ts +13 -0
  64. package/dist/commands/solution/summary.js +5 -0
  65. package/dist/commands/solution/things.d.ts +118 -0
  66. package/dist/commands/solution/things.js +78 -0
  67. package/dist/commands/solution/thread.d.ts +13 -0
  68. package/dist/commands/solution/thread.js +53 -0
  69. package/dist/commands/solution/upload.d.ts +24 -0
  70. package/dist/commands/solution/upload.js +27 -0
  71. package/dist/commands/solution/video.d.ts +88 -0
  72. package/dist/commands/solution/video.js +364 -0
  73. package/dist/commands/solution/working-folder.d.ts +44 -0
  74. package/dist/commands/solution/working-folder.js +64 -0
  75. package/dist/commands/sync.d.ts +2 -1
  76. package/dist/commands/sync.js +21 -5
  77. package/dist/convex.d.ts +54 -0
  78. package/dist/convex.js +64 -0
  79. package/dist/push/core.js +7 -2
  80. package/dist/rehearsal/derive.d.ts +23 -0
  81. package/dist/rehearsal/derive.js +112 -0
  82. package/dist/rehearsal/driver.d.ts +44 -0
  83. package/dist/rehearsal/driver.js +337 -0
  84. package/dist/rehearsal/earlier.d.ts +25 -0
  85. package/dist/rehearsal/earlier.js +138 -0
  86. package/dist/rehearsal/index.d.ts +5 -0
  87. package/dist/rehearsal/index.js +4 -0
  88. package/dist/rehearsal/live.d.ts +72 -0
  89. package/dist/rehearsal/live.js +301 -0
  90. package/dist/rehearsal/report.d.ts +4 -0
  91. package/dist/rehearsal/report.js +69 -0
  92. package/dist/rehearsal/types.d.ts +377 -0
  93. package/dist/rehearsal/types.js +8 -0
  94. package/dist/requirements/index.d.ts +6 -0
  95. package/dist/requirements/index.js +24 -0
  96. package/dist/schema/browser.d.ts +1 -1
  97. package/dist/schema/browser.js +3 -1
  98. package/dist/schema/file-writer.d.ts +5 -0
  99. package/dist/schema/file-writer.js +4 -0
  100. package/dist/schema/parser.d.ts +11 -2
  101. package/dist/schema/parser.js +11 -7
  102. package/dist/schema/schemas.d.ts +40 -0
  103. package/dist/schema/schemas.js +4 -0
  104. package/dist/sync/execute.d.ts +10 -1
  105. package/dist/sync/execute.js +69 -5
  106. package/dist/sync/index.d.ts +1 -1
  107. package/dist/sync/index.js +1 -1
  108. package/dist/sync/publish.d.ts +7 -0
  109. package/dist/sync/segment.js +4 -1
  110. package/dist/sync/snapshot.d.ts +23 -1
  111. package/dist/sync/snapshot.js +75 -3
  112. package/dist/sync/solution-documents.d.ts +64 -0
  113. package/dist/sync/solution-documents.js +97 -0
  114. package/dist/sync/types.d.ts +18 -0
  115. package/package.json +3 -2
@@ -86,6 +86,8 @@ 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
+ solution: z.ZodOptional<z.ZodString>;
90
+ outcome: z.ZodOptional<z.ZodString>;
89
91
  attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
90
92
  url: z.ZodString;
91
93
  title: z.ZodOptional<z.ZodString>;
@@ -112,6 +114,8 @@ export declare const MetadataSchema: z.ZodObject<{
112
114
  id?: string | undefined;
113
115
  title?: string | undefined;
114
116
  defaultPrefix?: string | undefined;
117
+ solution?: string | undefined;
118
+ outcome?: string | undefined;
115
119
  attachments?: {
116
120
  url: string;
117
121
  title?: string | undefined;
@@ -120,6 +124,8 @@ export declare const MetadataSchema: z.ZodObject<{
120
124
  id?: string | undefined;
121
125
  title?: string | undefined;
122
126
  defaultPrefix?: string | undefined;
127
+ solution?: string | undefined;
128
+ outcome?: string | undefined;
123
129
  attachments?: {
124
130
  url: string;
125
131
  title?: string | undefined;
@@ -133,6 +139,8 @@ export declare const MetadataSchema: z.ZodObject<{
133
139
  id?: string | undefined;
134
140
  title?: string | undefined;
135
141
  defaultPrefix?: string | undefined;
142
+ solution?: string | undefined;
143
+ outcome?: string | undefined;
136
144
  attachments?: {
137
145
  url: string;
138
146
  title?: string | undefined;
@@ -146,6 +154,8 @@ export declare const MetadataSchema: z.ZodObject<{
146
154
  id?: string | undefined;
147
155
  title?: string | undefined;
148
156
  defaultPrefix?: string | undefined;
157
+ solution?: string | undefined;
158
+ outcome?: string | undefined;
149
159
  attachments?: {
150
160
  url: string;
151
161
  title?: string | undefined;
@@ -213,6 +223,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
213
223
  id: z.ZodOptional<z.ZodString>;
214
224
  title: z.ZodOptional<z.ZodString>;
215
225
  defaultPrefix: z.ZodOptional<z.ZodString>;
226
+ solution: z.ZodOptional<z.ZodString>;
227
+ outcome: z.ZodOptional<z.ZodString>;
216
228
  attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
217
229
  url: z.ZodString;
218
230
  title: z.ZodOptional<z.ZodString>;
@@ -239,6 +251,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
239
251
  id?: string | undefined;
240
252
  title?: string | undefined;
241
253
  defaultPrefix?: string | undefined;
254
+ solution?: string | undefined;
255
+ outcome?: string | undefined;
242
256
  attachments?: {
243
257
  url: string;
244
258
  title?: string | undefined;
@@ -247,6 +261,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
247
261
  id?: string | undefined;
248
262
  title?: string | undefined;
249
263
  defaultPrefix?: string | undefined;
264
+ solution?: string | undefined;
265
+ outcome?: string | undefined;
250
266
  attachments?: {
251
267
  url: string;
252
268
  title?: string | undefined;
@@ -260,6 +276,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
260
276
  id?: string | undefined;
261
277
  title?: string | undefined;
262
278
  defaultPrefix?: string | undefined;
279
+ solution?: string | undefined;
280
+ outcome?: string | undefined;
263
281
  attachments?: {
264
282
  url: string;
265
283
  title?: string | undefined;
@@ -273,6 +291,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
273
291
  id?: string | undefined;
274
292
  title?: string | undefined;
275
293
  defaultPrefix?: string | undefined;
294
+ solution?: string | undefined;
295
+ outcome?: string | undefined;
276
296
  attachments?: {
277
297
  url: string;
278
298
  title?: string | undefined;
@@ -288,6 +308,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
288
308
  id: z.ZodOptional<z.ZodString>;
289
309
  title: z.ZodOptional<z.ZodString>;
290
310
  defaultPrefix: z.ZodOptional<z.ZodString>;
311
+ solution: z.ZodOptional<z.ZodString>;
312
+ outcome: z.ZodOptional<z.ZodString>;
291
313
  attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
292
314
  url: z.ZodString;
293
315
  title: z.ZodOptional<z.ZodString>;
@@ -314,6 +336,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
314
336
  id?: string | undefined;
315
337
  title?: string | undefined;
316
338
  defaultPrefix?: string | undefined;
339
+ solution?: string | undefined;
340
+ outcome?: string | undefined;
317
341
  attachments?: {
318
342
  url: string;
319
343
  title?: string | undefined;
@@ -322,6 +346,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
322
346
  id?: string | undefined;
323
347
  title?: string | undefined;
324
348
  defaultPrefix?: string | undefined;
349
+ solution?: string | undefined;
350
+ outcome?: string | undefined;
325
351
  attachments?: {
326
352
  url: string;
327
353
  title?: string | undefined;
@@ -335,6 +361,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
335
361
  id?: string | undefined;
336
362
  title?: string | undefined;
337
363
  defaultPrefix?: string | undefined;
364
+ solution?: string | undefined;
365
+ outcome?: string | undefined;
338
366
  attachments?: {
339
367
  url: string;
340
368
  title?: string | undefined;
@@ -348,6 +376,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
348
376
  id?: string | undefined;
349
377
  title?: string | undefined;
350
378
  defaultPrefix?: string | undefined;
379
+ solution?: string | undefined;
380
+ outcome?: string | undefined;
351
381
  attachments?: {
352
382
  url: string;
353
383
  title?: string | undefined;
@@ -363,6 +393,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
363
393
  id: z.ZodOptional<z.ZodString>;
364
394
  title: z.ZodOptional<z.ZodString>;
365
395
  defaultPrefix: z.ZodOptional<z.ZodString>;
396
+ solution: z.ZodOptional<z.ZodString>;
397
+ outcome: z.ZodOptional<z.ZodString>;
366
398
  attachments: z.ZodEffects<z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
367
399
  url: z.ZodString;
368
400
  title: z.ZodOptional<z.ZodString>;
@@ -389,6 +421,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
389
421
  id?: string | undefined;
390
422
  title?: string | undefined;
391
423
  defaultPrefix?: string | undefined;
424
+ solution?: string | undefined;
425
+ outcome?: string | undefined;
392
426
  attachments?: {
393
427
  url: string;
394
428
  title?: string | undefined;
@@ -397,6 +431,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
397
431
  id?: string | undefined;
398
432
  title?: string | undefined;
399
433
  defaultPrefix?: string | undefined;
434
+ solution?: string | undefined;
435
+ outcome?: string | undefined;
400
436
  attachments?: {
401
437
  url: string;
402
438
  title?: string | undefined;
@@ -410,6 +446,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
410
446
  id?: string | undefined;
411
447
  title?: string | undefined;
412
448
  defaultPrefix?: string | undefined;
449
+ solution?: string | undefined;
450
+ outcome?: string | undefined;
413
451
  attachments?: {
414
452
  url: string;
415
453
  title?: string | undefined;
@@ -423,6 +461,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
423
461
  id?: string | undefined;
424
462
  title?: string | undefined;
425
463
  defaultPrefix?: string | undefined;
464
+ solution?: string | undefined;
465
+ outcome?: string | undefined;
426
466
  attachments?: {
427
467
  url: string;
428
468
  title?: string | undefined;
@@ -147,6 +147,10 @@ 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
+ // SOLDOC-5: the solution (by id) and the outcome (by name) this document
151
+ // belongs to. Written by the sync; the truth once present.
152
+ solution: z.string().optional(),
153
+ outcome: z.string().optional(),
150
154
  // ATTACH-10: links attached to the document (a Miro board, a reference
151
155
  // page). Round-trips through sync — the cloud stores them as document
152
156
  // metadata, the file carries them here.
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { PlannedAction, SyncMode } from "./plan.js";
11
11
  import type { CloudAuth } from "./snapshot.js";
12
+ import { type SolutionForSync } from "./solution-documents.js";
12
13
  import type { CloudSnapshot, DocumentComparison } from "./types.js";
13
14
  export interface SyncOutcome {
14
15
  cloudCreated: number;
@@ -59,6 +60,8 @@ export interface SyncOutcome {
59
60
  name: string;
60
61
  warning: string;
61
62
  }>;
63
+ /** SOLDOC-5.1/5.2: a file whose path and frontmatter disagree about where it belongs. Optional so an outcome built before the field existed still reads. */
64
+ belongingWarnings?: string[];
62
65
  /** SYNC-CLI-EDIT-3.1: files whose notation the cloud changed on storing —
63
66
  * list markers, emphasis characters, spacing. Counted so the sync can say
64
67
  * the reformatting happened and why, instead of leaving Jaime to discover
@@ -85,5 +88,11 @@ export interface SyncOutcome {
85
88
  * writes require a project slug in `auth`; a read-only share run has none and
86
89
  * its plan carries no cloud-writing actions.
87
90
  */
88
- export declare function executePlan(plan: PlannedAction[], cloud: CloudSnapshot, auth: CloudAuth, workspaceRoot: string, mode: SyncMode): Promise<SyncOutcome>;
91
+ export declare function executePlan(plan: PlannedAction[], cloud: CloudSnapshot, auth: CloudAuth, workspaceRoot: string, mode: SyncMode,
92
+ /**
93
+ * The project's solutions, as `projectSolutions` read them; null (the
94
+ * default) when the project's team doesn't have solutions
95
+ * (SOLACCESS-2.2), so no file is placed as a solution's.
96
+ */
97
+ solutions?: SolutionForSync[] | null): Promise<SyncOutcome>;
89
98
  //# sourceMappingURL=execute.d.ts.map
@@ -16,8 +16,9 @@ import { parseFilesForPushIndividually, WEB_APP_URL } from "../push/core.js";
16
16
  import { composeMarkdownWithTitle, splitLeadingH1, writeRequirementsFile, } from "../schema/index.js";
17
17
  import { commandErrorMessage } from "../utils/command-error.js";
18
18
  import { cloudAttachmentRemovals, resolveAttachments } from "./attachments.js";
19
- import { resolveLocalPath } from "./local-files.js";
19
+ import { resolveLocalPath, sanitizeFileName } from "./local-files.js";
20
20
  import { publishToCloud } from "./publish.js";
21
+ import { requirementsDocByName, resolveBelonging, solutionDirOf, solutionDocumentPath, } from "./solution-documents.js";
21
22
  function emptyOutcome() {
22
23
  return {
23
24
  cloudCreated: 0,
@@ -35,6 +36,7 @@ function emptyOutcome() {
35
36
  writeBackWarnings: [],
36
37
  importWarnings: [],
37
38
  publishWarnings: [],
39
+ belongingWarnings: [],
38
40
  normalized: [],
39
41
  renames: [],
40
42
  inferredPrefixes: [],
@@ -45,7 +47,13 @@ function emptyOutcome() {
45
47
  * writes require a project slug in `auth`; a read-only share run has none and
46
48
  * its plan carries no cloud-writing actions.
47
49
  */
48
- export async function executePlan(plan, cloud, auth, workspaceRoot, mode) {
50
+ export async function executePlan(plan, cloud, auth, workspaceRoot, mode,
51
+ /**
52
+ * The project's solutions, as `projectSolutions` read them; null (the
53
+ * default) when the project's team doesn't have solutions
54
+ * (SOLACCESS-2.2), so no file is placed as a solution's.
55
+ */
56
+ solutions = null) {
49
57
  const outcome = emptyOutcome();
50
58
  const client = new ConvexHttpClient(convexUrl());
51
59
  const cloudById = new Map(cloud.documents.map((d) => [d.documentId, d]));
@@ -64,7 +72,7 @@ export async function executePlan(plan, cloud, auth, workspaceRoot, mode) {
64
72
  outcome.invalid.push(doc);
65
73
  break;
66
74
  case "cloud_write":
67
- await cloudWrite(auth, doc, cloudById, outcome, mode);
75
+ await cloudWrite(auth, doc, cloudById, outcome, mode, solutions);
68
76
  break;
69
77
  case "cloud_delete":
70
78
  await cloudDelete(client, auth, doc, outcome);
@@ -161,7 +169,7 @@ async function syncResidualAttachments(client, auth, doc, action, outcome, mode)
161
169
  outcome.attachmentWrites++;
162
170
  }
163
171
  }
164
- async function cloudWrite(auth, doc, cloudById, outcome, mode) {
172
+ async function cloudWrite(auth, doc, cloudById, outcome, mode, solutions) {
165
173
  if (!auth.projectSlug || !doc.filePath) {
166
174
  throw new Error("Cannot write to the cloud without project credentials.");
167
175
  }
@@ -170,6 +178,24 @@ async function cloudWrite(auth, doc, cloudById, outcome, mode) {
170
178
  if (!parsed)
171
179
  throw new Error(`Could not parse ${path.basename(doc.filePath)}`);
172
180
  const meta = parsed.metadata.document;
181
+ // SOLDOC-3/4/5: what the file's name and place say about it. A file in a
182
+ // solution directory with no frontmatter declares its belonging by its
183
+ // path; one with frontmatter keeps that, and a disagreeing path is reported.
184
+ // SOLACCESS-2.2: without solutions, a file is placed as it was before them.
185
+ const withSolutions = solutions !== null;
186
+ const resolved = withSolutions
187
+ ? resolveBelonging(doc.filePath, { solution: meta?.solution, outcome: meta?.outcome }, solutionDirOf(doc.filePath), solutions)
188
+ : { belonging: undefined, warning: undefined };
189
+ if (resolved.warning) {
190
+ outcome.belongingWarnings = [
191
+ ...(outcome.belongingWarnings ?? []),
192
+ resolved.warning,
193
+ ];
194
+ }
195
+ const belonging = resolved.belonging;
196
+ const isRequirementsDoc = withSolutions
197
+ ? requirementsDocByName(doc.filePath)
198
+ : undefined;
173
199
  // SYNC-TITLE-1.1: a legacy file whose title lives only in frontmatter (no body
174
200
  // H1) must reach the cloud self-describing. Materialize the title as the body's
175
201
  // leading H1 BEFORE the push — so the cloud row carries the H1 on first write
@@ -214,6 +240,8 @@ async function cloudWrite(auth, doc, cloudById, outcome, mode) {
214
240
  : []
215
241
  : undefined,
216
242
  runMarker: parsed.metadata.ctsRun,
243
+ solution: belonging,
244
+ isRequirementsDoc,
217
245
  });
218
246
  const documentId = saveResult.documentId;
219
247
  // ATTACH-10.3.1.0: the publish above is the push that removes cloud
@@ -319,6 +347,9 @@ async function cloudWrite(auth, doc, cloudById, outcome, mode) {
319
347
  // the title.
320
348
  writeRequirementsFile(doc.filePath, {
321
349
  documentId,
350
+ // SOLDOC-4.1: the belonging the path declared is written into the
351
+ // frontmatter, where it is the truth from now on.
352
+ belonging,
322
353
  // The prefix just pushed is the file's own (or the one inferred from
323
354
  // its first key, DOC-HEADER-11.4, which the write-back materializes).
324
355
  defaultPrefix: meta?.defaultPrefix
@@ -365,8 +396,9 @@ function localWrite(doc, cloudById, usedPaths, requirementsDir, outcome, mode) {
365
396
  throw new Error(`No cloud content for ${doc.title}`);
366
397
  if (!fs.existsSync(requirementsDir))
367
398
  fs.mkdirSync(requirementsDir, { recursive: true });
368
- const filePath = resolveLocalPath(cloudDoc, doc.filePath, usedPaths, requirementsDir);
399
+ const filePath = placeCloudDocument(cloudDoc, doc.filePath, usedPaths, requirementsDir);
369
400
  usedPaths.add(filePath);
401
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
370
402
  // DOC-HEADER-11.6/11.7: a cloud document with no prefix means "nothing to
371
403
  // impose" under an additive sync and "no prefix" under cloud-wins authority
372
404
  // — so a prefix Jaime wrote is only ever removed by the mode they ran to
@@ -390,10 +422,42 @@ function localWrite(doc, cloudById, usedPaths, requirementsDir, outcome, mode) {
390
422
  defaultPrefix,
391
423
  attachments,
392
424
  version: cloudDoc.version,
425
+ belonging: cloudDoc.solution
426
+ ? {
427
+ solutionId: cloudDoc.solution.solutionId,
428
+ outcome: cloudDoc.solution.outcome,
429
+ }
430
+ : undefined,
393
431
  }, cloudDoc.body);
394
432
  if (changed)
395
433
  outcome.localWritten++;
396
434
  }
435
+ /**
436
+ * SOLDOC-6: where a pulled document lands. A file it is already linked to
437
+ * keeps its path (SYNC-DISCOVERY-2.0); a document that belongs to a solution
438
+ * lands in that solution's directory, named for its outcome; a document
439
+ * that is not a requirements document and belongs to no solution has no
440
+ * place, and the pull says so rather than guessing.
441
+ */
442
+ function placeCloudDocument(cloudDoc, existingPath, usedPaths, requirementsDir) {
443
+ if (existingPath)
444
+ return existingPath;
445
+ const isRequirementsDoc = cloudDoc.isRequirementsDoc !== false;
446
+ if (cloudDoc.solution) {
447
+ // SOLDOC-9: the solution's own directory name; a backend from before it
448
+ // was sent leaves it out, and the title's slug stands in.
449
+ const candidate = solutionDocumentPath(requirementsDir, cloudDoc.solution.directory ??
450
+ sanitizeFileName(cloudDoc.solution.title ?? cloudDoc.solution.solutionId), cloudDoc.solution.outcome, isRequirementsDoc);
451
+ if (usedPaths.has(candidate) || fs.existsSync(candidate)) {
452
+ throw new Error(`"${cloudDoc.title}" belongs at ${path.relative(path.dirname(requirementsDir), candidate)}, but another file is already there.`);
453
+ }
454
+ return candidate;
455
+ }
456
+ if (!isRequirementsDoc) {
457
+ throw new Error(`"${cloudDoc.title}" is not a requirements document and belongs to no solution, so the pull has nowhere to put it.`);
458
+ }
459
+ return resolveLocalPath(cloudDoc, existingPath, usedPaths, requirementsDir);
460
+ }
397
461
  function localDelete(doc, outcome) {
398
462
  if (!doc.filePath)
399
463
  return;
@@ -6,7 +6,7 @@ import type { ComparisonResult, DocumentComparison } from "./types.js";
6
6
  export { compareSnapshots, duplicateIdAbortMessage, duplicateLocalDocumentIds, } from "./compare.js";
7
7
  export { displayName, formatUnitDetail, formatVerdictLine, resolutionHint, scopeToken, } from "./render.js";
8
8
  export { segmentBody } from "./segment.js";
9
- export { acquireCloudSnapshot, acquireLocalSnapshot, type CloudAuth, readLocalDocument, } from "./snapshot.js";
9
+ export { acquireCloudSnapshot, acquireLocalSnapshot, type CloudAuth, foreignDocumentNotes, projectSolutions, readLocalDocument, } from "./snapshot.js";
10
10
  export type { CloudDocument, CloudSnapshot, ComparisonResult, DocumentComparison, LocalDocument, LocalSnapshot, UnitDetail, Verdict, } from "./types.js";
11
11
  export { verdictLabel } from "./types.js";
12
12
  /**
@@ -6,7 +6,7 @@ import * as path from "node:path";
6
6
  export { compareSnapshots, duplicateIdAbortMessage, duplicateLocalDocumentIds, } from "./compare.js";
7
7
  export { displayName, formatUnitDetail, formatVerdictLine, resolutionHint, scopeToken, } from "./render.js";
8
8
  export { segmentBody } from "./segment.js";
9
- export { acquireCloudSnapshot, acquireLocalSnapshot, readLocalDocument, } from "./snapshot.js";
9
+ export { acquireCloudSnapshot, acquireLocalSnapshot, foreignDocumentNotes, projectSolutions, readLocalDocument, } from "./snapshot.js";
10
10
  export { verdictLabel } from "./types.js";
11
11
  /**
12
12
  * Does a document match one scope token? A token may name a local file (path or
@@ -32,6 +32,13 @@ export interface PublishParams {
32
32
  title?: string;
33
33
  }>;
34
34
  runMarker?: string;
35
+ /** SOLDOC-4/5: the belonging the file declares. */
36
+ solution?: {
37
+ solutionId: string;
38
+ outcome: string;
39
+ };
40
+ /** SOLDOC-3: what the file's name says; undefined where the rule doesn't reach. */
41
+ isRequirementsDoc?: boolean;
35
42
  }
36
43
  /**
37
44
  * What the server reports back: the document, any run-marker complaint, and any
@@ -6,7 +6,10 @@
6
6
  * normalized text (prose has no stable id, so a reworded passage reads as a
7
7
  * removal-plus-addition — a conflict, per DIFF-4.3).
8
8
  */
9
- import { canonicalRequirementKey, canonicalRequirementPath, getAllRequirements, parseRequirementBlock, splitRequirementFenceContent, } from "../schema/index.js";
9
+ // From the fs-free modules, not the schema index, so the backend can bundle
10
+ // the comparator for merging a solution's branch (BRANCH-7).
11
+ import { getAllRequirements, parseRequirementBlock, splitRequirementFenceContent, } from "../schema/parser-core.js";
12
+ import { canonicalRequirementKey, canonicalRequirementPath, } from "../schema/schemas.js";
10
13
  /** Matches a fenced dotrequirements block; group 1 is its inner content. */
11
14
  const FENCE = /```dotrequirements\n([\s\S]*?)```/gm;
12
15
  /**
@@ -3,6 +3,7 @@
3
3
  * the comparator stays a pure function (compare.ts). The local half reads the
4
4
  * workspace; the cloud half is one export query.
5
5
  */
6
+ import type { SolutionForSync } from "./solution-documents.js";
6
7
  import type { CloudSnapshot, LocalDocument, LocalSnapshot } from "./types.js";
7
8
  /**
8
9
  * Read and classify one requirements file into a LocalDocument. A file whose
@@ -15,7 +16,21 @@ export declare function readLocalDocument(filePath: string): LocalDocument;
15
16
  * Build the local snapshot from the workspace, or from an explicit set of file
16
17
  * paths (scope). Files are always read fresh from disk.
17
18
  */
18
- export declare function acquireLocalSnapshot(workspaceRoot: string, filePaths?: string[]): Promise<LocalSnapshot>;
19
+ export declare function acquireLocalSnapshot(workspaceRoot: string, filePaths?: string[], options?: {
20
+ solutions?: boolean;
21
+ }): Promise<LocalSnapshot>;
22
+ /**
23
+ * The project's solutions as the sync places files by them (SOLDOC-4), or
24
+ * null when the project's team doesn't have solutions, so the sync behaves
25
+ * as it did before solutions (SOLACCESS-2.2). Whether it has them is the
26
+ * export's answer — the one the cloud side was read with — so the local
27
+ * side can't follow a different one. A share run places nothing, so it
28
+ * needs no list. A failure to fetch the list fails the sync, as does a
29
+ * switch flipped between the two reads: guessing would leave solution
30
+ * documents out of the repo's side, and a repo-wins sync would delete them
31
+ * from the cloud.
32
+ */
33
+ export declare function projectSolutions(auth: CloudAuth, cloud: CloudSnapshot): Promise<SolutionForSync[] | null>;
19
34
  export interface CloudAuth {
20
35
  /** projectSlug + projectSecret for full access, or a bare share token. */
21
36
  projectSlug?: string;
@@ -27,4 +42,11 @@ export interface CloudAuth {
27
42
  * names the project) — the read-only path never writes back.
28
43
  */
29
44
  export declare function acquireCloudSnapshot(auth: CloudAuth): Promise<CloudSnapshot>;
45
+ /**
46
+ * SYNC-FAIL-5: of the ids the workspace's files carry, the ones that name
47
+ * another project's document, each with the words a push of that file gets,
48
+ * so `dotreq diff` says what `dotreq sync` will. A backend that can't answer
49
+ * leaves the diff as it was rather than failing it.
50
+ */
51
+ export declare function foreignDocumentNotes(auth: CloudAuth, documentIds: string[]): Promise<Map<string, string>>;
30
52
  //# sourceMappingURL=snapshot.d.ts.map
@@ -8,7 +8,7 @@ import { ConvexHttpClient } from "convex/browser";
8
8
  import YAML from "yaml";
9
9
  import { convexUrl } from "../cloud.js";
10
10
  import { api } from "../convex.js";
11
- import { findRequirementsFiles } from "../requirements/index.js";
11
+ import { findRequirementsFiles, findSyncedFiles, } from "../requirements/index.js";
12
12
  import { splitLeadingH1, validateMetadata } from "../schema/index.js";
13
13
  import { parseRequirementBlocksFromMarkdown, splitFrontmatter, stripFrontmatterBlock, } from "../schema/parser-core.js";
14
14
  /**
@@ -33,6 +33,8 @@ export function readLocalDocument(filePath) {
33
33
  let title;
34
34
  let defaultPrefix;
35
35
  let attachments;
36
+ let solution;
37
+ let outcome;
36
38
  let frontmatterError;
37
39
  const split = splitFrontmatter(raw);
38
40
  if (split) {
@@ -57,6 +59,11 @@ export function readLocalDocument(filePath) {
57
59
  title = d.title;
58
60
  if (typeof d.defaultPrefix === "string")
59
61
  defaultPrefix = d.defaultPrefix;
62
+ // SOLDOC-5: the belonging, read like the id — before validation.
63
+ if (typeof d.solution === "string")
64
+ solution = d.solution;
65
+ if (typeof d.outcome === "string")
66
+ outcome = d.outcome;
60
67
  // ATTACH-10.1: attachments from frontmatter. Shape-filtered here for
61
68
  // the same reason as the fields above; a malformed entry is caught by
62
69
  // validateMetadata below and flags the file rather than half-syncing.
@@ -108,6 +115,8 @@ export function readLocalDocument(filePath) {
108
115
  title: derivedTitle,
109
116
  defaultPrefix,
110
117
  attachments,
118
+ solution,
119
+ outcome,
111
120
  body,
112
121
  };
113
122
  }
@@ -115,10 +124,44 @@ export function readLocalDocument(filePath) {
115
124
  * Build the local snapshot from the workspace, or from an explicit set of file
116
125
  * paths (scope). Files are always read fresh from disk.
117
126
  */
118
- export async function acquireLocalSnapshot(workspaceRoot, filePaths) {
119
- const paths = filePaths ?? (await findRequirementsFiles(workspaceRoot));
127
+ export async function acquireLocalSnapshot(workspaceRoot, filePaths, options = {}) {
128
+ // SOLACCESS-2.2: a project whose team doesn't have solutions syncs the
129
+ // files it always did — solution directories' Markdown is left alone.
130
+ const paths = filePaths ??
131
+ (options.solutions === false
132
+ ? await findRequirementsFiles(workspaceRoot)
133
+ : await findSyncedFiles(workspaceRoot));
120
134
  return { documents: paths.map(readLocalDocument) };
121
135
  }
136
+ /**
137
+ * The project's solutions as the sync places files by them (SOLDOC-4), or
138
+ * null when the project's team doesn't have solutions, so the sync behaves
139
+ * as it did before solutions (SOLACCESS-2.2). Whether it has them is the
140
+ * export's answer — the one the cloud side was read with — so the local
141
+ * side can't follow a different one. A share run places nothing, so it
142
+ * needs no list. A failure to fetch the list fails the sync, as does a
143
+ * switch flipped between the two reads: guessing would leave solution
144
+ * documents out of the repo's side, and a repo-wins sync would delete them
145
+ * from the cloud.
146
+ */
147
+ export async function projectSolutions(auth, cloud) {
148
+ if (cloud.solutions !== true)
149
+ return null;
150
+ if (!auth.projectSlug)
151
+ return [];
152
+ const client = new ConvexHttpClient(convexUrl());
153
+ const answer = await client.query(api.solutions.queries.forSync, {
154
+ projectAuth: {
155
+ projectSlug: auth.projectSlug,
156
+ projectSecret: auth.projectSecret,
157
+ },
158
+ project: auth.projectSlug,
159
+ });
160
+ if (!Array.isArray(answer)) {
161
+ throw new Error("The project's solutions were switched off while syncing. Run the sync again.");
162
+ }
163
+ return answer;
164
+ }
122
165
  /**
123
166
  * Build the cloud snapshot with one export query: the project's own, with a
124
167
  * project secret, or the share endpoint, with a bare share token (the token
@@ -140,6 +183,7 @@ export async function acquireCloudSnapshot(auth) {
140
183
  return {
141
184
  projectName: result.projectName,
142
185
  projectSlug: result.projectSlug,
186
+ solutions: result.solutions === true,
143
187
  documents: result.documents.map((d) => ({
144
188
  documentId: d.documentId,
145
189
  title: d.title,
@@ -148,7 +192,35 @@ export async function acquireCloudSnapshot(auth) {
148
192
  body: d.markdownContent,
149
193
  version: d.version,
150
194
  updatedAt: d.updatedAt,
195
+ solution: d.solution,
196
+ isRequirementsDoc: d.isRequirementsDoc,
151
197
  })),
152
198
  };
153
199
  }
200
+ /**
201
+ * SYNC-FAIL-5: of the ids the workspace's files carry, the ones that name
202
+ * another project's document, each with the words a push of that file gets,
203
+ * so `dotreq diff` says what `dotreq sync` will. A backend that can't answer
204
+ * leaves the diff as it was rather than failing it.
205
+ */
206
+ export async function foreignDocumentNotes(auth, documentIds) {
207
+ // A share token only syncs down, so it pushes nothing to warn about.
208
+ if (documentIds.length === 0 || !auth.projectSlug)
209
+ return new Map();
210
+ const client = new ConvexHttpClient(convexUrl());
211
+ try {
212
+ const foreign = (await client.query(api.documents.queries.foreignDocumentIds, {
213
+ projectAuth: {
214
+ projectSlug: auth.projectSlug,
215
+ projectSecret: auth.projectSecret,
216
+ },
217
+ project: auth.projectSlug,
218
+ documentIds,
219
+ }));
220
+ return new Map(foreign.map((f) => [f.documentId, f.message]));
221
+ }
222
+ catch {
223
+ return new Map();
224
+ }
225
+ }
154
226
  //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Where a solution's documents live, the sync's half (SOLDOC; the decision
3
+ * is in docs/working/four-areas-prototype.md, "Where a solution's documents
4
+ * live"). A document that belongs to a solution and is not a requirements
5
+ * document lives in a solution directory, `.requirements/solutions/<solution>/`,
6
+ * named for its outcome; the `.requirements.md` name is what makes a file a
7
+ * requirements document, wherever it sits. A new file declares its belonging
8
+ * by its path; once the frontmatter carries it, the frontmatter is the truth.
9
+ */
10
+ export declare const SOLUTIONS_DIR = "solutions";
11
+ /** What a file's place in the workspace says about it, before any frontmatter. */
12
+ export interface PathClaim {
13
+ /** The solution directory's name and the file's stem, as slugs. */
14
+ solutionSlug: string;
15
+ outcomeSlug: string;
16
+ }
17
+ /** The solution directory a file sits directly in, if any (SOLDOC-8.1). */
18
+ export declare function solutionDirOf(filePath: string): PathClaim | undefined;
19
+ /** `pitch` for `pitch.md` and for `pitch.requirements.md`. */
20
+ export declare function stemOf(fileName: string): string;
21
+ /** SOLDOC-3: the name, never the content. Undefined for a file the rule doesn't reach. */
22
+ export declare function requirementsDocByName(filePath: string): boolean | undefined;
23
+ export interface SolutionForSync {
24
+ solutionId: string;
25
+ title: string;
26
+ /**
27
+ * The solution's directory name, which no other solution in the project
28
+ * has (SOLDOC-9). A backend from before it was sent leaves it out, and the
29
+ * title's slug stands in.
30
+ */
31
+ directory?: string;
32
+ outcomes: string[];
33
+ }
34
+ /** The directory a solution's documents live in, by name. */
35
+ export declare function directoryOf(solution: SolutionForSync): string;
36
+ export interface Belonging {
37
+ solutionId: string;
38
+ outcome: string;
39
+ }
40
+ export type Resolved = {
41
+ belonging: Belonging;
42
+ warning?: string;
43
+ } | {
44
+ belonging: undefined;
45
+ warning?: string;
46
+ };
47
+ /**
48
+ * What a pushed file belongs to (SOLDOC-4, SOLDOC-5): the frontmatter's
49
+ * belonging when it has one, else what its path claims, resolved against
50
+ * the project's solutions. A frontmatter that disagrees with the path is
51
+ * kept and reported; a path that resolves to nothing refuses the push.
52
+ */
53
+ export declare function resolveBelonging(filePath: string, frontmatter: {
54
+ solution?: string;
55
+ outcome?: string;
56
+ }, claim: PathClaim | undefined, solutions: SolutionForSync[] | undefined): Resolved;
57
+ /**
58
+ * SOLDOC-6, SOLDOC-9: where a pulled document that belongs to a solution
59
+ * lands when it has no file yet: in the solution's own directory, named for
60
+ * its outcome, with the `.requirements.md` name when it is a requirements
61
+ * document.
62
+ */
63
+ export declare function solutionDocumentPath(requirementsDir: string, solutionDirectory: string, outcome: string, isRequirementsDoc: boolean): string;
64
+ //# sourceMappingURL=solution-documents.d.ts.map