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
package/src/hooks-settings.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
PACKAGE_NAME,
|
|
32
32
|
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
33
33
|
import { messageOf } from "./domain/text.ts";
|
|
34
|
+
import { fieldOf, isRecord } from "./domain/record.ts";
|
|
34
35
|
|
|
35
36
|
/** The committed, shared settings file — husky's `.husky/` analogue, except git carries it. */
|
|
36
37
|
export const SETTINGS_PATH = join(".claude", "settings.json");
|
|
@@ -151,6 +152,25 @@ const prop = (v: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
|
151
152
|
? Object.fromEntries(Object.entries(v))
|
|
152
153
|
: undefined;
|
|
153
154
|
|
|
155
|
+
const isHookCommand = (v: unknown): v is HookCommand =>
|
|
156
|
+
typeof fieldOf(v, "type") === "string" &&
|
|
157
|
+
typeof fieldOf(v, "command") === "string";
|
|
158
|
+
|
|
159
|
+
function isHookEntry(v: unknown): v is HookEntry {
|
|
160
|
+
const matcher = fieldOf(v, "matcher");
|
|
161
|
+
const hooks = fieldOf(v, "hooks");
|
|
162
|
+
return (
|
|
163
|
+
["undefined", "string"].includes(typeof matcher) &&
|
|
164
|
+
Array.isArray(hooks) &&
|
|
165
|
+
hooks.every(isHookCommand)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A `hooks` map of the shape Claude Code reads: event → entries → commands. */
|
|
170
|
+
const isHooksMap = (v: unknown): v is HooksMap =>
|
|
171
|
+
isRecord(v) &&
|
|
172
|
+
Object.values(v).every((es) => Array.isArray(es) && es.every(isHookEntry));
|
|
173
|
+
|
|
154
174
|
export interface Wiring {
|
|
155
175
|
readonly compiled: HooksMap;
|
|
156
176
|
/** Derived from the commands, in order. Never empty — an empty set throws. */
|
|
@@ -162,8 +182,12 @@ export interface Wiring {
|
|
|
162
182
|
* check that silently wired nothing would report "✓ wired" over an empty set.
|
|
163
183
|
*/
|
|
164
184
|
export function shippedWiring(file: string = WIRING_FILE): Wiring {
|
|
165
|
-
const json = JSON.parse(readFileSync(file, "utf8"))
|
|
166
|
-
const compiled = json
|
|
185
|
+
const json: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
186
|
+
const compiled = fieldOf(json, "hooks") ?? {};
|
|
187
|
+
if (!isHooksMap(compiled))
|
|
188
|
+
throw new Error(
|
|
189
|
+
`${file}: "hooks" is not a map of event → [{ matcher?, hooks: [{ type, command }] }]`,
|
|
190
|
+
);
|
|
167
191
|
const names = commandsIn({ hooks: compiled })
|
|
168
192
|
.map((c) => hookRun(c.command)?.name)
|
|
169
193
|
.filter((n): n is string => typeof n === "string");
|
|
@@ -225,9 +249,9 @@ export function readSettings(root: string): SettingsRead {
|
|
|
225
249
|
const raw = readFileSync(path, "utf8");
|
|
226
250
|
try {
|
|
227
251
|
const parsed: unknown = JSON.parse(raw);
|
|
228
|
-
if (!
|
|
252
|
+
if (!isRecord(parsed))
|
|
229
253
|
return { status: "unparsable", path, reason: "not a JSON object" };
|
|
230
|
-
return { status: "read", path, settings: parsed
|
|
254
|
+
return { status: "read", path, settings: parsed, raw };
|
|
231
255
|
} catch (e) {
|
|
232
256
|
return { status: "unparsable", path, reason: messageOf(e) };
|
|
233
257
|
}
|
package/src/init.test.ts
CHANGED
|
@@ -70,7 +70,7 @@ describe("paperlint init — a declared papers directory", () => {
|
|
|
70
70
|
err: (s: string) => out.push(s),
|
|
71
71
|
interactive: false,
|
|
72
72
|
hooks: false,
|
|
73
|
-
run: (
|
|
73
|
+
run: () => ({ status: 0 }),
|
|
74
74
|
link: () => ({ ok: false, error: "not linked in this test" }),
|
|
75
75
|
});
|
|
76
76
|
const text = out.join("\n");
|
|
@@ -97,7 +97,7 @@ describe("paperlint init — a declared papers directory", () => {
|
|
|
97
97
|
interactive: true,
|
|
98
98
|
ask: (q) => Promise.resolve((asked.push(q), "n")),
|
|
99
99
|
hooks: false,
|
|
100
|
-
run: (
|
|
100
|
+
run: () => ({ status: 0 }),
|
|
101
101
|
link: () => ({ ok: false, error: "not linked in this test" }),
|
|
102
102
|
});
|
|
103
103
|
expect(asked.filter((q) => /papers roots/.test(q))).toEqual([]);
|
|
@@ -116,7 +116,7 @@ describe("paperlint init — writes paperlint.json only when something differs f
|
|
|
116
116
|
err: () => {},
|
|
117
117
|
interactive: false,
|
|
118
118
|
hooks: false,
|
|
119
|
-
run: (
|
|
119
|
+
run: () => ({ status: 0 }),
|
|
120
120
|
link: () => ({ ok: false as const, error: "not linked in this test" }),
|
|
121
121
|
};
|
|
122
122
|
const fresh = (files: Record<string, string>): string => {
|
|
@@ -170,7 +170,7 @@ const texRun = async (
|
|
|
170
170
|
ask: (q) =>
|
|
171
171
|
Promise.resolve((asked.push(q), /TeX Live/.test(q) ? answer : "")),
|
|
172
172
|
hooks: false,
|
|
173
|
-
run: (
|
|
173
|
+
run: () => ({ status: 0 }),
|
|
174
174
|
link: () => ({ ok: false as const, error: "not linked in this test" }),
|
|
175
175
|
tex,
|
|
176
176
|
});
|
|
@@ -249,7 +249,7 @@ async function initRun(
|
|
|
249
249
|
err: (s: string) => err.push(s),
|
|
250
250
|
interactive: false,
|
|
251
251
|
hooks: false,
|
|
252
|
-
run: (
|
|
252
|
+
run: () => ({ status: 0 }),
|
|
253
253
|
link: () => ({ ok: false, error: "not linked in this test" }),
|
|
254
254
|
...opts,
|
|
255
255
|
});
|
|
@@ -280,6 +280,26 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
|
|
|
280
280
|
]);
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
+
it("🔴 a paperlint.json holding JSON that is not an object is refused, not read as settings", async () => {
|
|
284
|
+
// Guards: `null` threw a TypeError reading the papers field; `[1]` was spread into a settings
|
|
285
|
+
// object and the consumer's file rewritten as `{ "0": 1, "papersDir": … }`.
|
|
286
|
+
for (const json of ["null\n", "[1]\n"]) {
|
|
287
|
+
const root = tree({
|
|
288
|
+
"paperlint.json": json,
|
|
289
|
+
"drafts/a/paper.md": "# a\n",
|
|
290
|
+
});
|
|
291
|
+
const decl = await declarePapers(root, () =>
|
|
292
|
+
choosePapers(root, { interactive: false }),
|
|
293
|
+
);
|
|
294
|
+
expect(decl).toEqual({
|
|
295
|
+
status: "unparsable",
|
|
296
|
+
path: join(root, "paperlint.json"),
|
|
297
|
+
reason: "not a JSON object",
|
|
298
|
+
});
|
|
299
|
+
expect(readFileSync(join(root, "paperlint.json"), "utf8")).toBe(json);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
283
303
|
it("a file without a trailing newline keeps having none", async () => {
|
|
284
304
|
const root = tree({
|
|
285
305
|
"paperlint.json": '{"x":1}',
|
package/src/init.ts
CHANGED
|
@@ -46,7 +46,13 @@ import {
|
|
|
46
46
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
47
47
|
import { spawnSync } from "node:child_process";
|
|
48
48
|
import { dirname, join, relative, resolve } from "node:path";
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
doctor,
|
|
51
|
+
detectPapers,
|
|
52
|
+
found,
|
|
53
|
+
PROGRAMS,
|
|
54
|
+
type ProgramProbe,
|
|
55
|
+
} from "./doctor.ts";
|
|
50
56
|
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
51
57
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
52
58
|
import { actionRef } from "./action-ref.ts";
|
|
@@ -73,6 +79,7 @@ import {
|
|
|
73
79
|
PAPERS_DIR_FIELD,
|
|
74
80
|
} from "#lib/paper-config";
|
|
75
81
|
import { messageOf } from "./domain/text.ts";
|
|
82
|
+
import { isRecord } from "./domain/record.ts";
|
|
76
83
|
|
|
77
84
|
/** How the papers directory was arrived at. Printed, because a guess must not read as a fact. */
|
|
78
85
|
export type PapersHow =
|
|
@@ -227,13 +234,17 @@ export async function declarePapers(
|
|
|
227
234
|
): Promise<DeclarationResult> {
|
|
228
235
|
const path = join(root, CONFIG_FILE);
|
|
229
236
|
const raw = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
230
|
-
let settings: Record<string, unknown
|
|
237
|
+
let settings: Readonly<Record<string, unknown>> = {};
|
|
231
238
|
if (raw !== null) {
|
|
239
|
+
let parsed: unknown;
|
|
232
240
|
try {
|
|
233
|
-
|
|
241
|
+
parsed = JSON.parse(raw);
|
|
234
242
|
} catch (e) {
|
|
235
243
|
return { status: "unparsable", path, reason: messageOf(e) };
|
|
236
244
|
}
|
|
245
|
+
if (!isRecord(parsed))
|
|
246
|
+
return { status: "unparsable", path, reason: "not a JSON object" };
|
|
247
|
+
settings = parsed;
|
|
237
248
|
const existing = settings[PAPERS_DIR_FIELD];
|
|
238
249
|
if (existing !== undefined)
|
|
239
250
|
return { status: "kept", path, papers: existing };
|
|
@@ -362,7 +373,7 @@ export function reportWorkflow(
|
|
|
362
373
|
* passing checker produce the same silence.
|
|
363
374
|
*/
|
|
364
375
|
export function missingPrograms(
|
|
365
|
-
run = spawnSync,
|
|
376
|
+
run: ProgramProbe = spawnSync,
|
|
366
377
|
): readonly (typeof PROGRAMS)[number][] {
|
|
367
378
|
return PROGRAMS.filter((p) => !found(p.bin, run));
|
|
368
379
|
}
|
|
@@ -584,7 +595,7 @@ export interface InitOptions {
|
|
|
584
595
|
name: string,
|
|
585
596
|
format: PaperFormat,
|
|
586
597
|
) => Promise<number>;
|
|
587
|
-
run?:
|
|
598
|
+
run?: ProgramProbe;
|
|
588
599
|
/**
|
|
589
600
|
* What `paperlint lint` would resolve from the declaration, asked of the CLI's OWN reader. A second
|
|
590
601
|
* implementation here would be a second source of truth — the very defect `doctor` reports.
|
|
@@ -749,21 +760,28 @@ export async function init(
|
|
|
749
760
|
for (const line of papersLines(decl, why)) log(line);
|
|
750
761
|
log(``);
|
|
751
762
|
log(`settings`);
|
|
752
|
-
|
|
763
|
+
// Every step below uses the DECLARED directory. A kept declaration outranks what init
|
|
764
|
+
// measured or guessed: otherwise the first paper and the workflow would land in the
|
|
765
|
+
// guessed directory while lint and the hooks keep reading the declared one.
|
|
766
|
+
let papersDir: string;
|
|
767
|
+
if (decl.status === "written") {
|
|
768
|
+
papersDir = decl.papers;
|
|
753
769
|
log(
|
|
754
770
|
` ✓ ${here(decl.path)} → { "${PAPERS_DIR_FIELD}": ${JSON.stringify(decl.papers)} }`,
|
|
755
771
|
);
|
|
756
|
-
else if (decl.status === "default")
|
|
772
|
+
} else if (decl.status === "default") {
|
|
773
|
+
papersDir = decl.papers;
|
|
757
774
|
log(
|
|
758
775
|
` ✓ nothing to write — "${decl.papers}" is the default, so no ${CONFIG_FILE} is needed`,
|
|
759
776
|
);
|
|
760
|
-
else if (decl.status === "kept") {
|
|
777
|
+
} else if (decl.status === "kept") {
|
|
761
778
|
if (typeof decl.papers !== "string") {
|
|
762
779
|
err(
|
|
763
780
|
` ✗ ${here(decl.path)} declares ${PAPERS_DIR_FIELD} = ${JSON.stringify(decl.papers)} — it must be a directory path (a string)`,
|
|
764
781
|
);
|
|
765
782
|
return 2;
|
|
766
783
|
}
|
|
784
|
+
papersDir = decl.papers;
|
|
767
785
|
log(
|
|
768
786
|
` ✓ ${here(decl.path)} already declares ${PAPERS_DIR_FIELD} = ${JSON.stringify(decl.papers)} — kept, nothing overwritten`,
|
|
769
787
|
);
|
|
@@ -777,10 +795,6 @@ export async function init(
|
|
|
777
795
|
);
|
|
778
796
|
return 2;
|
|
779
797
|
}
|
|
780
|
-
// Every step below uses the DECLARED directory. A kept declaration outranks what init
|
|
781
|
-
// measured or guessed: otherwise the first paper and the workflow would land in the
|
|
782
|
-
// guessed directory while lint and the hooks keep reading the declared one.
|
|
783
|
-
const papersDir = decl.papers as string;
|
|
784
798
|
|
|
785
799
|
// ── 3. the skills, linked where Claude Code looks for them ─────────────────────────────
|
|
786
800
|
for (const line of reportSkillLinks(link(root), here)) log(line);
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* from a missing file into a created one;
|
|
8
8
|
* - the scorecard carries `researchQuestion` and NO `stages` — a new paper has shipped nothing,
|
|
9
9
|
* and a placeholder stage would be a red `paper/stages` on the very first run;
|
|
10
|
-
* - the scorecard
|
|
11
|
-
*
|
|
12
|
-
* skills' checker reads;
|
|
10
|
+
* - the scorecard carries the `**Readiness verdict:**` line the status hook surfaces on every
|
|
11
|
+
* paper edit — without it the hook reports that the paper cannot say whether it is ready;
|
|
13
12
|
* - it never overwrites, and on an existing folder adds only what is missing;
|
|
14
13
|
* - the project's `<papers>/.template/` wins over the package, file by file.
|
|
15
14
|
*
|
|
@@ -25,7 +24,6 @@ import {
|
|
|
25
24
|
rmSync,
|
|
26
25
|
writeFileSync,
|
|
27
26
|
} from "node:fs";
|
|
28
|
-
import { spawnSync } from "node:child_process";
|
|
29
27
|
import { tmpdir } from "node:os";
|
|
30
28
|
import { dirname, join } from "node:path";
|
|
31
29
|
import { fileURLToPath } from "node:url";
|
|
@@ -124,26 +122,12 @@ try {
|
|
|
124
122
|
/1 problem \(0 errors, 1 warning\)/.test(l.text),
|
|
125
123
|
);
|
|
126
124
|
|
|
127
|
-
//
|
|
128
|
-
const pc = spawnSync(
|
|
129
|
-
process.execPath,
|
|
130
|
-
[
|
|
131
|
-
join(ROOT, "skills", "paper-pipeline", "scripts", "pipeline-check.mjs"),
|
|
132
|
-
join(papers, "demo"),
|
|
133
|
-
"--json",
|
|
134
|
-
"--today=2026-09-23",
|
|
135
|
-
],
|
|
136
|
-
{ cwd: work, encoding: "utf8" },
|
|
137
|
-
);
|
|
138
|
-
let parsed = null;
|
|
139
|
-
try {
|
|
140
|
-
parsed = JSON.parse(pc.stdout);
|
|
141
|
-
} catch {
|
|
142
|
-
parsed = null;
|
|
143
|
-
}
|
|
125
|
+
// What the status hook reads on every paper edit: the verdict line.
|
|
144
126
|
check(
|
|
145
|
-
"the scorecard
|
|
146
|
-
|
|
127
|
+
"the scorecard carries the `**Readiness verdict:**` line the status hook surfaces",
|
|
128
|
+
/^\*\*Readiness verdict:\*\*/m.test(
|
|
129
|
+
readFileSync(join(papers, "demo", STATUS_FILE), "utf8"),
|
|
130
|
+
),
|
|
147
131
|
);
|
|
148
132
|
|
|
149
133
|
// ── never overwrite; on an existing folder only the missing files ─────────────────────
|
package/src/new-paper.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
28
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
29
|
+
import { isRecord } from "./domain/record.ts";
|
|
29
30
|
|
|
30
31
|
export type PaperFormat = "tex" | "md";
|
|
31
32
|
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
@@ -115,15 +116,18 @@ function withVenue(
|
|
|
115
116
|
src: string,
|
|
116
117
|
venue: VenueSetting,
|
|
117
118
|
): { ok: true; text: string } | { ok: false; reason: string } {
|
|
118
|
-
let obj:
|
|
119
|
+
let obj: unknown;
|
|
119
120
|
try {
|
|
120
|
-
obj = JSON.parse(text)
|
|
121
|
+
obj = JSON.parse(text);
|
|
121
122
|
} catch {
|
|
123
|
+
obj = null;
|
|
124
|
+
}
|
|
125
|
+
// Not JSON, or JSON that is not an object: there is no field to write "extends" into.
|
|
126
|
+
if (!isRecord(obj))
|
|
122
127
|
return {
|
|
123
128
|
ok: false,
|
|
124
129
|
reason: `--venue cannot be written into ${src}: it is not plain JSON — set "extends" by hand`,
|
|
125
130
|
};
|
|
126
|
-
}
|
|
127
131
|
const out: Record<string, unknown> = { ...obj, extends: venue.extends };
|
|
128
132
|
delete out["$comment"];
|
|
129
133
|
if (venue.kind !== null) out["kind"] = venue.kind;
|
package/src/paper-files.test.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 74 errors — parse errors and "Definition for rule … was not found" — none of them on a paper
|
|
9
9
|
* file, and the run failed. Nothing in the settings could turn that off.
|
|
10
10
|
*/
|
|
11
|
-
import { ESLint
|
|
11
|
+
import { ESLint } from "eslint";
|
|
12
12
|
import {
|
|
13
13
|
mkdirSync,
|
|
14
14
|
mkdtempSync,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { afterEach, describe, expect, it } from "vitest";
|
|
22
|
-
import { buildConfig, run } from "./cli.ts";
|
|
22
|
+
import { buildConfig, eslintConfig, run } from "./cli.ts";
|
|
23
23
|
import {
|
|
24
24
|
narrowToOwners,
|
|
25
25
|
ownedPatterns,
|
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
scopeToOwned,
|
|
29
29
|
} from "./paper-files.ts";
|
|
30
30
|
import { lintReport } from "../test/lint-report.ts";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
|
|
33
|
+
/** What `calculateConfigForFile` answers for a file inside the config's scope, read for its rules. */
|
|
34
|
+
const Computed = z.object({ rules: z.record(z.string(), z.unknown()) });
|
|
31
35
|
|
|
32
36
|
const dirs: string[] = [];
|
|
33
37
|
afterEach(() => {
|
|
@@ -232,25 +236,24 @@ describe("`rules` blocks — where the rule lands", () => {
|
|
|
232
236
|
const eslint = new ESLint({
|
|
233
237
|
cwd: root,
|
|
234
238
|
overrideConfigFile: true,
|
|
235
|
-
overrideConfig:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
239
|
+
overrideConfig: eslintConfig(
|
|
240
|
+
buildConfig(
|
|
241
|
+
{
|
|
242
|
+
rules: [
|
|
243
|
+
{
|
|
244
|
+
basePath: root,
|
|
245
|
+
files: ["papers/**"],
|
|
246
|
+
rules: { "paper/source": "warn", "paper/section-word": "off" },
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
null,
|
|
251
|
+
),
|
|
252
|
+
),
|
|
247
253
|
});
|
|
248
254
|
const on = async (file: string) =>
|
|
249
|
-
(
|
|
250
|
-
|
|
251
|
-
rules: Record<string, unknown>;
|
|
252
|
-
}
|
|
253
|
-
).rules;
|
|
255
|
+
Computed.parse(await eslint.calculateConfigForFile(join(root, file)))
|
|
256
|
+
.rules;
|
|
254
257
|
const status = await on("papers/a/PIPELINE-STATUS.md");
|
|
255
258
|
expect(status["paper/source"]).toEqual([1]);
|
|
256
259
|
expect(status["paper/section-word"]).toBeUndefined();
|
package/src/paper-files.ts
CHANGED
|
@@ -40,16 +40,7 @@
|
|
|
40
40
|
* - later global-ignore patterns win, across blocks, in config order;
|
|
41
41
|
* - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
|
|
42
42
|
*/
|
|
43
|
-
import type { RuleBlock, RuleEntry } from "./rules-config.ts";
|
|
44
|
-
|
|
45
|
-
/** What this module reads of a config block. */
|
|
46
|
-
interface Block {
|
|
47
|
-
readonly files?: readonly unknown[];
|
|
48
|
-
readonly ignores?: readonly unknown[];
|
|
49
|
-
readonly plugins?: Readonly<
|
|
50
|
-
Record<string, { readonly rules?: Readonly<Record<string, unknown>> }>
|
|
51
|
-
>;
|
|
52
|
-
}
|
|
43
|
+
import type { ConfigBlock, RuleBlock, RuleEntry } from "./rules-config.ts";
|
|
53
44
|
|
|
54
45
|
/**
|
|
55
46
|
* A set of files paperlint lints: an own block's globs and the exceptions that block makes. A file is
|
|
@@ -99,20 +90,18 @@ const merged = (scopes: readonly OwnedScope[]): OwnedScope[] => {
|
|
|
99
90
|
* THE FILES PAPERLINT LINTS: every own block that names `files`, with its `ignores`. A block without
|
|
100
91
|
* `files` (a global ignore, or the block registering `pdf` for every file) claims nothing.
|
|
101
92
|
*/
|
|
102
|
-
export function ownedScopes(own: readonly
|
|
93
|
+
export function ownedScopes(own: readonly ConfigBlock[]): OwnedScope[] {
|
|
103
94
|
return merged(
|
|
104
|
-
own
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
b.files
|
|
108
|
-
|
|
109
|
-
: [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
|
|
110
|
-
),
|
|
95
|
+
own.flatMap((b) =>
|
|
96
|
+
b.files === undefined
|
|
97
|
+
? []
|
|
98
|
+
: [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
|
|
99
|
+
),
|
|
111
100
|
);
|
|
112
101
|
}
|
|
113
102
|
|
|
114
103
|
/** Every glob paperlint's own blocks lint, once, in config order. */
|
|
115
|
-
export function ownedPatterns(own: readonly
|
|
104
|
+
export function ownedPatterns(own: readonly ConfigBlock[]): string[] {
|
|
116
105
|
return [...new Set(ownedScopes(own).flatMap((s) => s.files))];
|
|
117
106
|
}
|
|
118
107
|
|
|
@@ -134,7 +123,7 @@ export function scopeToOwned(owned: readonly string[]): {
|
|
|
134
123
|
}
|
|
135
124
|
|
|
136
125
|
/** Every `<plugin>/<rule>` a block registers. */
|
|
137
|
-
const ruleIdsOf = (b:
|
|
126
|
+
const ruleIdsOf = (b: ConfigBlock): string[] =>
|
|
138
127
|
Object.entries(b.plugins ?? {}).flatMap(([name, plugin]) =>
|
|
139
128
|
Object.keys(plugin.rules ?? {}).map((rule) => `${name}/${rule}`),
|
|
140
129
|
);
|
|
@@ -145,12 +134,11 @@ const ruleIdsOf = (b: Block): string[] =>
|
|
|
145
134
|
* one file more.
|
|
146
135
|
*/
|
|
147
136
|
export function ruleOwners(
|
|
148
|
-
own: readonly
|
|
137
|
+
own: readonly ConfigBlock[],
|
|
149
138
|
): Map<string, readonly OwnedScope[]> {
|
|
150
139
|
const all = ownedScopes(own);
|
|
151
140
|
const found = new Map<string, OwnedScope[]>();
|
|
152
|
-
for (const
|
|
153
|
-
const b = raw as Block;
|
|
141
|
+
for (const b of own) {
|
|
154
142
|
const scopes =
|
|
155
143
|
b.files === undefined
|
|
156
144
|
? all
|
|
@@ -171,17 +159,18 @@ export function narrowToOwners(
|
|
|
171
159
|
block: RuleBlock,
|
|
172
160
|
owners: ReadonlyMap<string, readonly OwnedScope[]>,
|
|
173
161
|
): ScopedBlock[] {
|
|
162
|
+
// Keyed by the owner's JSON, so rules with equal owners share one group of blocks.
|
|
174
163
|
const groups = new Map<string, Record<string, RuleEntry>>();
|
|
164
|
+
const ownerOf = new Map<string, readonly OwnedScope[]>();
|
|
175
165
|
for (const [id, entry] of Object.entries(block.rules)) {
|
|
176
166
|
const owner = owners.get(id);
|
|
177
167
|
if (owner === undefined) continue;
|
|
178
168
|
const key = JSON.stringify(owner);
|
|
179
169
|
groups.set(key, { ...groups.get(key), [id]: entry });
|
|
170
|
+
ownerOf.set(key, owner);
|
|
180
171
|
}
|
|
181
|
-
return [...
|
|
182
|
-
|
|
183
|
-
within(block, scope, rules),
|
|
184
|
-
),
|
|
172
|
+
return [...ownerOf].flatMap(([key, owner]) =>
|
|
173
|
+
owner.map((scope) => within(block, scope, { ...groups.get(key) })),
|
|
185
174
|
);
|
|
186
175
|
}
|
|
187
176
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
7
|
-
import
|
|
7
|
+
import { absolutePath } from "./domain/paths.ts";
|
|
8
8
|
import { findProjectRoot } from "../lib/paper-config.ts";
|
|
9
9
|
import {
|
|
10
10
|
paperRules,
|
|
@@ -215,7 +215,7 @@ describe("findProjectRoot — where the root paperlint.json is looked for", () =
|
|
|
215
215
|
[`${PAPER}/paperlint.json`]: '{"kind":"research"}',
|
|
216
216
|
[`${ROOT}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
217
217
|
});
|
|
218
|
-
const isFile = (p: string) => files.isFile(p
|
|
218
|
+
const isFile = (p: string) => files.isFile(absolutePath(p));
|
|
219
219
|
expect(findProjectRoot(PAPER, isFile)).toBe(ROOT);
|
|
220
220
|
// No root file anywhere: the package.json directory.
|
|
221
221
|
expect(findProjectRoot(PAPER, (p) => p === `${ROOT}/package.json`)).toBe(
|
package/src/paper-settings.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
SETTINGS_KEYS,
|
|
23
23
|
findProjectRoot,
|
|
24
24
|
} from "#lib/paper-config";
|
|
25
|
-
import
|
|
25
|
+
import { callerPath } from "./caller-path.ts";
|
|
26
26
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
27
27
|
import type { Files } from "./ports/files.ts";
|
|
28
28
|
import {
|
|
@@ -114,14 +114,12 @@ export function parsePaperSettings(
|
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
118
|
-
|
|
119
117
|
/** A JSON file through `files`: undefined when absent, the problem when it does not parse. */
|
|
120
118
|
function readJson(
|
|
121
119
|
files: Files,
|
|
122
120
|
path: string,
|
|
123
121
|
): Result<unknown, SettingsProblem> {
|
|
124
|
-
const bytes = files.readBytes(
|
|
122
|
+
const bytes = files.readBytes(callerPath(path));
|
|
125
123
|
if (bytes === null) return ok(undefined);
|
|
126
124
|
try {
|
|
127
125
|
return ok(JSON.parse(new TextDecoder().decode(bytes)));
|
|
@@ -139,7 +137,9 @@ function rootDefaults(
|
|
|
139
137
|
files: Files,
|
|
140
138
|
paperDir: string,
|
|
141
139
|
): Result<Defaults | null, SettingsProblem> {
|
|
142
|
-
const root = findProjectRoot(dirname(paperDir), (p) =>
|
|
140
|
+
const root = findProjectRoot(dirname(paperDir), (p) =>
|
|
141
|
+
files.isFile(callerPath(p)),
|
|
142
|
+
);
|
|
143
143
|
const json = readJson(files, join(root, CONFIG_FILE));
|
|
144
144
|
if (!json.ok) return json;
|
|
145
145
|
if (!isObject(json.value)) return ok(null);
|
package/src/pdf-facts.test.ts
CHANGED
|
@@ -40,11 +40,11 @@ function page(items: FakeItem[], fonts: Record<string, unknown>) {
|
|
|
40
40
|
}),
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
const doc = (pages: ReturnType<typeof page>[]) =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
const doc = (pages: ReturnType<typeof page>[]) => ({
|
|
44
|
+
numPages: pages.length,
|
|
45
|
+
getPage: (i: number) =>
|
|
46
|
+
Promise.resolve(present(pages[i - 1], `page ${String(i)}`)),
|
|
47
|
+
});
|
|
48
48
|
const item = (
|
|
49
49
|
fontName: string,
|
|
50
50
|
transform: number[],
|
|
@@ -141,3 +141,14 @@ test("a failure that is not an Error is named `Error` with its text", () => {
|
|
|
141
141
|
detail: "Error: boom",
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
test("🔴 a rejection with no value at all is a failure too, not a TypeError out of `readPdf`", () => {
|
|
146
|
+
// Guards: `readPdf` ends in `.catch(failureOf)`, and reading `.name` off `null` threw from inside it.
|
|
147
|
+
assert.deepEqual(
|
|
148
|
+
[failureOf(null), failureOf(undefined)],
|
|
149
|
+
[
|
|
150
|
+
{ ok: false, reason: "unreadable", detail: "Error: null" },
|
|
151
|
+
{ ok: false, reason: "unreadable", detail: "Error: undefined" },
|
|
152
|
+
],
|
|
153
|
+
);
|
|
154
|
+
});
|