@volter/twin-standard 1.0.148 → 1.0.150

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 (103) hide show
  1. package/README.md +12 -3
  2. package/client-sdks/bun.lock +0 -32
  3. package/client-sdks/package.json +0 -2
  4. package/dist/client-sdks/bun.lock +0 -32
  5. package/dist/client-sdks/package.json +0 -2
  6. package/dist/src/assessment.d.ts +6 -4
  7. package/dist/src/assessment.js +2 -1
  8. package/dist/src/cli.js +21 -6
  9. package/dist/src/clients/harness.d.ts +3 -1
  10. package/dist/src/clients/harness.js +4 -2
  11. package/dist/src/clients/jira.d.ts +2 -0
  12. package/dist/src/clients/jira.js +71 -0
  13. package/dist/src/clients/polar.d.ts +2 -0
  14. package/dist/src/clients/polar.js +73 -0
  15. package/dist/src/clients/resend.js +38 -0
  16. package/dist/src/clients/run.js +1 -1
  17. package/dist/src/clients/sentry.js +1 -26
  18. package/dist/src/clients/smtp.d.ts +2 -0
  19. package/dist/src/clients/smtp.js +54 -0
  20. package/dist/src/clients/stripe.js +1 -1
  21. package/dist/src/clients/volteridentity.js +4 -3
  22. package/dist/src/clients/x.d.ts +2 -0
  23. package/dist/src/clients/x.js +64 -0
  24. package/dist/src/conformance.d.ts +4 -3
  25. package/dist/src/conformance.js +293 -8
  26. package/dist/src/consumer.js +5 -1
  27. package/dist/src/create-pack.d.ts +5 -0
  28. package/dist/src/create-pack.js +38 -4
  29. package/dist/src/derive.js +21 -13
  30. package/dist/src/gate.d.ts +1 -0
  31. package/dist/src/gate.js +47 -4
  32. package/dist/src/grade.d.ts +1 -0
  33. package/dist/src/grade.js +1 -1
  34. package/dist/src/pack-facts.d.ts +9 -1
  35. package/dist/src/pack-facts.js +11 -2
  36. package/dist/src/pack-files.js +2 -1
  37. package/dist/src/protocol-3.js +40 -6
  38. package/dist/src/spec-ir-client.js +3 -2
  39. package/dist/src/spec-ir-commands.d.ts +73 -0
  40. package/dist/src/spec-ir-commands.js +63 -0
  41. package/dist/src/spec-ir.d.ts +2 -0
  42. package/dist/src/spec-ir.js +43 -0
  43. package/dist/src/tools/judge-brief.d.ts +1 -0
  44. package/dist/src/tools/judge-brief.js +67 -0
  45. package/dist/src/tools/life-coverage-walk.d.ts +2 -0
  46. package/dist/src/tools/life-coverage-walk.js +220 -0
  47. package/dist/src/tools/life-coverage.d.ts +2 -0
  48. package/dist/src/tools/life-coverage.js +271 -0
  49. package/dist/src/tools/packs-root.d.ts +9 -0
  50. package/dist/src/tools/packs-root.js +17 -0
  51. package/dist/src/tools/run-tool.d.ts +1 -0
  52. package/dist/src/tools/run-tool.js +18 -0
  53. package/dist/src/tools/score-pack.d.ts +2 -0
  54. package/dist/src/tools/score-pack.js +264 -0
  55. package/dist/src/tools/vendor-examples.d.ts +2 -0
  56. package/dist/src/tools/vendor-examples.js +1493 -0
  57. package/dist/src/types.d.ts +2 -0
  58. package/dist/src/verdict.js +1 -1
  59. package/dist/src/walk/behavior-journey.d.ts +6 -0
  60. package/dist/src/walk/behavior-journey.js +108 -8
  61. package/dist/src/walk/journey-kit.d.ts +23 -1
  62. package/dist/src/walk/journey-kit.js +7 -2
  63. package/dist/src/walk/rendered-scalar.d.ts +3 -0
  64. package/dist/src/walk/rendered-scalar.js +23 -0
  65. package/dist/src/walk/sse-example.js +45 -0
  66. package/package.json +2 -2
  67. package/src/assessment.ts +2 -1
  68. package/src/cli.ts +15 -2
  69. package/src/clients/harness.ts +4 -2
  70. package/src/clients/jira.ts +73 -0
  71. package/src/clients/polar.ts +73 -0
  72. package/src/clients/resend.ts +32 -0
  73. package/src/clients/run.ts +2 -2
  74. package/src/clients/sentry.ts +1 -25
  75. package/src/clients/smtp.ts +49 -0
  76. package/src/clients/stripe.ts +1 -1
  77. package/src/clients/volteridentity.ts +4 -3
  78. package/src/clients/x.ts +66 -0
  79. package/src/conformance.ts +226 -10
  80. package/src/consumer.ts +5 -1
  81. package/src/create-pack.ts +35 -5
  82. package/src/derive.ts +21 -14
  83. package/src/gate.ts +40 -6
  84. package/src/grade.ts +2 -2
  85. package/src/pack-facts.ts +10 -3
  86. package/src/pack-files.ts +2 -1
  87. package/src/protocol-3.ts +32 -6
  88. package/src/spec-ir-client.ts +3 -2
  89. package/src/spec-ir-commands.ts +81 -0
  90. package/src/spec-ir.ts +31 -0
  91. package/src/tools/judge-brief.ts +63 -0
  92. package/src/tools/life-coverage-walk.ts +173 -0
  93. package/src/tools/life-coverage.ts +237 -0
  94. package/src/tools/packs-root.ts +21 -0
  95. package/src/tools/run-tool.ts +10 -0
  96. package/src/tools/score-pack.ts +244 -0
  97. package/src/tools/vendor-examples.ts +1250 -0
  98. package/src/types.ts +2 -0
  99. package/src/verdict.ts +1 -1
  100. package/src/walk/behavior-journey.ts +65 -4
  101. package/src/walk/journey-kit.ts +21 -3
  102. package/src/walk/rendered-scalar.ts +17 -0
  103. package/src/walk/sse-example.ts +37 -0
