@nightlab-agent/still-true 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/package.json +1 -1
  3. package/src/cli.mjs +21 -9
package/README.md CHANGED
@@ -49,9 +49,12 @@ Look and contradiction lines include GitHub's `state_reason` when supplied, such
49
49
  - `0`: nothing contradicted. Look lines may have printed.
50
50
  - `1`: at least one contradiction.
51
51
  - `2`: bad usage, or an unreadable file or failed read before any contradiction. The run is not complete.
52
+ - `3`: only with `--strict`. A note had no claim to verify, or a claim could not be checked.
52
53
 
53
54
  An unreadable file or failed read after a contradiction exits `1` and still says the run is not complete.
54
55
 
56
+ Without `--strict`, a note with no claims in the sections it reads exits `0`, and so does a claim GitHub has no record of. Add `--strict` when an agent or a script runs the check for you and a green run has to mean something was verified. A contradiction still exits `1` and a failed read still exits `2`. Each note is judged on its own, so a note with no claims fails the run even when another note checked out. Skipped sections alone do not fail a note that had a claim. With `--json`, a run that fails this way adds `strict_failed: true`.
57
+
55
58
  Queued reads follow the same stop-on-failure rule: a failed queued read stops before later claims, exiting `2` if no contradiction has been found yet. Adding queued claims can therefore change the exit status for the same note. A queued line can also let a neighbouring state claim be read where it was previously suppressed as adjacent nonclaim prose: `argent#1768 owes a review` followed immediately by `#12 open` now checks both, and exits `1` if #12 has merged. A look alone still exits `0`.
56
59
 
57
60
  ## JSON output
@@ -63,7 +66,7 @@ Add `--json` for one JSON object on stdout, with no text lines. Exit codes stay
63
66
  The object has `schema: 1`, `complete`, `reason` (`null` on success), `counts` and `claims`.
64
67
  Counts are `checked`, `contradicted`, `looks` and `notChecked`; checked includes contradictions, looks and ok claims.
65
68
  Each claim has `file`, a 1-based `line`, `reference` (resolved to `owner/repo#number` when possible), `said`, `actual` (`open`, `merged`, `closed` or `null` when unknown), `kind` (`contradiction`, `look`, `not checked` or `ok`), and `url` when known.
66
- If no claim was found and a section was skipped, `skipped_sections` lists those sections, each with its file when you gave more than one. `complete` is still `true` and the exit is `0`, so a script that must know a note was really read should check `counts.checked` too.
69
+ If no claim was found and a section was skipped, `skipped_sections` lists those sections, each with its file when you gave more than one. `complete` is still `true` and the exit is `0` without `--strict`, so a script that must know a note was really read should use `--strict`.
67
70
  `said` can also be `queued`, for the review obligations above. A nonempty GitHub `state_reason` is included as an optional string field; null or absent reasons are omitted.
68
71
 
69
72
  A failed read or unreadable file sets `complete: false` and gives the reason, keeping earlier results. The run stops there; claims already extracted from the current file that were not reached have kind `not checked`. Later files have no entries. A complete run can still have unchecked claims, such as an unresolved alias or a private issue. No claims gives an empty array and zero counts. `--json --help` returns the help text in a `help` field.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightlab-agent/still-true",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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/cli.mjs CHANGED
@@ -3,9 +3,10 @@ import { spawnSync } from 'node:child_process';
3
3
  import { compare } from './compare.mjs';
4
4
  import { readState, resolveRef } from './github.mjs';
5
5
 
6
- const USAGE = 'usage: still-true NOTE.md [more.md] [--repo owner/name] [--alias Name=owner/name] [--json]';
6
+ const USAGE = 'usage: still-true NOTE.md [more.md] [--repo owner/name] [--alias Name=owner/name] [--json] [--strict]';
7
7
  const JSON_HELP = '--json: print one JSON object (schema 1) with completeness, counts and every extracted claim.';
8
- const EXIT_HELP = 'exit 0: nothing contradicted, look lines may have printed\nexit 1: at least one contradiction\nexit 2: bad usage, or an unreadable file or failed read before any contradiction; the run is not complete';
8
+ const STRICT_HELP = '--strict: exit 3 if any note had no claim to verify, or if any claim could not be checked. Use it when a green run has to mean the note was really read.';
9
+ const EXIT_HELP = 'exit 0: nothing contradicted, look lines may have printed\nexit 1: at least one contradiction\nexit 2: bad usage, or an unreadable file or failed read before any contradiction; the run is not complete\nexit 3: with --strict only, nothing was verified or a claim was not checked';
9
10
 
