paperlint 3.2.0 → 4.0.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/CLAUDE.md +2 -2
- package/dist/adapters/banal/failure.d.ts +1 -7
- package/dist/adapters/banal/failure.d.ts.map +1 -1
- package/dist/adapters/banal/failure.js +5 -7
- package/dist/adapters/banal/failure.js.map +1 -1
- package/dist/adapters/banal/index.d.ts.map +1 -1
- package/dist/adapters/banal/index.js +8 -11
- package/dist/adapters/banal/index.js.map +1 -1
- package/dist/adapters/banal/install.d.ts.map +1 -1
- package/dist/adapters/banal/install.js +4 -3
- package/dist/adapters/banal/install.js.map +1 -1
- package/dist/adapters/banal/invocation.d.ts +9 -4
- package/dist/adapters/banal/invocation.d.ts.map +1 -1
- package/dist/adapters/banal/invocation.js +13 -1
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/locate.d.ts +12 -9
- package/dist/adapters/banal/locate.d.ts.map +1 -1
- package/dist/adapters/banal/locate.js +19 -12
- package/dist/adapters/banal/locate.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +10 -1
- package/dist/adapters/banal/run.d.ts.map +1 -1
- package/dist/adapters/banal/run.js +8 -4
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +1 -1
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +2 -1
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +8 -2
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/node/process.io.d.ts +6 -3
- package/dist/adapters/node/process.io.d.ts.map +1 -1
- package/dist/adapters/node/process.io.js.map +1 -1
- package/dist/adapters/node/workspace.io.d.ts.map +1 -1
- package/dist/adapters/node/workspace.io.js +4 -2
- package/dist/adapters/node/workspace.io.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +4 -2
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +8 -1
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +2 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -6
- package/dist/build.js.map +1 -1
- package/dist/caller-path.d.ts +3 -0
- package/dist/caller-path.d.ts.map +1 -0
- package/dist/caller-path.js +13 -0
- package/dist/caller-path.js.map +1 -0
- package/dist/cli.d.ts +10 -17
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +62 -12
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +12 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/domain/paths.d.ts +8 -2
- package/dist/domain/paths.d.ts.map +1 -1
- package/dist/domain/paths.js +12 -1
- package/dist/domain/paths.js.map +1 -1
- package/dist/domain/ready.d.ts +24 -0
- package/dist/domain/ready.d.ts.map +1 -0
- package/dist/domain/ready.js +11 -0
- package/dist/domain/ready.js.map +1 -0
- package/dist/domain/record.d.ts +8 -0
- package/dist/domain/record.d.ts.map +1 -0
- package/dist/domain/record.js +8 -0
- package/dist/domain/record.js.map +1 -0
- package/dist/domain/sha256.d.ts.map +1 -1
- package/dist/domain/sha256.js +4 -2
- package/dist/domain/sha256.js.map +1 -1
- package/dist/engine.d.ts +23 -22
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/eslint-rules/bib-reachable-entry.d.ts +1 -1
- package/dist/eslint-rules/bib-reachable-entry.js.map +1 -1
- package/dist/eslint-rules/install-path-literals.d.ts +2 -2
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/paper-research-question.d.ts +1 -1
- package/dist/eslint-rules/paper-research-question.js.map +1 -1
- package/dist/eslint-rules/paper-stages.d.ts +2 -2
- package/dist/eslint-rules/paper-stages.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +6 -6
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -1
- package/dist/eslint-rules/review-frontmatter.d.ts +2 -2
- package/dist/eslint-rules/review-frontmatter.js.map +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -1
- package/dist/eslint-rules/temp-root-realpath.d.ts +1 -1
- package/dist/eslint-rules/temp-root-realpath.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts +2 -2
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +29 -26
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -2
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +3 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +18 -8
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +1 -5
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +5 -1
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -6
- package/dist/paper-files.js.map +1 -1
- package/dist/paper-settings.js +3 -3
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-facts.d.ts +51 -1
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +23 -15
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.d.ts.map +1 -1
- package/dist/pdf-geometry.js +2 -3
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/tool-installer.d.ts +2 -14
- package/dist/ports/tool-installer.d.ts.map +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +16 -4
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +4 -4
- package/dist/references.js.map +1 -1
- package/dist/rules-config.d.ts +26 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +9 -9
- package/dist/rules-config.js.map +1 -1
- package/dist/tex-requirements.d.ts +4 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +20 -7
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +2 -2
- package/eslint-rules/bib-reachable-entry.ts +1 -1
- package/eslint-rules/install-path-literals.ts +2 -2
- package/eslint-rules/paper-research-question.ts +1 -1
- package/eslint-rules/paper-stages.ts +2 -2
- package/eslint-rules/paper-typography.ts +2 -2
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint-rules/review-frontmatter.ts +1 -1
- package/eslint-rules/temp-root-realpath.ts +1 -1
- package/eslint-rules/tex-build.ts +2 -2
- package/eslint.config.mjs +7 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
- package/fixtures/install-path-literals/clean.md +2 -2
- package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
- package/fixtures/install-path-literals/defect.md +2 -2
- package/hooks/paper-status-gates.sh +9 -19
- package/lib/paper-config.harness.mjs +1 -1
- package/lib/paper-config.ts +1 -5
- package/lib/skill-checks.mjs +24 -381
- package/lib/skill-checks.test.mjs +63 -115
- package/lib/skill-corpus.mjs +10 -251
- package/lib/skill-corpus.test.mjs +2 -13
- package/lib/skill-trigger-cases.mjs +1 -1
- package/package.json +1 -1
- package/scripts/coverage-config.test.ts +0 -1
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/analyze-sibling-paper/SKILL.md +1 -39
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
- package/skills/argument-arc/SKILL.md +2 -36
- package/skills/argument-arc/SKILL.md.spec.ts +1 -43
- package/skills/build-benchmark/SKILL.md +1 -41
- package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
- package/skills/camera-ready/SKILL.md +1 -34
- package/skills/camera-ready/SKILL.md.spec.ts +0 -33
- package/skills/cold-read-diff/SKILL.md +1 -35
- package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
- package/skills/draft-paper/SKILL.md +2 -41
- package/skills/draft-paper/SKILL.md.spec.ts +1 -50
- package/skills/extend-paper/SKILL.md +2 -33
- package/skills/extend-paper/SKILL.md.spec.ts +1 -42
- package/skills/find-venue/SKILL.md +2 -38
- package/skills/find-venue/SKILL.md.spec.ts +1 -47
- package/skills/grade-paper-writing/SKILL.md +3 -38
- package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
- package/skills/grade-paper-writing/prose-lint.mjs +2 -3
- package/skills/grade-paper-writing/prose-lint.test.ts +1 -1
- package/skills/harden-paper/SKILL.md +1 -39
- package/skills/harden-paper/SKILL.md.spec.ts +0 -38
- package/skills/map-prior-work/SKILL.md +1 -35
- package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.ts +3 -4
- package/skills/paper-adversarial-review/SKILL.md +2 -35
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
- package/skills/paper-pipeline/SKILL.md +2 -28
- package/skills/paper-pipeline/SKILL.md.spec.ts +1 -27
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
- package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
- package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
- package/skills/paper-pipeline/scripts/README.md +30 -225
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
- package/skills/paper-pipeline/scripts/consumer.mjs +23 -104
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -42
- package/skills/paper-status/SKILL.md.spec.ts +1 -41
- package/skills/pc-panel-review/SKILL.md +2 -46
- package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
- package/skills/plan-paper-timeline/SKILL.md +2 -36
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
- package/skills/render-paper/SKILL.md +1 -37
- package/skills/render-paper/SKILL.md.spec.ts +0 -36
- package/skills/research-ideate/SKILL.md +2 -39
- package/skills/research-ideate/SKILL.md.spec.ts +1 -47
- package/skills/study-accepted-papers/SKILL.md +1 -34
- package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
- package/skills/submit-paper/SKILL.md +1 -33
- package/skills/submit-paper/SKILL.md.spec.ts +0 -32
- package/skills/sweep-design-space/SKILL.md +1 -38
- package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
- package/skills/tighten-paper/SKILL.md +2 -39
- package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
- package/skills/tighten-paper/structure.mjs +4 -11
- package/skills/verify-citations/SKILL.md +2 -38
- package/skills/verify-citations/SKILL.md.spec.ts +1 -37
- package/src/adapters/banal/failure.ts +9 -10
- package/src/adapters/banal/index.test.ts +2 -2
- package/src/adapters/banal/index.ts +8 -10
- package/src/adapters/banal/install.ts +9 -4
- package/src/adapters/banal/invocation.test.ts +20 -8
- package/src/adapters/banal/invocation.ts +23 -8
- package/src/adapters/banal/locate.test.ts +2 -2
- package/src/adapters/banal/locate.ts +27 -21
- package/src/adapters/banal/run.test.ts +2 -2
- package/src/adapters/banal/run.ts +16 -10
- package/src/adapters/banal/settings.ts +2 -2
- package/src/adapters/curl/download.test.ts +4 -4
- package/src/adapters/memory/index.ts +4 -3
- package/src/adapters/node/files.test.ts +5 -3
- package/src/adapters/node/process.io.ts +9 -2
- package/src/adapters/node/process.test.ts +4 -4
- package/src/adapters/node/workspace.io.ts +9 -4
- package/src/adapters/references/index.ts +7 -2
- package/src/build-engine.test.ts +11 -6
- package/src/build-engine.ts +10 -5
- package/src/build.test.ts +10 -11
- package/src/build.ts +8 -9
- package/src/caller-path.ts +13 -0
- package/src/cli.harness.mjs +0 -2
- package/src/cli.test.ts +19 -8
- package/src/cli.ts +82 -25
- package/src/doctor.test.ts +1 -1
- package/src/doctor.ts +14 -5
- package/src/domain/paths.test.ts +15 -0
- package/src/domain/paths.ts +20 -1
- package/src/domain/ready.ts +31 -0
- package/src/domain/record.test.ts +20 -0
- package/src/domain/record.ts +12 -0
- package/src/domain/sha256.ts +5 -3
- package/src/engine.ts +30 -2
- package/src/facts-file.ts +43 -28
- package/src/hooks-settings.harness.ts +17 -0
- package/src/hooks-settings.ts +28 -4
- package/src/init.test.ts +25 -5
- package/src/init.ts +26 -12
- package/src/new-paper.harness.mjs +7 -23
- package/src/new-paper.ts +7 -3
- package/src/paper-files.test.ts +22 -19
- package/src/paper-files.ts +16 -27
- package/src/paper-settings.test.ts +2 -2
- package/src/paper-settings.ts +5 -5
- package/src/pdf-facts.test.ts +16 -5
- package/src/pdf-facts.ts +79 -22
- package/src/pdf-geometry.ts +5 -3
- package/src/ports/tool-installer.ts +2 -17
- package/src/presets.ts +28 -14
- package/src/references.test.ts +25 -19
- package/src/references.ts +4 -5
- package/src/rules-config.ts +40 -11
- package/src/tex-requirements.test.ts +21 -0
- package/src/tex-requirements.ts +29 -6
- package/src/toolchain.test.ts +9 -5
- package/src/toolchain.ts +2 -2
- package/src/types.ts +5 -3
- package/src/venue-rules.test.ts +8 -4
- package/src/venue-rules.ts +21 -11
- package/skills/paper-pipeline/scripts/announce.mjs +0 -67
- package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
- package/skills/paper-pipeline/scripts/fixtures/scorecard.ts +0 -100
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
- package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
- package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -750
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
- package/skills/paper-pipeline/scripts/status.mjs +0 -295
- package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pipeline-check.mjs as a command — the banner every edit to a paper reprints (where the paper
|
|
3
|
-
* stands and the next move), the finding list under it, the warning for a scorecard that does not
|
|
4
|
-
* parse (a misspelled current template vs the retired single-table format), and the inputs from
|
|
5
|
-
* OUTSIDE the scorecard: the text's date (git or mtime), `reviews/`, and credentials in the env.
|
|
6
|
-
*/
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { execFileSync } from "node:child_process";
|
|
9
|
-
import { utimesSync } from "node:fs";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { test } from "vitest";
|
|
13
|
-
import { runNode, useTempDir, writeTree } from "../../../test/support.ts";
|
|
14
|
-
import { check, newestSourceDate, parseStatus } from "./pipeline-check.mjs";
|
|
15
|
-
import { readdirSync } from "node:fs";
|
|
16
|
-
import { scorecard } from "./fixtures/scorecard.ts";
|
|
17
|
-
|
|
18
|
-
const SCRIPT = join(
|
|
19
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
20
|
-
"pipeline-check.mjs",
|
|
21
|
-
);
|
|
22
|
-
const root = useTempDir("pipeline-check-cli-");
|
|
23
|
-
const TODAY = "2026-09-01";
|
|
24
|
-
const at = (iso) => new Date(`${iso}T12:00:00Z`);
|
|
25
|
-
// The temp paper sits outside any git repository, so the text's date is its mtime: pinned before
|
|
26
|
-
// every date in the scorecard fixture, so a gate is stale only when a case makes it so.
|
|
27
|
-
const TEXT_DATE = "2026-08-01";
|
|
28
|
-
|
|
29
|
-
/** A paper directory: `files` go in as written, `mtimes` pin `{ path: iso }` after writing. */
|
|
30
|
-
const paper = (name, card, { files = {}, mtimes = {} } = {}) => {
|
|
31
|
-
const dir = join(root, name);
|
|
32
|
-
writeTree(dir, {
|
|
33
|
-
...(card === null ? {} : { "PIPELINE-STATUS.md": card }),
|
|
34
|
-
"paper.md": "## 1. Heading\n\nProse.\n",
|
|
35
|
-
...files,
|
|
36
|
-
});
|
|
37
|
-
for (const [rel, iso] of Object.entries({
|
|
38
|
-
"paper.md": TEXT_DATE,
|
|
39
|
-
...mtimes,
|
|
40
|
-
}))
|
|
41
|
-
utimesSync(join(dir, rel), at(iso), at(iso));
|
|
42
|
-
return dir;
|
|
43
|
-
};
|
|
44
|
-
const clean = (r) => ({
|
|
45
|
-
...r,
|
|
46
|
-
stdout: r.stdout.replaceAll(root, "<root>"),
|
|
47
|
-
stderr: r.stderr.replaceAll(root, "<root>"),
|
|
48
|
-
});
|
|
49
|
-
const NO_TOKENS = { OSF_TOKEN: "", GITHUB_TOKEN: "" };
|
|
50
|
-
const run = (name, args = [], env = {}) =>
|
|
51
|
-
clean(
|
|
52
|
-
runNode(SCRIPT, [join(root, name), `--today=${TODAY}`, ...args], {
|
|
53
|
-
env: { ...NO_TOKENS, ...env },
|
|
54
|
-
}),
|
|
55
|
-
);
|
|
56
|
-
const json = (name, env) => {
|
|
57
|
-
const r = run(name, ["--json"], env);
|
|
58
|
-
assert.equal(r.status, 0, r.stderr);
|
|
59
|
-
return JSON.parse(r.stdout);
|
|
60
|
-
};
|
|
61
|
-
const card = (over = {}) => scorecard(over, TODAY);
|
|
62
|
-
const NEXT_CLEAN =
|
|
63
|
-
" ➡️ NEXT: no mechanical blocker — run paper-status for the measured picture (build page count, stale gates, owner-split) before deciding it is done";
|
|
64
|
-
const PAGE_NOTE =
|
|
65
|
-
" (page count comes from repro/build-submission.sh, never from this line)";
|
|
66
|
-
|
|
67
|
-
test("no scorecard: nothing to say, exit 0", () => {
|
|
68
|
-
paper("none", null);
|
|
69
|
-
assert.deepEqual(run("none"), { status: 0, stdout: "", stderr: "" });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("a clean scorecard: the banner, with the deadline counted down and no finding", () => {
|
|
73
|
-
paper("clean", card({ deadline: "2026-09-11" }));
|
|
74
|
-
assert.deepEqual(run("clean"), {
|
|
75
|
-
status: 0,
|
|
76
|
-
stderr: "",
|
|
77
|
-
stdout: [
|
|
78
|
-
"📊 clean — 2026-09-11 (T−10d) · 0 open finding(s)",
|
|
79
|
-
" verdict row: Submit-ready — every gate green, artifact reproduces clean, nothing blocking.",
|
|
80
|
-
NEXT_CLEAN,
|
|
81
|
-
PAGE_NOTE,
|
|
82
|
-
"",
|
|
83
|
-
].join("\n"),
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("findings: a passed deadline counts up, a missing verdict says so, every finding is listed", () => {
|
|
88
|
-
paper(
|
|
89
|
-
"late",
|
|
90
|
-
card({
|
|
91
|
-
deadline: "2026-08-20",
|
|
92
|
-
access: { status: "☐" },
|
|
93
|
-
cites: { date: "2026-07-15" },
|
|
94
|
-
}).replace(/^\*\*Readiness verdict:\*\*.*$/m, ""),
|
|
95
|
-
);
|
|
96
|
-
const stale =
|
|
97
|
-
"cites (skill) last ran 2026-07-15, the text changed 2026-08-01 — this ☑ is stale, not green";
|
|
98
|
-
const access =
|
|
99
|
-
"access (can you physically submit?) is ☐ with -12 day(s) to the deadline — profile moderation runs up to two weeks and there is no expedite route without an institutional email";
|
|
100
|
-
assert.deepEqual(run("late"), {
|
|
101
|
-
status: 0,
|
|
102
|
-
stderr: "",
|
|
103
|
-
stdout: [
|
|
104
|
-
"📊 late — 2026-08-20 (T+12d) · 2 open finding(s)",
|
|
105
|
-
" verdict row: (none)",
|
|
106
|
-
` ➡️ NEXT: ${stale}`,
|
|
107
|
-
PAGE_NOTE,
|
|
108
|
-
"⚠️ pipeline-check — 2 finding(s) in <root>/late/PIPELINE-STATUS.md:",
|
|
109
|
-
` [stale-continuous] ${stale}`,
|
|
110
|
-
` [submit-access] ${access}`,
|
|
111
|
-
"",
|
|
112
|
-
].join("\n"),
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("no deadline in the header: the banner says so, and open access is flagged without a count", () => {
|
|
117
|
-
paper(
|
|
118
|
-
"undated",
|
|
119
|
-
card({ access: { status: "" } }).replace(/ · Deadline: \S+/, ""),
|
|
120
|
-
);
|
|
121
|
-
const r = run("undated");
|
|
122
|
-
assert.equal(r.status, 0);
|
|
123
|
-
assert.match(
|
|
124
|
-
r.stdout,
|
|
125
|
-
/^📊 undated — deadline not in the header · 1 open finding\(s\)$/m,
|
|
126
|
-
);
|
|
127
|
-
assert.match(
|
|
128
|
-
r.stdout,
|
|
129
|
-
/\[submit-access\] access \(can you physically submit\?\) is blank — turn it green in the first week, not the last$/m,
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("open access far from the deadline is not a finding yet", () => {
|
|
134
|
-
paper("early", card({ access: { status: "◐" } }));
|
|
135
|
-
assert.deepEqual(json("early"), []);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("open access inside the moderation window is flagged even when the box is blank", () => {
|
|
139
|
-
paper("blank-near", card({ deadline: "2026-09-11", access: { status: "" } }));
|
|
140
|
-
assert.deepEqual(
|
|
141
|
-
json("blank-near").map((f) => f.msg),
|
|
142
|
-
[
|
|
143
|
-
"access (can you physically submit?) is blank with 10 day(s) to the deadline — profile moderation runs up to two weeks and there is no expedite route without an institutional email",
|
|
144
|
-
],
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("a continuous check that is not ☑, or ☑ without a date, is not called stale here", () => {
|
|
149
|
-
// The undated ☑ is `pipeline/undated-continuous`'s finding, not this checker's.
|
|
150
|
-
paper(
|
|
151
|
-
"not-done",
|
|
152
|
-
card({ cites: { date: "" }, render: { status: "◐", date: "2020-01-01" } }),
|
|
153
|
-
);
|
|
154
|
-
assert.deepEqual(json("not-done"), []);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("check() on its own: no sections and no directory is no finding", () => {
|
|
158
|
-
assert.deepEqual(check({ sections: {}, header: "" }, {}), []);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("--json prints the findings and nothing else", () => {
|
|
162
|
-
paper("js", card({ render: { date: "2026-07-20" } }));
|
|
163
|
-
assert.deepEqual(json("js"), [
|
|
164
|
-
{
|
|
165
|
-
kind: "stale-continuous",
|
|
166
|
-
msg: "render (skill) last ran 2026-07-20, the text changed 2026-08-01 — this ☑ is stale, not green",
|
|
167
|
-
},
|
|
168
|
-
]);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("without arguments it reads the current directory and today's date", () => {
|
|
172
|
-
const dir = paper("cwd", card());
|
|
173
|
-
const r = clean(runNode(SCRIPT, ["--json"], { cwd: dir, env: NO_TOKENS }));
|
|
174
|
-
assert.deepEqual(r, { status: 0, stdout: "[]\n", stderr: "" });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("a misspelled current template: the warning names the sections and headings it did find", () => {
|
|
178
|
-
paper("typo", card().replace("### LOOP", "### LOPO"));
|
|
179
|
-
const r = run("typo");
|
|
180
|
-
assert.equal(r.status, 0);
|
|
181
|
-
assert.equal(r.stdout, "");
|
|
182
|
-
assert.equal(
|
|
183
|
-
r.stderr,
|
|
184
|
-
[
|
|
185
|
-
"⚠️ <root>/typo/PIPELINE-STATUS.md: expected the four sections SETUP/LOOP/CONTINUOUS/GATES, found [SETUP, CONTINUOUS, GATES].",
|
|
186
|
-
' Parsed 4 table(s), 4 of them with an id column; headings: "PIPELINE-STATUS — harness fixture", "SETUP", "LOPO", "CONTINUOUS", "GATES".',
|
|
187
|
-
" The template is a format contract — see paper-pipeline/references/pipeline-status-template.md.",
|
|
188
|
-
" Until it parses, these checks are silent, which is the failure mode they exist to prevent.",
|
|
189
|
-
"",
|
|
190
|
-
].join("\n"),
|
|
191
|
-
);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("the retired single-table format is named as such, not as a typo", () => {
|
|
195
|
-
paper(
|
|
196
|
-
"legacy",
|
|
197
|
-
"Deadline: 2026-08-06\n\n| # | Gate | Status |\n|---|------|--------|\n| 1 | cites | ☑ |\n",
|
|
198
|
-
);
|
|
199
|
-
const r = run("legacy");
|
|
200
|
-
assert.equal(r.status, 0);
|
|
201
|
-
assert.match(r.stderr, /found \[none\]\.\n/);
|
|
202
|
-
assert.match(
|
|
203
|
-
r.stderr,
|
|
204
|
-
/Parsed 1 table\(s\), 0 of them with an id column; headings: none\.\n/,
|
|
205
|
-
);
|
|
206
|
-
assert.match(
|
|
207
|
-
r.stderr,
|
|
208
|
-
/this is the pre-2026-08 single-table format \(first column `#`, a row number\)/,
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("a scorecard with no table at all is not called legacy", () => {
|
|
213
|
-
paper("prose", "# Status\n\nNothing tabulated yet.\n");
|
|
214
|
-
const r = run("prose");
|
|
215
|
-
assert.match(
|
|
216
|
-
r.stderr,
|
|
217
|
-
/Parsed 0 table\(s\), 0 of them with an id column; headings: "Status"\.\n/,
|
|
218
|
-
);
|
|
219
|
-
assert.doesNotMatch(r.stderr, /single-table format/);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test("reviews/: a worst section named twice is a repeat finding; no cold read is a finding", () => {
|
|
223
|
-
paper("reviews", card(), {
|
|
224
|
-
files: {
|
|
225
|
-
"reviews/2026-08-10-writing.md": "WORST-SECTION: Abstract\n",
|
|
226
|
-
"reviews/2026-08-12-grade-paper-writing.md":
|
|
227
|
-
" WORST-SECTION: abstract \n",
|
|
228
|
-
// Unmarked: skipped rather than guessed at, so it neither makes nor breaks the repeat.
|
|
229
|
-
"reviews/2026-08-14-writing.md": "no marker here\n",
|
|
230
|
-
"reviews/2026-08-15-writing.txt": "WORST-SECTION: intro\n",
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
assert.deepEqual(
|
|
234
|
-
json("reviews").map((f) => f.kind),
|
|
235
|
-
["repeat-finding", "no-cold-read"],
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("reviews/: two different worst sections are not a repeat", () => {
|
|
240
|
-
paper("mixed", card(), {
|
|
241
|
-
files: {
|
|
242
|
-
"reviews/a-writing.md": "WORST-SECTION: abstract\n",
|
|
243
|
-
"reviews/b-writing.md": "WORST-SECTION: intro\n",
|
|
244
|
-
"reviews/c-coldread.md": "read\n",
|
|
245
|
-
},
|
|
246
|
-
mtimes: { "reviews/c-coldread.md": "2026-08-20" },
|
|
247
|
-
});
|
|
248
|
-
assert.deepEqual(json("mixed"), []);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("reviews/ as a file, and a report path that is a directory, are unreadable — not a crash", () => {
|
|
252
|
-
paper("reviews-file", card(), { files: { reviews: "not a folder\n" } });
|
|
253
|
-
assert.deepEqual(
|
|
254
|
-
json("reviews-file").map((f) => f.kind),
|
|
255
|
-
["no-cold-read"],
|
|
256
|
-
);
|
|
257
|
-
paper("reviews-dir", card(), {
|
|
258
|
-
files: {
|
|
259
|
-
"reviews/a-writing.md/inner.txt": "x",
|
|
260
|
-
"reviews/b-writing.md": "WORST-SECTION: abstract\n",
|
|
261
|
-
"reviews/c-coldread.md": "read\n",
|
|
262
|
-
},
|
|
263
|
-
mtimes: { "reviews/c-coldread.md": "2026-08-20" },
|
|
264
|
-
});
|
|
265
|
-
assert.deepEqual(json("reviews-dir"), []);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("reviews/: a cold read older than the text is stale, under either spelling", () => {
|
|
269
|
-
paper("stale-cr", card(), {
|
|
270
|
-
files: { "reviews/2026-07-01-cold-read.md": "read\n" },
|
|
271
|
-
mtimes: { "reviews/2026-07-01-cold-read.md": "2026-07-01" },
|
|
272
|
-
});
|
|
273
|
-
assert.deepEqual(json("stale-cr"), [
|
|
274
|
-
{
|
|
275
|
-
kind: "stale-cold-read",
|
|
276
|
-
msg: "the cold read ran 2026-07-01, the text changed 2026-08-01 — it describes prose that no longer exists",
|
|
277
|
-
},
|
|
278
|
-
]);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("a credential in the env un-parks the blocker it clears — unless the row already shows it done", () => {
|
|
282
|
-
paper(
|
|
283
|
-
"creds",
|
|
284
|
-
card({
|
|
285
|
-
harden: { status: "◐", result: "github release pending" },
|
|
286
|
-
panel: { status: "◐", result: "artifact hosted at osf.io/abc12" },
|
|
287
|
-
}),
|
|
288
|
-
);
|
|
289
|
-
assert.deepEqual(json("creds", { GITHUB_TOKEN: "t", OSF_TOKEN: "t" }), [
|
|
290
|
-
{
|
|
291
|
-
kind: "credential-available",
|
|
292
|
-
msg: "harden (repository work) is parked as open, but GITHUB_TOKEN is set in this environment — do it over the API yourself. Do not report this as waiting on the human.",
|
|
293
|
-
},
|
|
294
|
-
]);
|
|
295
|
-
assert.deepEqual(json("creds"), []);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test("the text's date ignores dot-dirs, node_modules, repro, build outputs and anything past depth 3", () => {
|
|
299
|
-
const late = "2026-09-20"; // after every row date: walked, any of these would make the gates stale
|
|
300
|
-
const files = {
|
|
301
|
-
".cache/paper.md": "x",
|
|
302
|
-
// eslint-disable-next-line port/js-install-path -- a fixture directory the walk must skip, not an install location
|
|
303
|
-
"node_modules/pkg/paper.md": "x",
|
|
304
|
-
"repro/main.tex": "x",
|
|
305
|
-
"build/main.tex": "x",
|
|
306
|
-
"a/b/c/d/deep.tex": "x",
|
|
307
|
-
};
|
|
308
|
-
paper("walk", card(), {
|
|
309
|
-
files,
|
|
310
|
-
mtimes: Object.fromEntries(Object.keys(files).map((f) => [f, late])),
|
|
311
|
-
});
|
|
312
|
-
assert.deepEqual(json("walk"), []);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test("a paper with no source text has no date, so nothing can be stale", () => {
|
|
316
|
-
const dir = paper("nosrc", card({ cites: { date: "2020-01-01" } }));
|
|
317
|
-
execFileSync("rm", [join(dir, "paper.md")]);
|
|
318
|
-
assert.deepEqual(json("nosrc"), []);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test("inside git: a committed file is dated by its commit, a dirty or ignored one by its mtime", () => {
|
|
322
|
-
// The paper is its own repository and the command runs from elsewhere (this package's repo):
|
|
323
|
-
// git has to be asked from where the file lives, or every paper outside the cwd's repository
|
|
324
|
-
// silently falls back to mtimes.
|
|
325
|
-
const dir = paper("repo", card({ cites: { date: "2026-08-20" } }), {
|
|
326
|
-
files: { "sec.tex": "a", "draft.md": "b", ".gitignore": "draft.md\n" },
|
|
327
|
-
mtimes: { "sec.tex": "2026-08-25", "draft.md": "2026-08-22" },
|
|
328
|
-
});
|
|
329
|
-
const git = (...a) =>
|
|
330
|
-
execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", ...a], {
|
|
331
|
-
cwd: dir,
|
|
332
|
-
env: {
|
|
333
|
-
...process.env,
|
|
334
|
-
GIT_COMMITTER_DATE: "2026-08-02T12:00:00Z",
|
|
335
|
-
GIT_AUTHOR_DATE: "2026-08-02T12:00:00Z",
|
|
336
|
-
},
|
|
337
|
-
stdio: "ignore",
|
|
338
|
-
});
|
|
339
|
-
git("init", "-q");
|
|
340
|
-
git("add", "paper.md", ".gitignore", "PIPELINE-STATUS.md");
|
|
341
|
-
git("commit", "-qm", "init");
|
|
342
|
-
// A checkout rewrites mtimes; the commit date is what the text's date is.
|
|
343
|
-
utimesSync(join(dir, "paper.md"), at("2026-09-20"), at("2026-09-20"));
|
|
344
|
-
const stale = (changed) => [
|
|
345
|
-
`cites (skill) last ran 2026-08-20, the text changed ${changed} — this ☑ is stale, not green`,
|
|
346
|
-
];
|
|
347
|
-
// sec.tex is untracked, i.e. dirty: its mtime (08-25) is the newest date.
|
|
348
|
-
assert.deepEqual(
|
|
349
|
-
json("repo").map((f) => f.msg),
|
|
350
|
-
stale("2026-08-25"),
|
|
351
|
-
);
|
|
352
|
-
execFileSync("rm", [join(dir, "sec.tex")]);
|
|
353
|
-
// paper.md by its commit (08-02, not its 09-20 mtime); draft.md is ignored — no status, no
|
|
354
|
-
// log — so by its mtime (08-22), which is now the newest.
|
|
355
|
-
assert.deepEqual(
|
|
356
|
-
json("repo").map((f) => f.msg),
|
|
357
|
-
stale("2026-08-22"),
|
|
358
|
-
);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test("parseStatus: a heading with no Latin word is no section; a short or id-less row is skipped", () => {
|
|
362
|
-
const parsed = parseStatus(
|
|
363
|
-
[
|
|
364
|
-
"## Итоги",
|
|
365
|
-
"| id | Work | Skill | Status |",
|
|
366
|
-
"|----|------|-------|--------|",
|
|
367
|
-
"| x | only | three |",
|
|
368
|
-
"| ** | a | b | ☑ |",
|
|
369
|
-
"",
|
|
370
|
-
"## SETUP",
|
|
371
|
-
"| id | Work | Skill | Status |",
|
|
372
|
-
"|----|------|-------|--------|",
|
|
373
|
-
"| idea | w | s | nothing recognisable |",
|
|
374
|
-
].join("\n"),
|
|
375
|
-
);
|
|
376
|
-
assert.deepEqual(parsed.sections, {
|
|
377
|
-
SETUP: [
|
|
378
|
-
{
|
|
379
|
-
id: "idea",
|
|
380
|
-
name: "w",
|
|
381
|
-
skill: "s",
|
|
382
|
-
status: "",
|
|
383
|
-
date: null,
|
|
384
|
-
raw: "idea | w | s | nothing recognisable",
|
|
385
|
-
section: "SETUP",
|
|
386
|
-
heading: "SETUP",
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
|
-
});
|
|
390
|
-
assert.deepEqual(parsed.shape, {
|
|
391
|
-
tables: 2,
|
|
392
|
-
scorecardTables: 2,
|
|
393
|
-
headings: ["Итоги", "SETUP"],
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test("newestSourceDate: a subdirectory that cannot be listed is skipped, the rest still dated", () => {
|
|
398
|
-
const dir = paper("unlistable", card(), {
|
|
399
|
-
files: { "locked/late.tex": "x" },
|
|
400
|
-
mtimes: { "locked/late.tex": "2026-09-20" },
|
|
401
|
-
});
|
|
402
|
-
const readdir = (d) => {
|
|
403
|
-
if (d.endsWith("locked"))
|
|
404
|
-
throw Object.assign(new Error("EACCES: permission denied"), {
|
|
405
|
-
code: "EACCES",
|
|
406
|
-
});
|
|
407
|
-
return readdirSync(d, { withFileTypes: true });
|
|
408
|
-
};
|
|
409
|
-
assert.deepEqual(
|
|
410
|
-
[newestSourceDate(dir, { readdir }), newestSourceDate(dir)],
|
|
411
|
-
[TEXT_DATE, "2026-09-20"],
|
|
412
|
-
);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
test("parseStatus reads a token stream that breaks markdown-it's shape without throwing", () => {
|
|
416
|
-
const T = (type, content) =>
|
|
417
|
-
content === undefined ? { type } : { type, content };
|
|
418
|
-
const tokens = [
|
|
419
|
-
// a heading whose inline token is missing
|
|
420
|
-
T("heading_open"),
|
|
421
|
-
T("paragraph_open"),
|
|
422
|
-
T("heading_open"),
|
|
423
|
-
T("inline", "SETUP"),
|
|
424
|
-
// a table with no thead, a cell before any row, a row closed that was never opened
|
|
425
|
-
T("table_open"),
|
|
426
|
-
T("td_open"),
|
|
427
|
-
T("inline", "stray"),
|
|
428
|
-
T("tr_close"),
|
|
429
|
-
T("table_close"),
|
|
430
|
-
// a scorecard table: a header cell with no inline, an empty row, then one real row — and no
|
|
431
|
-
// table_close at all, so the table runs to the end of the stream
|
|
432
|
-
T("table_open"),
|
|
433
|
-
T("thead_open"),
|
|
434
|
-
T("tr_open"),
|
|
435
|
-
T("th_open"),
|
|
436
|
-
T("inline", "id"),
|
|
437
|
-
T("th_open"),
|
|
438
|
-
T("th_close"),
|
|
439
|
-
T("tr_close"),
|
|
440
|
-
T("thead_close"),
|
|
441
|
-
T("tr_open"),
|
|
442
|
-
T("tr_close"),
|
|
443
|
-
T("tr_open"),
|
|
444
|
-
...["idea", "w", "s", "☑"].flatMap((c) => [T("td_open"), T("inline", c)]),
|
|
445
|
-
T("tr_close"),
|
|
446
|
-
];
|
|
447
|
-
const parsed = parseStatus("ignored", { parse: () => tokens });
|
|
448
|
-
assert.deepEqual(parsed.shape, {
|
|
449
|
-
tables: 2,
|
|
450
|
-
scorecardTables: 1,
|
|
451
|
-
headings: ["", "SETUP"],
|
|
452
|
-
});
|
|
453
|
-
assert.deepEqual(
|
|
454
|
-
parsed.sections.SETUP.map((r) => [r.id, r.name, r.skill, r.status]),
|
|
455
|
-
[["idea", "w", "s", "☑"]],
|
|
456
|
-
);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
test("check(): rows built by another caller without their raw text are read as empty", () => {
|
|
460
|
-
const saved = process.env.GITHUB_TOKEN;
|
|
461
|
-
process.env.GITHUB_TOKEN = "t";
|
|
462
|
-
try {
|
|
463
|
-
assert.deepEqual(
|
|
464
|
-
check(
|
|
465
|
-
{ sections: { GATES: [{ id: "g", status: "◐" }] }, header: "" },
|
|
466
|
-
{},
|
|
467
|
-
),
|
|
468
|
-
[],
|
|
469
|
-
);
|
|
470
|
-
} finally {
|
|
471
|
-
if (saved === undefined) delete process.env.GITHUB_TOKEN;
|
|
472
|
-
else process.env.GITHUB_TOKEN = saved;
|
|
473
|
-
}
|
|
474
|
-
});
|