provared 0.5.0 → 0.7.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # <img src="https://prova.red/favicon.svg" alt="" width="36"> Provared
2
2
 
3
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/provared/provared/badge)](https://scorecard.dev/viewer/?uri=github.com/provared/provared)
4
+
3
5
  Proof of what an AI agent was allowed to do, and what it then did.
4
6
 
5
7
  A person signs the permission with a passkey. The agent signs a receipt
@@ -86,6 +88,11 @@ agent = create_agent(model, tools=[order_supplies, send_message],
86
88
  })]) # last among the middleware that wrap tool calls
87
89
  ```
88
90
 
91
+ The package is listed among the middleware in
92
+ [LangChain's own documentation](https://docs.langchain.com/oss/python/integrations/middleware).
93
+ For JavaScript agents, [examples/frameworks/](examples/frameworks/) shows the
94
+ same with the Vercel AI SDK, through `recordTools`.
95
+
89
96
  ## The four parts
90
97
 
91
98
  | Part | What it is |
@@ -125,7 +132,7 @@ each part protects against, is in
125
132
 
126
133
  - **[The guide](docs/guide.md)**: the whole library, in one document.
127
134
  Installing and importing; the command-line checker; what a slip can
128
- say; seals, time-stamps and the chain of block headers; cancelling;
135
+ say; seals, time-stamps, witnesses and the chain of block headers; cancelling;
129
136
  who stands behind a name; covered fields; passing a slip on; the check
130
137
  before acting; the stub writer beside an agent and the connector for
131
138
  its tools; what the checker returns; what it never does; what is not
@@ -136,6 +143,14 @@ each part protects against, is in
136
143
  - **[The Python version](python/README.md)**, which gives the same
137
144
  answers, with examples for three agent frameworks and packages for
138
145
  two of them.
146
+ - **Two programs beside an agent, each on npm with its own README:**
147
+ [the recorder beside Claude Code](integrations/claude-code/README.md)
148
+ (`npm install -g provared-claude-code`, also installable as a Claude
149
+ Code plugin), which checks and receipts every tool call Claude Code
150
+ makes; and [the executor for MCP tool servers](mcp-executor/README.md)
151
+ (`npm install -g provared-mcp-executor`), which stands between an
152
+ agent and its tool servers, holds their credentials, and countersigns
153
+ each receipt with keys of its own.
139
154
  - **[The pages at prova.red](https://prova.red/)**: the checker's answer
140
155
  for the sample record, a worked example for developers, and the same
141
156
  in plain words for a reader who does not write code.
@@ -156,9 +171,9 @@ each part protects against, is in
156
171
 
157
172
  ## Status
158
173
 
159
- **Version 0.5.0, a draft.** Published as it is, by one author in their
174
+ **Version 0.7.0, a draft.** Published as it is, by one author in their
160
175
  own time, built with AI tools since 3 October 2026; fixes as time
161
- allows. The code has been through 50 rounds of review by AI reviewers,
176
+ allows. The code has been through more than 80 rounds of review by AI reviewers,
162
177
  each reviewer a fresh session given the code, the documents and a brief
163
178
  of what to look at; every fault was fixed, or stated as a limit, and
164
179
  kept as a test. No person other than the author has reviewed it yet.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // provared-check: check a book or a Show from the command line.
3
3
  //
4
- // provared-check <file> [--issuer <thumbprint>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
4
+ // provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
5
5
  // [--block <fingerprint>]... [--voucher <fingerprint>]... [--disclosures <file>] [--cancellation <file>]...
6
- // [--root <top fingerprint>] [--size <entries>] [--json]
6
+ // [--seal <file>]... [--witness <fingerprint>]... [--root <top fingerprint>] [--size <entries>] [--json]
7
7
  //
8
8
  // Exit codes:
9
9
  // 0 the record is intact (no problem found, every signature checked),
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { readFileSync } from 'node:fs';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, limitWords, neverWords } from '../src/check.js';
23
+ import { DENIAL_SOURCES, MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, isPageAddress, limitWords, neverWords } from '../src/check.js';
24
24
 
25
25
  // Node.js marks its ML-DSA and SLH-DSA as experimental and says so on every
26
26
  // run. The checker does not repeat that warning; any other warning is shown.
@@ -29,8 +29,9 @@ process.on('warning', (w) => {
29
29
  if (w.name !== 'ExperimentalWarning') console.error(`${w.name}: ${w.message}`);
30
30
  });
31
31
 
32
- const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
33
- [--block <fingerprint>]... [--voucher <fingerprint>]... [--disclosures <file>] [--cancellation <file>]...
32
+ const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]...
33
+ [--stamp-service <fingerprint>]... [--block <fingerprint>]... [--voucher <fingerprint>]...
34
+ [--disclosures <file>] [--cancellation <file>]... [--seal <file>]... [--witness <fingerprint>]...
34
35
  [--root <top fingerprint>] [--size <entries>] [--headers <file>] [--json]
35
36
  provared-check --sample [--json]
36
37
 
@@ -38,6 +39,9 @@ const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer
38
39
  --sample check the sample record that comes with the library, naming the passkey, the recorder and the
39
40
  time-stamp service it was made with as trusted (they are stand-ins, and the people are invented)
40
41
  --issuer the thumbprint of an issuer key you already trust; may be repeated
42
+ --origin the address of the page at which the person signs permissions, such as https://sign.example.org
43
+ (https://, a website name in lower case, a port only if it is needed, and nothing more); a
44
+ permission signed at any other address is refused; may be repeated
41
45
  --sealer the fingerprint of the key set of a recorder you expect to have sealed the book; may be repeated
42
46
  --stamp-service the fingerprint of the certificate of a time-stamp service you trust; may be repeated
43
47
  --block the fingerprint (64 hex characters) of a block of a public blockchain that you trust to be part of the chain;
@@ -47,6 +51,12 @@ const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer
47
51
  --cancellation a JSON file of the person's own copy of a cancellation, as the entry it would be in a book:
48
52
  {"cancellation": {...}, "stamps": [...]}. It counts even where the book leaves it out; may be repeated.
49
53
  It may hold "acknowledgements": what the agent's side signed when it was handed the cancellation
54
+ --seal a JSON file of a seal held elsewhere (by you, an auditor, a copy kept apart), as the entry it is in the book:
55
+ {"seal": {...}, "stamps": [...]}. The book must hold that seal at the place it states: one that ends
56
+ before it was cut short. It counts only with --sealer naming its recorder; may be repeated.
57
+ It may hold "witnesses": statements that witnesses signed for that seal, such as ones that came later
58
+ --witness the fingerprint of the key set of a witness you trust: a seal counts as witnessed only by such a
59
+ witness. Being witnessed excuses nothing; may be repeated
50
60
  --root the top fingerprint you already trust
51
61
  --size the number of entries you already trust; keep it with the top fingerprint
52
62
  --headers a file of Bitcoin block headers, as provared-headers keeps it. It is checked here, from the chain's
@@ -61,14 +71,14 @@ const FINGERPRINT = /^[A-Za-z0-9_-]{43}$/;
61
71
  const BLOCK = /^[0-9a-f]{64}$/;
62
72
 
63
73
  // Options that may be repeated, and those that may be given once.
64
- const LISTS = { '--issuer': 'issuerKeys', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--cancellation': 'cancellations' };
74
+ const LISTS = { '--issuer': 'issuerKeys', '--origin': 'issuerOrigins', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--witness': 'witnesses', '--cancellation': 'cancellations', '--seal': 'seals' };
65
75
  const ONCE = { '--disclosures': 'disclosures', '--root': 'expectedRoot', '--size': 'expectedSize', '--headers': 'headers' };
66
76
 
67
77
  // The arguments, or the reason they cannot be used. A value that is not a
68
78
  // fingerprint is refused, never set aside: a typing error must not quietly
69
79
  // leave a time-stamp or a name uncounted.
70
80
  function parseArguments(argv) {
71
- const out = { file: null, sample: false, issuerKeys: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], cancellations: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
81
+ const out = { file: null, sample: false, issuerKeys: [], issuerOrigins: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], witnesses: [], cancellations: [], seals: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
72
82
  for (let i = 0; i < argv.length; i++) {
73
83
  const a = argv[i];
74
84
  if (a === '--help' || a === '-h') return { help: true };
@@ -82,10 +92,13 @@ function parseArguments(argv) {
82
92
  if (i + 1 >= argv.length) return { error: `${a} needs a value.` };
83
93
  const value = argv[++i];
84
94
  if (Object.hasOwn(ONCE, a) && out[ONCE[a]] !== undefined) return { error: `${a} may be given once.` };
85
- if (['--issuer', '--sealer', '--stamp-service', '--voucher', '--root'].includes(a) && !FINGERPRINT.test(value)) {
95
+ if (['--issuer', '--sealer', '--stamp-service', '--voucher', '--witness', '--root'].includes(a) && !FINGERPRINT.test(value)) {
86
96
  return { error: `The value of ${a} is not a fingerprint. A fingerprint is 43 letters, digits, hyphens or underscores, with no spaces.` };
87
97
  }
88
98
  if (a === '--block' && !BLOCK.test(value)) return { error: 'The value of --block is not the fingerprint of a block: 64 characters, the digits 0 to 9 and the letters a to f.' };
99
+ if (a === '--origin' && !isPageAddress(value)) {
100
+ return { error: 'The value of --origin is not a page address in its plain form: https://, a website name in lower case, a port only if it is needed, and nothing more, such as https://sign.example.org.' };
101
+ }
89
102
  if (a === '--size' && !/^\d{1,6}$/.test(value)) return { error: 'The value of --size is not a number of entries.' };
90
103
  if (Object.hasOwn(LISTS, a)) out[LISTS[a]].push(value);
91
104
  else out[ONCE[a]] = a === '--size' ? Number(value) : value;
@@ -94,7 +107,7 @@ function parseArguments(argv) {
94
107
  }
95
108
  if (out.sample) {
96
109
  if (out.file !== null) return { error: '--sample names the sample record itself: give no file with it.' };
97
- for (const name of ['issuerKeys', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'cancellations']) {
110
+ for (const name of ['issuerKeys', 'issuerOrigins', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'cancellations', 'seals']) {
98
111
  if (out[name].length) return { error: '--sample names what to trust itself: give no other option with it but --json.' };
99
112
  }
100
113
  if (out.disclosures !== undefined || out.expectedRoot !== undefined || out.expectedSize !== undefined || out.headers !== undefined) {
@@ -282,10 +295,22 @@ function printSeal(e) {
282
295
  console.log(` Recorder's key set fingerprint: ${e.sealer}`);
