Skip to the content

Capsid menu

src/job-body-check.ts

83 lines and 8 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.

Functions

The functions of job-body-check.ts
FunctionLinesComplexityCoveredCRAPMutation
proseOnly23 to 427100%WatchCRAP 7No data: mutation testing has not run on this repo yet
placeholderRefusal54 to 654100%OkCRAP 4No data: mutation testing has not run on this repo yet
<anonymous:57>57 to 571100%OkCRAP 1No data: mutation testing has not run on this repo yet
jobTextRefusal68 to 704100%OkCRAP 4No data: mutation testing has not run on this repo yet
heading75 to 751100%OkCRAP 1No data: mutation testing has not run on this repo yet
missingPartsWarning85 to 902100%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:87>87 to 871100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:87>87 to 871100%OkCRAP 1No data: mutation testing has not run on this repo yet

Imports

It imports no other module of this repo. Imported by src/complete-ci.ts, src/job-checklist.ts, src/job-fact-check.ts, src/jobs-edit.ts, src/tools/jobs.ts.

Source

// A job body a session can act on (job_9a8d13d2d926, Dustin 2026-10-10). Three checks, run
// by the capsid_work_jobs tool on a caller's post and edit, and by nothing else: the
// watcher, the skills refresh and the finalize step post through postJob and are not
// subject to them.
//
// 1. An unfilled placeholder is refused. Tonight two sessions stalled on a pasted
//    template that still read <ID>. Text in a code block or an external fence is exempt,
//    since a body may quote a template or relay text it did not write.
// 2. A body missing any of the five headed parts is accepted with a warning, because a
//    refusal nobody can predict teaches people to route around the queue
//    (src/provenance.ts makes the same call).
// 3. A body stating a pull request merged, a version tagged or a package version published
//    is checked against GitHub and npm, and refused when what Capsid reads says otherwise;
//    a claim that could not be checked is a warning (src/job-fact-check.ts, which reads
//    prose through proseOnly and the parts through heading, both exported for it).

// A fenced block: ``` or ~~~ (an external fence is a ~~~ fence), closed by a line of at
// least as many of the same character, or by the end of the text.
const FENCE_OPEN = /^[ \t]{0,3}(`{3,}|~{3,})/;

/** `text` with fenced blocks and inline code spans blanked, so what is left is prose a
 *  session would read as instruction. Line structure is kept. */
export function proseOnly(text: string): string {
  const out: string[] = [];
  let fence: { char: string; length: number } | null = null;
  for (const line of text.split(/\r?\n/)) {
    if (fence) {
      const close = /^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
      if (close && close[1][0] === fence.char && close[1].length >= fence.length) fence = null;
      out.push("");
      continue;
    }
    const open = FENCE_OPEN.exec(line);
    if (open) {
      fence = { char: open[1][0], length: open[1].length };
      out.push("");
      continue;
    }
    out.push(line.replace(/(`+)[^`\n]*?\1/g, ""));
  }
  return out.join("\n");
}

const PLACEHOLDERS: Array<{ name: string; pattern: RegExp }> = [
  { name: "<ID>", pattern: /<id>/i },
  { name: "<...>", pattern: /<\.\.\.>/ },
  { name: "{{...}}", pattern: /\{\{[^{}\n]*\}\}/ },
  { name: "TODO:", pattern: /\bTODO:/ },
  { name: "TBD", pattern: /\bTBD\b/ },
];

/** The refusal for the first unfilled placeholder in `text`, naming it and where it is, or
 *  null. `field` is "title" or "body". */
export function placeholderRefusal(field: string, text: string): string | null {
  const lines = proseOnly(text).split("\n");
  for (const { name, pattern } of PLACEHOLDERS) {
    const at = lines.findIndex((line) => pattern.test(line));
    if (at >= 0) {
      // The text as written, so <id> is named as <id> and not as the <ID> pattern.
      const found = pattern.exec(lines[at])?.[0] ?? name;
      return `the ${field} has an unfilled placeholder, ${found} (line ${at + 1}). A session would stall on it. Fill it in, or put it in a code block or an external fence if it is meant literally. Nothing was written.`;
    }
  }
  return null;
}

/** The refusal for an unfilled placeholder in a job's title or body, or null. */
export function jobTextRefusal(title: string | undefined, body: string | undefined): string | null {
  return (title === undefined ? null : placeholderRefusal("title", title)) ?? (body === undefined ? null : placeholderRefusal("body", body));
}

// The five headed parts. DECIDED and RULES are alternatives for the fifth.
// A heading is the keyword at the start of a line, optionally behind a markdown heading
// marker or bold, and followed by a delimiter, so "DO NOT touch" and "GOALS" are prose.
export const heading = (words: string) => new RegExp(`^[ \\t]*(?:#{1,6}[ \\t]+)?\\*{0,2}(?:${words})(?=\\*{0,2}(?:[:.,(]|[ \\t]*$)|[ \\t]+\\()`, "m");
const PARTS: Array<{ name: string; heading: RegExp }> = [
  { name: "GOAL", heading: heading("GOAL") },
  { name: "DO", heading: heading("DO") },
  { name: "OUT OF SCOPE", heading: heading("OUT OF SCOPE") },
  { name: "DONE WHEN", heading: heading("DONE WHEN") },
  { name: "DECIDED or RULES", heading: heading("DECIDED|RULES") },
];

/** A warning naming the headed parts `body` lacks, or null when it has all five. */
export function missingPartsWarning(body: string): string | null {
  const prose = proseOnly(body);
  const missing = PARTS.filter((part) => !part.heading.test(prose)).map((part) => part.name);
  if (missing.length === 0) return null;
  return `the body has no heading for: ${missing.join(", ")}. Posted anyway. A session acts best on a body with GOAL, DO, OUT OF SCOPE, DONE WHEN and DECIDED or RULES (docs/work-queue.md, "A body a session can act on").`;
}