@popoverai/dotrequirements 0.39.5 → 0.40.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.
@@ -1,8 +1,10 @@
1
1
  import { api } from "../../convex.js";
2
2
  import { solutionClient } from "./client.js";
3
+ import { readImage } from "./image.js";
3
4
  import { pushBeforeRecord } from "./pushing.js";
4
5
  import { readRequest } from "./request.js";
5
6
  import { onOutcome, requireWhere } from "./run-file.js";
7
+ import { uploadToApp } from "./upload.js";
6
8
  /**
7
9
  * `dotreq solution ask <request-file>`: the drafter or a reviewer puts a
8
10
  * decision to the owner, on the outcome it is at or the one `--outcome`
@@ -14,22 +16,54 @@ export async function askCommand(file, options) {
14
16
  if ((file === undefined) === (options.approvalRequest === undefined)) {
15
17
  throw new Error("Ask with a request file, or with --approval-request '<action>' alone: one of them.");
16
18
  }
17
- const asking = file !== undefined
18
- ? { request: readRequest(file) }
19
- : { approvalRequest: actionFrom(options.approvalRequest ?? "") };
19
+ // DIRECTION-1.1: a picture that can't be shown is refused before anything happens.
20
+ const request = file !== undefined ? readRequest(file) : undefined;
21
+ const pictures = request !== undefined ? picturesIn(request) : [];
22
+ const approvalRequest = file === undefined ? actionFrom(options.approvalRequest ?? "") : undefined;
20
23
  const where = requireWhere(options);
21
24
  const { solutionId, agent } = where;
22
25
  const client = solutionClient();
23
26
  // SOLBRANCH-3.0: the drafter's commits reach the branch before Quinn is asked.
24
27
  await pushBeforeRecord(client, where);
28
+ for (const { written, image } of pictures) {
29
+ const storageId = await uploadToApp(client, api.solutions.images.uploadUrl, { solutionId }, image, "so nothing was put to the owner. Ask again.");
30
+ // The file becomes the copy the app keeps (`pictureSchema`).
31
+ delete written.file;
32
+ Object.assign(written, { storageId, name: image.name });
33
+ }
25
34
  const decision = await client.mutation(api.solutions.mutations.ask, {
26
35
  solutionId,
27
36
  agent,
28
37
  ...(where.outcome ? { outcome: where.outcome } : {}),
29
- ...asking,
38
+ ...(request !== undefined ? { request } : { approvalRequest }),
30
39
  });
31
40
  console.log(`Asked${onOutcome(where.outcome)}. It waits on the owner's decision${decision ? ` (${decision})` : ""}.`);
32
41
  }
42
+ /**
43
+ * The pictures on a request's options, each read from its file: a request
44
+ * whose picture isn't there, or isn't an image a browser can show, goes no
45
+ * further (DIRECTION-1.1). Anything else about the request is the backend's
46
+ * to check against the contract.
47
+ */
48
+ function picturesIn(request) {
49
+ const options = request?.options;
50
+ if (!Array.isArray(options))
51
+ return [];
52
+ return options.flatMap((option) => {
53
+ const pictures = option?.pictures;
54
+ if (!Array.isArray(pictures))
55
+ return [];
56
+ return pictures.map((written) => {
57
+ if (typeof written?.file !== "string") {
58
+ throw new Error('A picture on an option names its image with "file", so nothing was put to the owner.');
59
+ }
60
+ return {
61
+ written,
62
+ image: readImage(written.file, "nothing was put to the owner"),
63
+ };
64
+ });
65
+ });
66
+ }
33
67
  /** The action `--approval-request` names, as JSON. The backend checks it. */