283
296
  printStamps(e.stamps);
284
297
  if (e.stamps.length === 0) console.log(' No outside time-stamp: the time is only the recorder\'s own word.');
298
+ printWitnesses(e.witnesses);
285
299
  }
286
300
  if (e.signatures && e.signatures.length) console.log(` Recorder's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
287
301
  }
288
302
 
303
+ // The witness statements beside a seal. What a statement says is shown only where it passed its check.
304
+ function printWitnesses(list) {
305
+ for (const w of list ?? []) {
306
+ if (w.state === 'invalid') console.log(' Witness statement, NOT USED: it did not pass its check (see the problems)');
307
+ else if (w.state === 'unavailable') console.log(' Witness statement, NOT CHECKED: this device cannot check both of its signatures');
308
+ else if (w.counted) console.log(` WITNESSED by ${label(w.name)} (a label), a witness you named as trusted (key set ${w.witness}), at ${w.when} (the witness's own word)`);
309
+ else if (w.trusted) console.log(` Witness statement, NOT COUNTED: from a witness you named as trusted (key set ${w.witness}), but it was not counted (see the notes beside the seal)`);
310
+ else console.log(` Witness statement, NOT COUNTED: from a witness you did not name as trusted (key set ${w.witness})`);
311
+ }
312
+ }
313
+
289
314
  function printCancellation(e) {
290
315
  console.log(`\nEntry ${e.index}: CANCELLATION ${e.fingerprint ?? ''}`);
291
316
  if (hidden(e)) console.log(hidden(e));
@@ -313,8 +338,53 @@ function printAcknowledgement(e) {
313
338
  if (e.signatures && e.signatures.length) console.log(` Agent's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
314
339
  }
315
340
 
341
+ // A denial: the agent's own side says it did not take an action, and why.
342
+ function printDenial(e) {
343
+ console.log(`\nEntry ${e.index}: DENIAL ${e.fingerprint ?? ''}`);
344
+ if (hidden(e)) console.log(hidden(e));
345
+ else {
346
+ const c = e.content;
347
+ const amount = c.amount ? `, ${c.amount.value} ${safe(c.amount.unit)}` : '';
348
+ const service = c.with ? `, with [${safe(c.with)}]` : '';
349
+ console.log(` ${c.pass ? `The helper agent of pass ${c.pass}` : 'The slip\'s own agent'} states that it did not take an action`);
350
+ console.log(` Under slip ${c.slip}`);
351
+ console.log(` ${c.when} (the agent's side's own word): not taken ${safe(c.action)}${amount}${service}`);
352
+ if (c.details) console.log(` Documents named: ${c.details.length}, by their fingerprints only`);
353
+ console.log(` Who said no: ${DENIAL_SOURCES[c.source]}`);
354
+ if (c.codes) console.log(` Reasons given: ${c.codes.join(', ')}`);
355
+ console.log(' This shows what the agent\'s side said. It is not counted against the agent, and it does not show that the action was not taken in some other way.');
356
+ }
357
+ if (e.signatures && e.signatures.length) console.log(` Agent's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
358
+ }
359
+
360
+ // A seal held elsewhere, handed over beside the book.
361
+ function printHeldSeal(h, n) {
362
+ console.log(`\nHanded over beside the book, ${n}: SEAL HELD ELSEWHERE ${h.fingerprint ?? ''}`);
363
+ if (h.problems.length) {
364
+ console.log(' It did not pass its check, so it was not used. See the problems at the top.');
365
+ return;
366
+ }
367
+ // What a seal says is shown only where this device checked at least one of its signatures.
368
+ if (h.verified === 'none') {
369
+ console.log(' What this seal says is not shown, because this device could not check its signatures.');
370
+ for (const n of h.notes) console.log(` Note: ${n}`);
371
+ return;
372
+ }
373
+ const c = h.content;
374
+ console.log(` Covers the first ${c.size} entries, with the top fingerprint ${c.root}, and stands at entry ${h.place}`);
375
+ console.log(` Sealed at (the recorder's own word): ${c.when}`);
376
+ console.log(` Recorder: ${label(c.by.name)} (a label, not checked)`);
377
+ console.log(` Recorder's key set fingerprint: ${h.sealer}`);
378
+ printStamps(h.stamps);
379
+ printWitnesses(h.witnesses);
380
+ console.log(h.used ? (h.inBook === null ? ' In the book at that place: NO' : ` In the book at that place: yes, at entry ${h.inBook}`) : ' In the book at that place: not compared, because it was not used');
381
+ console.log(` Recorder's signatures: ${h.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
382
+ for (const n of h.notes) console.log(` Note: ${n}`);
383
+ }
384
+
316
385
  // A cancellation the person kept, handed over beside the book.
317
386
  function printHeld(h, n) {
387
+ if (h.kind === 'seal') return printHeldSeal(h, n);
318
388
  console.log(`\nHanded over beside the book, ${n}: CANCELLATION ${h.fingerprint ?? ''}`);
319
389
  if (h.problems.length) {
320
390
  console.log(' It did not pass its check, so it was not used. See the problems at the top.');
@@ -403,6 +473,7 @@ function print(result, isShow) {
403
473
  else if (e.kind === 'seal') printSeal(e);
404
474
  else if (e.kind === 'cancellation') printCancellation(e);
405
475
  else if (e.kind === 'acknowledgement') printAcknowledgement(e);
476
+ else if (e.kind === 'denial') printDenial(e);
406
477
  else if (e.kind === 'pass') printPass(e);
407
478
  else if (e.kind === 'vouching') printVouching(e);
408
479
  else if (e.kind === 'withdrawal') printWithdrawal(e);
@@ -436,7 +507,13 @@ function print(result, isShow) {
436
507
  if (!s.problemFound) {
437
508
  const stamped = s.sealed ? `yes: the first ${s.sealed.entries} entries existed by ${s.sealed.when}` : 'no';
438
509
  console.log(` Time-stamped in a way you trust? ${stamped}`);
510
+ if (s.counts.seals || args.witnesses.length) console.log(` Witnessed by a witness you trust? ${witnessedWords(s)}${isShow && s.witnessed ? ' (single pages: this says nothing about totals over the whole book)' : ''}`);
439
511
  console.log(` Checked against keys you named? ${trustWords(s)}`);
512
+ // Shown only where the person named an address: a permission signed
513
+ // elsewhere is a problem, reported above, so here every one was signed
514
+ // at an address they named.
515
+ if (args.issuerOrigins.length) console.log(` Signed at an address you named? yes: every permission was signed at ${[...new Set(args.issuerOrigins)].join(' or ')}`);
516
+ if (args.seals.length) console.log(` Seals held elsewhere compared? ${heldWords(result)}`);
440
517
  }
441
518
  console.log(` Slips ${s.counts.slips}, stubs ${s.counts.stubs} (countersigned ${s.counts.countersigned}, one-sided ${s.counts.oneSided})${s.counts.seals ? `, seals ${s.counts.seals}` : ''}`);
442
519
  if (s.counts.cancellations || s.counts.acknowledgements || s.counts.vouchings || s.counts.passes) {
@@ -445,15 +522,39 @@ function print(result, isShow) {
445
522
  if (s.counts.approved || s.counts.refusals || s.counts.terms) {
446
523
  console.log(` Approved by the person ${s.counts.approved}, refusals ${s.counts.refusals}, terms for agents ${s.counts.terms}`);
447
524
  }
525
+ if (s.counts.denials) console.log(` Denials (the agent's side's own word, not counted against it) ${s.counts.denials}`);
448
526
  console.log('\nWhat this check does not show');
449
527
  for (const l of s.limits) console.log(` - ${l}`);
450
528
  }
451
529
 
452
530
  // Whether the check rests on keys the person named: the passkey they trust
453
- // and, where the record has a seal, the recorder they expect. Without them a
454
- // record agrees only with the keys inside it, which anyone could have made.
531
+ // and, where the record has a seal or seals held elsewhere were handed over,
532
+ // the recorder they expect. Without them a record agrees only with the keys
533
+ // inside it, which anyone could have made, and a seal handed over is not
534
+ // compared with the book.
455
535
  function named(s) {
456
- return args.issuerKeys.length > 0 && (s.counts.seals === 0 || args.sealKeys.length > 0);
536
+ return args.issuerKeys.length > 0 && ((s.counts.seals === 0 && args.seals.length === 0) || args.sealKeys.length > 0);
537
+ }
538
+
539
+ // How far a witness the person named reaches. It excuses nothing, and is not needed for exit code 0.
540
+ function witnessedWords(s) {
541
+ if (!s.witnessed) return args.witnesses.length ? 'no' : 'no (you named no witness you trust, --witness)';
542
+ const w = s.witnessed;
543
+ return `yes: the seal at entry ${w.seal}, which covers the first ${w.entries} entries (${w.seals.length} of ${s.counts.seals} seals witnessed)`;
544
+ }
545
+
546
+ // Whether the seals held elsewhere that were handed over were compared with the book.
547
+ function heldWords(result) {
548
+ if (args.sealKeys.length === 0) return 'NO: you named no recorder you expect (--sealer), so the seals handed over were not compared with the book. This is not a pass.';
549
+ const seals = (result.held ?? []).filter((h) => h.kind === 'seal');
550
+ const used = seals.filter((h) => h.used).length;
551
+ return used === seals.length ? 'yes' : `NO: ${seals.length - used} of ${seals.length} handed over were not compared with the book (see the note beside each one). This is not a pass.`;
552
+ }
553
+
554
+ // Whether every seal handed over beside the book was compared with it. One that was not (it gives another book
555
+ // number, or could not be used) gives no pass, as one handed over with no recorder named does not.
556
+ function heldAllCompared(result) {
557
+ return (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
457
558
  }
458
559
 
459
560
  function trustWords(s) {
@@ -461,6 +562,7 @@ function trustWords(s) {
461
562
  const recorders = new Set(args.sealKeys).size === 1 ? 'the recorder you expect' : 'the recorders you expect';
462
563
  if (named(s)) return s.counts.seals ? `yes: ${passkeys}, and ${recorders}` : `yes: ${passkeys}`;
463
564
  if (args.issuerKeys.length === 0) return 'NO: you named no passkey you trust (--issuer), so anyone could have made this record. This is not a pass.';
565
+ if (s.counts.seals === 0) return `NO: you named ${passkeys}, but no recorder you expect (--sealer) for the seals handed over beside the book. This is not a pass.`;
464
566
  return 'NO: the record has a seal, and you named no recorder you expect (--sealer), so anyone could have sealed it. This is not a pass.';
465
567
  }
466
568
 
@@ -516,10 +618,12 @@ try {
516
618
 
517
619
  const options = {};
518
620
  if (args.issuerKeys.length) options.issuerKeys = args.issuerKeys;
621
+ if (args.issuerOrigins.length) options.issuerOrigins = args.issuerOrigins;
519
622
  if (args.sealKeys.length) options.sealKeys = args.sealKeys;
520
623
  if (args.stampServices.length) options.stampServices = args.stampServices;
521
624
  if (args.blocks.length) options.blocks = args.blocks;
522
625
  if (args.vouchers.length) options.vouchers = args.vouchers;
626
+ if (args.witnesses.length) options.witnesses = args.witnesses;
523
627
  if (args.disclosures !== undefined) {
524
628
  try {
525
629
  options.disclosures = JSON.parse(readFileSync(args.disclosures, 'utf8'));
@@ -539,6 +643,17 @@ if (args.cancellations.length) {
539
643
  }
540
644
  }
541
645
  }
646
+ if (args.seals.length) {
647
+ options.seals = [];
648
+ for (const file of args.seals) {
649
+ try {
650
+ options.seals.push(JSON.parse(readFileSync(file, 'utf8')));
651
+ } catch {
652
+ console.error(`The file of a seal could not be read: ${file}`);
653
+ process.exit(2);
654
+ }
655
+ }
656
+ }
542
657
  if (args.expectedRoot !== undefined) options.expectedRoot = args.expectedRoot;
543
658
  if (args.expectedSize !== undefined) options.expectedSize = args.expectedSize;
544
659
 
@@ -594,7 +709,7 @@ try {
594
709
  if (chain) printChain(chain, fromChain);
595
710
  }
596
711
  const s = result.summary;
597
- code = s.problemFound ? 2 : s.intact && s.withinSlips && named(s) ? 0 : 1;
712
+ code = s.problemFound ? 2 : s.intact && s.withinSlips && named(s) && heldAllCompared(result) ? 0 : 1;
598
713
  } catch {
599
714
  // The checker is written never to throw. If it ever does, that is not a pass.
600
715
  console.error('The check could not be completed. This is not a pass.');