paperlint 3.1.0 → 3.1.1
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/.github/workflows/ci.yml +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -8
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +13 -9
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +27 -7
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +3 -3
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -65
- package/src/structure.mutations.mjs +0 -83
package/src/link-skills.ts
CHANGED
|
@@ -60,13 +60,18 @@ export type Located =
|
|
|
60
60
|
*
|
|
61
61
|
* @returns `dir` — the real directory; `spelled` — the same directory as the project names it.
|
|
62
62
|
*/
|
|
63
|
-
export function locatePackage(
|
|
63
|
+
export function locatePackage(
|
|
64
|
+
project: string,
|
|
65
|
+
{
|
|
66
|
+
resolveManifest = (from: string): string =>
|
|
67
|
+
createRequire(
|
|
68
|
+
pathToFileURL(join(from, "__paperlint_locate__.js")).href,
|
|
69
|
+
).resolve(`${PACKAGE_NAME}/package.json`),
|
|
70
|
+
}: { resolveManifest?: (project: string) => string } = {},
|
|
71
|
+
): Located {
|
|
64
72
|
let manifest: string;
|
|
65
73
|
try {
|
|
66
|
-
|
|
67
|
-
pathToFileURL(join(project, "__paperlint_locate__.js")).href,
|
|
68
|
-
);
|
|
69
|
-
manifest = req.resolve(`${PACKAGE_NAME}/package.json`);
|
|
74
|
+
manifest = resolveManifest(project);
|
|
70
75
|
} catch (e) {
|
|
71
76
|
return {
|
|
72
77
|
error: `${(e as NodeJS.ErrnoException).code ?? "error"}: ${(e as Error).message.split("\n")[0]}`,
|
|
@@ -126,10 +131,28 @@ export type LinkReport =
|
|
|
126
131
|
}
|
|
127
132
|
| { readonly ok: false; readonly error: string };
|
|
128
133
|
|
|
134
|
+
/** The filesystem calls linking makes — the disk by default; a test stages a race or a refusal. */
|
|
135
|
+
export interface LinkFs {
|
|
136
|
+
readonly lstatSync: (p: string) => {
|
|
137
|
+
isSymbolicLink(): boolean;
|
|
138
|
+
isDirectory(): boolean;
|
|
139
|
+
};
|
|
140
|
+
readonly realpathSync: (p: string) => string;
|
|
141
|
+
readonly readlinkSync: (p: string) => string;
|
|
142
|
+
readonly symlinkSync: (target: string, path: string, type: "dir") => void;
|
|
143
|
+
}
|
|
144
|
+
const nodeLinkFs: LinkFs = {
|
|
145
|
+
lstatSync,
|
|
146
|
+
realpathSync,
|
|
147
|
+
readlinkSync,
|
|
148
|
+
symlinkSync,
|
|
149
|
+
};
|
|
150
|
+
|
|
129
151
|
/** What occupies `entry`, judged against the directory it should lead to. */
|
|
130
152
|
function inspect(
|
|
131
153
|
entry: string,
|
|
132
154
|
want: string,
|
|
155
|
+
{ lstatSync, realpathSync, readlinkSync }: LinkFs,
|
|
133
156
|
): { status: "present" | "missing" | "foreign"; reason?: string } {
|
|
134
157
|
let st;
|
|
135
158
|
try {
|
|
@@ -163,15 +186,22 @@ function linkOne(
|
|
|
163
186
|
target,
|
|
164
187
|
want,
|
|
165
188
|
write,
|
|
166
|
-
|
|
189
|
+
fs,
|
|
190
|
+
}: {
|
|
191
|
+
entry: string;
|
|
192
|
+
target: string;
|
|
193
|
+
want: string;
|
|
194
|
+
write: boolean;
|
|
195
|
+
fs: LinkFs;
|
|
196
|
+
},
|
|
167
197
|
): SkillLink {
|
|
168
|
-
const seen = inspect(entry, want);
|
|
198
|
+
const seen = inspect(entry, want, fs);
|
|
169
199
|
if (!write || seen.status !== "missing")
|
|
170
200
|
return seen.reason
|
|
171
201
|
? { name, status: seen.status, reason: seen.reason }
|
|
172
202
|
: { name, status: seen.status };
|
|
173
203
|
try {
|
|
174
|
-
symlinkSync(target, entry, "dir");
|
|
204
|
+
fs.symlinkSync(target, entry, "dir");
|
|
175
205
|
return { name, status: "created" };
|
|
176
206
|
} catch (e) {
|
|
177
207
|
return {
|
|
@@ -182,6 +212,16 @@ function linkOne(
|
|
|
182
212
|
}
|
|
183
213
|
}
|
|
184
214
|
|
|
215
|
+
/** Create the skills home; the reason it could not be, or null. */
|
|
216
|
+
function makeHome(home: string): string | null {
|
|
217
|
+
try {
|
|
218
|
+
mkdirSync(home, { recursive: true });
|
|
219
|
+
return null;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return `cannot create ${home}: ${(e as Error).message}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
185
225
|
/**
|
|
186
226
|
* Link every shipped skill into `<project>/.claude/skills/`, or — with `write: false` — only
|
|
187
227
|
* report which ones are missing (that is what `paperlint doctor` asks).
|
|
@@ -191,7 +231,12 @@ export function linkSkills(
|
|
|
191
231
|
{
|
|
192
232
|
write = true,
|
|
193
233
|
locate = locatePackage,
|
|
194
|
-
|
|
234
|
+
fs = nodeLinkFs,
|
|
235
|
+
}: {
|
|
236
|
+
write?: boolean;
|
|
237
|
+
locate?: (p: string) => Located;
|
|
238
|
+
fs?: LinkFs;
|
|
239
|
+
} = {},
|
|
195
240
|
): LinkReport {
|
|
196
241
|
const pkg = locate(project);
|
|
197
242
|
if ("error" in pkg)
|
|
@@ -205,20 +250,28 @@ export function linkSkills(
|
|
|
205
250
|
const spelledSkills = join(pkg.spelled, relative(pkg.dir, shipped.skillsDir));
|
|
206
251
|
|
|
207
252
|
const home = join(project, SKILLS_HOME);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
const made = write && shipped.names.length > 0 ? makeHome(home) : null;
|
|
254
|
+
if (made !== null) return { ok: false, error: made };
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
home,
|
|
258
|
+
...linkAll(shipped, { spelledSkills, home, write, fs }),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Every shipped skill's link, and the first one's target as the report's example. */
|
|
263
|
+
function linkAll(
|
|
264
|
+
shipped: { readonly skillsDir: string; readonly names: readonly string[] },
|
|
265
|
+
{
|
|
266
|
+
spelledSkills,
|
|
267
|
+
home,
|
|
268
|
+
write,
|
|
269
|
+
fs,
|
|
270
|
+
}: { spelledSkills: string; home: string; write: boolean; fs: LinkFs },
|
|
271
|
+
): { example: string | null; links: SkillLink[] } {
|
|
218
272
|
// A relative target resolves against the directory that PHYSICALLY holds the link, so it is
|
|
219
273
|
// computed from the real one — `.claude` may itself be a symlink.
|
|
220
274
|
const physicalHome = existsSync(home) ? realpathSync(home) : resolve(home);
|
|
221
|
-
|
|
222
275
|
let example: string | null = null;
|
|
223
276
|
const links = shipped.names.map((name): SkillLink => {
|
|
224
277
|
const target = relative(physicalHome, join(spelledSkills, name));
|
|
@@ -228,7 +281,8 @@ export function linkSkills(
|
|
|
228
281
|
target,
|
|
229
282
|
want: realpathSync(join(shipped.skillsDir, name)),
|
|
230
283
|
write,
|
|
284
|
+
fs,
|
|
231
285
|
});
|
|
232
286
|
});
|
|
233
|
-
return {
|
|
287
|
+
return { example, links };
|
|
234
288
|
}
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* ⚠️ Assertions at the TOP LEVEL: `vigiles test` imports the file and counts "did not throw"
|
|
17
17
|
* as a pass.
|
|
18
18
|
*/
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
19
|
import {
|
|
21
20
|
existsSync,
|
|
22
21
|
mkdirSync,
|
|
@@ -32,6 +31,7 @@ import { dirname, join } from "node:path";
|
|
|
32
31
|
import { fileURLToPath } from "node:url";
|
|
33
32
|
import { load } from "js-yaml";
|
|
34
33
|
import { frontmatterBlock } from "../lib/markdown.mjs";
|
|
34
|
+
import { createChecker } from "../lib/check.mjs";
|
|
35
35
|
|
|
36
36
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
37
|
const ROOT = dirname(HERE);
|
|
@@ -46,11 +46,7 @@ const {
|
|
|
46
46
|
} = await import(join(HERE, "new-paper.ts"));
|
|
47
47
|
const { run } = await import(join(HERE, "cli.ts"));
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
const check = (label, cond) => {
|
|
51
|
-
n++;
|
|
52
|
-
assert.ok(cond, label);
|
|
53
|
-
};
|
|
49
|
+
const check = createChecker();
|
|
54
50
|
|
|
55
51
|
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-")));
|
|
56
52
|
const read = (...p) => readFileSync(join(...p), "utf8");
|
|
@@ -217,5 +213,5 @@ try {
|
|
|
217
213
|
}
|
|
218
214
|
|
|
219
215
|
console.log(
|
|
220
|
-
`✓ ${String(
|
|
216
|
+
`✓ ${String(check.count)} assertions passed — paperlint new scaffolds what paperlint lint accepts`,
|
|
221
217
|
);
|
package/src/new-paper.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { tmpdir } from "node:os";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { afterEach, describe, expect, it } from "vitest";
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
|
-
import { newPaper, OVERRIDE_DIR } from "./new-paper.ts";
|
|
18
|
+
import { newPaper, OVERRIDE_DIR, reportNewPaper } from "./new-paper.ts";
|
|
19
19
|
import { parsePaperSettings } from "./paper-settings.ts";
|
|
20
20
|
import { chooseVenue, run } from "./cli.ts";
|
|
21
21
|
import { shippedPresets } from "./presets.ts";
|
|
@@ -302,3 +302,33 @@ describe("chooseVenue — on a terminal", () => {
|
|
|
302
302
|
expect(t.asked).toEqual([]);
|
|
303
303
|
});
|
|
304
304
|
});
|
|
305
|
+
|
|
306
|
+
describe("a project's own template", () => {
|
|
307
|
+
it("🔴 a template paperlint.json with comments refuses --venue by name, and nothing is made", () => {
|
|
308
|
+
const papers = join(tmp(), "papers");
|
|
309
|
+
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
310
|
+
const src = join(papers, OVERRIDE_DIR, "paperlint.json");
|
|
311
|
+
writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
|
|
312
|
+
const r = newPaper(papers, "p", "tex", {
|
|
313
|
+
venue: { extends: "paperlint:aisec", kind: null },
|
|
314
|
+
});
|
|
315
|
+
expect(r).toEqual({
|
|
316
|
+
ok: false,
|
|
317
|
+
reason: `--venue cannot be written into ${src}: it is not plain JSON — set "extends" by hand`,
|
|
318
|
+
});
|
|
319
|
+
expect(existsSync(join(papers, "p"))).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("the report names where each created file came from", () => {
|
|
323
|
+
const papers = join(tmp(), "papers");
|
|
324
|
+
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
325
|
+
writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
|
|
326
|
+
const lines = reportNewPaper(newPaper(papers, "p", "tex"), (p) => p);
|
|
327
|
+
expect(lines).toContain(
|
|
328
|
+
" + paperlint.json (from the project's .template/ template)",
|
|
329
|
+
);
|
|
330
|
+
expect(
|
|
331
|
+
lines.filter((l) => l.endsWith("(from the package template)")).length,
|
|
332
|
+
).toBeGreaterThan(0);
|
|
333
|
+
});
|
|
334
|
+
});
|
package/src/paper-files.test.ts
CHANGED
|
@@ -319,6 +319,18 @@ describe("paper-files — the pieces", () => {
|
|
|
319
319
|
});
|
|
320
320
|
});
|
|
321
321
|
|
|
322
|
+
describe("paper-files — blocks that carry no rules", () => {
|
|
323
|
+
it("a plugin with only a language registers no rule; a block with no files owns no pattern of its own", () => {
|
|
324
|
+
const own = [
|
|
325
|
+
{ plugins: { tex: { languages: {} } } },
|
|
326
|
+
{ ignores: ["**/x.md"], plugins: { tex: { languages: {} } } },
|
|
327
|
+
{ files: ["**/a.md"], plugins: { p: { rules: { one: {} } } } },
|
|
328
|
+
];
|
|
329
|
+
expect([...ruleOwners(own).keys()]).toEqual(["p/one"]);
|
|
330
|
+
expect(ownedPatterns(own)).toEqual(["**/a.md"]);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
322
334
|
describe("paper-files — narrowing a block", () => {
|
|
323
335
|
const own = [
|
|
324
336
|
{ plugins: { g: { rules: { any: {} } } } },
|
package/src/paper-files.ts
CHANGED
|
@@ -71,8 +71,9 @@ export interface ScopedBlock {
|
|
|
71
71
|
readonly rules: Readonly<Record<string, RuleEntry>>;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
/** A block's `files`, the string globs only. */
|
|
75
|
+
const globsOf = (files: readonly unknown[]): string[] =>
|
|
76
|
+
files.filter((f): f is string => typeof f === "string");
|
|
76
77
|
|
|
77
78
|
const stringsOf = (xs: readonly unknown[] | undefined): string[] =>
|
|
78
79
|
(xs ?? []).filter((x): x is string => typeof x === "string");
|
|
@@ -102,8 +103,11 @@ export function ownedScopes(own: readonly unknown[]): OwnedScope[] {
|
|
|
102
103
|
return merged(
|
|
103
104
|
own
|
|
104
105
|
.map((raw) => raw as Block)
|
|
105
|
-
.
|
|
106
|
-
|
|
106
|
+
.flatMap((b) =>
|
|
107
|
+
b.files === undefined
|
|
108
|
+
? []
|
|
109
|
+
: [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
|
|
110
|
+
),
|
|
107
111
|
);
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -150,7 +154,7 @@ export function ruleOwners(
|
|
|
150
154
|
const scopes =
|
|
151
155
|
b.files === undefined
|
|
152
156
|
? all
|
|
153
|
-
: [{ files: globsOf(b), ignores: stringsOf(b.ignores) }];
|
|
157
|
+
: [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }];
|
|
154
158
|
for (const id of ruleIdsOf(b))
|
|
155
159
|
found.set(id, [...(found.get(id) ?? []), ...scopes]);
|
|
156
160
|
}
|
|
@@ -118,6 +118,26 @@ describe("readPaperSettings", () => {
|
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
describe("readPaperSettings — a root paperlint.json that cannot be used", () => {
|
|
122
|
+
const ROOT = "/work";
|
|
123
|
+
const withFiles = (f: Record<string, string>) =>
|
|
124
|
+
memoryFiles({ [`${ROOT}/package.json`]: "{}", ...f });
|
|
125
|
+
|
|
126
|
+
it("a root file that does not parse, or names a field of the wrong type, is the paper's problem too", () => {
|
|
127
|
+
const broken = (root: string) =>
|
|
128
|
+
readPaperSettings(withFiles({ [`${ROOT}/paperlint.json`]: root }), PAPER);
|
|
129
|
+
const notJson = broken("{ nope");
|
|
130
|
+
expect(!notJson.ok && notJson.error.kind).toBe("broken");
|
|
131
|
+
expect(broken('{"kind":3}')).toEqual({
|
|
132
|
+
ok: false,
|
|
133
|
+
error: {
|
|
134
|
+
kind: "broken",
|
|
135
|
+
why: expect.stringMatching(/^the root paperlint\.json: .*kind/),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
121
141
|
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
122
142
|
const ROOT = "/work";
|
|
123
143
|
const withFiles = (f: Record<string, string>) =>
|
|
@@ -11,17 +11,14 @@ import { writeFileSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { createChecker } from "../lib/check.mjs";
|
|
14
15
|
|
|
15
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const FIX = resolve(HERE, "..", "fixtures", "pdf-facts");
|
|
17
18
|
const { readPdf } = await import(join(HERE, "pdf-facts.ts"));
|
|
18
19
|
const { classifyLastPage } = await import(join(HERE, "pdf-geometry.ts"));
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
const check = (label, cond, detail = "") => {
|
|
22
|
-
assert.ok(cond, detail ? `${label} — ${detail}` : label);
|
|
23
|
-
n++;
|
|
24
|
-
};
|
|
21
|
+
const check = createChecker();
|
|
25
22
|
|
|
26
23
|
const read = async (name) => {
|
|
27
24
|
const r = await readPdf(join(FIX, name));
|
|
@@ -184,5 +181,5 @@ try {
|
|
|
184
181
|
}
|
|
185
182
|
|
|
186
183
|
console.log(
|
|
187
|
-
`✓ ${String(
|
|
184
|
+
`✓ ${String(check.count)} assertions passed — pdf-facts: pdf.js on real PDFs agrees with poppler, and says what it cannot read`,
|
|
188
185
|
);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pdf-facts.ts on document shapes real test PDFs do not carry, through a fake pdf.js document
|
|
3
|
+
* (the real pdf.js library for its transforms and operator codes): a font object that is not one,
|
|
4
|
+
* a font without a name or metrics, an item whose font never resolved, zero-scale transforms, a
|
|
5
|
+
* document with no pages, text with no font, and a failure that is not an Error.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { getResolvedPDFJS } from "unpdf";
|
|
9
|
+
import { test } from "vitest";
|
|
10
|
+
import { factsOf, failureOf, rawFontOf } from "./pdf-facts.ts";
|
|
11
|
+
|
|
12
|
+
const lib = await getResolvedPDFJS();
|
|
13
|
+
const H = 792;
|
|
14
|
+
|
|
15
|
+
interface FakeItem {
|
|
16
|
+
str: string;
|
|
17
|
+
fontName: string;
|
|
18
|
+
transform: number[];
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
|
22
|
+
/** A page whose text items and loaded fonts are given; its viewport flips y like pdf.js's. */
|
|
23
|
+
function page(items: FakeItem[], fonts: Record<string, unknown>) {
|
|
24
|
+
const objs = new Map(Object.entries(fonts));
|
|
25
|
+
return {
|
|
26
|
+
getOperatorList: async () => ({ fnArray: [], argsArray: [] }),
|
|
27
|
+
getTextContent: async () => ({ items }),
|
|
28
|
+
// Iterable like pdf.js's `commonObjs`, with its `has`/`get`.
|
|
29
|
+
commonObjs: {
|
|
30
|
+
has: (id: string) => objs.has(id),
|
|
31
|
+
get: (id: string) => objs.get(id),
|
|
32
|
+
[Symbol.iterator]: () => objs[Symbol.iterator](),
|
|
33
|
+
},
|
|
34
|
+
getViewport: () => ({
|
|
35
|
+
width: 612,
|
|
36
|
+
height: H,
|
|
37
|
+
transform: [1, 0, 0, -1, 0, H],
|
|
38
|
+
convertToViewportPoint: (x: number, y: number) => [x, H - y],
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const doc = (pages: ReturnType<typeof page>[]) =>
|
|
43
|
+
({
|
|
44
|
+
numPages: pages.length,
|
|
45
|
+
getPage: async (i: number) => pages[i - 1],
|
|
46
|
+
}) as never;
|
|
47
|
+
const item = (
|
|
48
|
+
fontName: string,
|
|
49
|
+
transform: number[],
|
|
50
|
+
height = 10,
|
|
51
|
+
): FakeItem => ({
|
|
52
|
+
str: "word",
|
|
53
|
+
fontName,
|
|
54
|
+
transform,
|
|
55
|
+
width: 20,
|
|
56
|
+
height,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("rawFontOf: not an object, not a font, and a font without a name", () => {
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
[
|
|
62
|
+
rawFontOf("a", null),
|
|
63
|
+
rawFontOf("b", { x: 1 }),
|
|
64
|
+
rawFontOf("c", { loadedName: "L" }),
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
null,
|
|
68
|
+
null,
|
|
69
|
+
{
|
|
70
|
+
id: "c",
|
|
71
|
+
name: "",
|
|
72
|
+
type: undefined,
|
|
73
|
+
subtype: undefined,
|
|
74
|
+
isType3Font: false,
|
|
75
|
+
missingFile: false,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a document with no pages is `empty`; text drawn with no font is refused", async () => {
|
|
82
|
+
assert.deepEqual(await factsOf(doc([]), lib), {
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: "empty",
|
|
85
|
+
detail: "the PDF has no pages",
|
|
86
|
+
});
|
|
87
|
+
const r = await factsOf(
|
|
88
|
+
doc([page([item("f0", [10, 0, 0, 10, 50, 700])], {})]),
|
|
89
|
+
lib,
|
|
90
|
+
);
|
|
91
|
+
assert.equal(r.ok, false);
|
|
92
|
+
assert.equal(!r.ok && r.reason, "zero-fonts-on-text");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("metrics, sizes and names fall back when pdf.js gives none", async () => {
|
|
96
|
+
const fonts = {
|
|
97
|
+
// a font with a name and an ascent but no descent
|
|
98
|
+
f1: { loadedName: "f1", name: "Named", ascent: 0.8 },
|
|
99
|
+
// a font with no name and no metrics at all
|
|
100
|
+
f2: { loadedName: "f2" },
|
|
101
|
+
};
|
|
102
|
+
const r = await factsOf(
|
|
103
|
+
doc([
|
|
104
|
+
page(
|
|
105
|
+
[
|
|
106
|
+
item("f1", [10, 0, 0, 10, 50, 700]),
|
|
107
|
+
// zero horizontal scale: the size comes from the item's height…
|
|
108
|
+
item("f2", [0, 0, 0, 0, 100, 700], 12),
|
|
109
|
+
// …or, with no height either, from the vertical scale
|
|
110
|
+
item("f2", [0, 0, 0, 9, 150, 700], 0),
|
|
111
|
+
// an item whose font pdf.js never resolved
|
|
112
|
+
item("gone", [10, 0, 0, 10, 200, 700]),
|
|
113
|
+
],
|
|
114
|
+
fonts,
|
|
115
|
+
),
|
|
116
|
+
]),
|
|
117
|
+
lib,
|
|
118
|
+
);
|
|
119
|
+
assert.equal(r.ok, true);
|
|
120
|
+
const boxes = r.ok ? r.facts.layout[0]!.boxes : [];
|
|
121
|
+
assert.deepEqual(
|
|
122
|
+
boxes.map((b) => [b.font, b.size, b.fill]),
|
|
123
|
+
[
|
|
124
|
+
["Named", 10, { kind: "unknown" }],
|
|
125
|
+
["f2", 12, { kind: "unknown" }],
|
|
126
|
+
["f2", 9, { kind: "unknown" }],
|
|
127
|
+
["gone", 10, { kind: "unknown" }],
|
|
128
|
+
],
|
|
129
|
+
);
|
|
130
|
+
assert.deepEqual(r.ok ? r.facts.last.words.map((w) => w.text) : [], [
|
|
131
|
+
"word",
|
|
132
|
+
"word",
|
|
133
|
+
"word",
|
|
134
|
+
"word",
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a failure that is not an Error is named `Error` with its text", () => {
|
|
139
|
+
assert.deepEqual(failureOf("boom"), {
|
|
140
|
+
ok: false,
|
|
141
|
+
reason: "unreadable",
|
|
142
|
+
detail: "Error: boom",
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/pdf-facts.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "./pdf-geometry.ts";
|
|
34
34
|
import type { PageLayout, TextBox } from "./domain/page-layout.ts";
|
|
35
35
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
36
|
-
import {
|
|
36
|
+
import { fillsFor, isUpright } from "./adapters/pdfjs/fill.ts";
|
|
37
37
|
|
|
38
38
|
type PdfJs = Awaited<ReturnType<typeof import("unpdf").getResolvedPDFJS>>;
|
|
39
39
|
type Doc = Awaited<ReturnType<PdfJs["getDocument"]>["promise"]>;
|
|
@@ -241,20 +241,18 @@ function layoutOf(
|
|
|
241
241
|
lib: PdfJs,
|
|
242
242
|
): PageLayout {
|
|
243
243
|
const vp = page.getViewport({ scale: 1 });
|
|
244
|
-
const
|
|
245
|
-
lib
|
|
246
|
-
ops,
|
|
247
|
-
items.map((it) => it.str),
|
|
244
|
+
const boxes = fillsFor(lib.OPS, ops, items, (it) => it.str).map(
|
|
245
|
+
({ item, fill }): TextBox => ({ ...boxOf(page, vp, item, lib), fill }),
|
|
248
246
|
);
|
|
249
|
-
const boxes = items.map((it, k): TextBox => ({
|
|
250
|
-
...boxOf(page, vp, it, lib),
|
|
251
|
-
fill: fills[k] ?? { kind: "unknown" },
|
|
252
|
-
}));
|
|
253
247
|
return { widthPt: vp.width, heightPt: vp.height, boxes };
|
|
254
248
|
}
|
|
255
249
|
|
|
256
|
-
/**
|
|
257
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Every page's fonts, and the last page's words. Exported with `failureOf` so a test can hand it a
|
|
252
|
+
* document whose objects pdf.js shapes only in rare files (a font without metrics, a zero-scale
|
|
253
|
+
* transform, no pages) — `readPdf` below is the one production caller.
|
|
254
|
+
*/
|
|
255
|
+
export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
|
|
258
256
|
const read: {
|
|
259
257
|
page: Page;
|
|
260
258
|
raw: RawPage;
|
|
@@ -290,7 +288,7 @@ async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
|
|
|
290
288
|
}
|
|
291
289
|
|
|
292
290
|
/** pdf.js's own exception names for the two conditions a caller can act on. */
|
|
293
|
-
function failureOf(e: unknown): PdfRead {
|
|
291
|
+
export function failureOf(e: unknown): PdfRead {
|
|
294
292
|
const err = e as { name?: string; message?: string };
|
|
295
293
|
const detail = `${err.name ?? "Error"}: ${err.message ?? String(e)}`;
|
|
296
294
|
return fail(
|
|
@@ -309,13 +307,13 @@ export const readPdf: PdfReader = async (path) => {
|
|
|
309
307
|
}
|
|
310
308
|
const lib = await loadPdfJs();
|
|
311
309
|
const task = lib.getDocument({ data, ...PDFJS_OPTIONS });
|
|
312
|
-
try
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
310
|
+
// A promise chain rather than try/catch/finally: every path returns a value here, and the code
|
|
311
|
+
// after such a statement is a block that can never run — which coverage reports as unrun.
|
|
312
|
+
const read = await task.promise
|
|
313
|
+
.then((doc) => factsOf(doc, lib))
|
|
314
|
+
.catch(failureOf);
|
|
315
|
+
await task.destroy();
|
|
316
|
+
return read;
|
|
319
317
|
};
|
|
320
318
|
|
|
321
319
|
/** The one wording of a failed read, as a build step or a CLI prints it. */
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* on 2026-08-29 (the midpoint cut, the review build, years mistaken for line numbers), ported from
|
|
7
7
|
* the harness of the function this replaces.
|
|
8
8
|
*/
|
|
9
|
-
import assert from "node:assert/strict";
|
|
10
9
|
import { dirname, join } from "node:path";
|
|
11
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { createChecker } from "../lib/check.mjs";
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const {
|
|
@@ -21,11 +21,7 @@ const {
|
|
|
21
21
|
STUB_WORDS,
|
|
22
22
|
} = await import(join(HERE, "pdf-geometry.ts"));
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
const check = (label, cond) => {
|
|
26
|
-
assert.ok(cond, label);
|
|
27
|
-
n++;
|
|
28
|
-
};
|
|
24
|
+
const check = createChecker();
|
|
29
25
|
|
|
30
26
|
const W = 612; // US letter, points
|
|
31
27
|
|
|
@@ -250,5 +246,5 @@ check(
|
|
|
250
246
|
);
|
|
251
247
|
|
|
252
248
|
console.log(
|
|
253
|
-
`✓ ${String(
|
|
249
|
+
`✓ ${String(check.count)} assertions passed — pdf-geometry: columns, stub/review, words from runs, fonts`,
|
|
254
250
|
);
|
package/src/pdf-geometry.ts
CHANGED
|
@@ -136,7 +136,7 @@ export function wordsOf(run: TextRun): Word[] {
|
|
|
136
136
|
const y0 = run.baseline - run.ascent * run.size;
|
|
137
137
|
const y1 = run.baseline - run.descent * run.size;
|
|
138
138
|
return [...run.text.matchAll(/\S+/g)].map((m) => {
|
|
139
|
-
const x0 = run.x +
|
|
139
|
+
const x0 = run.x + m.index * perChar;
|
|
140
140
|
return { x0, y0, x1: x0 + m[0].length * perChar, y1, text: m[0] };
|
|
141
141
|
});
|
|
142
142
|
}
|
package/src/presets.test.ts
CHANGED
|
@@ -230,6 +230,8 @@ describe("labelOf — display only, never used to resolve", () => {
|
|
|
230
230
|
["paperlint:agenticdev", "agenticdev"],
|
|
231
231
|
["./venues/usenix-sec.jsonc", "usenix-sec"],
|
|
232
232
|
["../shared/acl.json", "acl"],
|
|
233
|
+
["./.hidden", ".hidden"],
|
|
234
|
+
["paperlint:", ""],
|
|
233
235
|
])("%s → %s", (spec, label) => {
|
|
234
236
|
expect(labelOf(spec)).toBe(label);
|
|
235
237
|
});
|
package/src/presets.ts
CHANGED
|
@@ -293,5 +293,11 @@ export function paperPresetProblem(
|
|
|
293
293
|
if (p.kind === "preset-problem")
|
|
294
294
|
return `${join(paperDir, CONFIG_FILE)}: ${presetProblemText(p.problem)}`;
|
|
295
295
|
if (p.kind !== "settings-problem") return null;
|
|
296
|
-
return
|
|
296
|
+
return settingsProblemLine(paperDir, p.problem);
|
|
297
297
|
}
|
|
298
|
+
|
|
299
|
+
/** The one line naming a paper's `paperlint.json` that does not parse, and why. */
|
|
300
|
+
export const settingsProblemLine = (
|
|
301
|
+
paperDir: string,
|
|
302
|
+
problem: SettingsProblem,
|
|
303
|
+
): string => `${join(paperDir, CONFIG_FILE)}: ${problem.why}`;
|