@popoverai/dotrequirements 0.39.3 → 0.39.5

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.
@@ -4,7 +4,9 @@
4
4
  * JSON), its title, its description, its plan in the stored shape, and the
5
5
  * inputs that already exist. `--description` says what the solution is
6
6
  * beyond its title, in Markdown, and wins over the file's. The backend validates the plan and refuses an invalid one
7
- * with the rules it broke.
7
+ * with the rules it broke. `--idea` names where the idea is: an image is
8
+ * read and uploaded first, so the solution is created only once each one
9
+ * has (PLAN-13).
8
10
  */
9
11
  export declare function createCommand(file: string | undefined, options?: {
10
12
  title?: string;
@@ -2,18 +2,24 @@ import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { api } from "../../convex.js";
4
4
  import { solutionClient } from "./client.js";
5
+ import { readImage } from "./image.js";
5
6
  import { planPayload, readPlanFile } from "./plan-file.js";
6
7
  import { parseThing } from "./things.js";
8
+ import { uploadToApp } from "./upload.js";
9
+ const NOT_CREATED = "no solution was created";
7
10
  /**
8
11
  * `dotreq solution create [file]`: with no file, a solution from the
9
12
  * project's default plan, titled with `--title`; with a plan file (YAML or
10
13
  * JSON), its title, its description, its plan in the stored shape, and the
11
14
  * inputs that already exist. `--description` says what the solution is
12
15
  * beyond its title, in Markdown, and wins over the file's. The backend validates the plan and refuses an invalid one
13
- * with the rules it broke.
16
+ * with the rules it broke. `--idea` names where the idea is: an image is
17
+ * read and uploaded first, so the solution is created only once each one
18
+ * has (PLAN-13).
14
19
  */
15
20
  export async function createCommand(file, options = {}) {
16
- const idea = options.idea?.map(parseThing);
21
+ const references = options.idea?.map(parseThing);
22
+ const images = readIdeaImages(references ?? []);
17
23
  const described = options.description?.trim() || undefined;
18
24
  if (!file) {
19
25
  const title = options.title?.trim();
@@ -21,6 +27,7 @@ export async function createCommand(file, options = {}) {
21
27
  throw new Error("A solution from the project's default plan needs a title: --title <title>.");
22
28
  }
23
29
  const client = solutionClient();
30
+ const idea = await ideaThings(client, references, images);
24
31
  const id = await client.mutation(api.solutions.mutations.create, {
25
32
  project: client.project,
26
33
  title,
@@ -37,6 +44,7 @@ export async function createCommand(file, options = {}) {
37
44
  throw new Error(`${file} needs a plan, and a title in the file or as --title (docs/working/plan-surfaces.md).`);
38
45
  }
39
46
  const client = solutionClient();
47
+ const idea = await ideaThings(client, references, images);
40
48
  const id = await client.mutation(api.solutions.mutations.create, {
41
49
  project: client.project,
42
50
  title,
@@ -49,4 +57,47 @@ export async function createCommand(file, options = {}) {
49
57
  });
50
58
  console.log(`Created "${title}": ${id}`);
51
59
  }
60
+ /**
61
+ * Each image the idea names, read and checked before anything is uploaded
62
+ * or created: a path with no file, a format a browser can't show (PLAN-13.2,
63
+ * 13.3), or two of one file name, which the solution can't tell apart
64
+ * (13.4, SIGHT-10).
65
+ */
66
+ function readIdeaImages(references) {
67
+ const images = new Map();
68
+ const named = new Map();
69
+ for (const thing of references) {
70
+ if (thing.kind !== "image")
71
+ continue;
72
+ const image = readImage(thing.path, NOT_CREATED);
73
+ const earlier = named.get(image.name);
74
+ if (earlier !== undefined) {
75
+ throw new Error(`${earlier} and ${thing.path} are both called ${image.name}, so ${NOT_CREATED}. Rename one of them.`);
76
+ }
77
+ named.set(image.name, thing.path);
78
+ images.set(thing, image);
79
+ }
80
+ return images;
81
+ }
82
+ /**
83
+ * The idea as the backend keeps it, in the order given: each image uploaded
84
+ * for the project, as a rehearsal's input image is, and kept as its stored
85
+ * copy's id and name (PLAN-13.0, 13.1). One that doesn't upload stops the
86
+ * command before the solution is created (13.5).
87
+ */
88
+ async function ideaThings(client, references, images) {
89
+ if (!references)
90
+ return undefined;
91
+ const idea = [];
92
+ for (const thing of references) {
93
+ const image = images.get(thing);
94
+ if (!image) {
95
+ idea.push(thing);
96
+ continue;
97
+ }
98
+ const storageId = await uploadToApp(client, api.solutions.images.inputUploadUrl, { project: client.project }, image, `so ${NOT_CREATED}. Run the command again.`);
99
+ idea.push({ kind: "image", storageId, name: image.name });
100
+ }
101
+ return idea;
102
+ }
52
103
  //# sourceMappingURL=create.js.map
@@ -27,8 +27,11 @@ export type Image = {
27
27
  contentType: ImageContentType;
28
28
  bytes: Uint8Array<ArrayBuffer>;
29
29
  };
30
- /** The image at a path, or a refusal naming the accepted formats (SIGHT-9.0.0). */
31
- export declare function readImage(path: string): Image;
30
+ /**
31
+ * The image at a path, or a refusal naming the accepted formats
32
+ * (SIGHT-9.0.0) and what came of it: by default, that nothing was submitted.
33
+ */
34
+ export declare function readImage(path: string, outcome?: string): Image;
32
35
  /** What image format the bytes hold, read from the file's signature. */
33
36
  export declare function imageFormat(bytes: Uint8Array): ImageFormat;
34
37
  /**
@@ -18,16 +18,19 @@ import { uploadToApp } from "./upload.js";
18
18
  * with the backend's `ACCEPTED_IMAGE_FORMATS` (convex/solutions/images.ts).
19
19
  */
20
20
  export const ACCEPTED_IMAGE_FORMATS = "a PNG, JPEG, GIF, WebP or AVIF image";
21
- /** The image at a path, or a refusal naming the accepted formats (SIGHT-9.0.0). */
22
- export function readImage(path) {
21
+ /**
22
+ * The image at a path, or a refusal naming the accepted formats
23
+ * (SIGHT-9.0.0) and what came of it: by default, that nothing was submitted.
24
+ */
25
+ export function readImage(path, outcome = "nothing was submitted") {
23
26
  const full = resolve(path);
24
27
  if (!existsSync(full) || !statSync(full).isFile()) {
25
- throw new Error(`There is no file at ${path}, so nothing was submitted.`);
28
+ throw new Error(`There is no file at ${path}, so ${outcome}.`);
26
29
  }
27
30
  const bytes = new Uint8Array(readFileSync(full));
28
31
  const format = imageFormat(bytes);
29
32
  if ("refused" in format) {
30
- throw new Error(`${path} isn't an image a browser can show (${format.refused}), so nothing was submitted. Submit ${ACCEPTED_IMAGE_FORMATS}.`);
33
+ throw new Error(`${path} isn't an image a browser can show (${format.refused}), so ${outcome}. Submit ${ACCEPTED_IMAGE_FORMATS}.`);
31
34
  }
32
35
  return { name: basename(full), contentType: format.contentType, bytes };
33
36
  }
@@ -200,7 +200,7 @@ export function registerSolution(program) {
200
200
  .description("Create a solution from the project's default plan, or from a plan file (YAML or JSON: title, plan, inputs)")
201
201
  .option("--title <title>", "The solution's title: required without a file, else when the file has none")
202
202
  .option("--description <text>", "What the solution is beyond its title, in Markdown: wins over the file's")
203
- .option("--idea <reference...>", "Where the idea is written down, for the outcome that makes the solution exist: document:<id>, file:<path>[@<branch>], link:<url>")
203
+ .option("--idea <reference...>", "Where the idea is written down, for the outcome that makes the solution exist: document:<id>, file:<path>[@<branch>], link:<url>, image:<path>")
204
204
  .action(wrap(createCommand));
205
205
  whereOptions(solution
206
206
  .command("status")
@@ -12,7 +12,8 @@ export type Upload = {
12
12
  * the solution it is for (none for an image a rehearsal gives an input,
13
13
  * REHEARSE-4.7), the file POSTed to it with its type, and the stored copy's
14
14
  * id back. Convex sets no size
15
- * limit on these uploads; the POST has two minutes.
15
+ * limit on these uploads; the POST has two minutes. A failure names the
16
+ * file, then says what came of it: by default, that nothing was submitted.
16
17
  */
17
18
  export declare function uploadToApp(client: ReturnType<typeof solutionClient>, uploadUrl: FunctionReference<"mutation">,
18
19
  /** The upload URL endpoint's arguments: the solution, or the project for a rehearsal's input. */
@@ -20,5 +21,5 @@ args: {
20
21
  solutionId: string;
21
22
  } | {
22
23
  project: string;
23
- }, file: Upload): Promise<string>;
24
+ }, file: Upload, outcome?: string): Promise<string>;
24
25
  //# sourceMappingURL=upload.d.ts.map
@@ -4,23 +4,27 @@
4
4
  * the solution it is for (none for an image a rehearsal gives an input,
5
5
  * REHEARSE-4.7), the file POSTed to it with its type, and the stored copy's
6
6
  * id back. Convex sets no size
7
- * limit on these uploads; the POST has two minutes.
7
+ * limit on these uploads; the POST has two minutes. A failure names the
8
+ * file, then says what came of it: by default, that nothing was submitted.
8
9
  */
9
10
  export async function uploadToApp(client, uploadUrl,
10
11
  /** The upload URL endpoint's arguments: the solution, or the project for a rehearsal's input. */
11
- args, file) {
12
+ args, file, outcome = "so nothing was submitted. Submit it again.") {
12
13
  const url = await client.mutation(uploadUrl, args);
13
14
  const response = await fetch(url, {
14
15
  method: "POST",
15
16
  headers: { "Content-Type": file.contentType },
16
17
  body: file.bytes,
18
+ }).catch((error) => {
19
+ // The connection dropped, or never opened.
20
+ throw new Error(`Uploading ${file.name} failed (${error instanceof Error ? error.message : String(error)}), ${outcome}`);
17
21
  });
18
22
  if (!response.ok) {
19
- throw new Error(`Uploading ${file.name} failed (${response.status} ${response.statusText}), so nothing was submitted. Submit it again.`);
23
+ throw new Error(`Uploading ${file.name} failed (${response.status} ${response.statusText}), ${outcome}`);
20
24
  }
21
25
  const { storageId } = (await response.json());
22
26
  if (!storageId) {
23
- throw new Error(`Uploading ${file.name} returned no id, so nothing was submitted. Submit it again.`);
27
+ throw new Error(`Uploading ${file.name} returned no id, ${outcome}`);
24
28
  }
25
29
  return storageId;
26
30
  }
@@ -121,6 +121,11 @@ export function planForOutcomes(plan, names) {
121
121
  if (surface.mergeCloses !== undefined && !kept.has(surface.mergeCloses)) {
122
122
  delete own.mergeCloses;
123
123
  }
124
+ // PULL-20.6: nor does approving a checkpoint left out merge it.
125
+ if (surface.mergeOn !== undefined &&
126
+ !checkpoints.some((checkpoint) => checkpoint.name === surface.mergeOn)) {
127
+ delete own.mergeOn;
128
+ }
124
129
  return own;
125
130
  }),
126
131
  phases,
@@ -105,6 +105,8 @@ export type Plan = {
105
105
  watchedBy?: string;
106
106
  /** The outcome a merge of its pull request closes (PULL-12). */
107
107
  mergeCloses?: string;
108
+ /** The checkpoint whose approval merges its pull request (PULL-20). */
109
+ mergeOn?: string;
108
110
  }>;
109
111
  phases: Array<{
110
112
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.39.3",
3
+ "version": "0.39.5",
4
4
  "description": "Requirements as testable, human-readable data — CLI and test harness for spec-driven development with AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {