tickmarkr 2.7.6 → 2.7.7

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.
@@ -179,12 +179,14 @@ export const COLLECTABLE_TESTS = "tests/**/*.test.ts";
179
179
  export const LEGACY_PREFIX = ["dro", "vr"].join("");
180
180
  export const TICKMARKR_NATIVE_MARKER = /^<!--\s*tickmarkr:spec(?:\s+v1)?\s*-->\s*$/m;
181
181
  export const NATIVE_MARKER = new RegExp(`^<!--\\s*(?:tickmarkr|${LEGACY_PREFIX}):spec(?:\\s+v1)?\\s*-->\\s*$`, "m");
182
- const HEAD_RE = /^## (T\d+):\s*(.+)$/;
183
- const FIELD_RE = /^- (\w+):\s*(.*)$/;
184
- const NESTED_RE = /^\s+- (.+)$/;
182
+ // Queue row 130: the rest of a spec line is [^\r\n], never `.` — `.` misses U+2028/U+2029, and an acceptance item holding
183
+ // one was glued onto its neighbour as a plain judge string, losing its own test: oracle (PRD and Spec Kit dropped it).
184
+ const HEAD_RE = /^## (T\d+):\s*([^\r\n]+)$/;
185
+ const FIELD_RE = /^- (\w+):\s*([^\r\n]*)$/;
186
+ const NESTED_RE = /^\s+- ([^\r\n]+)$/;
185
187
  // v1.19: a typed acceptance oracle line — "command: ...", "test: ...", or "judge: ...". Anything
186
188
  // without one of these prefixes is a plain-string judge criterion (compat path, emits a warning).
187
- const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*(.*)$`);
189
+ const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*([^\\r\\n]*)$`);
188
190
  const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance", "timeout", "pins", "outofscope"]);
189
191
  const listItems = (draft) => draft.list === "acceptance" ? draft.acceptanceRaw
190
192
  : draft.list === "pins" ? draft.pinsRaw
@@ -199,7 +201,7 @@ const PIN_GLOB_SEP = "| glob:";
199
201
  const LANDING_SEP = "| suite:";
200
202
  function parsePin(task, raw, index, pathSet) {
201
203
  const bad = (detail) => invalid(task, "pins", `item ${index + 1} (${JSON.stringify(raw)}) ${detail}`);
202
- const typed = raw.match(/^(\w+):\s*(.*)$/);
204
+ const typed = raw.match(/^(\w+):\s*([^\r\n]*)$/);
203
205
  if (typed?.[1] === "fixture") {
204
206
  const paths = pathSet(typed[2]);
205
207
  if (!paths.length)
@@ -275,7 +277,7 @@ function testsAtHead(root) {
275
277
  }
276
278
  const bodies = new Map();
277
279
  for (const line of grep.stdout.split("\n")) {
278
- const match = line.match(/^HEAD:(tests\/.*\.test\.ts):\d+:(.*)$/);
280
+ const match = line.match(/^HEAD:(tests\/[^\r\n]*\.test\.ts):\d+:([^\r\n]*)$/);
279
281
  if (!match)
280
282
  continue;
281
283
  const lines = bodies.get(match[1]) ?? [];
@@ -544,7 +546,7 @@ export function compileNative(file, options = {}) {
544
546
  }
545
547
  const draft = drafts.at(-1);
546
548
  if (!draft) {
547
- const base = line.match(/^base:\s*(.*)$/);
549
+ const base = line.match(/^base:\s*([^\r\n]*)$/);
548
550
  if (base) {
549
551
  const value = base[1].trim();
550
552
  if (!value) {
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { validateGraph } from "../graph/schema.js";
3
3
  import { CompileError, inferShape, sha256 } from "./common.js";
4
- const HEAD_RE = /^## (T\d+):\s*(.+)$/;
5
- const FIELD_RE = /^- (\w+):\s*(.*)$/;
6
- const NESTED_RE = /^\s+- (.+)$/;
4
+ // Queue row 130: [^\r\n], never `.` (U+2028/U+2029 inside a line).
5
+ const HEAD_RE = /^## (T\d+):\s*([^\r\n]+)$/;
6
+ const FIELD_RE = /^- (\w+):\s*([^\r\n]*)$/;
7
+ const NESTED_RE = /^\s+- ([^\r\n]+)$/;
7
8
  export function compilePrd(file) {
8
9
  if (!existsSync(file))
9
10
  throw new CompileError(`no such PRD file: ${file}`);
@@ -2,8 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { validateGraph } from "../graph/schema.js";
4
4
  import { CompileError, inferShape, sha256 } from "./common.js";
5
- const TASK_RE = /^- \[( |x)\] (T\d+)\s*(\[P\])?\s*(.+)$/;
6
- const SUB_RE = /^\s+- (\w+):\s*(.+)$/;
5
+ // Queue row 130: [^\r\n], never `.` (U+2028/U+2029 inside a line).
6
+ const TASK_RE = /^- \[( |x)\] (T\d+)\s*(\[P\])?\s*([^\r\n]+)$/;
7
+ const SUB_RE = /^\s+- (\w+):\s*([^\r\n]+)$/;
7
8
  export function compileSpecKit(dir) {
8
9
  const path = join(dir, "tasks.md");
9
10
  if (!existsSync(path)) {
@@ -307,13 +307,14 @@ function compressLines(lines) {
307
307
  }
308
308
  return parts.join(", ");
309
309
  }
310
- const DIFF_SECTIONS = /(?=^diff --git )/m;
310
+ // Queue rows 128/129: "\n"-only line semantics — the m flag would also split a line at U+2028/U+2029.
311
+ const DIFF_SECTIONS = /(?<=^|\n)(?=diff --git )/;
311
312
  // the a-side path of a whole-file deletion section, or null if this section is not one.
312
313
  function deletedPath(section) {
313
- if (!/^deleted file mode /m.test(section))
314
+ if (!/(?<=^|\n)deleted file mode /.test(section))
314
315
  return null;
315
- const oldPath = /^--- (.+)$/m.exec(section)?.[1]
316
- ?? /^Binary files (.+) and \/dev\/null differ$/m.exec(section)?.[1];
316
+ const oldPath = /(?<=^|\n)--- ([^\r\n]+)/.exec(section)?.[1]
317
+ ?? /(?<=^|\n)Binary files ([^\r\n]+) and \/dev\/null differ(?=[\r\n]|$)/.exec(section)?.[1];
317
318
  if (!oldPath || oldPath === "/dev/null")
318
319
  return null;
319
320
  const unquoted = oldPath.startsWith('"') && oldPath.endsWith('"') ? oldPath.slice(1, -1) : oldPath;
@@ -3,7 +3,7 @@
3
3
  * path receives capture accounting only when its manifest row names a
4
4
  * registered producer and repeats that producer's current provenance exactly.
5
5
  */
6
- import { diffSidePath, gitHeaderPaths, unquoteGitPath } from "./git-paths.js";
6
+ import { diffSidePath, gitHeaderPaths, quoteGitPath, unquoteGitPath } from "./git-paths.js";
7
7
  const GOLDEN_PROVENANCE = {
8
8
  source: "src/tui/cockpit/capture.ts",
9
9
  entrypoint: "regenerateGoldenFrames",
@@ -191,17 +191,21 @@ export function classifyArtifactPath(path, manifest = CAPTURE_ARTIFACT_MANIFEST)
191
191
  provenance: producer.provenance,
192
192
  };
193
193
  }
194
- const SET_ASIDE_RECEIPT = /^set aside: regenerable capture (.+?) — \d+ bytes withheld\b/m;
194
+ // Queue rows 128/129: patch text is split into lines at "\n" only. Under the m flag JS `^` and `$` also break at
195
+ // U+2028/U+2029 (and `.` never matches them), so an added line holding one could forge a header line; these patterns
196
+ // anchor on "\n" instead, and a name written back into the text is quoted (quoteGitPath) so it cannot end a line.
197
+ const SET_ASIDE_RECEIPT = /(?<=^|\n)set aside: regenerable capture ([^\r\n]+?) — \d+ bytes withheld\b/;
195
198
  /** The path named by a citable capture receipt, or null when there is none. */
196
199
  export function setAsideReceiptPath(section) {
197
- return SET_ASIDE_RECEIPT.exec(section)?.[1] ?? null;
200
+ const path = SET_ASIDE_RECEIPT.exec(section)?.[1];
201
+ return path === undefined ? null : unquoteGitPath(path);
198
202
  }
199
- const DIFF_SECTIONS = /(?=^diff --git )/m;
203
+ const DIFF_SECTIONS = /(?<=^|\n)(?=diff --git )/;
200
204
  function deletedPath(section) {
201
- if (!/^deleted file mode /m.test(section))
205
+ if (!/(?<=^|\n)deleted file mode /.test(section))
202
206
  return null;
203
- const oldPath = /^--- (.+)$/m.exec(section)?.[1]
204
- ?? /^Binary files (.+) and \/dev\/null differ$/m.exec(section)?.[1];
207
+ const oldPath = /(?<=^|\n)--- ([^\r\n]+)/.exec(section)?.[1]
208
+ ?? /(?<=^|\n)Binary files ([^\r\n]+) and \/dev\/null differ(?=[\r\n]|$)/.exec(section)?.[1];
205
209
  return oldPath ? diffSidePath(oldPath) : null;
206
210
  }
207
211
  /**
@@ -216,7 +220,7 @@ export function reviewableLogicDiff(diff) {
216
220
  const path = deletedPath(section);
217
221
  if (!path || isProtectedEvidence(path))
218
222
  return section;
219
- return `deleted file: ${path}\n`;
223
+ return `deleted file: ${quoteGitPath(path)}\n`;
220
224
  }).join("");
221
225
  }
222
226
  function parseContentSection(section) {
@@ -240,12 +244,12 @@ function parseContentSection(section) {
240
244
  function sectionPaths(section, parsed) {
241
245
  if (parsed)
242
246
  return [...new Set(parsed.sides.filter((path) => path !== null))];
243
- const renamedFrom = /^rename from (.+)$/m.exec(section)?.[1];
244
- const renamedTo = /^rename to (.+)$/m.exec(section)?.[1];
247
+ const renamedFrom = /(?<=^|\n)rename from ([^\r\n]+)/.exec(section)?.[1];
248
+ const renamedTo = /(?<=^|\n)rename to ([^\r\n]+)/.exec(section)?.[1];
245
249
  if (renamedFrom || renamedTo) {
246
250
  return [...new Set([renamedFrom, renamedTo].filter((path) => path !== undefined).map(unquoteGitPath))];
247
251
  }
248
- const header = /^diff --git (.+)$/m.exec(section)?.[1];
252
+ const header = /(?<=^|\n)diff --git ([^\r\n]+)/.exec(section)?.[1];
249
253
  return header ? gitHeaderPaths(header) : [];
250
254
  }
251
255
  function hunkPayload(body, sign) {
@@ -293,7 +297,7 @@ export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST)
293
297
  sections: [],
294
298
  };
295
299
  }
296
- const rawSections = diff.split(/(?=^diff --git )/m);
300
+ const rawSections = diff.split(DIFF_SECTIONS);
297
301
  const kindOnly = kindOnlyPaths(rawSections);
298
302
  const rendered = [];
299
303
  const measurements = [];
@@ -321,7 +325,7 @@ export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST)
321
325
  const withheld = parsed.lines.slice(parsed.minus).join("\n");
322
326
  const captureBytes = Buffer.byteLength(withheld, "utf8");
323
327
  const receiptPath = paths.at(-1);
324
- const receipt = `set aside: regenerable capture ${receiptPath} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
328
+ const receipt = `set aside: regenerable capture ${quoteGitPath(receiptPath)} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
325
329
  const compact = `${parsed.lines.slice(0, parsed.minus).join("\n")}\n${receipt}\n`;
326
330
  rendered.push(compact);
327
331
  measurements.push({
@@ -1,5 +1,11 @@
1
1
  /** A path exactly as git printed it, quoted or not, decoded to the name on disk. Unquoted input is returned as is. */
2
2
  export declare function unquoteGitPath(raw: string): string;
3
+ /**
4
+ * Queue rows 128/129: the inverse of unquoteGitPath, for a name tickmarkr writes back into patch text it parses again.
5
+ * A name holding a control character, U+2028/U+2029, a double quote or a backslash is C-quoted (those characters
6
+ * escaped, every other character kept), so no name can end a line; any other name is returned as is.
7
+ */
8
+ export declare function quoteGitPath(path: string): string;
3
9
  /**
4
10
  * One side of a `--- ` / `+++ ` line, after its four-character marker: null for /dev/null, else the path without its
5
11
  * a/ or b/ prefix. git ends a side holding a space with ONE tab (quoted or not, measured on git 2.54) — exactly that tab
@@ -34,6 +34,28 @@ export function unquoteGitPath(raw) {
34
34
  }
35
35
  return Buffer.from(bytes).toString("utf8");
36
36
  }
37
+ const NAMED = Object.fromEntries(Object.entries(ESCAPES).map(([name, code]) => [code, name]));
38
+ /**
39
+ * Queue rows 128/129: the inverse of unquoteGitPath, for a name tickmarkr writes back into patch text it parses again.
40
+ * A name holding a control character, U+2028/U+2029, a double quote or a backslash is C-quoted (those characters
41
+ * escaped, every other character kept), so no name can end a line; any other name is returned as is.
42
+ */
43
+ export function quoteGitPath(path) {
44
+ if (!/[\x00-\x1f\x7f"\\\u2028\u2029]/.test(path))
45
+ return path;
46
+ let quoted = "";
47
+ for (const ch of path) {
48
+ const code = ch.codePointAt(0);
49
+ if (NAMED[code] !== undefined)
50
+ quoted += `\\${NAMED[code]}`;
51
+ else if (code < 0x20 || code === 0x7f || code === 0x2028 || code === 0x2029) {
52
+ quoted += [...Buffer.from(ch, "utf8")].map((byte) => `\\${byte.toString(8).padStart(3, "0")}`).join("");
53
+ }
54
+ else
55
+ quoted += ch;
56
+ }
57
+ return `"${quoted}"`;
58
+ }
37
59
  /**
38
60
  * One side of a `--- ` / `+++ ` line, after its four-character marker: null for /dev/null, else the path without its
39
61
  * a/ or b/ prefix. git ends a side holding a space with ONE tab (quoted or not, measured on git 2.54) — exactly that tab
@@ -104,7 +104,7 @@ export function readProcessIdentity(pid, host = {}) {
104
104
  const ps = spawnSync("ps", ["-ww", "-o", "pgid=", "-o", "stat=", "-o", "lstart=", "-o", "command=", "-p", String(pid)], {
105
105
  encoding: "utf8", env: { ...process.env, LC_ALL: "C" },
106
106
  });
107
- const row = /^\s*(\d+)\s+(\S+)\s+(\w{3} \w{3} [ \d]\d \d\d:\d\d:\d\d \d{4})\s+(.*\S)\s*$/.exec(ps.stdout ?? "");
107
+ const row = /^\s*(\d+)\s+(\S+)\s+(\w{3} \w{3} [ \d]\d \d\d:\d\d:\d\d \d{4})\s+(.*\S)\s*$/s.exec(ps.stdout ?? "");
108
108
  if (ps.status !== 0 || !row)
109
109
  return confirmedDead(pid) ? "DEAD" : "UNKNOWN";
110
110
  if (row[2].startsWith("Z"))
@@ -1244,7 +1244,7 @@ export async function observeWorkerProcessTree(marker, cwd) {
1244
1244
  return "unmeasurable";
1245
1245
  const rows = [];
1246
1246
  for (const line of snapshot.stdout.split("\n")) {
1247
- const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
1247
+ const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/s.exec(line);
1248
1248
  if (match)
1249
1249
  rows.push({ pid: match[1], ppid: match[2], command: match[3] });
1250
1250
  }
@@ -1272,7 +1272,7 @@ const pathAtOrBelow = (root, candidate) => {
1272
1272
  export function countLiveSuites(snapshot, repoRoot, daemonPid, cwdForPid, suiteParentForPid) {
1273
1273
  const rows = [];
1274
1274
  for (const line of snapshot.split("\n")) {
1275
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
1275
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
1276
1276
  if (match && !match[3].startsWith("Z")) {
1277
1277
  rows.push({ pid: Number(match[1]), ppid: Number(match[2]), command: match[4] });
1278
1278
  }
@@ -14,7 +14,7 @@ import { basename } from "node:path";
14
14
  export const LAUNCHSERVICES_PROBE_CEILING_MS = 5_000;
15
15
  // lstart in the C locale: "Wed Sep 30 21:54:33 2026"; comm is the executable path on Darwin.
16
16
  const PS_ARGS = ["-A", "-o", "pid=,lstart=,comm="];
17
- const PS_LINE = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s*$/;
17
+ const PS_LINE = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s*$/s;
18
18
  const MAX_PS_BYTES = 4 * 1024 * 1024;
19
19
  /** Exactly one launchservicesd row, or null (absent, ambiguous or malformed output is unknown). */
20
20
  export function parsePsIdentity(stdout) {
package/dist/run/lease.js CHANGED
@@ -330,7 +330,7 @@ export async function describeLeaseBlockers(pids) {
330
330
  done("");
331
331
  }
332
332
  });
333
- const executables = new Map(rows.split("\n").flatMap((line) => { const row = /^\s*(\d+)\s+(.*)$/.exec(line); return row ? [[Number(row[1]), row[2].trim()]] : []; }));
333
+ const executables = new Map(rows.split("\n").flatMap((line) => { const row = /^\s*(\d+)\s+(.*)$/s.exec(line); return row ? [[Number(row[1]), row[2].trim()]] : []; }));
334
334
  const cwds = await import("./suite-census.js").then(({ batchedProcessCwds }) => batchedProcessCwds(pids)).catch(() => undefined);
335
335
  return pids.map((pid) => {
336
336
  const cwd = cwds?.get(pid);
@@ -363,7 +363,7 @@ export async function protectedProcesses(holder, tokenEnv = REPOSITORY_LEASE_TOK
363
363
  return !leader || sameProcessBirth(processBirth(pid), holder.rootBirths?.[pid]);
364
364
  }));
365
365
  const parsed = lines.flatMap(line => {
366
- const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
366
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
367
367
  return match ? [{ pid: Number(match[1]), parent: Number(match[2]), group: Number(match[3]), stat: match[4], command: match[5] }] : [];
368
368
  });
369
369
  const parents = new Map(parsed.map(row => [row.pid, row.parent]));
package/dist/run/stall.js CHANGED
@@ -118,7 +118,8 @@ export const workerReapHost = {
118
118
  if (!lines.length || lines.length > 20_000)
119
119
  return undefined;
120
120
  for (const line of lines) {
121
- const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+\s+\S+\s+\d+\s+\S+\s+\d+)\s+(.*)$/.exec(line);
121
+ // Queue row 127: the s flag — ps prints an argv's U+2028/U+2029 raw, and `.` alone never matches them.
122
+ const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+\s+\S+\s+\d+\s+\S+\s+\d+)\s+(.*)$/s.exec(line);
122
123
  if (!m)
123
124
  return undefined;
124
125
  if (m[5].startsWith("Z"))
@@ -418,7 +419,7 @@ async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
418
419
  continue;
419
420
  if (group !== undefined && Number(ids[3]) === group)
420
421
  grouped.push(ids[1]);
421
- const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)$/.exec(line);
422
+ const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)$/s.exec(line);
422
423
  if (!m)
423
424
  continue;
424
425
  const cpu = parsePsCpu(m[3]);
@@ -56,7 +56,7 @@ export async function batchedProcessCwds(pids, exec = probeExec, proc = procRead
56
56
  export function runnerPids(snapshot) {
57
57
  const pids = [];
58
58
  for (const line of snapshot.split("\n")) {
59
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
59
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
60
60
  if (match && !match[3].startsWith("Z") && isRunnerCommand(match[4]))
61
61
  pids.push(Number(match[1]));
62
62
  }
@@ -95,7 +95,7 @@ export async function batchedProcessProbes(pids, exec = probeExec, proc = procRe
95
95
  if (rest.length === 0)
96
96
  return;
97
97
  for (const line of (await ask("ps", ["eww", "-p", rest.join(","), "-o", "pid=,command="])).split("\n")) {
98
- const row = /^\s*(\d+)\s+(.*)$/.exec(line);
98
+ const row = /^\s*(\d+)\s+(.*)$/s.exec(line);
99
99
  const value = row ? parentIn(row[2]) : undefined;
100
100
  if (row && value)
101
101
  parents.set(Number(row[1]), Number(value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.7.6",
3
+ "version": "2.7.7",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",