34
68
  function actionFrom(text) {
35
69
  try {
@@ -22,6 +22,12 @@ export type Brief = {
22
22
  text: string;
23
23
  reviewing?: BriefOutcome[];
24
24
  against?: BriefOutcome[];
25
+ /** As far as the CLI reads it: the pictures a decision was decided on (DIRECTION-6.4.0, 6.5). */
26
+ thread?: Array<{
27
+ pictures?: Array<{
28
+ storageId: string;
29
+ }>;
30
+ }>;
25
31
  };
26
32
  /** An outcome the brief lists, as far as the CLI reads it. */
27
33
  export type BriefOutcome = {
@@ -5,5 +5,5 @@
5
5
  * request is validated against; the CLI can't import the backend, so this
6
6
  * is that text, and a test keeps the two the same.
7
7
  */
8
- export declare const REQUEST_CONTRACT = "Ask the person to decide something about the outcome you are drafting or reviewing. A choice presents options with a recommendation. The person may answer in their own words instead of picking an option, and their words are then the decision you work from. One decision per request: if two things need deciding, ask twice.\n\nA request is JSON with a \"kind\" and the fields of that kind:\n\nchoice:\n question (text, up to 200 characters): The question, phrased as a question in the words the person will read it, not as a topic.\n options (list, at least 2): The live alternatives. More than four makes deciding harder; give more only when each is a genuine choice. Leave out anything you have already ruled out.\n name (text, up to 200 characters): The option as you would say it to the person: a noun phrase they can repeat back, not a label.\n benefits (list of text, each up to 200 characters): What is better if this option is taken. One line each, in your own words.\n costs (list of text, each up to 200 characters): What is worse if this option is taken: what it would take to build, and what the person lives with afterwards. Effort is a cost like any other.\n note (text, up to 600 characters, optional): Anything you learned or checked about this option that is not a benefit or a cost. Leave it out when there is nothing to add.\n action (object, optional): An action you may take only with the person's permission, which choosing this option gives. Once they choose it, take it with `dotreq solution approval complete <decision>`. Leave it out for anything you may do yourself.\n tool (text, up to 200 characters): The action, by name. Those that need the person's permission: \"fix\", starting a fix, and \"make with you\", making the outcome you're on with the person and the solution's agent instead (args: { outcome }), when it needs their judgement more than your best guess (CONVO-8).\n args (object): The action's arguments, as it takes them: a fix's are below.\n recommendation (object): Which option you would take, and the one reason why.\n option (text, up to 200 characters): The name of the option you recommend, exactly as it appears in options.\n because (text, up to 200 characters): The one reason that carries it, in one line. The full reasoning belongs in background.\n background (text, up to 4000 characters): What the person needs to know to decide well that the options do not already say. Include only context that would substantially change the quality of the decision. The person reads this after the options, so do not reiterate them.\n\nThe args of a \"fix\" action:\n name (text, up to 120 characters): A short name for the fix, from what it puts right, as \"dice below the controls\".\n words (text, up to 2000 characters): What you found, in your own words.\n plan (text, up to 2000 characters): The fix as you would put it to the person, in a sentence or two.\n redo (list, at least 1): The outcomes the fix redoes, and what would change in each.\n outcome (text, up to 200 characters): An outcome to redo, by its name.\n change (text, up to 2000 characters): What would change in it.\n rerun (list of text, each up to 200 characters): The outcomes that await the ones redone and should run again from them. Empty when none should.";
8
+ export declare const REQUEST_CONTRACT = "Ask the person to decide something about the outcome you are drafting or reviewing. A choice presents options with a recommendation. The person may answer in their own words instead of picking an option, and their words are then the decision you work from. One decision per request: if two things need deciding, ask twice.\n\nA request is JSON with a \"kind\" and the fields of that kind:\n\nchoice:\n question (text, up to 200 characters): The question, phrased as a question in the words the person will read it, not as a topic.\n options (list, at least 2): The live alternatives. More than four makes deciding harder; give more only when each is a genuine choice. Leave out anything you have already ruled out.\n name (text, up to 200 characters): The option as you would say it to the person: a noun phrase they can repeat back, not a label.\n benefits (list of text, each up to 200 characters): What is better if this option is taken. One line each, in your own words.\n costs (list of text, each up to 200 characters): What is worse if this option is taken: what it would take to build, and what the person lives with afterwards. Effort is a cost like any other.\n note (text, up to 600 characters, optional): Anything you learned or checked about this option that is not a benefit or a cost. Leave it out when there is nothing to add.\n pictures (list, optional): Pictures of this option, when the choice is about how something looks. The person flips between options to compare them, so give each option the same screens, in the same order, at the same window size. Leave it out of a choice that isn't about how something looks.\n file (text, up to 200 characters): The image's path, from where you run `dotreq solution ask`: a PNG, JPEG, GIF, WebP or AVIF file. Its file name must be one no other image in the solution has, so name it for the option and the screen.\n caption (text, up to 200 characters, optional): What this picture shows, the same on each option, as \"With work waiting on you\". Leave it out when there is one picture and nothing to say.\n action (object, optional): An action you may take only with the person's permission, which choosing this option gives. Once they choose it, take it with `dotreq solution approval complete <decision>`. Leave it out for anything you may do yourself.\n tool (text, up to 200 characters): The action, by name. Those that need the person's permission: \"fix\", starting a fix, and \"make with you\", making the outcome you're on with the person and the solution's agent instead (args: { outcome }), when it needs their judgement more than your best guess (CONVO-8).\n args (object): The action's arguments, as it takes them: a fix's are below.\n recommendation (object): Which option you would take, and the one reason why.\n option (text, up to 200 characters): The name of the option you recommend, exactly as it appears in options.\n because (text, up to 200 characters): The one reason that carries it, in one line. The full reasoning belongs in background.\n background (text, up to 4000 characters): What the person needs to know to decide well that the options do not already say. Include only context that would substantially change the quality of the decision. The person reads this after the options, so do not reiterate them.\n\nThe args of a \"fix\" action:\n name (text, up to 120 characters): A short name for the fix, from what it puts right, as \"dice below the controls\".\n words (text, up to 2000 characters): What you found, in your own words.\n plan (text, up to 2000 characters): The fix as you would put it to the person, in a sentence or two.\n redo (list, at least 1): The outcomes the fix redoes, and what would change in each.\n outcome (text, up to 200 characters): An outcome to redo, by its name.\n change (text, up to 2000 characters): What would change in it.\n rerun (list of text, each up to 200 characters): The outcomes that await the ones redone and should run again from them. Empty when none should.";
9
9
  //# sourceMappingURL=contract.d.ts.map
@@ -16,6 +16,9 @@ choice:
16
16
  benefits (list of text, each up to 200 characters): What is better if this option is taken. One line each, in your own words.
17
17
  costs (list of text, each up to 200 characters): What is worse if this option is taken: what it would take to build, and what the person lives with afterwards. Effort is a cost like any other.
18
18
  note (text, up to 600 characters, optional): Anything you learned or checked about this option that is not a benefit or a cost. Leave it out when there is nothing to add.
19
+ pictures (list, optional): Pictures of this option, when the choice is about how something looks. The person flips between options to compare them, so give each option the same screens, in the same order, at the same window size. Leave it out of a choice that isn't about how something looks.
20
+ file (text, up to 200 characters): The image's path, from where you run \`dotreq solution ask\`: a PNG, JPEG, GIF, WebP or AVIF file. Its file name must be one no other image in the solution has, so name it for the option and the screen.
21
+ caption (text, up to 200 characters, optional): What this picture shows, the same on each option, as "With work waiting on you". Leave it out when there is one picture and nothing to say.
19
22
  action (object, optional): An action you may take only with the person's permission, which choosing this option gives. Once they choose it, take it with \`dotreq solution approval complete <decision>\`. Leave it out for anything you may do yourself.
20
23
  tool (text, up to 200 characters): The action, by name. Those that need the person's permission: "fix", starting a fix, and "make with you", making the outcome you're on with the person and the solution's agent instead (args: { outcome }), when it needs their judgement more than your best guess (CONVO-8).
21
24
  args (object): The action's arguments, as it takes them: a fix's are below.
@@ -1,26 +1,12 @@
1
+ import { ACCEPTED_IMAGE_FORMATS, type ImageContentType, type ImageFormat, imageFormat } from "../../schema/image-format.js";
1
2
  import type { solutionClient } from "./client.js";
2
3
  /**
3
4
  * Images on an outcome (SIGHT-9), such as a screenshot: the drafter submits an
4
5
  * image file from its checkout or anywhere it can read, and the app keeps a
5
- * copy; nothing is committed.
6
- *
7
- * Every browser we support (Chrome, Edge, Firefox, Safari on macOS and iOS)
8
- * shows PNG, JPEG, GIF, WebP and AVIF (MDN's image type guide; AVIF from
9
- * Safari 16.4). HEIC and TIFF show only in Safari, and JPEG XL not in Edge,
10
- * so they are refused. SVG is refused too: opened at full size in its own
11
- * tab, an SVG runs whatever script it carries. The format is read from the
12
- * file's first bytes (the WHATWG MIME Sniffing signatures; AVIF's brand in
13
- * its `ftyp` box), since an extension says nothing about it. Kept in step
14
- * with the backend's `ACCEPTED_IMAGE_FORMATS` (convex/solutions/images.ts).
6
+ * copy; nothing is committed. Which files are images a browser can show is
7
+ * `schema/image-format.ts`.
15
8
  */
16
- export declare const ACCEPTED_IMAGE_FORMATS = "a PNG, JPEG, GIF, WebP or AVIF image";
17
- export type ImageContentType = "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/avif";
18
- /** What the file is: a format a browser shows, or why it isn't one. */
19
- export type ImageFormat = {
20
- contentType: ImageContentType;
21
- } | {
22
- refused: string;
23
- };
9
+ export { ACCEPTED_IMAGE_FORMATS, type ImageContentType, type ImageFormat, imageFormat, };
24
10
  /** An image read from the checkout, ready to upload. */
25
11
  export type Image = {
26
12
  name: string;
@@ -32,8 +18,6 @@ export type Image = {
32
18
  * (SIGHT-9.0.0) and what came of it: by default, that nothing was submitted.
33
19
  */
34
20
  export declare function readImage(path: string, outcome?: string): Image;
35
- /** What image format the bytes hold, read from the file's signature. */
36
- export declare function imageFormat(bytes: Uint8Array): ImageFormat;
37
21
  /**
38
22
  * Where an image's copy is saved in the checkout, relative to the
39
23
  * repository's root (SIGHT-9.3): a folder git ignores, then one folder per
@@ -59,5 +43,4 @@ type BriefOutcome = {
59
43
  export declare function saveImageCopies(client: ReturnType<typeof solutionClient>, solutionId: string, outcomes: readonly BriefOutcome[], cwd?: string): Promise<void>;
60
44
  /** Upload an image to the app (SIGHT-9.0), and the stored copy's id back. */
61
45
  export declare function uploadImage(client: ReturnType<typeof solutionClient>, solutionId: string, image: Image): Promise<string>;
62
- export {};
63
46
  //# sourceMappingURL=image.d.ts.map
@@ -1,23 +1,16 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import { api } from "../../convex.js";
4
+ import { ACCEPTED_IMAGE_FORMATS, imageFormat, } from "../../schema/image-format.js";
4
5
  import { repositoryRoot } from "./branch.js";
5
6
  import { uploadToApp } from "./upload.js";
6
7
  /**
7
8
  * Images on an outcome (SIGHT-9), such as a screenshot: the drafter submits an
8
9
  * image file from its checkout or anywhere it can read, and the app keeps a
9
- * copy; nothing is committed.
10
- *
11
- * Every browser we support (Chrome, Edge, Firefox, Safari on macOS and iOS)
12
- * shows PNG, JPEG, GIF, WebP and AVIF (MDN's image type guide; AVIF from
13
- * Safari 16.4). HEIC and TIFF show only in Safari, and JPEG XL not in Edge,
14
- * so they are refused. SVG is refused too: opened at full size in its own
15
- * tab, an SVG runs whatever script it carries. The format is read from the
16
- * file's first bytes (the WHATWG MIME Sniffing signatures; AVIF's brand in
17
- * its `ftyp` box), since an extension says nothing about it. Kept in step
18
- * with the backend's `ACCEPTED_IMAGE_FORMATS` (convex/solutions/images.ts).
10
+ * copy; nothing is committed. Which files are images a browser can show is
11
+ * `schema/image-format.ts`.
19
12
  */
20
- export const ACCEPTED_IMAGE_FORMATS = "a PNG, JPEG, GIF, WebP or AVIF image";
13
+ export { ACCEPTED_IMAGE_FORMATS, imageFormat, };
21
14
  /**
22
15
  * The image at a path, or a refusal naming the accepted formats
23
16
  * (SIGHT-9.0.0) and what came of it: by default, that nothing was submitted.
@@ -34,65 +27,6 @@ export function readImage(path, outcome = "nothing was submitted") {
34
27
  }
35
28
  return { name: basename(full), contentType: format.contentType, bytes };
36
29
  }
37
- const AVIF_BRANDS = new Set(["avif", "avis"]);
38
- const HEIF_BRANDS = new Set(["heic", "heix", "heim", "heis", "hevc", "hevx"]);
39
- /** What image format the bytes hold, read from the file's signature. */
40
- export function imageFormat(bytes) {
41
- const starts = (...signature) => signature.every((byte, at) => bytes[at] === byte);
42
- if (starts(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)) {
43
- return { contentType: "image/png" };
44
- }
45
- if (starts(0xff, 0xd8, 0xff))
46
- return { contentType: "image/jpeg" };
47
- if (ascii(bytes, 0, 6) === "GIF87a" || ascii(bytes, 0, 6) === "GIF89a") {
48
- return { contentType: "image/gif" };
49
- }
50
- if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 6) === "WEBPVP") {
51
- return { contentType: "image/webp" };
52
- }
53
- if (ascii(bytes, 4, 4) === "ftyp") {
54
- // ISO-BMFF: the major brand, then compatible brands to the box's end.
55
- // AVIF lists avif or avis; HEIC shares the box and lists its own.
56
- const size = readUint32(bytes, 0);
57
- const end = Math.min(bytes.length, size);
58
- const brands = [ascii(bytes, 8, 4)];
59
- for (let at = 16; at + 4 <= end; at += 4)
60
- brands.push(ascii(bytes, at, 4));
61
- if (brands.some((brand) => AVIF_BRANDS.has(brand))) {
62
- return { contentType: "image/avif" };
63
- }
64
- if (brands.some((brand) => HEIF_BRANDS.has(brand))) {
65
- return { refused: "it is a HEIC image, which only Safari shows" };
66
- }
67
- return { refused: "it isn't an image file" };
68
- }
69
- if (starts(0x49, 0x49, 0x2a, 0x00) || starts(0x4d, 0x4d, 0x00, 0x2a)) {
70
- return { refused: "it is a TIFF image, which only Safari shows" };
71
- }
72
- if (/^\s*(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*<svg[\s>]/i.test(head(bytes))) {
73
- return {
74
- refused: "it is an SVG, which runs its scripts when opened at full size",
75
- };
76
- }
77
- return { refused: "it isn't an image file" };
78
- }
79
- function ascii(bytes, at, length) {
80
- return at + length <= bytes.length
81
- ? String.fromCharCode(...bytes.subarray(at, at + length))
82
- : "";
83
- }
84
- function readUint32(bytes, at) {
85
- let value = 0;
86
- for (let i = 0; i < 4; i++)
87
- value = value * 256 + (bytes[at + i] ?? 0);
88
- return value;
89
- }
90
- /** The file's start as text, for telling an SVG by its root element. */
91
- function head(bytes) {
92
- return new TextDecoder("utf-8", { fatal: false })
93
- .decode(bytes.subarray(0, 1024))
94
- .replace(/^/, "");
95
- }
96
30
  /**
97
31
  * Where an image's copy is saved in the checkout, relative to the
98
32
  * repository's root (SIGHT-9.3): a folder git ignores, then one folder per
@@ -119,6 +119,13 @@ would fix it. Say exactly what has to change:
119
119
 
120
120
  dotreq solution stall "<what has to change>"
121
121
 
122
+ Open it on one sentence that stands on its own and says what is needed and
123
+ from whom: the owner, written "from you"; a teammate, by name; or a fix to the
124
+ product. The owner sees that sentence first, wherever the stall shows, and
125
+ opens the rest. For example:
126
+
127
+ Needs a way past Vercel's login on previews, from you.
128
+
122
129
  The outcome waits until that is fixed and you are started again.
123
130
  `;
124
131
  /** The command as the Stop hook prints it, after the request contract on `ask --help`. */
@@ -43,6 +43,13 @@ export async function nextCommand(options) {
43
43
  await saveImageCopies(client, solutionId, [
44
44
  ...(result.reviewing ?? []),
45
45
  ...(result.against ?? []),
46
+ // DIRECTION-6.4.0, 6.5: the pictures a decision on it was decided on.
47
+ {
48
+ things: (result.thread ?? []).flatMap((entry) => (entry.pictures ?? []).map(({ storageId }) => ({
49
+ kind: "image",
50
+ storageId,
51
+ }))),
52
+ },
46
53
  ]);
47
54
  }
48
55
  //# sourceMappingURL=next.js.map
@@ -6,6 +6,7 @@
6
6
  export { buildRequirementMarkdown, buildRequirementsMarkdown, DEFAULT_DELIMITER, } from "./builder.js";
7
7
  export type { ConvexRequirement } from "./conversions.js";
8
8
  export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
9
+ export { imageFormat } from "./image-format.js";
9
10
  export { DELIMITER_PATTERN, type ExtractedRequirementBlock, extractFrontmatterBlock, extractRequirementBlocks, findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementBlocksFromMarkdown, parseRootLine, ROOT_LABEL_MARKER, splitRequirementFenceContent, stripFrontmatterBlock, } from "./parser-core.js";
10
11
  export { carriesBranchVersion } from "./provenance.js";
11
12
  export type { Metadata, ParsedCriterion, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
@@ -6,6 +6,9 @@
6
6
  // Building (uses yaml package - browser-safe)
7
7
  export { buildRequirementMarkdown, buildRequirementsMarkdown, DEFAULT_DELIMITER, } from "./builder.js";
8
8
  export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
9
+ // What format an image's bytes hold, for a picture the web app's runner reads
10
+ // from a sandbox (DIRECTION-5.2) as the CLI reads one from a checkout
11
+ export { imageFormat } from "./image-format.js";
9
12
  // Parser core (pure TypeScript - browser-safe, no fs dependency)
10
13
  // These functions parse markdown strings directly without file I/O
11
14
  export { DELIMITER_PATTERN,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What image format a file's bytes hold (SIGHT-9), read from its signature.
3
+ *
4
+ * Every browser we support (Chrome, Edge, Firefox, Safari on macOS and iOS)
5
+ * shows PNG, JPEG, GIF, WebP and AVIF (MDN's image type guide; AVIF from
6
+ * Safari 16.4). HEIC and TIFF show only in Safari, and JPEG XL not in Edge,
7
+ * so they are refused. SVG is refused too: opened at full size in its own
8
+ * tab, an SVG runs whatever script it carries. The format is read from the
9
+ * file's first bytes (the WHATWG MIME Sniffing signatures; AVIF's brand in
10
+ * its `ftyp` box), since an extension says nothing about it. Kept in step
11
+ * with the backend's `ACCEPTED_IMAGE_FORMATS` (convex/solutions/images.ts).
12
+ *
13
+ * Browser-safe, since the web app's runner reads a picture the same way
14
+ * (DIRECTION-5.2).
15
+ */
16
+ export declare const ACCEPTED_IMAGE_FORMATS = "a PNG, JPEG, GIF, WebP or AVIF image";
17
+ export type ImageContentType = "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/avif";
18
+ /** What the file is: a format a browser shows, or why it isn't one. */
19
+ export type ImageFormat = {
20
+ contentType: ImageContentType;
21
+ } | {
22
+ refused: string;
23
+ };
24
+ /** What image format the bytes hold, read from the file's signature. */
25
+ export declare function imageFormat(bytes: Uint8Array): ImageFormat;
26
+ //# sourceMappingURL=image-format.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * What image format a file's bytes hold (SIGHT-9), read from its signature.
3
+ *
4
+ * Every browser we support (Chrome, Edge, Firefox, Safari on macOS and iOS)
5
+ * shows PNG, JPEG, GIF, WebP and AVIF (MDN's image type guide; AVIF from
6
+ * Safari 16.4). HEIC and TIFF show only in Safari, and JPEG XL not in Edge,
7
+ * so they are refused. SVG is refused too: opened at full size in its own
8
+ * tab, an SVG runs whatever script it carries. The format is read from the
9
+ * file's first bytes (the WHATWG MIME Sniffing signatures; AVIF's brand in
10
+ * its `ftyp` box), since an extension says nothing about it. Kept in step
11
+ * with the backend's `ACCEPTED_IMAGE_FORMATS` (convex/solutions/images.ts).
12
+ *
13
+ * Browser-safe, since the web app's runner reads a picture the same way
14
+ * (DIRECTION-5.2).
15
+ */
16
+ export const ACCEPTED_IMAGE_FORMATS = "a PNG, JPEG, GIF, WebP or AVIF image";
17
+ const AVIF_BRANDS = new Set(["avif", "avis"]);
18
+ const HEIF_BRANDS = new Set(["heic", "heix", "heim", "heis", "hevc", "hevx"]);
19
+ /** What image format the bytes hold, read from the file's signature. */
20
+ export function imageFormat(bytes) {
21
+ const starts = (...signature) => signature.every((byte, at) => bytes[at] === byte);
22
+ if (starts(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)) {
23
+ return { contentType: "image/png" };
24
+ }
25
+ if (starts(0xff, 0xd8, 0xff))
26
+ return { contentType: "image/jpeg" };
27
+ if (ascii(bytes, 0, 6) === "GIF87a" || ascii(bytes, 0, 6) === "GIF89a") {
28
+ return { contentType: "image/gif" };
29
+ }
30
+ if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 6) === "WEBPVP") {
31
+ return { contentType: "image/webp" };
32
+ }
33
+ if (ascii(bytes, 4, 4) === "ftyp") {
34
+ // ISO-BMFF: the major brand, then compatible brands to the box's end.
35
+ // AVIF lists avif or avis; HEIC shares the box and lists its own.
36
+ const size = readUint32(bytes, 0);
37
+ const end = Math.min(bytes.length, size);
38
+ const brands = [ascii(bytes, 8, 4)];
39
+ for (let at = 16; at + 4 <= end; at += 4)
40
+ brands.push(ascii(bytes, at, 4));
41
+ if (brands.some((brand) => AVIF_BRANDS.has(brand))) {
42
+ return { contentType: "image/avif" };
43
+ }
44
+ if (brands.some((brand) => HEIF_BRANDS.has(brand))) {
45
+ return { refused: "it is a HEIC image, which only Safari shows" };
46
+ }
47
+ return { refused: "it isn't an image file" };
48
+ }
49
+ if (starts(0x49, 0x49, 0x2a, 0x00) || starts(0x4d, 0x4d, 0x00, 0x2a)) {
50
+ return { refused: "it is a TIFF image, which only Safari shows" };
51
+ }
52
+ if (/^\s*(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*<svg[\s>]/i.test(head(bytes))) {
53
+ return {
54
+ refused: "it is an SVG, which runs its scripts when opened at full size",
55
+ };
56
+ }
57
+ return { refused: "it isn't an image file" };
58
+ }
59
+ function ascii(bytes, at, length) {
60
+ return at + length <= bytes.length
61
+ ? String.fromCharCode(...bytes.subarray(at, at + length))
62
+ : "";
63
+ }
64
+ function readUint32(bytes, at) {
65
+ let value = 0;
66
+ for (let i = 0; i < 4; i++)
67
+ value = value * 256 + (bytes[at + i] ?? 0);
68
+ return value;
69
+ }
70
+ /** The file's start as text, for telling an SVG by its root element. */
71
+ function head(bytes) {
72
+ return new TextDecoder("utf-8", { fatal: false })
73
+ .decode(bytes.subarray(0, 1024))
74
+ .replace(/^/, "");
75
+ }
76
+ //# sourceMappingURL=image-format.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.39.5",
3
+ "version": "0.40.0",
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": {