liteagents 2.22.0 → 2.22.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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/packages/ampcode/commands/docs-builder/docs-builder.cjs +25 -8
- package/packages/claude/commands/docs-builder/docs-builder.cjs +25 -8
- package/packages/droid/commands/docs-builder/docs-builder.cjs +25 -8
- package/packages/opencode/command/docs-builder/docs-builder.cjs +25 -8
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [2.22.1] - 2026-09-01
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **`/docs-builder` — the trailing-newline phantom line is dropped from every line count.**
|
|
16
|
+
`text.split('\n')` returns a trailing empty element for any file ending in a newline, so
|
|
17
|
+
`lines.length` was one over the real count. That phantom line reached the index row's "N
|
|
18
|
+
lines" (every row +1), the last H2's line range (one line past EOF), `scan`'s outline.json
|
|
19
|
+
`s`/`e`/`lines`, the ledger's per-file line count, the cleanup cost estimate, and the PARTIAL
|
|
20
|
+
guard (a page one line short of `MIN_PAGE_LINES` passed as complete). Fixed with a
|
|
21
|
+
`splitLines()` helper applied at the 7 counting/bounding sites, deliberately not at the sites
|
|
22
|
+
that map-and-rejoin file text. `docs/index.md` regenerated. Mirrored across all four kits.
|
|
23
|
+
- **`/docs-builder` — an empty page now reports PARTIAL instead of crashing `plan`.** A
|
|
24
|
+
regression from the fix above: `splitLines()` returns `[]` for a 0-byte file where the raw
|
|
25
|
+
split returned `['']`, so `pageStatus`'s unguarded `lines[0].trim()` threw a `TypeError` and
|
|
26
|
+
took `plan` down with it. An empty `.md` page is reachable (a touched placeholder, or
|
|
27
|
+
page-writing interrupted). Fixed at the indexing site — 0 lines is the correct count for an
|
|
28
|
+
empty file — so `pageStatus` guards on `lines.length` instead. Mirrored across all four kits.
|
|
29
|
+
|
|
12
30
|
## [2.22.0] - 2026-09-01
|
|
13
31
|
|
|
14
32
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "liteagents",
|
|
3
|
-
"version": "2.22.
|
|
3
|
+
"version": "2.22.1",
|
|
4
4
|
"description": "AI development toolkit with 11 specialized agents and 18 commands including live-canvas UI design with click-to-annotate feedback. Simple one-question installer for Claude, Opencode, Ampcode, and Droid.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -103,6 +103,18 @@ function fenceMask(lines) {
|
|
|
103
103
|
// prose), sentences()'s own regex strip, and checkCitations/checkLinks doing none at all — so
|
|
104
104
|
// a page documenting the citation/link syntax INSIDE a fence got its own example flagged as
|
|
105
105
|
// a real violation. One mechanism: mask with fenceMask(), drop the masked lines.
|
|
106
|
+
// `text.split('\n')` returns a trailing EMPTY element for any file ending in a newline —
|
|
107
|
+
// which is nearly every file — so `lines.length` is real_lines + 1. That phantom line reached
|
|
108
|
+
// the index row's "N lines", the last H2's range (one line past EOF) and scan()'s outline.json.
|
|
109
|
+
// Use this wherever lines are COUNTED or a range is BOUNDED. The raw `.split('\n')` is still
|
|
110
|
+
// correct where the array is mapped and re-joined back into file text (stripFences,
|
|
111
|
+
// replaceOutsideFences): dropping the element there would strip the file's final newline.
|
|
112
|
+
function splitLines(text) {
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
function stripFences(text) {
|
|
107
119
|
const lines = text.split('\n');
|
|
108
120
|
const mask = fenceMask(lines);
|
|
@@ -229,7 +241,7 @@ function scan(files) {
|
|
|
229
241
|
if (!files.length) die('usage: docs-builder.cjs scan <file.md...>');
|
|
230
242
|
const records = [];
|
|
231
243
|
for (const f of files) {
|
|
232
|
-
const lines = read(f)
|
|
244
|
+
const lines = splitLines(read(f));
|
|
233
245
|
const mask = fenceMask(lines);
|
|
234
246
|
const { h1, heads } = headings(lines, mask);
|
|
235
247
|
const h2s = heads.filter(h => h.lvl === 2);
|
|
@@ -522,8 +534,13 @@ const MIN_PAGE_LINES = 10;
|
|
|
522
534
|
function pageStatus(file) {
|
|
523
535
|
if (!fs.existsSync(file)) return 'TODO';
|
|
524
536
|
const txt = fs.readFileSync(file, 'utf8');
|
|
525
|
-
const lines = txt
|
|
526
|
-
|
|
537
|
+
const lines = splitLines(txt);
|
|
538
|
+
// splitLines() returns [] for a 0-byte file — 0 lines is the right COUNT, but it means
|
|
539
|
+
// lines[0] can be undefined, where the old raw split('\n') always yielded ['']. An empty
|
|
540
|
+
// page is reachable (a touched placeholder, or page-writing interrupted before it wrote
|
|
541
|
+
// anything) and must read as PARTIAL, not throw and take `plan` down with it.
|
|
542
|
+
const hasFrontmatter = lines.length > 0 && lines[0].trim() === '---'
|
|
543
|
+
&& lines.slice(1).some(l => l.trim() === '---');
|
|
527
544
|
return hasFrontmatter && lines.length >= MIN_PAGE_LINES ? 'done' : 'PARTIAL';
|
|
528
545
|
}
|
|
529
546
|
|
|
@@ -732,7 +749,7 @@ const ARCHIVE_WARN_ROWS = 100; // stated default, not measured — see docs-buil
|
|
|
732
749
|
// section a reader is being routed into, so its row stays H1 + line count + link only.
|
|
733
750
|
function indexRow(rel, dest, includeH2) {
|
|
734
751
|
const text = read(rel);
|
|
735
|
-
const lines = text
|
|
752
|
+
const lines = splitLines(text);
|
|
736
753
|
// Same headings()+fenceMask() path scan() uses -- no second parser -- so an H2 inside a
|
|
737
754
|
// ``` fence is masked out here exactly as it is there.
|
|
738
755
|
const mask = fenceMask(lines);
|
|
@@ -1398,7 +1415,7 @@ function ledger() {
|
|
|
1398
1415
|
const head = git(['rev-parse', 'HEAD'], 'reading HEAD (is this a git repo?)');
|
|
1399
1416
|
const docs = docFiles().map(f => ({
|
|
1400
1417
|
path: f,
|
|
1401
|
-
lines: read(f)
|
|
1418
|
+
lines: splitLines(read(f)).length,
|
|
1402
1419
|
sha256: sha(path.join(REPO, f)).slice(0, 16)
|
|
1403
1420
|
}));
|
|
1404
1421
|
const out = { sha: head, at: new Date().toISOString(),
|
|
@@ -1492,7 +1509,7 @@ function lint(files) {
|
|
|
1492
1509
|
if (!files.length) die('usage: docs-builder.cjs lint <file.md...>');
|
|
1493
1510
|
const sections = [];
|
|
1494
1511
|
for (const f of files) {
|
|
1495
|
-
const lines = read(f)
|
|
1512
|
+
const lines = splitLines(read(f));
|
|
1496
1513
|
const mask = fenceMask(lines);
|
|
1497
1514
|
let cur = null;
|
|
1498
1515
|
const close = i => { if (cur) { cur.e = i; cur.body = lines.slice(cur.s, i).join('\n'); } };
|
|
@@ -1665,7 +1682,7 @@ function isIncludeStub(lines) {
|
|
|
1665
1682
|
// but its size. Oversized is now orthogonal to sorting: a product doc that's too big is
|
|
1666
1683
|
// still a product doc.
|
|
1667
1684
|
function classifyDoc(rel, text) {
|
|
1668
|
-
const lines = text
|
|
1685
|
+
const lines = splitLines(text);
|
|
1669
1686
|
const mask = fenceMask(lines);
|
|
1670
1687
|
const { h1 } = headings(lines, mask);
|
|
1671
1688
|
const snip = snippet(lines, mask, 0, lines.length, 200);
|
|
@@ -2214,7 +2231,7 @@ function cleanup(files) {
|
|
|
2214
2231
|
+ `would overwrite that split's still-in-flight outline.json/labels.json. Finish it `
|
|
2215
2232
|
+ `first: write its remaining pages, then re-run \`cleanup-apply ${inFlight} ...\` until `
|
|
2216
2233
|
+ `it archives — THEN run \`cleanup ${file}\`.`);
|
|
2217
|
-
const lines = read(file)
|
|
2234
|
+
const lines = splitLines(read(file)).length;
|
|
2218
2235
|
const est = writeCostEstimate(1, lines);
|
|
2219
2236
|
console.log(`${file}: ${lines} lines`);
|
|
2220
2237
|
console.log(`est. write cost: $${est.toFixed(2)} (mid tier, floor assuming 1 page — the `
|
|
@@ -103,6 +103,18 @@ function fenceMask(lines) {
|
|
|
103
103
|
// prose), sentences()'s own regex strip, and checkCitations/checkLinks doing none at all — so
|
|
104
104
|
// a page documenting the citation/link syntax INSIDE a fence got its own example flagged as
|
|
105
105
|
// a real violation. One mechanism: mask with fenceMask(), drop the masked lines.
|
|
106
|
+
// `text.split('\n')` returns a trailing EMPTY element for any file ending in a newline —
|
|
107
|
+
// which is nearly every file — so `lines.length` is real_lines + 1. That phantom line reached
|
|
108
|
+
// the index row's "N lines", the last H2's range (one line past EOF) and scan()'s outline.json.
|
|
109
|
+
// Use this wherever lines are COUNTED or a range is BOUNDED. The raw `.split('\n')` is still
|
|
110
|
+
// correct where the array is mapped and re-joined back into file text (stripFences,
|
|
111
|
+
// replaceOutsideFences): dropping the element there would strip the file's final newline.
|
|
112
|
+
function splitLines(text) {
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
function stripFences(text) {
|
|
107
119
|
const lines = text.split('\n');
|
|
108
120
|
const mask = fenceMask(lines);
|
|
@@ -229,7 +241,7 @@ function scan(files) {
|
|
|
229
241
|
if (!files.length) die('usage: docs-builder.cjs scan <file.md...>');
|
|
230
242
|
const records = [];
|
|
231
243
|
for (const f of files) {
|
|
232
|
-
const lines = read(f)
|
|
244
|
+
const lines = splitLines(read(f));
|
|
233
245
|
const mask = fenceMask(lines);
|
|
234
246
|
const { h1, heads } = headings(lines, mask);
|
|
235
247
|
const h2s = heads.filter(h => h.lvl === 2);
|
|
@@ -522,8 +534,13 @@ const MIN_PAGE_LINES = 10;
|
|
|
522
534
|
function pageStatus(file) {
|
|
523
535
|
if (!fs.existsSync(file)) return 'TODO';
|
|
524
536
|
const txt = fs.readFileSync(file, 'utf8');
|
|
525
|
-
const lines = txt
|
|
526
|
-
|
|
537
|
+
const lines = splitLines(txt);
|
|
538
|
+
// splitLines() returns [] for a 0-byte file — 0 lines is the right COUNT, but it means
|
|
539
|
+
// lines[0] can be undefined, where the old raw split('\n') always yielded ['']. An empty
|
|
540
|
+
// page is reachable (a touched placeholder, or page-writing interrupted before it wrote
|
|
541
|
+
// anything) and must read as PARTIAL, not throw and take `plan` down with it.
|
|
542
|
+
const hasFrontmatter = lines.length > 0 && lines[0].trim() === '---'
|
|
543
|
+
&& lines.slice(1).some(l => l.trim() === '---');
|
|
527
544
|
return hasFrontmatter && lines.length >= MIN_PAGE_LINES ? 'done' : 'PARTIAL';
|
|
528
545
|
}
|
|
529
546
|
|
|
@@ -732,7 +749,7 @@ const ARCHIVE_WARN_ROWS = 100; // stated default, not measured — see docs-buil
|
|
|
732
749
|
// section a reader is being routed into, so its row stays H1 + line count + link only.
|
|
733
750
|
function indexRow(rel, dest, includeH2) {
|
|
734
751
|
const text = read(rel);
|
|
735
|
-
const lines = text
|
|
752
|
+
const lines = splitLines(text);
|
|
736
753
|
// Same headings()+fenceMask() path scan() uses -- no second parser -- so an H2 inside a
|
|
737
754
|
// ``` fence is masked out here exactly as it is there.
|
|
738
755
|
const mask = fenceMask(lines);
|
|
@@ -1398,7 +1415,7 @@ function ledger() {
|
|
|
1398
1415
|
const head = git(['rev-parse', 'HEAD'], 'reading HEAD (is this a git repo?)');
|
|
1399
1416
|
const docs = docFiles().map(f => ({
|
|
1400
1417
|
path: f,
|
|
1401
|
-
lines: read(f)
|
|
1418
|
+
lines: splitLines(read(f)).length,
|
|
1402
1419
|
sha256: sha(path.join(REPO, f)).slice(0, 16)
|
|
1403
1420
|
}));
|
|
1404
1421
|
const out = { sha: head, at: new Date().toISOString(),
|
|
@@ -1492,7 +1509,7 @@ function lint(files) {
|
|
|
1492
1509
|
if (!files.length) die('usage: docs-builder.cjs lint <file.md...>');
|
|
1493
1510
|
const sections = [];
|
|
1494
1511
|
for (const f of files) {
|
|
1495
|
-
const lines = read(f)
|
|
1512
|
+
const lines = splitLines(read(f));
|
|
1496
1513
|
const mask = fenceMask(lines);
|
|
1497
1514
|
let cur = null;
|
|
1498
1515
|
const close = i => { if (cur) { cur.e = i; cur.body = lines.slice(cur.s, i).join('\n'); } };
|
|
@@ -1665,7 +1682,7 @@ function isIncludeStub(lines) {
|
|
|
1665
1682
|
// but its size. Oversized is now orthogonal to sorting: a product doc that's too big is
|
|
1666
1683
|
// still a product doc.
|
|
1667
1684
|
function classifyDoc(rel, text) {
|
|
1668
|
-
const lines = text
|
|
1685
|
+
const lines = splitLines(text);
|
|
1669
1686
|
const mask = fenceMask(lines);
|
|
1670
1687
|
const { h1 } = headings(lines, mask);
|
|
1671
1688
|
const snip = snippet(lines, mask, 0, lines.length, 200);
|
|
@@ -2214,7 +2231,7 @@ function cleanup(files) {
|
|
|
2214
2231
|
+ `would overwrite that split's still-in-flight outline.json/labels.json. Finish it `
|
|
2215
2232
|
+ `first: write its remaining pages, then re-run \`cleanup-apply ${inFlight} ...\` until `
|
|
2216
2233
|
+ `it archives — THEN run \`cleanup ${file}\`.`);
|
|
2217
|
-
const lines = read(file)
|
|
2234
|
+
const lines = splitLines(read(file)).length;
|
|
2218
2235
|
const est = writeCostEstimate(1, lines);
|
|
2219
2236
|
console.log(`${file}: ${lines} lines`);
|
|
2220
2237
|
console.log(`est. write cost: $${est.toFixed(2)} (mid tier, floor assuming 1 page — the `
|
|
@@ -103,6 +103,18 @@ function fenceMask(lines) {
|
|
|
103
103
|
// prose), sentences()'s own regex strip, and checkCitations/checkLinks doing none at all — so
|
|
104
104
|
// a page documenting the citation/link syntax INSIDE a fence got its own example flagged as
|
|
105
105
|
// a real violation. One mechanism: mask with fenceMask(), drop the masked lines.
|
|
106
|
+
// `text.split('\n')` returns a trailing EMPTY element for any file ending in a newline —
|
|
107
|
+
// which is nearly every file — so `lines.length` is real_lines + 1. That phantom line reached
|
|
108
|
+
// the index row's "N lines", the last H2's range (one line past EOF) and scan()'s outline.json.
|
|
109
|
+
// Use this wherever lines are COUNTED or a range is BOUNDED. The raw `.split('\n')` is still
|
|
110
|
+
// correct where the array is mapped and re-joined back into file text (stripFences,
|
|
111
|
+
// replaceOutsideFences): dropping the element there would strip the file's final newline.
|
|
112
|
+
function splitLines(text) {
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
function stripFences(text) {
|
|
107
119
|
const lines = text.split('\n');
|
|
108
120
|
const mask = fenceMask(lines);
|
|
@@ -229,7 +241,7 @@ function scan(files) {
|
|
|
229
241
|
if (!files.length) die('usage: docs-builder.cjs scan <file.md...>');
|
|
230
242
|
const records = [];
|
|
231
243
|
for (const f of files) {
|
|
232
|
-
const lines = read(f)
|
|
244
|
+
const lines = splitLines(read(f));
|
|
233
245
|
const mask = fenceMask(lines);
|
|
234
246
|
const { h1, heads } = headings(lines, mask);
|
|
235
247
|
const h2s = heads.filter(h => h.lvl === 2);
|
|
@@ -522,8 +534,13 @@ const MIN_PAGE_LINES = 10;
|
|
|
522
534
|
function pageStatus(file) {
|
|
523
535
|
if (!fs.existsSync(file)) return 'TODO';
|
|
524
536
|
const txt = fs.readFileSync(file, 'utf8');
|
|
525
|
-
const lines = txt
|
|
526
|
-
|
|
537
|
+
const lines = splitLines(txt);
|
|
538
|
+
// splitLines() returns [] for a 0-byte file — 0 lines is the right COUNT, but it means
|
|
539
|
+
// lines[0] can be undefined, where the old raw split('\n') always yielded ['']. An empty
|
|
540
|
+
// page is reachable (a touched placeholder, or page-writing interrupted before it wrote
|
|
541
|
+
// anything) and must read as PARTIAL, not throw and take `plan` down with it.
|
|
542
|
+
const hasFrontmatter = lines.length > 0 && lines[0].trim() === '---'
|
|
543
|
+
&& lines.slice(1).some(l => l.trim() === '---');
|
|
527
544
|
return hasFrontmatter && lines.length >= MIN_PAGE_LINES ? 'done' : 'PARTIAL';
|
|
528
545
|
}
|
|
529
546
|
|
|
@@ -732,7 +749,7 @@ const ARCHIVE_WARN_ROWS = 100; // stated default, not measured — see docs-buil
|
|
|
732
749
|
// section a reader is being routed into, so its row stays H1 + line count + link only.
|
|
733
750
|
function indexRow(rel, dest, includeH2) {
|
|
734
751
|
const text = read(rel);
|
|
735
|
-
const lines = text
|
|
752
|
+
const lines = splitLines(text);
|
|
736
753
|
// Same headings()+fenceMask() path scan() uses -- no second parser -- so an H2 inside a
|
|
737
754
|
// ``` fence is masked out here exactly as it is there.
|
|
738
755
|
const mask = fenceMask(lines);
|
|
@@ -1398,7 +1415,7 @@ function ledger() {
|
|
|
1398
1415
|
const head = git(['rev-parse', 'HEAD'], 'reading HEAD (is this a git repo?)');
|
|
1399
1416
|
const docs = docFiles().map(f => ({
|
|
1400
1417
|
path: f,
|
|
1401
|
-
lines: read(f)
|
|
1418
|
+
lines: splitLines(read(f)).length,
|
|
1402
1419
|
sha256: sha(path.join(REPO, f)).slice(0, 16)
|
|
1403
1420
|
}));
|
|
1404
1421
|
const out = { sha: head, at: new Date().toISOString(),
|
|
@@ -1492,7 +1509,7 @@ function lint(files) {
|
|
|
1492
1509
|
if (!files.length) die('usage: docs-builder.cjs lint <file.md...>');
|
|
1493
1510
|
const sections = [];
|
|
1494
1511
|
for (const f of files) {
|
|
1495
|
-
const lines = read(f)
|
|
1512
|
+
const lines = splitLines(read(f));
|
|
1496
1513
|
const mask = fenceMask(lines);
|
|
1497
1514
|
let cur = null;
|
|
1498
1515
|
const close = i => { if (cur) { cur.e = i; cur.body = lines.slice(cur.s, i).join('\n'); } };
|
|
@@ -1665,7 +1682,7 @@ function isIncludeStub(lines) {
|
|
|
1665
1682
|
// but its size. Oversized is now orthogonal to sorting: a product doc that's too big is
|
|
1666
1683
|
// still a product doc.
|
|
1667
1684
|
function classifyDoc(rel, text) {
|
|
1668
|
-
const lines = text
|
|
1685
|
+
const lines = splitLines(text);
|
|
1669
1686
|
const mask = fenceMask(lines);
|
|
1670
1687
|
const { h1 } = headings(lines, mask);
|
|
1671
1688
|
const snip = snippet(lines, mask, 0, lines.length, 200);
|
|
@@ -2214,7 +2231,7 @@ function cleanup(files) {
|
|
|
2214
2231
|
+ `would overwrite that split's still-in-flight outline.json/labels.json. Finish it `
|
|
2215
2232
|
+ `first: write its remaining pages, then re-run \`cleanup-apply ${inFlight} ...\` until `
|
|
2216
2233
|
+ `it archives — THEN run \`cleanup ${file}\`.`);
|
|
2217
|
-
const lines = read(file)
|
|
2234
|
+
const lines = splitLines(read(file)).length;
|
|
2218
2235
|
const est = writeCostEstimate(1, lines);
|
|
2219
2236
|
console.log(`${file}: ${lines} lines`);
|
|
2220
2237
|
console.log(`est. write cost: $${est.toFixed(2)} (mid tier, floor assuming 1 page — the `
|
|
@@ -103,6 +103,18 @@ function fenceMask(lines) {
|
|
|
103
103
|
// prose), sentences()'s own regex strip, and checkCitations/checkLinks doing none at all — so
|
|
104
104
|
// a page documenting the citation/link syntax INSIDE a fence got its own example flagged as
|
|
105
105
|
// a real violation. One mechanism: mask with fenceMask(), drop the masked lines.
|
|
106
|
+
// `text.split('\n')` returns a trailing EMPTY element for any file ending in a newline —
|
|
107
|
+
// which is nearly every file — so `lines.length` is real_lines + 1. That phantom line reached
|
|
108
|
+
// the index row's "N lines", the last H2's range (one line past EOF) and scan()'s outline.json.
|
|
109
|
+
// Use this wherever lines are COUNTED or a range is BOUNDED. The raw `.split('\n')` is still
|
|
110
|
+
// correct where the array is mapped and re-joined back into file text (stripFences,
|
|
111
|
+
// replaceOutsideFences): dropping the element there would strip the file's final newline.
|
|
112
|
+
function splitLines(text) {
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
function stripFences(text) {
|
|
107
119
|
const lines = text.split('\n');
|
|
108
120
|
const mask = fenceMask(lines);
|
|
@@ -229,7 +241,7 @@ function scan(files) {
|
|
|
229
241
|
if (!files.length) die('usage: docs-builder.cjs scan <file.md...>');
|
|
230
242
|
const records = [];
|
|
231
243
|
for (const f of files) {
|
|
232
|
-
const lines = read(f)
|
|
244
|
+
const lines = splitLines(read(f));
|
|
233
245
|
const mask = fenceMask(lines);
|
|
234
246
|
const { h1, heads } = headings(lines, mask);
|
|
235
247
|
const h2s = heads.filter(h => h.lvl === 2);
|
|
@@ -522,8 +534,13 @@ const MIN_PAGE_LINES = 10;
|
|
|
522
534
|
function pageStatus(file) {
|
|
523
535
|
if (!fs.existsSync(file)) return 'TODO';
|
|
524
536
|
const txt = fs.readFileSync(file, 'utf8');
|
|
525
|
-
const lines = txt
|
|
526
|
-
|
|
537
|
+
const lines = splitLines(txt);
|
|
538
|
+
// splitLines() returns [] for a 0-byte file — 0 lines is the right COUNT, but it means
|
|
539
|
+
// lines[0] can be undefined, where the old raw split('\n') always yielded ['']. An empty
|
|
540
|
+
// page is reachable (a touched placeholder, or page-writing interrupted before it wrote
|
|
541
|
+
// anything) and must read as PARTIAL, not throw and take `plan` down with it.
|
|
542
|
+
const hasFrontmatter = lines.length > 0 && lines[0].trim() === '---'
|
|
543
|
+
&& lines.slice(1).some(l => l.trim() === '---');
|
|
527
544
|
return hasFrontmatter && lines.length >= MIN_PAGE_LINES ? 'done' : 'PARTIAL';
|
|
528
545
|
}
|
|
529
546
|
|
|
@@ -732,7 +749,7 @@ const ARCHIVE_WARN_ROWS = 100; // stated default, not measured — see docs-buil
|
|
|
732
749
|
// section a reader is being routed into, so its row stays H1 + line count + link only.
|
|
733
750
|
function indexRow(rel, dest, includeH2) {
|
|
734
751
|
const text = read(rel);
|
|
735
|
-
const lines = text
|
|
752
|
+
const lines = splitLines(text);
|
|
736
753
|
// Same headings()+fenceMask() path scan() uses -- no second parser -- so an H2 inside a
|
|
737
754
|
// ``` fence is masked out here exactly as it is there.
|
|
738
755
|
const mask = fenceMask(lines);
|
|
@@ -1398,7 +1415,7 @@ function ledger() {
|
|
|
1398
1415
|
const head = git(['rev-parse', 'HEAD'], 'reading HEAD (is this a git repo?)');
|
|
1399
1416
|
const docs = docFiles().map(f => ({
|
|
1400
1417
|
path: f,
|
|
1401
|
-
lines: read(f)
|
|
1418
|
+
lines: splitLines(read(f)).length,
|
|
1402
1419
|
sha256: sha(path.join(REPO, f)).slice(0, 16)
|
|
1403
1420
|
}));
|
|
1404
1421
|
const out = { sha: head, at: new Date().toISOString(),
|
|
@@ -1492,7 +1509,7 @@ function lint(files) {
|
|
|
1492
1509
|
if (!files.length) die('usage: docs-builder.cjs lint <file.md...>');
|
|
1493
1510
|
const sections = [];
|
|
1494
1511
|
for (const f of files) {
|
|
1495
|
-
const lines = read(f)
|
|
1512
|
+
const lines = splitLines(read(f));
|
|
1496
1513
|
const mask = fenceMask(lines);
|
|
1497
1514
|
let cur = null;
|
|
1498
1515
|
const close = i => { if (cur) { cur.e = i; cur.body = lines.slice(cur.s, i).join('\n'); } };
|
|
@@ -1665,7 +1682,7 @@ function isIncludeStub(lines) {
|
|
|
1665
1682
|
// but its size. Oversized is now orthogonal to sorting: a product doc that's too big is
|
|
1666
1683
|
// still a product doc.
|
|
1667
1684
|
function classifyDoc(rel, text) {
|
|
1668
|
-
const lines = text
|
|
1685
|
+
const lines = splitLines(text);
|
|
1669
1686
|
const mask = fenceMask(lines);
|
|
1670
1687
|
const { h1 } = headings(lines, mask);
|
|
1671
1688
|
const snip = snippet(lines, mask, 0, lines.length, 200);
|
|
@@ -2214,7 +2231,7 @@ function cleanup(files) {
|
|
|
2214
2231
|
+ `would overwrite that split's still-in-flight outline.json/labels.json. Finish it `
|
|
2215
2232
|
+ `first: write its remaining pages, then re-run \`cleanup-apply ${inFlight} ...\` until `
|
|
2216
2233
|
+ `it archives — THEN run \`cleanup ${file}\`.`);
|
|
2217
|
-
const lines = read(file)
|
|
2234
|
+
const lines = splitLines(read(file)).length;
|
|
2218
2235
|
const est = writeCostEstimate(1, lines);
|
|
2219
2236
|
console.log(`${file}: ${lines} lines`);
|
|
2220
2237
|
console.log(`est. write cost: $${est.toFixed(2)} (mid tier, floor assuming 1 page — the `
|