@@ -56,6 +56,41 @@ export function unpublishedUnitFiles(unit) {
56
56
  }
57
57
  /** Each of UNIT_FILES a unit lacks where conformance reads it: the manifest of every unit, the life at its vendor's root
58
58
  * (lifeServed), the spec's record and the decision table of a unit with a spec (decided()). */
59
+ const trackedJourneySources = new Map();
60
+ /** Table ownership follows one index snapshot per checkout/run. */
61
+ function hasJourneySource(unit) {
62
+ const dir = join(unit, 'journeys');
63
+ let checkout = resolve(unit);
64
+ while (!existsSync(join(checkout, '.git'))) {
65
+ const parent = dirname(checkout);
66
+ if (parent === checkout) {
67
+ // An unpacked published package has no Git index; its delivered files
68
+ // are its source. Markers and empty directories are not table inputs.
69
+ if (!existsSync(dir))
70
+ return false;
71
+ const holdsFile = (at) => readdirSync(at, { withFileTypes: true }).some(entry => entry.name !== '.metadata_never_index'
72
+ && (entry.isFile() || (entry.isDirectory() && holdsFile(join(at, entry.name)))));
73
+ return holdsFile(dir);
74
+ }
75
+ checkout = parent;
76
+ }
77
+ const env = { ...process.env };
78
+ for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_PREFIX', 'GIT_INDEX_FILE'])
79
+ delete env[key];
80
+ if (process.env.VOLTER_PACK_HOOK_INDEX)
81
+ env.GIT_INDEX_FILE = process.env.VOLTER_PACK_HOOK_INDEX;
82
+ const key = `${checkout}\0${env.GIT_INDEX_FILE ?? ''}`;
83
+ let source = trackedJourneySources.get(key);
84
+ if (!source) {
85
+ const files = spawnSync('git', ['-C', checkout, 'ls-files', '-z', '--cached', '--', ':(glob)**/journeys/**'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024 });
86
+ if (files.status !== 0 || files.error)
87
+ throw new Error(`cannot read tracked journey source: ${String(files.stderr || files.error?.message)}`);
88
+ source = files.stdout.split('\0').filter(path => path && !path.split('/').includes('.metadata_never_index'));
89
+ trackedJourneySources.set(key, source);
90
+ }
91
+ const prefix = relative(checkout, dir).replaceAll('\\', '/') + '/';
92
+ return source.some(path => path.startsWith(prefix));
93
+ }
59
94
  export function unitFilesMissing(unit) {
60
95
  const vendor = existsSync(join(unit, 'package.json')) ? unit : dirname(unit);
61
96
  const spec = hasOwnSurface(unit);
@@ -63,7 +98,7 @@ export function unitFilesMissing(unit) {
63
98
  'src/manifest.ts': unit,
64
99
  'spec/SOURCE.md': spec ? unit : undefined,
65
100
  'journeys/customer-life.json': vendor,
66
- 'journeys/decisions.json': spec ? (existsSync(join(unit, 'journeys')) ? unit : vendor) : undefined,
101
+ 'journeys/decisions.json': spec ? (hasJourneySource(unit) ? unit : vendor) : undefined,
67
102
  };
68
103
  return UNIT_FILES.filter((f) => at[f] !== undefined && !existsSync(join(at[f], f))).map((f) => `${relative(unit, join(at[f], f))} is missing (conformance reads it)`);
69
104
  }
