comprehende 0.4.1 → 0.5.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/README.md CHANGED
@@ -14,28 +14,35 @@ From this checkout:
14
14
  npx skills add ./ --skill comprehende
15
15
  ```
16
16
 
17
+ ## Why
18
+
19
+ Usage of AI agents to write code has given us previously unseen ability to write code. A lot of code.
20
+
21
+ Three concepts are important to understand:
22
+
23
+ - Cognitive offloading: when we use tools by handing off the _how_ but keeping the _why_ and the _what_ - like a calculator adding numbers, we know why we need the sum, we tell it the two numbers to add together, it doesn't invent them
24
+ - Cognitive surrender: when we stop constructing an answer and adopt the answer from the tool instead, without having our own view to compare against - like letting the calculator add two numbers together, letting it make up the numbers and accepting the answer.
25
+ - Comprehension debt: the gap between the amount of code in the system and the amount of understanding the humans developing, maintaining or operating the system has.
26
+ - Comprehension debt compares to technical debt in a way, but there are very important differences
27
+ - Technical debt is a conscious tradeoff, meanwhile comprehension debt builds up without human decision
28
+ - Unlike tech debt, dragging us down (slow builds, dependencies etc.), comprehension debt is invisible, everything seems good until it doesn't - tests green, codebase looks clean, but nobody really understands what's going on
29
+ - Until something breaks, no-one knows where these debts are located.
30
+
31
+ Cognitive surrender is the means to increase the comprehension debt in the project. In terms of AI coding agents, every time we are blindly accepting code generated by the agents, we increase this debt. This debt eventually bites back, and it does so where it hurts the most: during an incident, during a debug session, while struggling with trying to keep the AI bills down so that we don't run out of tokens after a day of work.
32
+
33
+ This tool is for preventing this cognitive surrender while trying to maintain momentum created by the AI agents. By moving the balance from cognitive surrender to cognitive offloading, we want to keep ourselves in the loop while still allowing us to make advantage of the efficiency gains provided by AI coding agents.
34
+
17
35
  ## Develop
18
36
 
19
37
  [pnpm](https://pnpm.io/).
20
38
 
21
- ```sh
22
- pnpm install
23
- pnpm test
24
- pnpm typecheck
25
- pnpm build
26
- pnpm pack:smoke
27
- pnpm sync:skill
28
- pnpm release:skill
29
- pnpm dev -- --help
30
- ```
31
-
32
39
  `pnpm build` emits `dist/cli` and `dist/ui`. `prepack` runs that build, so `pnpm pack` / `pnpm publish` always ship the UI.
33
40
 
34
41
  `pnpm sync:skill` copies the JSON Schema into `skills-next/comprehende/`, pins `npx comprehende@<version>` there, and mirrors that tree into `.agents/skills/comprehende` so agents in this checkout use the next skill. It does not touch `skills/comprehende/`.
35
42
 
36
- `comprehende serve` and `comprehende export` share one UI and one git payload layer. Serve computes those payloads on each request. Export writes the same JSON (and image bytes) next to the UI so any static file server can host the review.
43
+ `comprehende serve` and `comprehende export` share one UI and one git payload. Serve resolves refs to commit SHAs when it starts, then computes those payloads from the objects on each request. Export writes the same JSON (and image bytes) next to the UI so any static file server can host the review.
37
44
 
38
- `pnpm dev` and `pnpm exec` run with this package as cwd, so they only make sense when _this_ repo is the one under review. To review a different project from a checkout, `cd` into it and run `npx comprehende@0.4.1` (or `node /path/to/comprehende/dist/cli/main.js` after `pnpm build`).
45
+ `pnpm dev` and `pnpm exec` run with this package as cwd, so they only make sense when _this_ repo is the one under review. To review a different project from a checkout, `cd` into it and run `npx comprehende@0.5.0` (or `node /path/to/comprehende/dist/cli/main.js` after `pnpm build`).
39
46
 
40
47
  ## Release
41
48
 
package/dist/api/live.js CHANGED
@@ -1,22 +1,23 @@
1
1
  import { loadDocument } from "../cli/commands.js";
2
2
  import { blameFile } from "../git/blame.js";
3
3
  import { readImageBlob } from "../git/blob.js";
4
- import { fileLanguage, filePatchFromGit, readPathDiff, resolveSource, toHunkRef } from "../git/diff.js";
4
+ import { fileLanguage, filePatchFromGit, readPathDiff, toHunkRef } from "../git/diff.js";
5
5
  import { GitError } from "../git/exec.js";
6
6
  import { listCommits } from "../git/log.js";
7
- import { mergeBase } from "../git/repo.js";
7
+ import { pinRange } from "../git/repo.js";
8
8
  import { showFile } from "../git/show.js";
9
9
  import { coverReview } from "../review/coverage.js";
10
10
  import { isLockfilePath } from "../schema/lockfile.js";
11
11
  import { ApiError } from "./error.js";
12
- export async function openReview(cwd, dataPath) {
12
+ export async function pinReviewSource(cwd, dataPath) {
13
13
  const document = await loadDocument(dataPath);
14
- const resolvedRefs = await resolveSource(cwd, document.source.baseRef, document.source.headRef);
15
- const { files, coverage } = await coverReview(cwd, document);
16
- const [mergeBaseSha, commits] = await Promise.all([
17
- mergeBase(cwd, document.source.baseRef, document.source.headRef),
18
- listCommits(cwd, document.source.baseRef, document.source.headRef),
19
- ]);
14
+ return pinRange(cwd, document.source.baseRef, document.source.headRef);
15
+ }
16
+ export async function openReview(cwd, dataPath, pin) {
17
+ const document = await loadDocument(dataPath);
18
+ const range = pin ?? (await pinRange(cwd, document.source.baseRef, document.source.headRef));
19
+ const { files, coverage } = await coverReview(cwd, document, range);
20
+ const commits = await listCommits(cwd, range.baseSha, range.headSha);
20
21
  return {
21
22
  cwd,
22
23
  document,
@@ -24,12 +25,12 @@ export async function openReview(cwd, dataPath) {
24
25
  baseRef: document.source.baseRef,
25
26
  headRef: document.source.headRef,
26
27
  range: document.source.range ?? `${document.source.baseRef}...${document.source.headRef}`,
27
- baseSha: resolvedRefs.baseSha,
28
- headSha: resolvedRefs.headSha,
28
+ baseSha: range.baseSha,
29
+ headSha: range.headSha,
29
30
  },
30
31
  files,
31
32
  coverage,
32
- mergeBaseSha,
33
+ mergeBaseSha: range.mergeBaseSha,
33
34
  commits,
34
35
  };
35
36
  }
@@ -92,7 +93,7 @@ export function hunksPayload(ctx, groupId) {
92
93
  throw new ApiError(400, "missing group");
93
94
  }
94
95
  if (groupId === "unassigned") {
95
- return serializeLayer(ctx.files, ctx.coverage.unassigned);
96
+ return serializeGroup(ctx.files, ctx.coverage.unassigned);
96
97
  }
97
98
  if (groupId === "lockfiles") {
98
99
  return serializeLockfiles(ctx.files);
@@ -101,13 +102,13 @@ export function hunksPayload(ctx, groupId) {
101
102
  if (group === undefined) {
102
103
  throw new ApiError(404, `unknown group "${groupId}"`);
103
104
  }
104
- return serializeLayer(ctx.files, group.hunks);
105
+ return serializeGroup(ctx.files, group.hunks);
105
106
  }
106
107
  export async function filePayload(ctx, path, side) {
107
108
  const file = findFile(ctx.files, path);
108
109
  const lookup = side === "old" ? (file.oldPath ?? file.path) : file.path;
109
110
  assertSideExists(file, side);
110
- const ref = side === "old" ? ctx.mergeBaseSha : ctx.document.source.headRef;
111
+ const ref = side === "old" ? ctx.mergeBaseSha : ctx.resolved.headSha;
111
112
  try {
112
113
  const content = await showFile(ctx.cwd, ref, lookup);
113
114
  return { path: lookup, ref, side, content, language: fileLanguage(lookup) };
@@ -120,7 +121,7 @@ export async function blamePayload(ctx, path, side) {
120
121
  const file = findFile(ctx.files, path);
121
122
  const lookup = side === "old" ? (file.oldPath ?? file.path) : file.path;
122
123
  assertSideExists(file, side);
123
- const ref = side === "old" ? ctx.mergeBaseSha : ctx.document.source.headRef;
124
+ const ref = side === "old" ? ctx.mergeBaseSha : ctx.resolved.headSha;
124
125
  try {
125
126
  const lines = await blameFile(ctx.cwd, ref, lookup);
126
127
  return { path: lookup, ref, side, lines };
@@ -136,7 +137,7 @@ export async function imagePayload(ctx, path, side) {
136
137
  }
137
138
  const lookup = side === "old" ? (file.oldPath ?? file.path) : file.path;
138
139
  assertSideExists(file, side);
139
- const ref = side === "old" ? ctx.mergeBaseSha : ctx.document.source.headRef;
140
+ const ref = side === "old" ? ctx.mergeBaseSha : ctx.resolved.headSha;
140
141
  try {
141
142
  const blob = await readImageBlob(ctx.cwd, ref, lookup);
142
143
  if (!blob.ok) {
@@ -202,10 +203,10 @@ function sidesFor(file) {
202
203
  }
203
204
  return ["old", "new"];
204
205
  }
205
- function serializeLayer(files, hunks) {
206
- const groups = [];
206
+ function serializeGroup(files, hunks) {
207
+ const filesByPath = [];
207
208
  for (const hunk of hunks) {
208
- const existing = groups.find((group) => group.file.path === hunk.path);
209
+ const existing = filesByPath.find((entry) => entry.file.path === hunk.path);
209
210
  if (existing !== undefined) {
210
211
  existing.hunks.push(hunk);
211
212
  continue;
@@ -214,13 +215,13 @@ function serializeLayer(files, hunks) {
214
215
  if (file === undefined) {
215
216
  continue;
216
217
  }
217
- groups.push({ file, hunks: [hunk] });
218
+ filesByPath.push({ file, hunks: [hunk] });
218
219
  }
219
- const serializedFiles = groups.map(({ file, hunks: fileHunks }) => serializeLayerFile(file, fileHunks, true));
220
+ const serializedFiles = filesByPath.map(({ file, hunks: fileHunks }) => serializeGroupFile(file, fileHunks, true));
220
221
  return { hunks: hunks.map(serializeHunk), files: serializedFiles };
221
222
  }
222
223
  function serializeLockfiles(files) {
223
- const serializedFiles = lockfileFiles(files).map((file) => serializeLayerFile(file, [], true));
224
+ const serializedFiles = lockfileFiles(files).map((file) => serializeGroupFile(file, [], true));
224
225
  return { hunks: [], files: serializedFiles };
225
226
  }
226
227
  function lockfileFiles(files) {
@@ -231,13 +232,13 @@ async function patchPayload(ctx, path) {
231
232
  if (!isLockfilePath(file.path) || file.binary || file.image) {
232
233
  throw new ApiError(404, "path is not a deferred lockfile");
233
234
  }
234
- const live = await readPathDiff(ctx.cwd, ctx.document.source.baseRef, ctx.document.source.headRef, file.path);
235
+ const live = await readPathDiff(ctx.cwd, ctx.resolved.baseSha, ctx.resolved.headSha, file.path);
235
236
  if (live === undefined) {
236
237
  throw new ApiError(404, `no live diff for ${path}`);
237
238
  }
238
- return serializeLayerFile(live, live.hunks, false);
239
+ return serializeGroupFile(live, live.hunks, false);
239
240
  }
240
- function serializeLayerFile(file, fileHunks, deferLockfile) {
241
+ function serializeGroupFile(file, fileHunks, deferLockfile) {
241
242
  const lockfile = isLockfilePath(file.path) && !file.binary && !file.image;
242
243
  const deferred = deferLockfile && lockfile;
243
244
  const next = {
@@ -245,6 +246,7 @@ function serializeLayerFile(file, fileHunks, deferLockfile) {
245
246
  kind: file.image ? "image" : lockfile ? "lockfile" : "text",
246
247
  status: file.status,
247
248
  patch: deferred ? "" : file.image ? file.headerPatch : filePatchFromGit(file, fileHunks),
249
+ complete: file.hunks.length === fileHunks.length,
248
250
  hunks: fileHunks.map(serializeHunk),
249
251
  };
250
252
  if (file.oldPath !== undefined) {
package/dist/cli/args.js CHANGED
@@ -11,7 +11,7 @@ Commands:
11
11
  Check schema, ref resolution, and hunk coverage
12
12
 
13
13
  serve --data <review.json> [--port <n>] [--open]
14
- Serve the local UI on 127.0.0.1 (re-reads git on each request)
14
+ Serve the local UI on 127.0.0.1 (pins commit SHAs at start)
15
15
 
16
16
  export --data <review.json> --out <dir>
17
17
  Write a static site (same UI + frozen git payloads). No server after that.
@@ -26,8 +26,8 @@ Options:
26
26
  -h, --help Show this help
27
27
  -v, --version Show version
28
28
 
29
- Diffs always come from git in cwd. The review document is interpretation only.
30
- Export is a point-in-time copy. Rebase or new commits need a new export.
29
+ Diffs come from git objects at the pinned SHAs. The review document is interpretation only.
30
+ Serve resolves refs when it starts. A later checkout does not change that review.
31
31
  `;
32
32
  export function parseArgv(argv, cwd = process.cwd()) {
33
33
  const args = argv[0] === "--" ? argv.slice(1) : [...argv];
package/dist/cli/main.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { parseArgv, USAGE } from "./args.js";
7
7
  import { cmdIndex, cmdValidate, resolveDataPath, resolveOutPath } from "./commands.js";
8
8
  import { exportStaticSite } from "../api/snapshot.js";
9
- import { openReview } from "../api/live.js";
9
+ import { openReview, pinReviewSource } from "../api/live.js";
10
10
  import { coverageErrors } from "../review/coverage.js";
11
11
  import { assertWorkTree } from "../git/repo.js";
12
12
  import { readPackageVersion } from "../package-root.js";
@@ -42,9 +42,10 @@ export async function run(argv) {
42
42
  }
43
43
  case "serve": {
44
44
  const dataPath = resolveDataPath(request.data, request.cwd);
45
- const ctx = await openReview(request.cwd, dataPath);
45
+ const pin = await pinReviewSource(request.cwd, dataPath);
46
+ const ctx = await openReview(request.cwd, dataPath, pin);
46
47
  warnCoverage(ctx.coverage, "serve continues; git wins, unassigned/stale are visible");
47
- const running = await startServer({ cwd: request.cwd, dataPath, port: request.port });
48
+ const running = await startServer({ cwd: request.cwd, dataPath, port: request.port, pin });
48
49
  console.log(running.url);
49
50
  console.error(`serving ${dataPath} cwd=${request.cwd} localhost only`);
50
51
  if (request.open) {
package/dist/git/repo.js CHANGED
@@ -14,6 +14,13 @@ export async function mergeBase(cwd, baseRef, headRef) {
14
14
  const sha = await git(cwd, ["merge-base", baseRef, headRef]);
15
15
  return sha.trim();
16
16
  }
17
+ /** Resolve refs to commits once. Later checkout or branch motion does not move these SHAs. */
18
+ export async function pinRange(cwd, baseRef, headRef) {
19
+ const baseSha = await resolveCommit(cwd, baseRef);
20
+ const headSha = await resolveCommit(cwd, headRef);
21
+ const mergeBaseSha = await mergeBase(cwd, baseSha, headSha);
22
+ return { baseRef, headRef, baseSha, headSha, mergeBaseSha };
23
+ }
17
24
  export async function defaultBaseRef(cwd) {
18
25
  const remoteHead = await git(cwd, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], { allowFail: true });
19
26
  const trimmed = remoteHead.trim();
package/dist/git/show.js CHANGED
@@ -1,9 +1,9 @@
1
+ import { readBlob } from "./blob.js";
1
2
  import { assertSafePath, assertSafeRef } from "./repo.js";
2
3
  import { git } from "./exec.js";
3
4
  export async function showFile(cwd, ref, path) {
4
- assertSafeRef(ref);
5
- assertSafePath(path);
6
- return git(cwd, ["show", "--end-of-options", `${ref}:${path}`]);
5
+ const bytes = await readBlob(cwd, ref, path);
6
+ return bytes.toString("utf8");
7
7
  }
8
8
  export async function fileExistsAt(cwd, ref, path) {
9
9
  assertSafeRef(ref);
@@ -1,8 +1,10 @@
1
1
  import { flattenHunks, readDiff, toHunkRef } from "../git/diff.js";
2
+ import { pinRange } from "../git/repo.js";
2
3
  import { hunkKey } from "../schema/identity.js";
3
4
  import { isLockfilePath } from "../schema/lockfile.js";
4
- export async function coverReview(cwd, document) {
5
- const files = await readDiff(cwd, document.source.baseRef, document.source.headRef);
5
+ export async function coverReview(cwd, document, pin) {
6
+ const range = pin ?? (await pinRange(cwd, document.source.baseRef, document.source.headRef));
7
+ const files = await readDiff(cwd, range.baseSha, range.headSha);
6
8
  const live = flattenHunks(files);
7
9
  return { files, coverage: joinCoverage(document, live) };
8
10
  }
@@ -2,7 +2,7 @@ import { createServer } from "node:http";
2
2
  import { createReadStream, existsSync, statSync } from "node:fs";
3
3
  import { extname, join, resolve } from "node:path";
4
4
  import { ApiError } from "../api/error.js";
5
- import { openReview, renderResource, snapshotJson } from "../api/live.js";
5
+ import { openReview, pinReviewSource, renderResource, snapshotJson } from "../api/live.js";
6
6
  import { parseApiPath } from "../api/paths.js";
7
7
  import { GitError } from "../git/exec.js";
8
8
  import { findPackageRoot } from "../package-root.js";
@@ -25,8 +25,10 @@ const MIME = {
25
25
  };
26
26
  export async function startServer(opts) {
27
27
  const uiRoot = opts.uiRoot ?? join(findPackageRoot(), "dist/ui");
28
+ const pin = opts.pin ?? (await pinReviewSource(opts.cwd, opts.dataPath));
29
+ const bound = { ...opts, pin };
28
30
  const server = createServer((req, res) => {
29
- void handle(req, res, opts, uiRoot);
31
+ void handle(req, res, bound, uiRoot);
30
32
  });
31
33
  await new Promise((resolveListen, reject) => {
32
34
  server.listen(opts.port, "127.0.0.1", () => resolveListen());
@@ -53,7 +55,7 @@ async function handle(req, res, opts, uiRoot) {
53
55
  }
54
56
  const resource = parseApiPath(url.pathname);
55
57
  if (resource !== undefined) {
56
- const ctx = await openReview(opts.cwd, opts.dataPath);
58
+ const ctx = await openReview(opts.cwd, opts.dataPath, opts.pin);
57
59
  sendSnapshot(res, 200, await renderResource(ctx, resource));
58
60
  return;
59
61
  }