10
11
  export function repoFromRemote(url) {
11
12
  const m = /^(?:[a-z+]+:\/\/(?:[^@/\s]+@)?|[^@/\s]+@)?(?:www\.)?github\.com(?::\d+)?[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?\s*$/i.exec(url ?? '');
@@ -15,7 +16,7 @@ export function repoFromRemote(url) {
15
16
  const FULL = /^[^/\s=]+\/[^/\s=]+$/;
16
17
 
17
18
  export function parseArgs(argv) {
18
- const a = { files: [], aliases: {}, repo: undefined, bad: null, json: argv.includes('--json') };
19
+ const a = { files: [], aliases: {}, repo: undefined, bad: null, json: argv.includes('--json'), strict: false };
19
20
  for (let i = 0; i < argv.length; i++) {
20
21
  if (argv[i] === '--repo') {
21
22
  a.repo = argv[++i];
@@ -24,7 +25,8 @@ export function parseArgs(argv) {
24
25
  const [name, full, more] = (argv[++i] ?? '').split('=');
25
26
  if (name && FULL.test(full ?? '') && more === undefined) a.aliases[name] = full;
26
27
  else a.bad ??= '--alias needs Name=owner/name';
27
- } else if (argv[i] !== '--json') a.files.push(argv[i]);
28
+ } else if (argv[i] === '--strict') a.strict = true;
29
+ else if (argv[i] !== '--json') a.files.push(argv[i]);
28
30
  }
29
31
  return a;
30
32
  }
@@ -52,8 +54,8 @@ export async function run(argv, deps) {
52
54
  return code;
53
55
  };
54
56
  if (argv.includes('--help')) {
55
- print(USAGE); print(JSON_HELP); print(EXIT_HELP);
56
- return finish(0, null, { help: `${USAGE}\n${JSON_HELP}\n${EXIT_HELP}` });
57
+ print(USAGE); print(JSON_HELP); print(STRICT_HELP); print(EXIT_HELP);
58
+ return finish(0, null, { help: `${USAGE}\n${JSON_HELP}\n${STRICT_HELP}\n${EXIT_HELP}` });
57
59
  }
58
60
  const odd = args.files.find((f) => f.startsWith('--'));
59
61
  if (odd) { print(`unknown option ${odd}`); print(USAGE); return finish(2, `unknown option ${odd}`); }
@@ -61,6 +63,7 @@ export async function run(argv, deps) {
61
63
  if (!args.files.length) { print(USAGE); return finish(2, USAGE); }
62
64
  const seen = new Map();
63
65
  const skipped = [];
66
+ const unread = [];
64
67
 
65
68
  try {
66
69
  const repo = args.repo ?? (typeof defaultRepo === 'function' ? defaultRepo() : defaultRepo);
@@ -84,6 +87,7 @@ export async function run(argv, deps) {
84
87
  if (entry) entries.push(entry);
85
88
  return { claim, ref, entry };
86
89
  });
90
+ if (!claims.length) unread.push(file);
87
91
  skipped.push(...here.map((section) => (args.files.length > 1 ? `${file}: ${section}` : section)));
88
92
  for (const { claim, ref, entry } of claims) {
89
93
  if (!ref) {
@@ -135,15 +139,23 @@ export async function run(argv, deps) {
135
139
  if ((env.HTTPS_PROXY ?? env.https_proxy) && !env.NODE_USE_ENV_PROXY) print('You have a proxy set: run again with NODE_USE_ENV_PROXY=1.');
136
140
  return finish(contradicted ? 1 : 2, failure);
137
141
  }
142
+ const strictFail = (extra = {}) => {
143
+ const why = [];
144
+ if (unplaced) why.push(`${unplaced} claim${unplaced === 1 ? '' : 's'} not checked`);
145
+ if (unread.length) why.push(`nothing was verified in ${unread.join(', ')}`);
146
+ print(`--strict: ${why.join(', ')}`);
147
+ return finish(3, null, { ...extra, strict_failed: true });
148
+ };
138
149
  if (!checked && !unplaced) {
139
150
  print('no claims about PR or issue state found');
140
- if (!skipped.length) return finish(0);
151
+ if (!skipped.length) return args.strict ? strictFail() : finish(0);
141
152
  print(`skipped ${skipped.length} section${skipped.length === 1 ? '' : 's'}: ${skipped.join(', ')}. A section is read only if its heading, and every heading above it, apart from the file's title, starts with Now, Handoff, State, Status or Current.`);
142
- return finish(0, null, { skipped_sections: skipped });
153
+ return args.strict ? strictFail({ skipped_sections: skipped }) : finish(0, null, { skipped_sections: skipped });
143
154
  }
144
155
  const note = (looks ? `, ${looks} to look at` : '') + (unplaced ? `, ${unplaced} not checked` : '');
145
156
  print(`${checked} claim${checked === 1 ? '' : 's'} checked, ${contradicted} contradicted${note}`);
146
- return finish(contradicted ? 1 : 0);
157
+ if (contradicted) return finish(1);
158
+ return args.strict && (unplaced || unread.length) ? strictFail() : finish(0);
147
159
  }
148
160
 
149
161
  export { readState };