paperlint 2.1.0 → 3.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 +6 -1
- package/README.md +233 -224
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +38 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +28 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +277 -265
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +143 -172
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +2 -13
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.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/configuration.md +119 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +60 -23
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +4 -13
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build.harness.mjs +8 -4
- package/src/build.ts +62 -6
- package/src/cli.harness.mjs +147 -292
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +344 -304
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +186 -230
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/new-paper.test.ts +1 -13
- package/src/new-paper.ts +2 -17
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +84 -121
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/structure.harness.mjs +3 -1
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +20 -11
- package/src/venue-rules.ts +11 -13
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
package/src/doctor.harness.mjs
CHANGED
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
* Killed by: src/doctor.mutations.mjs
|
|
13
13
|
*/
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
|
-
import {
|
|
16
|
-
OLD_PAPERS_DIR_FIELD,
|
|
17
|
-
PAPERS_DIR_FIELD,
|
|
18
|
-
} from "../lib/paper-config.mjs";
|
|
15
|
+
import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
|
|
19
16
|
import {
|
|
20
17
|
mkdtempSync,
|
|
21
18
|
mkdirSync,
|
|
@@ -41,8 +38,8 @@ const check = (label, cond) => {
|
|
|
41
38
|
assert.ok(cond, label);
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
/** A consumer on disk: a papers directory, and maybe a declaration in
|
|
45
|
-
function consumer({ papersDir,
|
|
41
|
+
/** A consumer on disk: a papers directory, and maybe a declaration in the root paperlint.json. */
|
|
42
|
+
function consumer({ papersDir, declared, makeDir = true }) {
|
|
46
43
|
const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-doctor-")));
|
|
47
44
|
if (makeDir && papersDir) {
|
|
48
45
|
mkdirSync(join(dir, papersDir, "some-paper"), { recursive: true });
|
|
@@ -51,9 +48,15 @@ function consumer({ papersDir, pkgKey, makeDir = true }) {
|
|
|
51
48
|
"# s\n",
|
|
52
49
|
);
|
|
53
50
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
writeFileSync(
|
|
52
|
+
join(dir, "package.json"),
|
|
53
|
+
JSON.stringify({ name: "consumer", version: "1.0.0" }, null, 2),
|
|
54
|
+
);
|
|
55
|
+
if (declared !== undefined)
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(dir, "paperlint.json"),
|
|
58
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: declared }, null, 2),
|
|
59
|
+
);
|
|
57
60
|
return dir;
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -76,7 +79,7 @@ const runDoctor = (
|
|
|
76
79
|
|
|
77
80
|
// ── I. A CONSISTENT INSTALL IS SILENT ───────────────────────────────────────────────────────
|
|
78
81
|
{
|
|
79
|
-
const dir = consumer({ papersDir: "papers",
|
|
82
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
80
83
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
81
84
|
check("a consistent install — exit ZERO", r.code === 0);
|
|
82
85
|
check(
|
|
@@ -86,30 +89,9 @@ const runDoctor = (
|
|
|
86
89
|
rmSync(dir, { recursive: true, force: true });
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
// ── I½. THE OLD FIELD NAME IS A FAILURE, NAMED ─────────────────────────────────────────────
|
|
90
|
-
{
|
|
91
|
-
const dir = consumer({ papersDir: "papers", pkgKey: "papers" });
|
|
92
|
-
writeFileSync(
|
|
93
|
-
join(dir, "package.json"),
|
|
94
|
-
JSON.stringify({
|
|
95
|
-
name: "consumer",
|
|
96
|
-
paperlint: { [OLD_PAPERS_DIR_FIELD]: "papers" },
|
|
97
|
-
}),
|
|
98
|
-
);
|
|
99
|
-
const r = runDoctor(dir, { cliPapers: null });
|
|
100
|
-
check("the old field name — exit NON-zero", r.code !== 0);
|
|
101
|
-
check(
|
|
102
|
-
"and doctor says what the field was renamed to",
|
|
103
|
-
r.out.includes(
|
|
104
|
-
`"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "paperlint"`,
|
|
105
|
-
),
|
|
106
|
-
);
|
|
107
|
-
rmSync(dir, { recursive: true, force: true });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
92
|
// ── II. THE VERY DEFECT: AN INSTALL FOLLOWING THE DOCS ─────────────────────────────────────
|
|
111
|
-
// The
|
|
112
|
-
//
|
|
93
|
+
// The CLI lints one directory and the hook guards another: the install that motivated doctor
|
|
94
|
+
// (issue #33). Here the CLI was pointed elsewhere while the hook took the default.
|
|
113
95
|
{
|
|
114
96
|
const dir = consumer({ papersDir: "writing/drafts" });
|
|
115
97
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
@@ -137,21 +119,31 @@ const runDoctor = (
|
|
|
137
119
|
rmSync(dir, { recursive: true, force: true });
|
|
138
120
|
}
|
|
139
121
|
|
|
140
|
-
// ── II-bis.
|
|
141
|
-
// The
|
|
142
|
-
// It must not fail here (an error-level false positive costs more than a miss), but it must
|
|
143
|
-
// not stay silent either.
|
|
122
|
+
// ── II-bis. NO paperlint.json — EVERY DEFAULT, AND THAT IS A VALID PROJECT ─────────────────
|
|
123
|
+
// The CLI and the hooks both take `papers` when nothing is declared, so they cannot disagree.
|
|
144
124
|
{
|
|
145
125
|
const dir = consumer({ papersDir: "papers" });
|
|
146
126
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
147
|
-
check("
|
|
127
|
+
check("no paperlint.json — exit ZERO", r.code === 0);
|
|
148
128
|
check(
|
|
149
|
-
"
|
|
150
|
-
|
|
129
|
+
"and it says the defaults are in play, naming the directory",
|
|
130
|
+
/✓ no paperlint\.json — every setting has its default; papers are in "papers"/.test(
|
|
131
|
+
r.out,
|
|
132
|
+
),
|
|
151
133
|
);
|
|
134
|
+
rmSync(dir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── II-quater. A paperlint.json THAT DOES NOT PARSE — the guard refuses every command ─────
|
|
138
|
+
{
|
|
139
|
+
const dir = consumer({ papersDir: "papers" });
|
|
140
|
+
writeFileSync(join(dir, "paperlint.json"), '{ "papersDir": <<<<<<< HEAD\n');
|
|
141
|
+
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
142
|
+
check("an unparsable paperlint.json — a FAILURE", r.code === 2);
|
|
152
143
|
check(
|
|
153
|
-
"
|
|
154
|
-
|
|
144
|
+
"named, with the way out (a file edit, not a command)",
|
|
145
|
+
/✗ paperlint\.json is not valid JSON/.test(r.out) &&
|
|
146
|
+
/Edit or Write/.test(r.out),
|
|
155
147
|
);
|
|
156
148
|
rmSync(dir, { recursive: true, force: true });
|
|
157
149
|
}
|
|
@@ -163,7 +155,7 @@ const runDoctor = (
|
|
|
163
155
|
{
|
|
164
156
|
const dir = consumer({
|
|
165
157
|
papersDir: "papers",
|
|
166
|
-
|
|
158
|
+
declared: "papers",
|
|
167
159
|
makeDir: false,
|
|
168
160
|
});
|
|
169
161
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
@@ -177,42 +169,9 @@ const runDoctor = (
|
|
|
177
169
|
rmSync(dir, { recursive: true, force: true });
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
// ── II-quater. THE KEY'S OLD NAME IS READ AND NAMED; TWO DIFFERENT KEYS ARE A FAILURE ──────
|
|
181
|
-
{
|
|
182
|
-
const dir = consumer({ papersDir: "papers" });
|
|
183
|
-
writeFileSync(
|
|
184
|
-
join(dir, "package.json"),
|
|
185
|
-
JSON.stringify({
|
|
186
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
190
|
-
check(
|
|
191
|
-
"the old key — read (not a failure), and named with the new one",
|
|
192
|
-
r.code === 0 &&
|
|
193
|
-
/⚠ "research-paper-pipeline" in package\.json is the old name/.test(
|
|
194
|
-
r.out,
|
|
195
|
-
) &&
|
|
196
|
-
/the hooks will guard papers/.test(r.out),
|
|
197
|
-
);
|
|
198
|
-
writeFileSync(
|
|
199
|
-
join(dir, "package.json"),
|
|
200
|
-
JSON.stringify({
|
|
201
|
-
paperlint: { [PAPERS_DIR_FIELD]: "papers" },
|
|
202
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "elsewhere" },
|
|
203
|
-
}),
|
|
204
|
-
);
|
|
205
|
-
const both = runDoctor(dir, { cliPapers: "papers" });
|
|
206
|
-
check(
|
|
207
|
-
"both keys with different contents — a failure",
|
|
208
|
-
both.code === 2 && /✗ package\.json has both/.test(both.out),
|
|
209
|
-
);
|
|
210
|
-
rmSync(dir, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
172
|
// ── III. TWO DECLARATIONS HAVE DRIFTED APART ────────────────────────────────────────────────
|
|
214
173
|
{
|
|
215
|
-
const dir = consumer({ papersDir: "writing/drafts",
|
|
174
|
+
const dir = consumer({ papersDir: "writing/drafts", declared: "papers" });
|
|
216
175
|
mkdirSync(join(dir, "papers"), { recursive: true });
|
|
217
176
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
218
177
|
check("both declarations exist, but differ — a FAILURE", r.code === 2);
|
|
@@ -223,12 +182,10 @@ const runDoctor = (
|
|
|
223
182
|
// This is load-bearing: a copy of the logic would drift silently and print a confident wrong
|
|
224
183
|
// answer.
|
|
225
184
|
{
|
|
226
|
-
const dir = consumer({ papersDir: "docs/papers",
|
|
185
|
+
const dir = consumer({ papersDir: "docs/papers", declared: "docs/papers/" });
|
|
227
186
|
const r = runDoctor(dir, { cliPapers: "docs/papers" });
|
|
228
187
|
const fromHook = papersRoot(
|
|
229
|
-
JSON.stringify({
|
|
230
|
-
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers/" },
|
|
231
|
-
}),
|
|
188
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "docs/papers/" }),
|
|
232
189
|
);
|
|
233
190
|
check("the hook itself trims the trailing slash", fromHook === "docs/papers");
|
|
234
191
|
check(
|
|
@@ -244,7 +201,7 @@ const runDoctor = (
|
|
|
244
201
|
|
|
245
202
|
// ── V. THE HOOK'S REFUSAL IS PASSED ALONG, NOT TURNED INTO A DIRECTORY ─────────────────────
|
|
246
203
|
{
|
|
247
|
-
const dir = consumer({ papersDir: "papers",
|
|
204
|
+
const dir = consumer({ papersDir: "papers", declared: "" });
|
|
248
205
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
249
206
|
check(
|
|
250
207
|
"an empty string in the declaration — the hook refuses, and doctor NAMES it",
|
|
@@ -255,7 +212,7 @@ const runDoctor = (
|
|
|
255
212
|
|
|
256
213
|
// ── VI. EXTERNAL PROGRAMS ARE A FACT, NOT A VERDICT ─────────────────────────────────────────
|
|
257
214
|
{
|
|
258
|
-
const dir = consumer({ papersDir: "papers",
|
|
215
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
259
216
|
const none = runDoctor(dir, { cliPapers: "papers", have: () => 1 });
|
|
260
217
|
check(
|
|
261
218
|
"🔴 a missing tex install does NOT fail the run — a gate on advice would mute the whole thing",
|
|
@@ -330,7 +287,7 @@ const runDoctor = (
|
|
|
330
287
|
// nothing anywhere said so. The link state itself is `link-skills.harness.mjs`'s subject; here
|
|
331
288
|
// only doctor's REPORTING of it is judged, so the state is handed in rather than built on disk.
|
|
332
289
|
{
|
|
333
|
-
const dir = consumer({ papersDir: "papers",
|
|
290
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
334
291
|
const state = {
|
|
335
292
|
ok: true,
|
|
336
293
|
home: join(dir, ".claude", "skills"),
|
package/src/doctor.mutations.mjs
CHANGED
|
@@ -46,8 +46,8 @@ process.exit(
|
|
|
46
46
|
edits: [
|
|
47
47
|
[
|
|
48
48
|
SRC,
|
|
49
|
-
|
|
50
|
-
' const hookRoot =
|
|
49
|
+
' const hookRoot = papersRoot(raw ?? "{}");',
|
|
50
|
+
' const hookRoot = raw === null ? "papers" : (() => { try { return JSON.parse(raw)?.papersDir ?? "papers"; } catch { return "papers"; } })();',
|
|
51
51
|
],
|
|
52
52
|
],
|
|
53
53
|
},
|
|
@@ -115,18 +115,18 @@ process.exit(
|
|
|
115
115
|
],
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
|
-
name: "
|
|
118
|
+
name: "an unparsable paperlint.json is reported as all-clear",
|
|
119
119
|
harness: HARNESS,
|
|
120
|
-
expect: "
|
|
120
|
+
expect: "an unparsable paperlint.json — a FAILURE",
|
|
121
121
|
disables:
|
|
122
|
-
"the
|
|
123
|
-
"
|
|
124
|
-
"
|
|
122
|
+
"the one failure left in the settings file: while it does not parse, the edit guard " +
|
|
123
|
+
"refuses every Bash command. A doctor that counts it as fine sends the reader looking " +
|
|
124
|
+
"everywhere but the file",
|
|
125
125
|
edits: [
|
|
126
126
|
[
|
|
127
127
|
SRC,
|
|
128
|
-
|
|
129
|
-
"
|
|
128
|
+
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 1,",
|
|
129
|
+
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 0,",
|
|
130
130
|
],
|
|
131
131
|
],
|
|
132
132
|
},
|
package/src/doctor.ts
CHANGED
|
@@ -29,19 +29,11 @@ import { spawnSync } from "node:child_process";
|
|
|
29
29
|
// the hook itself rather than at a shared module.
|
|
30
30
|
import {
|
|
31
31
|
papersRoot,
|
|
32
|
-
CONFIG_KEY,
|
|
33
32
|
DEFAULT_PAPERS_ROOT,
|
|
34
33
|
PAPERS_DIR_FIELD,
|
|
35
|
-
|
|
34
|
+
CONFIG_FILE,
|
|
36
35
|
} from "../hooks/paper-edit-guard.hook.mjs";
|
|
37
|
-
import {
|
|
38
|
-
LEGACY_CONFIG_KEY,
|
|
39
|
-
LEGACY_KEY_MESSAGE,
|
|
40
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
41
|
-
PAPER_SETTINGS_FILE,
|
|
42
|
-
declaredSettings,
|
|
43
|
-
} from "../lib/paper-config.mjs";
|
|
44
|
-
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
36
|
+
import { PAPER_MARKERS } from "./build.ts";
|
|
45
37
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
46
38
|
import { doctorHooks } from "./hooks-settings.ts";
|
|
47
39
|
|
|
@@ -148,51 +140,44 @@ export function detectPapers(cwd: string, depth = 2): string[] {
|
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
/**
|
|
151
|
-
* The
|
|
152
|
-
*
|
|
143
|
+
* The root `paperlint.json`'s verdict. Absent, or silent on `papersDir`, is fine: the default is
|
|
144
|
+
* `papers`, and the CLI and the hooks take it alike. Only a file that does not parse is a failure —
|
|
145
|
+
* the edit guard refuses every Bash command while it cannot read it.
|
|
153
146
|
*/
|
|
154
|
-
function declarationVerdict(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (found.legacy) out.push(` ⚠ ${LEGACY_KEY_MESSAGE}`);
|
|
170
|
-
// The old field name is a failure, not a warning: every reader refuses it.
|
|
171
|
-
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD))
|
|
147
|
+
function declarationVerdict(raw: string | null): {
|
|
148
|
+
lines: string[];
|
|
149
|
+
bad: number;
|
|
150
|
+
} {
|
|
151
|
+
if (raw === null)
|
|
152
|
+
return {
|
|
153
|
+
lines: [
|
|
154
|
+
` ✓ no ${CONFIG_FILE} — every setting has its default; papers are in "${DEFAULT_PAPERS_ROOT}"`,
|
|
155
|
+
],
|
|
156
|
+
bad: 0,
|
|
157
|
+
};
|
|
158
|
+
let settings: unknown;
|
|
159
|
+
try {
|
|
160
|
+
settings = JSON.parse(raw);
|
|
161
|
+
} catch (e) {
|
|
172
162
|
return {
|
|
173
163
|
lines: [
|
|
174
|
-
|
|
175
|
-
`
|
|
164
|
+
` ✗ ${CONFIG_FILE} is not valid JSON: ${(e as Error).message}`,
|
|
165
|
+
` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,
|
|
176
166
|
],
|
|
177
167
|
bad: 1,
|
|
178
168
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
else
|
|
192
|
-
out.push(
|
|
193
|
-
` ✓ package.json → ${key}.${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
|
|
194
|
-
);
|
|
195
|
-
return { lines: out, bad: 0 };
|
|
169
|
+
}
|
|
170
|
+
const declared = (settings as Record<string, unknown> | null)?.[
|
|
171
|
+
PAPERS_DIR_FIELD
|
|
172
|
+
];
|
|
173
|
+
return {
|
|
174
|
+
lines: [
|
|
175
|
+
declared === undefined
|
|
176
|
+
? ` ✓ ${CONFIG_FILE} names no ${PAPERS_DIR_FIELD} — the default "${DEFAULT_PAPERS_ROOT}"`
|
|
177
|
+
: ` ✓ ${CONFIG_FILE} → ${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
|
|
178
|
+
],
|
|
179
|
+
bad: 0,
|
|
180
|
+
};
|
|
196
181
|
}
|
|
197
182
|
|
|
198
183
|
/**
|
|
@@ -235,28 +220,6 @@ function papersVerdict(
|
|
|
235
220
|
return { lines: out, bad };
|
|
236
221
|
}
|
|
237
222
|
|
|
238
|
-
/**
|
|
239
|
-
* A pre-2.1.0 `venue.json` left in a paper: it is no longer read, so that paper's venue checks
|
|
240
|
-
* silently would not run if nothing named it. `init` moves it.
|
|
241
|
-
*/
|
|
242
|
-
function leftoverSettingsVerdict(
|
|
243
|
-
root: string,
|
|
244
|
-
papersDir: string | null,
|
|
245
|
-
): { lines: string[]; bad: number } {
|
|
246
|
-
if (papersDir === null) return { lines: [], bad: 0 };
|
|
247
|
-
const lines = papersIn(resolve(root, papersDir), [
|
|
248
|
-
...PAPER_MARKERS,
|
|
249
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
250
|
-
])
|
|
251
|
-
.map((dir) => join(dir, LEGACY_PAPER_SETTINGS_FILE))
|
|
252
|
-
.filter((f) => existsSync(f))
|
|
253
|
-
.map(
|
|
254
|
-
(f) =>
|
|
255
|
-
` ✗ ${relative(root, f)} is no longer read — paperlint 2.1.0 renamed it ${PAPER_SETTINGS_FILE}, so this paper's venue checks do not run. \`npx paperlint init\` moves it`,
|
|
256
|
-
);
|
|
257
|
-
return { lines, bad: lines.length };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
223
|
export interface DoctorOptions {
|
|
261
224
|
log?: typeof console.log;
|
|
262
225
|
cwd?: string;
|
|
@@ -285,7 +248,7 @@ export function doctor({
|
|
|
285
248
|
skillLinks = (r: string) => linkSkills(r, { write: false }),
|
|
286
249
|
}: DoctorOptions = {}): number {
|
|
287
250
|
const root = projectDir ?? cwd;
|
|
288
|
-
const
|
|
251
|
+
const configPath = join(root, CONFIG_FILE);
|
|
289
252
|
const out: string[] = [
|
|
290
253
|
"",
|
|
291
254
|
"paperlint doctor — what is wired, and what only looks wired",
|
|
@@ -293,22 +256,15 @@ export function doctor({
|
|
|
293
256
|
];
|
|
294
257
|
let bad = 0;
|
|
295
258
|
|
|
296
|
-
out.push("
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
` the hooks read their papers directory from there and refuse without it`,
|
|
302
|
-
);
|
|
303
|
-
bad++;
|
|
304
|
-
} else {
|
|
305
|
-
const declaration = declarationVerdict(rawPkg);
|
|
306
|
-
out.push(...declaration.lines);
|
|
307
|
-
bad += declaration.bad;
|
|
308
|
-
}
|
|
259
|
+
out.push("settings");
|
|
260
|
+
const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
|
|
261
|
+
const declaration = declarationVerdict(raw);
|
|
262
|
+
out.push(...declaration.lines);
|
|
263
|
+
bad += declaration.bad;
|
|
309
264
|
|
|
310
265
|
out.push("", "papers directory");
|
|
311
|
-
|
|
266
|
+
// The hook is given what its provider gives it: the file, or `{}` when there is none.
|
|
267
|
+
const hookRoot = papersRoot(raw ?? "{}");
|
|
312
268
|
const hookSays = typeof hookRoot === "string" ? hookRoot : null;
|
|
313
269
|
out.push(
|
|
314
270
|
` the CLI will lint ${cliPapers ?? "(nothing — no declaration found)"}`,
|
|
@@ -317,9 +273,8 @@ export function doctor({
|
|
|
317
273
|
` the hooks will guard ${hookSays ?? "(nothing — the guard refuses and says why on first use)"}`,
|
|
318
274
|
);
|
|
319
275
|
const verdict = papersVerdict(root, cliPapers, hookSays);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
bad += verdict.bad + leftover.bad;
|
|
276
|
+
out.push(...verdict.lines);
|
|
277
|
+
bad += verdict.bad;
|
|
323
278
|
|
|
324
279
|
// A skill that is not linked is ADVISORY, like a missing program: `paperlint lint`, the hooks and CI
|
|
325
280
|
// work without it, and an entry of the same name that `init` refused to replace is the
|
package/src/facts-file.test.ts
CHANGED
|
@@ -272,13 +272,6 @@ test("declaredVenue: the label is the preset's, and an extends that resolves now
|
|
|
272
272
|
assert.throws(() => declaredVenue(typo, PAPER), /shipped presets/);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
test("declaredVenue: a venue.json alone is refused with the command that moves it, not read", () => {
|
|
276
|
-
const files = memoryFiles({
|
|
277
|
-
[`${PAPER}/venue.json`]: '{"venue":"agenticdev"}',
|
|
278
|
-
});
|
|
279
|
-
assert.throws(() => declaredVenue(files, PAPER), /npx paperlint init/);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
275
|
test("declaredVenue: a paperlint.json with an unknown key is refused, naming the key", () => {
|
|
283
276
|
const files = memoryFiles({
|
|
284
277
|
[`${PAPER}/paperlint.json`]: '{"venu":"agenticdev"}',
|
package/src/facts-file.ts
CHANGED
|
@@ -75,8 +75,8 @@ export interface VenueDecl {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* What a paper's `paperlint.json` declares, or null when it has none. THROWS with a one-line reason
|
|
78
|
-
* when the file does not parse
|
|
79
|
-
*
|
|
78
|
+
* when the file does not parse or its `extends` does not resolve: the build reports that as its
|
|
79
|
+
* `facts` failure rather than building against no venue.
|
|
80
80
|
*/
|
|
81
81
|
export function declaredVenue(
|
|
82
82
|
files: Files,
|
|
@@ -36,7 +36,6 @@ const {
|
|
|
36
36
|
shippedWiring,
|
|
37
37
|
wireHooks,
|
|
38
38
|
wiredCounts,
|
|
39
|
-
pluginEnabledHere,
|
|
40
39
|
doctorHooks,
|
|
41
40
|
readSettings,
|
|
42
41
|
MANAGED_BY,
|
|
@@ -83,36 +82,26 @@ try {
|
|
|
83
82
|
// ── which spelling runs which hook ─────────────────────────────────────────────────────
|
|
84
83
|
const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
|
|
85
84
|
const cases = [
|
|
86
|
-
[ours, { name: "paper-edit-guard", ours: true
|
|
85
|
+
[ours, { name: "paper-edit-guard", ours: true }],
|
|
87
86
|
[
|
|
88
87
|
`node "$CLAUDE_PROJECT_DIR/${MANAGED_BY}" hook paper-status-gates`,
|
|
89
|
-
{ name: "paper-status-gates", ours: true
|
|
90
|
-
],
|
|
91
|
-
// Guards: what an older install wrote is recognised as ours-but-stale. Spelled literally, not
|
|
92
|
-
// built from a constant: these are what 1.x and 2.0.0 actually put in users' settings files.
|
|
93
|
-
[
|
|
94
|
-
`node "$CLAUDE_PROJECT_DIR/node_modules/research-paper-pipeline/bin/rpp.mjs" hook paper-edit-guard`,
|
|
95
|
-
{ name: "paper-edit-guard", ours: false, legacy: true },
|
|
96
|
-
],
|
|
97
|
-
[
|
|
98
|
-
`node "\${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs" hook paper-status-gates`,
|
|
99
|
-
{ name: "paper-status-gates", ours: false, legacy: true },
|
|
88
|
+
{ name: "paper-status-gates", ours: true },
|
|
100
89
|
],
|
|
101
90
|
[
|
|
102
91
|
`node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/paperlint/hooks/paper-edit-guard.hook.mjs"`,
|
|
103
|
-
{ name: "paper-edit-guard", ours: false
|
|
92
|
+
{ name: "paper-edit-guard", ours: false },
|
|
104
93
|
],
|
|
105
94
|
[
|
|
106
95
|
`npx paperlint hook paper-skills-nudge`,
|
|
107
|
-
{ name: "paper-skills-nudge", ours: false
|
|
96
|
+
{ name: "paper-skills-nudge", ours: false },
|
|
108
97
|
],
|
|
109
98
|
[
|
|
110
99
|
`npx paperlint hook paper-skills-nudge`,
|
|
111
|
-
{ name: "paper-skills-nudge", ours: false
|
|
100
|
+
{ name: "paper-skills-nudge", ours: false },
|
|
112
101
|
],
|
|
113
102
|
[
|
|
114
103
|
`node /abs/proj/node_modules/paperlint/bin/paperlint.mjs hook paper-edit-guard`,
|
|
115
|
-
{ name: "paper-edit-guard", ours: false
|
|
104
|
+
{ name: "paper-edit-guard", ours: false },
|
|
116
105
|
],
|
|
117
106
|
[`node my-own-lint.mjs`, null],
|
|
118
107
|
[`node node_modules/paperlint/bin/paperlint.mjs lint`, null],
|
|
@@ -158,46 +147,6 @@ try {
|
|
|
158
147
|
);
|
|
159
148
|
}
|
|
160
149
|
|
|
161
|
-
// ── what an older install wrote is migrated, the user's own command kept ─────────────────
|
|
162
|
-
// Two real histories, spelled literally: 1.x wrote the old package directory, 2.0.0 wrote the
|
|
163
|
-
// new directory with the old entry file name. Neither file exists after an upgrade.
|
|
164
|
-
for (const [release, stale] of [
|
|
165
|
-
["1.x", "node_modules/research-paper-pipeline/bin/rpp.mjs"],
|
|
166
|
-
["2.0.0", "node_modules/paperlint/bin/rpp.mjs"],
|
|
167
|
-
]) {
|
|
168
|
-
const legacyWired = JSON.parse(
|
|
169
|
-
JSON.stringify(merge({}, wiring.compiled, MANAGED_BY)).replaceAll(
|
|
170
|
-
MANAGED_BY,
|
|
171
|
-
stale,
|
|
172
|
-
),
|
|
173
|
-
);
|
|
174
|
-
legacyWired.hooks.PostToolUse[0].hooks.push({
|
|
175
|
-
type: "command",
|
|
176
|
-
command: "node my-own-lint.mjs",
|
|
177
|
-
});
|
|
178
|
-
const dir = project(`legacy-${release}`, legacyWired);
|
|
179
|
-
const before = doctorHooks(dir, wiring).join("\n");
|
|
180
|
-
check(
|
|
181
|
-
`🔴 doctor names hook commands ${release} left behind — the missing file, and the fix`,
|
|
182
|
-
before.includes(`run ${stale}, which this version does not install`) &&
|
|
183
|
-
/npx paperlint init` replaces them/.test(before),
|
|
184
|
-
);
|
|
185
|
-
const r = wireHooks(dir, merge, wiring);
|
|
186
|
-
const s = JSON.parse(text(dir));
|
|
187
|
-
const counts = [...wiredCounts(s, wiring.names).values()];
|
|
188
|
-
check(
|
|
189
|
-
`🔴 init replaces what ${release} wrote: every hook wired once, none left at the old path`,
|
|
190
|
-
r.status === "written" &&
|
|
191
|
-
r.replaced === wiring.names.length &&
|
|
192
|
-
counts.every((c) => c.ours === 1 && c.legacy === 0 && c.other === 0) &&
|
|
193
|
-
!text(dir).includes(stale),
|
|
194
|
-
);
|
|
195
|
-
check(
|
|
196
|
-
`and the user's own command in the same matcher survives the ${release} migration`,
|
|
197
|
-
text(dir).includes("node my-own-lint.mjs"),
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
150
|
// ── the user's own hooks and keys survive ──────────────────────────────────────────────
|
|
202
151
|
{
|
|
203
152
|
const dir = project("user", {
|
|
@@ -250,7 +199,7 @@ try {
|
|
|
250
199
|
{
|
|
251
200
|
type: "command",
|
|
252
201
|
command:
|
|
253
|
-
'node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/
|
|
202
|
+
'node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/paperlint/hooks/paper-edit-guard.hook.mjs"',
|
|
254
203
|
},
|
|
255
204
|
],
|
|
256
205
|
},
|
|
@@ -296,25 +245,6 @@ try {
|
|
|
296
245
|
);
|
|
297
246
|
}
|
|
298
247
|
|
|
299
|
-
// ── the plugin, enabled by the project ─────────────────────────────────────────────────
|
|
300
|
-
check(
|
|
301
|
-
"the project's enabledPlugins entry for this package is found",
|
|
302
|
-
pluginEnabledHere({
|
|
303
|
-
enabledPlugins: {
|
|
304
|
-
"research-paper-pipeline@research-paper-pipeline": true,
|
|
305
|
-
"other@x": true,
|
|
306
|
-
},
|
|
307
|
-
}).join() === "research-paper-pipeline@research-paper-pipeline",
|
|
308
|
-
);
|
|
309
|
-
check(
|
|
310
|
-
"a disabled entry does not count",
|
|
311
|
-
pluginEnabledHere({
|
|
312
|
-
enabledPlugins: {
|
|
313
|
-
"research-paper-pipeline@research-paper-pipeline": false,
|
|
314
|
-
},
|
|
315
|
-
}).length === 0,
|
|
316
|
-
);
|
|
317
|
-
|
|
318
248
|
// ── doctor's section ───────────────────────────────────────────────────────────────────
|
|
319
249
|
const doc = (settings) => {
|
|
320
250
|
const dir = project(`doc-${String(n)}`, settings);
|
|
@@ -361,21 +291,6 @@ try {
|
|
|
361
291
|
handPartial,
|
|
362
292
|
) && /writes nothing then/.test(handPartial),
|
|
363
293
|
);
|
|
364
|
-
const pluginText = doc({
|
|
365
|
-
...wiredOnce,
|
|
366
|
-
enabledPlugins: { "research-paper-pipeline@research-paper-pipeline": true },
|
|
367
|
-
});
|
|
368
|
-
check(
|
|
369
|
-
"the project also enabling the plugin → told to uninstall it, with the command",
|
|
370
|
-
/ALSO enables the plugin/.test(pluginText) &&
|
|
371
|
-
/\/plugin uninstall research-paper-pipeline@research-paper-pipeline/.test(
|
|
372
|
-
pluginText,
|
|
373
|
-
),
|
|
374
|
-
);
|
|
375
|
-
check(
|
|
376
|
-
"🔴 and it says it cannot see a USER-scope plugin, rather than implying there is none",
|
|
377
|
-
/USER scope is not visible from here/.test(doc({})),
|
|
378
|
-
);
|
|
379
294
|
check(
|
|
380
295
|
"an unparsable file is named in doctor, not crashed on",
|
|
381
296
|
/does not parse/.test(
|
|
@@ -24,12 +24,12 @@ process.exit(
|
|
|
24
24
|
harness: HARNESS,
|
|
25
25
|
// The spelling truth table is the first owner: `npx paperlint hook …` must read as NOT ours,
|
|
26
26
|
// and that row is where it goes red.
|
|
27
|
-
expect: '→ {"name":"paper-
|
|
27
|
+
expect: '→ {"name":"paper-skills-nudge","ours":false}',
|
|
28
28
|
disables:
|
|
29
29
|
"the duplicate guard for every bin spelling but ours. A hook wired by hand as " +
|
|
30
30
|
"`npx paperlint hook …` or through an absolute path to the bin reads as ours, init merges its own " +
|
|
31
31
|
"copy beside it, and the hook runs twice per event",
|
|
32
|
-
edits: [[SRC, "
|
|
32
|
+
edits: [[SRC, "ours: t === MANAGED_BY,", "ours: true,"]],
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
name: "another spelling no longer stops the write",
|
|
@@ -94,21 +94,6 @@ process.exit(
|
|
|
94
94
|
"same thing again — a loop with no exit",
|
|
95
95
|
edits: [[SRC, " const remedy = handWired", " const remedy = false"]],
|
|
96
96
|
},
|
|
97
|
-
{
|
|
98
|
-
name: "a disabled plugin counts as enabled",
|
|
99
|
-
harness: HARNESS,
|
|
100
|
-
expect: "a disabled entry does not count",
|
|
101
|
-
disables:
|
|
102
|
-
"the plugin warning's precision. A project that already switched the plugin off would " +
|
|
103
|
-
"be told to uninstall it",
|
|
104
|
-
edits: [
|
|
105
|
-
[
|
|
106
|
-
SRC,
|
|
107
|
-
' ([id, on]) => on === true && id.split("@")[0] === LEGACY_PACKAGE_NAME,',
|
|
108
|
-
' ([id]) => id.split("@")[0] === LEGACY_PACKAGE_NAME,',
|
|
109
|
-
],
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
97
|
],
|
|
113
98
|
}),
|
|
114
99
|
);
|