@nightlab-agent/still-true 0.1.0 → 0.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/README.md +1 -1
- package/package.json +1 -1
- package/src/extract.mjs +42 -13
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Give it one or more Markdown files. For a bare `#12` it uses `--repo owner/name`
|
|
|
18
18
|
|
|
19
19
|
Set `GITHUB_TOKEN` for private repositories and a higher rate limit. Without one, GitHub allows 60 requests an hour. Needs Node 20 or newer. Behind a proxy, run with `NODE_USE_ENV_PROXY=1`, tested on Node 22; Node prints an experimental warning.
|
|
20
20
|
|
|
21
|
-
In a Markdown file with headings, it reads a section only if its heading starts with Now, Handoff, State, Status or Current, and any heading above it does too, or is the file's level 1 title. Plans and history stay out. Text before the first heading is read, and a file with no headings is read whole.
|
|
21
|
+
In a Markdown file with headings, it reads a section only if its heading starts with Now, Handoff, State, Status or Current, and any heading above it does too, or is the file's first level 1 heading. A first level 1 heading starting with History, Archive or Old is not a title. Plans and history stay out. Text before the first heading is read, and a file with no headings is read whole.
|
|
22
22
|
|
|
23
23
|
## What each line means
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nightlab-agent/still-true",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Check a handoff note against GitHub: finds what it says about PRs and issues and prints the claims that are no longer true. Exits 1 on a contradiction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "still-true": "bin/still-true.js" },
|
package/src/extract.mjs
CHANGED
|
@@ -9,10 +9,12 @@ const WAIT = new RegExp(`(?:^|\\s)(?:waits?|waiting)\\s+(?:on|for)\\s+(${TOKEN}|
|
|
|
9
9
|
const LABEL_LIST = new RegExp(`(?:^|:\\s*)(open|merged|closed)(?:\\s+(?:PRs|issues|pull requests))?:\\s*(${TOKEN}(?:\\s*(?:,|and)\\s*${TOKEN})*)$`, 'i');
|
|
10
10
|
|
|
11
11
|
const REF = /^(?:([a-z0-9-]+)\/)?([a-z0-9_.-]+)?#([1-9][0-9]*)$/i;
|
|
12
|
+
const PLANNED = /\b(?:plans?|planned|planning|proposed|intended|expected|ideas?|goals?|draft|to-?dos?|next|later|someday|old|previous|history|example)\b/i;
|
|
12
13
|
const CURRENT_HEADING = /^(?:handoff|state|status|now|current)\b(?!-)/i;
|
|
13
14
|
const PREAMBLE = /^(?:if|unless|once|after|when|until|yesterday|old note|example|suppose|imagine)\b/i;
|
|
14
15
|
const UNCERTAIN = /\b(?:probably|maybe|might|should|could|would|if|unless|when|until|need|get|think|believe|locally|re-?opened)\b|\bonce\s+(?!at\b)\S|n['’]t\b|\bby\s+(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday|tomorrow|next)\b/i;
|
|
15
16
|
const NONSTATE = /\b(?:(?:not|never|no longer)\s+(?:(?:yet|still|currently|already)\s+)?(?:open|merged|closed)|(?:was|were|used to be)\s+(?:(?:still|currently|already)\s+)?(?:open|closed)|(?:open|merged|closed)\s+or\s+(?:open|merged|closed))\b/i;
|
|
17
|
+
const NOT_WAITING = /\b(?:(?:not|never|no\s+longer|no\s+more|stop(?:s|ped)?|done|finished|quit|gave\s+up|nothing(?:\s+is)?|without|instead\s+of)\s+(?:(?:yet|still|currently|really)\s+)?(?:waits?|waiting)|(?:n['’]t)\s+(?:waits?|waiting))\b/i;
|
|
16
18
|
const QUALIFIED_STATE = /^\s+(?:to|into|for|or|after)\b/i;
|
|
17
19
|
// A later comma field must look like a state fact, not "open question".
|
|
18
20
|
const COMMA_STATE_TAIL = /^(?:$|\s+(?:\d|today\b|yesterday\b|last\b|since\b|by\b|as\s+a\s+duplicate\b))/i;
|
|
@@ -140,7 +142,7 @@ function claimsFromClause(quote, line) {
|
|
|
140
142
|
context.list = false;
|
|
141
143
|
}
|
|
142
144
|
const waiting = WAIT.exec(tail);
|
|
143
|
-
if (waiting) add(REF.test(waiting[1]) ? [waiting[1]] : context.subjects, 'waiting');
|
|
145
|
+
if (waiting && !NOT_WAITING.test(text)) add(REF.test(waiting[1]) ? [waiting[1]] : context.subjects, 'waiting');
|
|
144
146
|
const before = BEFORE_REF.exec(text);
|
|
145
147
|
if (before && (!subject || before.index < subject[1].length) &&
|
|
146
148
|
(before[1].toLowerCase() !== 'open' || (before[2] &&
|
|
@@ -179,11 +181,17 @@ function claimsFromLine(raw, line) {
|
|
|
179
181
|
return claims;
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
function closes(lines, from, open) {
|
|
185
|
+
let quote = open;
|
|
186
|
+
for (let i = from; i < lines.length && quote !== null; i++) quote = endQuote(lines[i], quote);
|
|
187
|
+
return quote === null;
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
// Aliases and the default repository are resolved by the reader. Preserve the
|
|
183
191
|
// identity written in the note, including refs that the reader cannot place.
|
|
184
192
|
export function extractClaims(text, _options = {}) {
|
|
185
193
|
const claims = [];
|
|
186
|
-
let paragraph = [], fence = null, quote = null, comment = false, sections = [], frontMatter = false;
|
|
194
|
+
let paragraph = [], fence = null, quote = null, comment = false, sections = [], frontMatter = false, titled = false, planned = null;
|
|
187
195
|
const flush = () => {
|
|
188
196
|
// A wrapped condition, quotation or historical preamble can change the
|
|
189
197
|
// following line. Separate list items may contain independent nonclaims.
|
|
@@ -191,8 +199,13 @@ export function extractClaims(text, _options = {}) {
|
|
|
191
199
|
// A historical label ("Old note:") covers the whole list under it.
|
|
192
200
|
const old = paragraph.findIndex((entry) => !entry.list && !entry.claims.length && entry.label);
|
|
193
201
|
paragraph.forEach((entry, i) => {
|
|
194
|
-
|
|
195
|
-
if (
|
|
202
|
+
// A planning list item covers the items nested under it, blank lines included.
|
|
203
|
+
if (planned !== null && entry.indent <= planned) planned = null;
|
|
204
|
+
const nested = planned !== null;
|
|
205
|
+
if (!nested && entry.parent) planned = entry.indent;
|
|
206
|
+
if (nested || (old >= 0 && i > old && entry.list)) return;
|
|
207
|
+
const after = paragraph[i + 1]; // A planning item says nothing of the line before it.
|
|
208
|
+
if (!blocks(paragraph[i - 1]) && (!blocks(after) || after.parent)) claims.push(...entry.claims);
|
|
196
209
|
});
|
|
197
210
|
paragraph = [];
|
|
198
211
|
};
|
|
@@ -200,7 +213,8 @@ export function extractClaims(text, _options = {}) {
|
|
|
200
213
|
sections = sections.filter((section) => section.level < level);
|
|
201
214
|
// Unknown headings may introduce plans, examples or history. Only a
|
|
202
215
|
// current section (and current ancestors) permits extraction.
|
|
203
|
-
sections.push({ level, current: CURRENT_HEADING.test(title) });
|
|
216
|
+
sections.push({ level, current: CURRENT_HEADING.test(title), title: level === 1 && !titled && !/^(?:history|archive|old)\b/i.test(title) });
|
|
217
|
+
titled ||= level === 1;
|
|
204
218
|
};
|
|
205
219
|
|
|
206
220
|
const lines = text.replace(/^\uFEFF/, '').split(/\r\n|\n|\r/);
|
|
@@ -220,13 +234,20 @@ export function extractClaims(text, _options = {}) {
|
|
|
220
234
|
}
|
|
221
235
|
if (comment || raw.includes('<!--')) {
|
|
222
236
|
comment = !raw.includes('-->') || raw.lastIndexOf('<!--') > raw.lastIndexOf('-->');
|
|
223
|
-
paragraph.push({ claims: [], list: false });
|
|
237
|
+
paragraph.push({ claims: [], list: false, indent: Infinity });
|
|
224
238
|
continue;
|
|
225
239
|
}
|
|
226
240
|
const quoted = quote !== null;
|
|
227
241
|
quote = endQuote(raw, quote);
|
|
242
|
+
// A mark inside text that nothing closes was a stray.
|
|
243
|
+
if (!quoted && quote !== null && raw.trim().length > 1 && !closes(lines, index + 1, quote)) quote = null;
|
|
228
244
|
if (quoted || quote !== null) {
|
|
229
|
-
|
|
245
|
+
// A heading inside a quote still changes the section the quote ends in.
|
|
246
|
+
const inner = /^ {0,3}(#{1,6})\s+(.+?)(?:\s+#+)?\s*$/.exec(raw);
|
|
247
|
+
const rule = /^ {0,3}(=+|-+)\s*$/.exec(raw);
|
|
248
|
+
if (inner) enterSection(inner[1].length, inner[2]);
|
|
249
|
+
else if (rule && lines[index - 1]?.trim()) enterSection(rule[1][0] === '=' ? 1 : 2, lines[index - 1].trim());
|
|
250
|
+
paragraph.push({ claims: [], list: false, indent: Infinity });
|
|
230
251
|
continue;
|
|
231
252
|
}
|
|
232
253
|
const opening = /^ {0,3}(`{3,}|~{3,})/.exec(raw);
|
|
@@ -238,19 +259,27 @@ export function extractClaims(text, _options = {}) {
|
|
|
238
259
|
} else if (heading) {
|
|
239
260
|
flush();
|
|
240
261
|
enterSection(heading[1].length, heading[2]);
|
|
241
|
-
} else if (underline && paragraph.length) {
|
|
262
|
+
} else if (underline && paragraph.length && !(underline[1][0] === '-' && paragraph.some((entry) => entry.item))) {
|
|
242
263
|
const title = paragraph.length === 1 ? lines[index - 1].trim() : '';
|
|
243
264
|
paragraph = [];
|
|
244
265
|
enterSection(underline[1][0] === '=' ? 1 : 2, title);
|
|
266
|
+
} else if (underline && paragraph.length) {
|
|
267
|
+
flush(); // A rule after a list item, not a heading underline.
|
|
245
268
|
} else if (!raw.trim()) {
|
|
246
269
|
flush();
|
|
247
270
|
} else {
|
|
248
|
-
//
|
|
249
|
-
const readable = sections.every((section, i) => section.current || (section.
|
|
271
|
+
// The file's level 1 title above a current section does not switch it off.
|
|
272
|
+
const readable = sections.every((section, i) => section.current || (section.title && i < sections.length - 1));
|
|
250
273
|
const claims = readable ? claimsFromLine(raw, index + 1) : [];
|
|
251
|
-
const
|
|
252
|
-
const label = /:\s*$/.test(raw) && PREAMBLE.test(
|
|
253
|
-
|
|
274
|
+
const body = raw.trim().replace(/^(?:[-+*]|[0-9]+[.)])\s+/, '');
|
|
275
|
+
const label = /:\s*$/.test(raw) && PREAMBLE.test(body);
|
|
276
|
+
const bare = body.replace(/[*_`]/g, '');
|
|
277
|
+
const item = /^\s*(?:[-+*]|[0-9]+[.)])\s+/.test(raw);
|
|
278
|
+
const colon = /:\s*$/.test(bare);
|
|
279
|
+
const parent = item && colon && PLANNED.test(bare);
|
|
280
|
+
// A list item that opens a current list does not silence its first child.
|
|
281
|
+
const list = item && (!colon || CURRENT_HEADING.test(bare));
|
|
282
|
+
paragraph.push({ claims, list, item, label, parent, indent: /^\s*/.exec(raw)[0].length });
|
|
254
283
|
}
|
|
255
284
|
}
|
|
256
285
|
flush();
|