@@ -119,7 +154,7 @@ async function decided(pack) {
119
154
  for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
120
155
  const name = nameOf(pack, unit);
121
156
  const key = unit === pack ? '' : basename(unit);
122
- const journeys = existsSync(join(unit, 'journeys')) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
157
+ const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
123
158
  const owners = byUnit ? byUnit[key] ?? {} : (await makeOf(unit))?.().owners?.();
124
159
  if (!owners) {
125
160
  out.push(`${name}: its fetch (${fetchName(unit)}) names no owners() to check the decision table against`);
@@ -321,6 +356,190 @@ async function standalone(pack) {
321
356
  }
322
357
  return out;
323
358
  }
359
+ /** The request an application's first call of an operation sends when it knows nothing yet: every path parameter, every
360
+ * required query parameter and every header the operation is routed by holding a placeholder, its discriminator's
361
+ * values, and an empty body in the operation's encoding. What it measures is the twin's answer to a call it did not
362
+ * script: a refusal in the vendor's form, or a result in the vendor's shape, and never a crash, a hang or a row. */
363
+ function sweepRequest(op, basePath, host, credential) {
364
+ const fill = (name, type) => (type === 'integer' || type === 'number' ? '1' : type === 'boolean' ? 'true' : `conformance-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`);
365
+ const path = `${op.basePath ?? basePath}${op.path}`.replace(/\{([^}+]+)\+?\}/g, (_m, name) => fill(name));
366
+ const url = new URL(`https://${host}${path}`);
367
+ for (const q of op.query ?? [])
368
+ if (q.required)
369
+ url.searchParams.set(q.name, fill(q.name, q.type));
370
+ for (const [name, value] of Object.entries(op.discriminator ?? {}))
371
+ url.searchParams.set(name, value ?? fill(name));
372
+ const headers = new Headers(credential);
373
+ for (const [name, value] of Object.entries(op.headers ?? {}))
374
+ headers.set(name, value ?? fill(name));
375
+ const method = op.method.toUpperCase();
376
+ let body;
377
+ if (op.bodyEncoding === 'form') {
378
+ headers.set('content-type', 'application/x-www-form-urlencoded');
379
+ body = '';
380
+ }
381
+ else if (op.bodyEncoding === 'xml') {
382
+ headers.set('content-type', 'application/xml');
383
+ body = '';
384
+ }
385
+ else if (op.bodyEncoding === 'json' || op.bodyEncoding === 'multipart') {
386
+ headers.set('content-type', 'application/json');
387
+ body = '{}';
388
+ }
389
+ return new Request(url, { method, headers, ...(body !== undefined && !['GET', 'HEAD'].includes(method) ? { body } : {}) });
390
+ }
391
+ /** Every operation the pack serves, called over HTTP as an application calls it, against its server.ts served as a World
392
+ * runs it: once plainly and once with a scenario loaded (an empty one, as a World that scripts nothing loads it). Each
393
+ * answers within 10 s, never with a 500 the spec does not declare, never with a field of the twin's own (`_`
394
+ * bookkeeping, the kernel's `updatedAt`) and, answered successfully, in the shape the spec gives the operation; and every
395
+ * refresh list a fresh account can read (one with no parameter in its path) answers it successfully, so a World rooted
396
+ * at this twin reads its resources back. Escaped before: the scenario hang (a served pack given a scenario answered
397
+ * nothing), leaked kernel fields in answers no life step read, and refresh scopes on operations the pack answered with
398
+ * the gap. The life walks what a customer does; this calls what the pack declares it serves. */
399
+ async function surfaceSwept(pack, scratch) {
400
+ const out = [];
401
+ const descriptor = (await manifestOf(pack)).descriptor;
402
+ const fetchOf = (await import(__rewriteRelativeImportExtension(join(pack, 'src', 'fetch.ts'))))[fetchName(pack)];
403
+ const root = fetchOf?.();
404
+ const byUnit = root?.ownersByUnit?.() ?? { '': root?.owners?.() ?? {} };
405
+ const { shapeChecker } = await import("./walk/shape.js");
406
+ const { leaked } = await import("./walk/behavior-journey.js");
407
+ const shape = await shapeChecker(pack);
408
+ const units = [];
409
+ const vendorFields = new Set();
410
+ for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
411
+ const surface = readJson(join(unit, 'src', 'generated', 'surface.gen.json'));
412
+ for (const r of surface?.resources ?? [])
413
+ for (const f of r.fields)
414
+ vendorFields.add(f.name);
415
+ // a line protocol and a command table ride in no path of their own: the life's line steps and the examples read them
416
+ if (!surface?.operations || ['command-reply', 'command-table'].includes(String(surface.format)))
417
+ continue;
418
+ const m = (await manifestOf(unit));
419
+ const host = m.hosts?.find((h) => h.host)?.host ?? descriptor?.hosts?.find((h) => h.host)?.host;
420
+ if (!host) {
421
+ out.push(`${nameOf(pack, unit)}: no host is named (its manifest's hosts or the descriptor's) for an application to call it at`);
422
+ continue;
423
+ }
424
+ const owners = byUnit[unit === pack ? '' : basename(unit)] ?? {};
425
+ const refreshLists = Object.entries(m.resources ?? {}).flatMap(([resource, decl]) => (decl.refresh?.list ? [{ resource, id: decl.refresh.list }] : []));
426
+ units.push({ name: nameOf(pack, unit), host, basePath: surface.basePath ?? '', ops: surface.operations.filter((o) => owners[o.id] !== undefined && owners[o.id] !== 'gap'), refreshLists, ...(m.auth ? { auth: m.auth } : {}) });
427
+ }
428
+ if (!units.length)
429
+ return { failures: out, note: 'no unit serves an operation on a path of its own' };
430
+ const tally = { calls: 0, ok: 0, refused: 0 };
431
+ const env = globalThis.process.env;
432
+ const scenarioPath = join(scratch, 'surface-scenario.json');
433
+ writeFileSync(scenarioPath, '{"handlers":[]}');
434
+ for (const mode of ['plain', 'scenario']) {
435
+ const before = env.VOLTER_WORLD_BOOT_ID;
436
+ env.VOLTER_WORLD_BOOT_ID = 'conformance';
437
+ let server;
438
+ try {
439
+ server = await serve(pack, { root: join(scratch, `surface-${mode}`), ...(mode === 'scenario' ? { scenarioPath } : {}) });
440
+ }
441
+ finally {
442
+ if (before === undefined)
443
+ delete env.VOLTER_WORLD_BOOT_ID;
444
+ else
445
+ env.VOLTER_WORLD_BOOT_ID = before;
446
+ }
447
+ const http = servedHttpFetch(server.inspectPort ?? server.port);
448
+ const send = async (request) => {
449
+ await server.managedDatabaseUrl?.();
450
+ const abort = new AbortController();
451
+ try {
452
+ return await within(10_000, http(request, { signal: abort.signal }));
453
+ }
454
+ catch (error) {
455
+ return error;
456
+ }
457
+ finally {
458
+ abort.abort();
459
+ }
460
+ };
461
+ try {
462
+ // the account an application holds: the key the credential door issues, sent as the vendor reads its credential
463
+ let issued;
464
+ if (descriptor?.credentialDoor) {
465
+ const door = descriptor.credentialDoor;
466
+ const answer = await send(new Request(`https://${descriptor.hosts?.find((h) => h.host)?.host ?? units[0].host}${door.path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(door.body) }));
467
+ const fields = answer instanceof Response ? (await answer.json().catch(() => ({}))) : {};
468
+ issued = Object.values(door.fill).map((f) => fields[f]).find((v) => typeof v === 'string');
469
+ }
470
+ const said = mode === 'scenario' ? ' with a scenario loaded' : '';
471
+ for (const u of units) {
472
+ // a unit whose manifest declares no `auth` checks its key in its own front: the key goes as most vendors read
473
+ // one, a bearer token, and a refusal of it is no finding of the refresh below
474
+ const auth = u.auth ?? { header: 'authorization', scheme: 'Bearer' };
475
+ const credential = issued ? { [auth.header]: auth.scheme ? `${auth.scheme} ${issued}` : issued } : {};
476
+ const answered = new Map();
477
+ for (const op of u.ops) {
478
+ const request = sweepRequest(op, u.basePath, u.host, credential);
479
+ const where = `${u.name}: ${op.id} (${request.method} ${new URL(request.url).pathname})`;
480
+ const res = await send(request);
481
+ if (res === 'timeout') {
482
+ out.push(`${where} answered nothing in 10 s${said}`);
483
+ continue;
484
+ }
485
+ if (res instanceof Error) {
486
+ out.push(`${where}: the connection failed${said} (${res.message})`);
487
+ continue;
488
+ }
489
+ answered.set(op.id, res.status);
490
+ if (mode === 'plain') {
491
+ tally.calls++;
492
+ if (res.status < 300)
493
+ tally.ok++;
494
+ else
495
+ tally.refused++;
496
+ }
497
+ const text = await res.text();
498
+ if (res.status === 500 && !op.responseMedia?.['500'] && !op.responseMedia?.['5XX']) {
499
+ out.push(`${where} answered 500${said}, which the spec does not declare: ${text.slice(0, 160)}`);
500
+ continue;
501
+ }
502
+ if (mode === 'scenario')
503
+ continue;
504
+ const type = res.headers.get('content-type') ?? '';
505
+ let body;
506
+ if (/json/.test(type)) {
507
+ try {
508
+ body = JSON.parse(text);
509
+ }
510
+ catch {
511
+ body = undefined;
512
+ }
513
+ }
514
+ for (const key of leaked(body, vendorFields, '', !op.answers || res.status >= 300))
515
+ out.push(`${where} answers the twin's own field ${key}`);
516
+ if (res.status >= 200 && res.status < 300 && shape) {
517
+ const hit = shape.operationOf(request.method, request.url, Object.fromEntries(request.headers));
518
+ if (hit)
519
+ for (const detail of shape.failures(hit.op, body, type, request.headers.get('accept') ?? undefined, res.status, { method: request.method, responseText: text }))
520
+ out.push(`${where}: ${detail}`);
521
+ }
522
+ }
523
+ if (mode === 'plain')
524
+ for (const r of u.refreshLists) {
525
+ const op = u.ops.find((o) => o.id === r.id);
526
+ if (!op || op.path.includes('{') || (op.basePath ?? '').includes('{'))
527
+ continue;
528
+ const status = answered.get(r.id);
529
+ if (status !== undefined && (status === 401 || status === 403) && (!u.auth || !issued))
530
+ continue;
531
+ if (status !== undefined && (status < 200 || status >= 300))
532
+ out.push(`${u.name}: ${r.resource}'s refresh lists by ${r.id}, which answered a fresh account ${status}: a World rooted at this twin reads nothing back`);
533
+ }
534
+ }
535
+ }
536
+ finally {
537
+ await server.stop();
538
+ }
539
+ }
540
+ // one defect repeated over a surface reads as one: the first 40 findings, and how many more
541
+ return { failures: out.length > 40 ? [...out.slice(0, 40), `… ${out.length - 40} more`] : out, note: `${tally.calls} served operations called, twice each: ${tally.ok} answered successfully, ${tally.refused} refused, unscripted` };
542
+ }
324
543
  /** A line protocol's connection over TCP, in the walk's stream shape. A line drew its replies when bytes arrived and then
325
544
  * 50 ms passed with no more (a multi-line reply arrives in pieces), or when 500 ms passed with none (a line that draws no
326
545
  * reply: a message's body before its end); 10 s bounds a server that keeps writing. The windows are timing, not protocol:
@@ -394,7 +613,9 @@ async function lifeServed(pack) {
394
613
  socket: (url, headers) => new WebSocket(`ws://127.0.0.1:${port}${url}`, { headers }),
395
614
  // a line connection opened as an application's client opens it: TCP to the protocol listener (the primary port)
396
615
  stream: lineConnection(server.port),
397
- stop: () => server.stop(),
616
+ // bounded: Bun's server.stop(true) never settles once a WebSocket peer has closed under it (discord's life, Bun
617
+ // 1.3.11: the walk ended and the stop spun at full CPU for 45 minutes), so the check waits 5 s and goes on
618
+ stop: async () => { await within(5000, Promise.resolve(server.stop())); },
398
619
  };
399
620
  };
400
621
  const world = inMemoryWorld(`conform-${basename(pack)}`);
@@ -430,7 +651,9 @@ async function clientCases(pack, scratch) {
430
651
  const descriptor = (await manifestOf(pack)).descriptor;
431
652
  // the first host that names one: a descriptor may lead with a suffix (a region's hosts under one domain), which no
432
653
  // request addresses; the case's client reaches whatever host its issued credentials name
433
- const host = descriptor?.hosts?.find((h) => h.host)?.host;
654
+ // a raw protocol's pack (hostsNone: SMTP, whose relay is whatever host the operator names) is reached by its TCP
655
+ // connection alone; its client names a stand-in host the connection never resolves
656
+ const host = descriptor?.hosts?.find((h) => h.host)?.host ?? (descriptor?.hostsNone ? 'relay.world.test' : undefined);
434
657
  if (!host)
435
658
  return { failures: ['the descriptor names no host for its client to address'] };
436
659
  const scenarioPath = join(scratch, 'client-scenario.json');
@@ -453,13 +676,31 @@ async function clientCases(pack, scratch) {
453
676
  const issued = {};
454
677
  const door = descriptor?.credentialDoor;
455
678
  if (door) {
456
- const answer = (await (await fetch(`http://127.0.0.1:${server.port}${door.path}`, { method: 'POST', headers: { host, 'content-type': 'application/json' }, body: JSON.stringify(door.body) })).json().catch(() => ({})));
679
+ // a door that answers nothing is the pack's failure, never conform's hang (a pack served with a scenario that answered
680
+ // nothing once held this request open for good)
681
+ const abort = new AbortController();
682
+ const asked = await within(10_000, fetch(`http://127.0.0.1:${server.port}${door.path}`, { method: 'POST', headers: { host, 'content-type': 'application/json' }, body: JSON.stringify(door.body), signal: abort.signal }).then((r) => r.json().catch(() => ({}))));
683
+ abort.abort();
684
+ if (asked === 'timeout')
685
+ return { failures: [`the credential door (POST ${door.path}) answered nothing in 10 s, served with a scenario loaded`] };
686
+ const answer = asked;
457
687
  for (const [name, field] of Object.entries(door.fill))
458
688
  if (answer[field] !== undefined)
459
689
  issued[name] = String(answer[field]);
460
690
  }
461
691
  const job = join(scratch, 'client-job.json');
462
- writeFileSync(job, JSON.stringify({ vendor, port: server.port, host, env: issued, scenarioPath, packFacts: join(pack, 'generated', 'pack-facts.json') }));
692
+ // a release carries its facts (generated/pack-facts.json, `twin-standard facts`); a source checkout does not, so its
693
+ // facts are compiled here, as that command compiles them, for the client the case runs
694
+ let packFacts = join(pack, 'generated', 'pack-facts.json');
695
+ if (!existsSync(packFacts)) {
696
+ const { compilePackFacts, renderPackFacts } = await import("./pack-facts.js");
697
+ const compiled = await compilePackFacts([{ vendor, dir: pack }]);
698
+ if (compiled.undeclared.length)
699
+ throw new Error('Pack descriptor is missing');
700
+ packFacts = join(scratch, 'pack-facts.json');
701
+ writeFileSync(packFacts, renderPackFacts(compiled));
702
+ }
703
+ writeFileSync(job, JSON.stringify({ vendor, port: server.port, host, env: issued, scenarioPath, packFacts, ...(descriptor?.hostsNone ? { raw: true } : {}) }));
463
704
  // Node, as an application runs its SDK (asynchronous: this process serves the pack meanwhile)
464
705
  const out = await new Promise((resolve) => {
465
706
  const child = spawn('node', ['--experimental-strip-types', '--no-warnings', join(clients, `run${extension}`), job], { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -479,8 +720,46 @@ async function clientCases(pack, scratch) {
479
720
  await server.stop();
480
721
  }
481
722
  }
723
+ /** The gate over every unit of the pack: the form section of the grade, which holds the rules an author's code keeps
724
+ * (p3-rules.ts: ids from ctx.mint, the twin's URL from ctx.publicBase, the kernel's crypto, bytes in ctx.blobs, secrets
725
+ * drawn from ctx.secret) and the pack layout, handler, kernel and manifest contracts. A contributor's conform is the
726
+ * whole of what a machine decides of the pack, as the catalog's admission reads it. */
727
+ async function formOf(pack) {
728
+ const { p3Gate } = await import("./gate.js");
729
+ return (await p3Gate(unitsIn(pack))).flatMap((r) => r.failures.map((f) => `${nameOf(pack, r.unit)}: ${f}`));
730
+ }
731
+ /** The pack's records hold what a reviewer reads them for: each demand row cites the application's line it was read at
732
+ * (`<path>:<line>`, or `<package>@<version>:<path>:<line>` for a call a dependency makes), the spec's record names a
733
+ * vendored file that is there, the life names the session that wrote it (what makes a judge's verdict independent),
734
+ * and the package says what it serves in a README. Rules every builder brief carried by hand, decided here. */
735
+ function records(pack) {
736
+ const out = [];
737
+ if (!existsSync(join(pack, 'README.md')))
738
+ out.push('README.md is missing: the package says what it serves, for whom, its doors and what it leaves out');
739
+ const life = readJson(join(pack, 'journeys', 'customer-life.json'));
740
+ if (life && (typeof life.author !== 'string' || !life.author.trim()))
741
+ out.push('journeys/customer-life.json names no author (the session that wrote it), so no verdict on it can be shown independent');
742
+ for (const unit of unitsIn(pack)) {
743
+ const name = nameOf(pack, unit);
744
+ const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
745
+ const demand = unit === pack || hasJourneySource(unit) ? readJson(join(journeys, 'demand.json')) : undefined;
746
+ for (const app of demand?.applications ?? [])
747
+ for (const row of app.backend ?? []) {
748
+ if (typeof row.evidence !== 'string' || !/:\d+/.test(row.evidence))
749
+ out.push(`${name}: ${app.name ?? 'an application'}'s call of ${row.operation ?? '(no operation)'} cites no line it was read at (evidence "<path>:<line>")`);
750
+ }
751
+ if (!hasOwnSurface(unit))
752
+ continue;
753
+ const source = existsSync(join(unit, 'spec', 'SOURCE.md')) ? readFileSync(join(unit, 'spec', 'SOURCE.md'), 'utf8') : '';
754
+ const file = /\*\*File:\*\*[^`\n]*`([^`]+)`/.exec(source)?.[1];
755
+ if (file && !existsSync(join(unit, 'spec', file)))
756
+ out.push(`${name}: spec/SOURCE.md names ${file} as the vendored spec, and spec/ holds no such file`);
757
+ }
758
+ return out;
759
+ }
482
760
  /** Every check over the pack at `dir`, in order. */
483
- export async function conformance(dir) {
761
+ /** `onCheck` hears each check's id as it starts, so a caller can show where a long run stands. */
762
+ export async function conformance(dir, options = {}) {
484
763
  const pack = resolve(dir);
485
764
  if (!existsSync(join(pack, 'package.json')))
486
765
  throw new Error(`${dir} is no pack (no package.json): conformance takes a pack's directory`);
@@ -490,8 +769,10 @@ export async function conformance(dir) {
490
769
  const here = process.cwd();
491
770
  process.chdir(scratch);
492
771
  const run = async (id, asks, check) => {
772
+ options.onCheck?.(id);
493
773
  try {
494
- return { id, asks, failures: await check() };
774
+ const r = await check();
775
+ return Array.isArray(r) ? { id, asks, failures: r } : { id, asks, failures: r.failures, note: r.note };
495
776
  }
496
777
  catch (error) {
497
778
  return { id, asks, failures: [`the check could not run: ${error.message.split('\n')[0]}`] };
@@ -506,6 +787,7 @@ export async function conformance(dir) {
506
787
  await run('registered', 'a vendor-backed unit, lane or not, reaches the registered pack\'s state system', () => registered(pack, runtime)),
507
788
  await run('allowance', 'a rate budget above the fallback rests on the vendor\'s documented allowance, cited in its manifest', () => allowance(pack, runtime)),
508
789
  await (async () => {
790
+ options.onCheck?.('client');
509
791
  const asks = "driven by the vendor's own client (its official SDK), served as a World runs it, the vendor's documented behaviour holds";
510
792
  try {
511
793
  const r = await clientCases(pack, scratch);
@@ -517,6 +799,9 @@ export async function conformance(dir) {
517
799
  })(),
518
800
  await run('life', 'the life walked over HTTP against the pack served as a World runs it, with its scenario: every check held', () => lifeServed(pack)),
519
801
  await run('standalone', 'served by its server.ts as a World runs it, the pack answers HTTP and the boot probe, and each declared socket upgrades', () => standalone(pack)),
802
+ await run('surface', 'every served operation called over HTTP as an application calls it, plainly and with a scenario loaded: answered within 10 s, no undeclared 500, no field of the twin\'s own, a success in the spec\'s shape, and every refresh list a fresh account reads answering', () => surfaceSwept(pack, scratch)),
803
+ await run('records', 'each demand row cites the line it was read at, the spec\'s record names a vendored file that is there, the life names its author, the package has its README', () => records(pack)),
804
+ await run('form', 'every unit passes the form section of the grade (the gate): the layout, the handler and kernel contracts, the manifest as data, the rules an author\'s code holds', () => formOf(pack)),
520
805
  ];
521
806
  }
522
807
  finally {
@@ -201,7 +201,11 @@ export async function runConsumerWorkflow(packDir, options) {
201
201
  throw new Error('Resume changed recorded application state before readback');
202
202
  await run('retained-readback', ['run', '--', ...recipe.retention.readback]);
203
203
  const after = JSON.parse(await run('log-after-readback', ['log', '--json']));
204
- if (JSON.stringify(before) !== JSON.stringify(after))
204
+ // the readback adds no write of the application's: what it adds may only be the vendor's own moves (a key's
205
+ // last-used time, recorded by the vendor as a request uses it: actor `system`, ctx.asVendor), never an API call's
206
+ const added = after.filter((entry) => !before.some((prior) => prior.id === entry.id));
207
+ const kept = before.every((prior) => after.some((entry) => JSON.stringify(entry) === JSON.stringify(prior)));
208
+ if (!kept || added.some((entry) => entry.actor?.kind !== 'system'))
205
209
  throw new Error('Retained readback added mutations; it must only inspect existing state');
206
210
  retainedReadback = true;
207
211
  }
@@ -39,4 +39,9 @@ export declare function initFiles(vendor: string): Record<string, string>;
39
39
  * surface of its own under `<lane>/src`, keeping the vendor's one store and sharing its `src/semantics/shared.ts`, and the
40
40
  * pack's server routing each lane's host to it. */
41
41
  export declare function laneInitFiles(vendor: string, lanes: string[]): Record<string, string>;
42
+ /** A unit's fixed files as the manifest and the spec make them now (architecture, "Creating a pack"): a lane's fetch, and
43
+ * a pack's fetch, index, server and cli. A pack with a wire of its own (`src/stream.ts`, SMTP's and Redis's byte-stream
44
+ * door) writes its server and cli itself: they carry its listener beside the fetch, so they are not fixed. `--index`
45
+ * writes these and the grade's `fixed-reproducible` compares them byte for byte. */
46
+ export declare function fixedFiles(dir: string): Promise<Record<string, string>>;
42
47
  export declare function createPack(args: string[]): Promise<void>;
@@ -160,6 +160,9 @@ export function packFetchFile(dir, name) {
160
160
  const hasLive = existsSync(join(semantics, 'live.ts'));
161
161
  // a GraphQL API beside the REST one: its resolvers and the vendor's schema (graphql-wire.ts)
162
162
  const hasGraphql = existsSync(join(semantics, 'graphql.ts')) && existsSync(join(dir, 'src', 'generated', 'graphql-sdl.gen.json'));
163
+ // a stream whose documents are BSON (the MongoDB wire): the pack's own bson library, resolved from the pack as graphql's is
164
+ const packageJson = existsSync(join(dir, 'package.json')) ? parseExactJson(readFileSync(join(dir, 'package.json'), 'utf8')) : {};
165
+ const hasBson = existsSync(join(dir, 'src', 'stream.ts')) && packageJson.dependencies?.bson !== undefined;
163
166
  const screensDir = join(dir, 'src', 'screens');
164
167
  const screens = existsSync(screensDir) ? readdirSync(screensDir).filter((n) => /\.tsx?$/.test(n) && !/\.(test|uitest)\.tsx?$/.test(n) && !/^shared\.tsx?$/.test(n)).sort() : [];
165
168
  const id = (file) => file.replace(/\.tsx?$/, '');
@@ -205,6 +208,7 @@ export function packFetchFile(dir, name) {
205
208
  ...screens.map((f) => `import { screen as ${identifier(`screen_${id(f)}`)} } from './screens/${f}';`),
206
209
  ...laneImports,
207
210
  '',
211
+ ...(hasBson ? ["/** The pack's BSON library, resolved from the pack as graphql's loader is (architecture, the MongoDB wire). */", "export const loadBson = (): Promise<unknown> => import('bson');", ''] : []),
208
212
  `export function ${name}(options: PackFetchOptions = {}): DerivedFetch {`,
209
213
  ' return createPackFetch({',
210
214
  ' manifest,',
@@ -507,6 +511,27 @@ export function laneInitFiles(vendor, lanes) {
507
511
  }
508
512
  return files;
509
513
  }
514
+ /** A unit's fixed files as the manifest and the spec make them now (architecture, "Creating a pack"): a lane's fetch, and
515
+ * a pack's fetch, index, server and cli. A pack with a wire of its own (`src/stream.ts`, SMTP's and Redis's byte-stream
516
+ * door) writes its server and cli itself: they carry its listener beside the fetch, so they are not fixed. `--index`
517
+ * writes these and the grade's `fixed-reproducible` compares them byte for byte. */
518
+ export async function fixedFiles(dir) {
519
+ const files = { 'src/fetch.ts': packFetchFile(dir, fetchName(dir)) };
520
+ if (!existsSync(join(dir, 'package.json')))
521
+ return files;
522
+ const { manifest } = await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')));
523
+ const vendor = manifest?.vendor;
524
+ if (!vendor || !/^[a-z][a-z0-9-]*$/.test(vendor))
525
+ throw new Error('Pack manifest must declare its vendor identity independently of the npm package name');
526
+ files['src/index.ts'] = packIndexFile(dir, vendor);
527
+ if (!existsSync(join(dir, 'src', 'stream.ts'))) {
528
+ // a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form
529
+ const lanesOnly = !hasOwnSurface(dir) && readdirSync(dir).some((n) => existsSync(join(dir, n, 'src', 'manifest.ts')));
530
+ files['src/server.ts'] = serverFile(vendor, !lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json')));
531
+ files['src/cli.ts'] = cliFile(vendor);
532
+ }
533
+ return files;
534
+ }
510
535
  export async function createPack(args) {
511
536
  const [unit, ...rest] = args;
512
537
  if (!unit || (!rest.includes('--index') && !rest.includes('--brief') && !rest.includes('--init'))) {
@@ -598,9 +623,12 @@ export async function createPack(args) {
598
623
  writeFileSync(path, handlerIndex(dir));
599
624
  console.log(`create-pack: wrote ${path}`);
600
625
  }
601
- // the fetch, again: a door file, the clock or a screen may have been added since
602
- writeFileSync(join(dir, 'src', 'fetch.ts'), packFetchFile(dir, fetchName(dir)));
603
- console.log(`create-pack: wrote ${join(dir, 'src', 'fetch.ts')}`);
626
+ // the fixed files, again: a door file, the clock, a screen, a lane or a proto may have been added since, and a pack
627
+ // made by an earlier create-pack takes the current form
628
+ for (const [rel, text] of Object.entries(await fixedFiles(dir))) {
629
+ writeFileSync(join(dir, rel), text);
630
+ console.log(`create-pack: wrote ${join(dir, rel)}`);
631
+ }
604
632
  // a pack's own index (a lane's is the vendor's to name): the descriptor with what the kernel derives from the surface
605
633
  if (existsSync(join(dir, 'package.json'))) {
606
634
  const { manifest } = await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')));
@@ -617,7 +645,13 @@ export async function createPack(args) {
617
645
  // a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form,
618
646
  // the one form a unit takes after --init (a server.ts of a wire of the pack's own, SMTP's, is never written over)
619
647
  if (!lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'))) {
620
- writeFileSync(join(dir, 'src', 'server.ts'), serverFile(vendor, true, existsSync(join(dir, 'src', 'stream.ts'))));
648
+ const wire = existsSync(join(dir, 'src', 'stream.ts'));
649
+ // a raw-tcp pack exporting one create<Name>TwinStream is taken to serve that stream on its main port, and is admitted
650
+ // by its TCP answer alone (world-runtime host.ts servesByteStream); this one serves gRPC there, its stream on a side port
651
+ if (wire && manifest?.descriptor?.transport === 'raw-tcp') {
652
+ throw new Error(`create-pack: ${vendor} serves gRPC on its main port and its own wire (./stream.ts) on a side port, so its descriptor's transport is not raw-tcp: raw-tcp with a stream export says the main port is that stream (world-runtime host.ts servesByteStream).`);
653
+ }
654
+ writeFileSync(join(dir, 'src', 'server.ts'), serverFile(vendor, true, wire));
621
655
  console.log(`create-pack: wrote ${join(dir, 'src', 'server.ts')}`);
622
656
  }
623
657
  else if (manifest?.live?.length) {
@@ -21,7 +21,7 @@ import { gunzipSync } from 'node:zlib';
21
21
  import { CRUD_CLASSES, fromOpenAPI, fromSmithy, fromSwagger2, slug } from "./spec-ir.js";
22
22
  import { fromCommandReplies } from "./spec-ir-lines.js";
23
23
  import { fromProto, protoSchema, readProtoFiles } from "./spec-ir-proto.js";
24
- import { fromRedisCommands } from "./spec-ir-commands.js";
24
+ import { fromDriverCommandBuilders, fromRedisCommands } from "./spec-ir-commands.js";
25
25
  import { fromClientCalls } from "./spec-ir-client.js";
26
26
  import { discoveryMethods, fromDiscovery, isDiscovery } from "./spec-ir-discovery.js";
27
27
  import { handlerCitations, quotedPage, sourceHeld, urlsIn } from "./check-sources.js";
@@ -34,22 +34,28 @@ if (!vendor) {
34
34
  console.error('usage: twin-standard derive <pack dir> [--out <dir>]');
35
35
  process.exit(1);
36
36
  }
37
+ // A source-declared refusal is deterministic over the pinned bytes. Unexpected
38
+ // loader, filesystem and process failures keep status 1 and must be retried.
39
+ const SOURCE_REFUSAL = 2;
37
40
  const PKG = unitAt(vendor).dir;
38
41
  // an OpenAPI document, an AWS Smithy JSON model (smithy.json), the RFC a line protocol is (rfcNNNN.txt), or the
39
42
  // Protocol Buffers files an RPC service is published as (spec/proto/, every file the service imports), or the calls a
40
43
  // vendor's own client makes where the vendor publishes no spec (client-ops.json, spec-ir-client.ts), or one API's several
41
44
  // OpenAPI documents (spec/openapi/, read as their union: spec-documents.ts), or a Google API Discovery document
42
- // (discovery.json, spec-ir-discovery.ts)
45
+ // (discovery.json, spec-ir-discovery.ts), or the command documents a vendor's own client builds where the vendor
46
+ // publishes no table a pack may derive from (command-builders.json, spec-ir-commands.ts)
43
47
  const rfc = existsSync(join(PKG, 'spec')) ? readdirSync(join(PKG, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
44
- const specPath = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'smithy.json.gz', 'smithy.json', 'discovery.json.gz', 'discovery.json', ...(rfc ? [rfc] : []), 'openapi', 'proto', 'commands', 'client-ops.json'].map((f) => join(PKG, 'spec', f)).find((p) => existsSync(p));
48
+ const specPath = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'smithy.json.gz', 'smithy.json', 'discovery.json.gz', 'discovery.json', ...(rfc ? [rfc] : []), 'openapi', 'proto', 'commands', 'command-builders.json', 'client-ops.json'].map((f) => join(PKG, 'spec', f)).find((p) => existsSync(p));
45
49
  if (!specPath) {
46
- console.error(`derive-pack: no spec at ${join(PKG, 'spec')}/openapi.{json,yaml}[.gz], smithy.json[.gz], rfcNNNN.txt, proto/, commands/ or client-ops.json`);
47
- process.exit(1);
50
+ console.error(`derive-pack: no spec at ${join(PKG, 'spec')}/openapi.{json,yaml}[.gz], smithy.json[.gz], rfcNNNN.txt, proto/, commands/, command-builders.json or client-ops.json`);
51
+ process.exit(SOURCE_REFUSAL);
48
52
  }
49
53
  const rpcService = basename(specPath) === 'proto';
50
54
  const severalDocuments = basename(specPath) === 'openapi';
51
55
  // a command language's table, one vendored file per command (Redis's src/commands/*.json), with its version in SOURCE.md
52
56
  const commandTable = basename(specPath) === 'commands';
57
+ // a language whose commands are documents: the table a pack's extractor read from the vendor's client
58
+ const commandBuilders = basename(specPath) === 'command-builders.json';
53
59
  const protoTree = (dir, at = '') => Object.fromEntries(readdirSync(join(dir, at), { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? Object.entries(protoTree(dir, join(at, e.name))) : e.name.endsWith('.proto') ? [[join(at, e.name), readFileSync(join(dir, at, e.name), 'utf8')]] : [])));
54
60
  const bytes = rpcService || commandTable || severalDocuments ? Buffer.from('') : readFileSync(specPath);
55
61
  const text = (specPath.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
@@ -164,7 +170,7 @@ if (existsSync(patchPath)) {
164
170
  for (const p of parseExactJson(readFileSync(patchPath, 'utf8')).patches) {
165
171
  if (!p.why?.trim()) {
166
172
  console.error(`${vendor}: spec patch ${p.op} ${p.path} gives no why`);
167
- process.exit(1);
173
+ process.exit(SOURCE_REFUSAL);
168
174
  }
169
175
  {
170
176
  const s = p.source;
@@ -183,7 +189,7 @@ if (severalDocuments) {
183
189
  const { doc: merged, contradictions } = mergeDocuments(doc);
184
190
  if (contradictions.length) {
185
191
  console.error(`${vendor}: the documents contradict each other; a patch (citing what shows which holds) rules each:\n ${contradictions.join('\n ')}`);
186
- process.exit(1);
192
+ process.exit(SOURCE_REFUSAL);
187
193
  }
188
194
  doc = merged;
189
195
  }
@@ -193,7 +199,9 @@ const out = outAt >= 0 && process.argv[outAt + 1] ? process.argv[outAt + 1] : jo
193
199
  mkdirSync(out, { recursive: true });
194
200
  // A line protocol's surface is its commands and the replies each may answer; its manifest's transitions
195
201
  // name commands and cite the RFC.
196
- if (lineProtocol || commandTable) {
202
+ if (commandBuilders)
203
+ doc = fromDriverCommandBuilders(doc);
204
+ if (lineProtocol || commandTable || commandBuilders) {
197
205
  const spec = doc;
198
206
  writeFileSync(join(out, 'surface.gen.json'), `${JSON.stringify({ generatedBy: 'scripts/derive-pack.ts', spec: basename(specPath), ...spec })}\n`);
199
207
  const manifestPath = join(PKG, 'src', 'manifest.ts');
@@ -213,7 +221,7 @@ if (lineProtocol || commandTable) {
213
221
  }
214
222
  if (evidence.length) {
215
223
  console.error(`${vendor}: ${evidence.length} citation(s) do not hold:\n ${[...new Set(evidence)].join('\n ')}`);
216
- process.exit(1);
224
+ process.exit(SOURCE_REFUSAL);
217
225
  }
218
226
  console.log(`${vendor}: spec ${spec.version}, ${spec.commands.length} commands${'anyCommand' in spec ? `; any command may answer ${spec.anyCommand.join(', ')}` : ''}`);
219
227
  process.exit(0);
@@ -322,7 +330,7 @@ if (existsSync(manifestPath)) {
322
330
  }
323
331
  if (evidence.length) {
324
332
  console.error(`${vendor}: ${evidence.length} citation(s) do not hold:\n ${[...new Set(evidence)].join('\n ')}`);
325
- process.exit(1);
333
+ process.exit(SOURCE_REFUSAL);
326
334
  }
327
335
  // every state candidate of a resource the pack models is ruled on: a state field, or named not one
328
336
  const unruled = ir.resources.flatMap((r) => {
@@ -331,7 +339,7 @@ if (existsSync(manifestPath)) {
331
339
  });
332
340
  if (unruled.length) {
333
341
  console.error(`${vendor}: the manifest does not rule on ${unruled.length} state candidate(s): ${unruled.join(', ')} (declare each a state field or list it in notState)`);
334
- process.exit(1);
342
+ process.exit(SOURCE_REFUSAL);
335
343
  }
336
344
  // a resource's refresh scope ("The real-system adapters", What a pack declares) names the list that enumerates it or
337
345
  // its retrieve, or says why the vendor offers no read-back; whether every stored resource declares one is
@@ -421,14 +429,14 @@ if (existsSync(manifestPath)) {
421
429
  });
422
430
  if (scopes.length) {
423
431
  console.error(`${vendor}: ${scopes.length} refresh scope(s) do not hold:\n ${scopes.join('\n ')}`);
424
- process.exit(1);
432
+ process.exit(SOURCE_REFUSAL);
425
433
  }
426
434
  // the architecture's mechanical rules over the pack's own code (./p3-rules.ts): a line breaking one records why, or
427
435
  // the build stops
428
436
  const hits = p3RuleHits(PKG);
429
437
  if (hits.length) {
430
438
  console.error(`${vendor}: ${hits.length} line(s) break a Protocol 3 rule (fix it, or record why on the line: // p3: <reason>):\n ${hits.map((h) => `${h.file}:${h.line}: ${h.rule}\n ${h.text}`).join('\n ')}`);
431
- process.exit(1);
439
+ process.exit(SOURCE_REFUSAL);
432
440
  }
433
441
  }
434
442
  const counts = new Map();
@@ -7,4 +7,5 @@ export declare function laneRootFailures(dir: string): string[];
7
7
  export declare function p3Gate(units: string[], opts?: Pick<GradeOptions, 'onCriterion'>): Promise<Array<{
8
8
  unit: string;
9
9
  failures: string[];
10
+ cacheable?: boolean;
10
11
  }>>;