@popoverai/dotrequirements 0.39.0 → 0.39.1

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,6 +4,7 @@ import { api } from "../../convex.js";
4
4
  import { currentBranch, headCommit, repositoryRoot } from "./branch.js";
5
5
  import { solutionClient } from "./client.js";
6
6
  import { readImage, uploadImage } from "./image.js";
7
+ import { ownChangesOf } from "./own-changes.js";
7
8
  import { screenshotPreview } from "./preview-screenshot.js";
8
9
  import { checkPullRequest } from "./pull-request.js";
9
10
  import { pushBeforeRecord, refuseUncommitted } from "./pushing.js";
@@ -76,6 +77,12 @@ export async function authorCommand(references, options) {
76
77
  // SOLDOC-12.2.0: a file from the checkout's own branch keeps the commit
77
78
  // it was submitted at, so the page can tell when it changes after.
78
79
  const commit = checkout ? headCommit(cwd) : null;
80
+ // CHECKPOINT-12.4 to 12.6: what the pull request would merge, taken after
81
+ // the push, so the checkpoint can tell a branch brought up to date from
82
+ // changed code. Without one, as when the fetch fails, the work is
83
+ // recorded all the same and reads as changed.
84
+ const merging = [...pullRequests.values()].find((made) => made.base);
85
+ const ownChanges = checkout && merging?.base ? ownChangesOf(cwd, merging.base) : null;
79
86
  /** A reference as it is submitted: where the thing is, as the backend keeps it. */
80
87
  const submissionOf = async (thing) => {
81
88
  const recording = videos.get(thing);
@@ -99,8 +106,12 @@ export async function authorCommand(references, options) {
99
106
  return { kind: "image", storageId, name: image.name };
100
107
  }
101
108
  const pullRequest = pullRequests.get(thing);
102
- if (pullRequest)
103
- return { kind: "pull request", ...pullRequest };
109
+ if (pullRequest) {
110
+ // The branch it merges into is the CLI's, for `ownChanges`; the
111
+ // backend keeps the pull request by its number and address.
112
+ const { base: _base, ...kept } = pullRequest;
113
+ return { kind: "pull request", ...kept };
114
+ }
104
115
  if (thing.kind === "preview") {
105
116
  // SIGHT-12.0, 12.2: a screenshot of the page it opens on, kept as an
106
117
  // image is; without one, it is submitted all the same.
@@ -141,6 +152,7 @@ export async function authorCommand(references, options) {
141
152
  comment,
142
153
  ...(summary ? { summary } : {}),
143
154
  ...(files.length ? { files } : {}),
155
+ ...(ownChanges ? { ownChanges } : {}),
144
156
  };
145
157
  const result = await client.mutation(api.solutions.mutations.complete, args);
146
158
  const saved = result?.saved ?? [];
@@ -220,7 +220,7 @@ export function registerSolution(program) {
220
220
  whereOptions(solution
221
221
  .command("author <reference...>", HIDDEN)
222
222
  .description("Record what you made, and what changed and why"))
223
- .option("--comment <text>", "What changed and why: the next reader sees it first (required)")
223
+ .option("--comment <text>", "What changed and why: the next reader sees it first, so open on a sentence that stands on its own and says what changed (required)")
224
224
  .option("--executive-summary <text>", "For the owner, when your work moves on before anyone reviews it (required there)")
225
225
  .option("--summary <text>", "Short for --executive-summary")
226
226
  .option("--title <text>", "A reference's name on the outcome's tab, for the owner: one per reference, in the order the references are given, or none", (value, previous = []) => [...previous, value])
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The pull request's own changes, fingerprinted from the checkout: what it
3
+ * would merge into `base`, as `git patch-id` sees it (CHECKPOINT-12.4 to
4
+ * 12.6). Merging a newer `base` in, or rebasing onto it, leaves the
5
+ * fingerprint as it was; resolving a conflict, a whitespace change or a new
6
+ * commit of the branch's own gives a new one.
7
+ *
8
+ * `--verbatim` keeps whitespace, and `-U0` drops the context lines and
9
+ * leaves line numbers only in the hunk headers, which `patch-id` ignores, so
10
+ * what moved under the change doesn't count as changing it. The cost, an
11
+ * accepted limit (12.4.0): a fix that only moves the branch's own lines
12
+ * elsewhere in the same file keeps the fingerprint too.
13
+ *
14
+ * Null when any step fails, the fetch included: the work is then recorded
15
+ * without one, and the checkpoint counts the pull request as changed
16
+ * (12.6). Never throws.
17
+ */
18
+ export declare function ownChangesOf(cwd: string, base: string): {
19
+ commit: string;
20
+ patchId: string;
21
+ } | null;
22
+ //# sourceMappingURL=own-changes.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { spawnSync } from "node:child_process";
2
+ /**
3
+ * The pull request's own changes, fingerprinted from the checkout: what it
4
+ * would merge into `base`, as `git patch-id` sees it (CHECKPOINT-12.4 to
5
+ * 12.6). Merging a newer `base` in, or rebasing onto it, leaves the
6
+ * fingerprint as it was; resolving a conflict, a whitespace change or a new
7
+ * commit of the branch's own gives a new one.
8
+ *
9
+ * `--verbatim` keeps whitespace, and `-U0` drops the context lines and
10
+ * leaves line numbers only in the hunk headers, which `patch-id` ignores, so
11
+ * what moved under the change doesn't count as changing it. The cost, an
12
+ * accepted limit (12.4.0): a fix that only moves the branch's own lines
13
+ * elsewhere in the same file keeps the fingerprint too.
14
+ *
15
+ * Null when any step fails, the fetch included: the work is then recorded
16
+ * without one, and the checkpoint counts the pull request as changed
17
+ * (12.6). Never throws.
18
+ */
19
+ export function ownChangesOf(cwd, base) {
20
+ try {
21
+ // The base as the repository has it now: an older one would put its
22
+ // newer commits, merged into the branch, among the branch's own.
23
+ const fetched = run(cwd, [
24
+ "fetch",
25
+ "--quiet",
26
+ "origin",
27
+ `+refs/heads/${base}:refs/remotes/origin/${base}`,
28
+ ]);
29
+ if (!fetched)
30
+ return null;
31
+ const commit = text(run(cwd, ["rev-parse", "--verify", "--quiet", "HEAD"]));
32
+ if (!commit)
33
+ return null;
34
+ const mergeBase = text(run(cwd, ["merge-base", "HEAD", `refs/remotes/origin/${base}`]));
35
+ if (!mergeBase)
36
+ return null;
37
+ const diff = run(cwd, [
38
+ "diff",
39
+ "--no-ext-diff",
40
+ "--no-textconv",
41
+ "--no-color",
42
+ "--binary",
43
+ "-U0",
44
+ mergeBase,
45
+ commit,
46
+ ]);
47
+ if (!diff)
48
+ return null;
49
+ // `patch-id` prints nothing for an empty diff.
50
+ if (diff.length === 0)
51
+ return { commit, patchId: "empty" };
52
+ const id = run(cwd, ["patch-id", "--verbatim"], diff);
53
+ if (!id)
54
+ return null;
55
+ // A diff it gives no id for, as one only changing a file's mode, can't
56
+ // be told from another: no fingerprint.
57
+ const patchId = text(id)?.split(/\s+/)[0];
58
+ return patchId ? { commit, patchId } : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /** Git's stdout, as bytes, or null when it failed. */
65
+ function run(cwd, args, input) {
66
+ const ran = spawnSync("git", args, {
67
+ cwd,
68
+ input,
69
+ maxBuffer: 1024 * 1024 * 1024,
70
+ // A fetch that wants a password fails rather than waiting for one.
71
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
72
+ });
73
+ return ran.status === 0 && !ran.error ? ran.stdout : null;
74
+ }
75
+ function text(out) {
76
+ return out?.toString("utf8").trim() || null;
77
+ }
78
+ //# sourceMappingURL=own-changes.js.map
@@ -1,8 +1,12 @@
1
1
  import { type Where, type WhereOptions } from "./run-file.js";
2
- /** The solution's pull request: its number and where it opens. */
2
+ /**
3
+ * The solution's pull request: its number and where it opens, and, when
4
+ * checked by its number, the branch it merges into (CHECKPOINT-12.4).
5
+ */
3
6
  export type PullRequest = {
4
7
  url: string;
5
8
  number: number;
9
+ base?: string;
6
10
  };
7
11
  /** The command that opens the solution's pull request and prints its reference. */
8
12
  export declare const OPEN_IT = "dotreq solution pr open";
@@ -91,9 +91,13 @@ export async function checkPullRequest(where, number) {
91
91
  return pullRequest;
92
92
  }
93
93
  function pullRequestIn(said) {
94
- return said.url && typeof said.number === "number"
95
- ? { url: said.url, number: said.number }
96
- : null;
94
+ if (!said.url || typeof said.number !== "number")
95
+ return null;
96
+ return {
97
+ url: said.url,
98
+ number: said.number,
99
+ ...(typeof said.base === "string" && said.base ? { base: said.base } : {}),
100
+ };
97
101
  }
98
102
  /**
99
103
  * Ask the app's pull-request route, on the project's credentials: with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.39.0",
3
+ "version": "0.39.1",
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": {