@githolon/dsl 0.100.6 → 0.101.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 (82) hide show
  1. package/dart/CHANGELOG.md +6 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +78 -16
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/bundle_version_pairing_test.dart +11 -22
  8. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  9. package/dart/test/errors_test.dart +63 -12
  10. package/dart/test/first_app_gauntlet_test.dart +29 -3
  11. package/dart/test/webview_bridge_test.dart +274 -0
  12. package/dart_types/CHANGELOG.md +6 -0
  13. package/dart_types/lib/src/errors.dart +136 -30
  14. package/dart_types/lib/src/surfaced.dart +48 -2
  15. package/dart_types/lib/src/wire.dart +3 -1
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/dart_types/test/surfaced_test.dart +19 -0
  18. package/package.json +1 -1
  19. package/src/activity.ts +43 -4
  20. package/src/aggregate.ts +132 -22
  21. package/src/attested_core.ts +1 -1
  22. package/src/authoring.ts +119 -6
  23. package/src/birth_meter.ts +4 -4
  24. package/src/birth_meter_ensure.ts +9 -1
  25. package/src/births.ts +53 -22
  26. package/src/build_package.ts +55 -14
  27. package/src/business_birth.ts +48 -7
  28. package/src/codegen_dart.ts +267 -37
  29. package/src/codegen_dart_client.ts +921 -82
  30. package/src/codegen_openapi.ts +28 -5
  31. package/src/codegen_proof.ts +107 -11
  32. package/src/codegen_ts.ts +961 -135
  33. package/src/compile_package_main.ts +155 -45
  34. package/src/conflicts.ts +252 -0
  35. package/src/describe_model.ts +448 -0
  36. package/src/directive.ts +5 -38
  37. package/src/drivers.ts +12 -4
  38. package/src/engine_entry.ts +4 -0
  39. package/src/evolve_preview.ts +72 -11
  40. package/src/fields.ts +201 -55
  41. package/src/framework/application_home.ts +103 -0
  42. package/src/framework/application_home_domain.ts +2 -0
  43. package/src/framework/attested_receipts.ts +2 -2
  44. package/src/framework/auth_provider_law.ts +51 -24
  45. package/src/framework/business_corrections.ts +170 -0
  46. package/src/framework/disclosure.ts +1 -1
  47. package/src/framework/domain_lifecycle.ts +250 -16
  48. package/src/framework/encryption_identity.ts +1 -1
  49. package/src/framework/encryption_scope.ts +21 -13
  50. package/src/framework/era_certification.ts +1 -1
  51. package/src/framework/framework_aggregates.ts +2 -0
  52. package/src/framework/genesis.ts +1 -1
  53. package/src/framework/home.ts +22 -9
  54. package/src/framework/identity.ts +5 -5
  55. package/src/framework/impure_capability.ts +2 -2
  56. package/src/framework/owned_custody.ts +561 -0
  57. package/src/framework/platform_admin.ts +1 -1
  58. package/src/framework/relations.ts +9 -1
  59. package/src/framework/repair.ts +1 -1
  60. package/src/framework/signing_identity.ts +2 -2
  61. package/src/framework/source_roster.ts +1 -1
  62. package/src/framework/workspaces.ts +17 -10
  63. package/src/generated_client_names.ts +39 -0
  64. package/src/index.ts +20 -15
  65. package/src/kernel_capabilities.ts +25 -1
  66. package/src/law_check.ts +4 -1
  67. package/src/manifest.ts +356 -63
  68. package/src/read_projection_manifest.ts +8 -0
  69. package/src/read_projection_namespace.ts +4 -0
  70. package/src/relation.ts +1 -1
  71. package/src/shape_compat.ts +47 -2
  72. package/src/stable_ids.ts +38 -6
  73. package/src/stable_ids_types.ts +30 -0
  74. package/src/surfaced_lint.ts +0 -0
  75. package/src/usd.ts +62 -11
  76. package/src/usd_layers.ts +54 -3
  77. package/src/value_object.ts +23 -19
  78. package/src/wire.ts +1 -0
  79. package/src/wire_encode.ts +48 -4
  80. package/src/workspace_routing.ts +57 -0
  81. package/src/workspace_sharding.ts +9 -9
  82. package/src/certified_read.ts +0 -313
package/src/directive.ts CHANGED
@@ -20,7 +20,6 @@ import type { AggregateHandle, ReadModelRow } from "./aggregate.js";
20
20
  import type { Field } from "./fields.js";
21
21
  import type { PlannedOp } from "./ops.js";
22
22
  import type { Ports } from "./ctx.js";
23
- import type { CertifiedReadDecl } from "./certified_read.js";
24
23
  import type { RelationDecl } from "./relation.js";
25
24
  import type { QueryDecl } from "./query.js";
26
25
  import { normalizeActivity, type ActivityInput, type ActivityDescriptor } from "./activity.js";
@@ -117,12 +116,14 @@ export interface Directive<P = unknown> {
117
116
  */
118
117
  readonly description?: string;
119
118
  /**
120
- * OPT-IN user-facing ACTIVITY metadata (`architecture/activity_descriptors.md`) — the kind/title/message
119
+ * Optional user-facing ACTIVITY polish (`architecture/activity_descriptors.md`) — the kind/title/message
121
120
  * template, refs, params, category/icon/severity a client uses to render a typed activity row from this
122
121
  * directive's intents (no string-enricher). Set by `.activity({...})`; NON-LAW, additive (same class as
123
122
  * `description` — a directive that never declares activity is byte-identical). Normalized + validated at
124
- * author time. `undefined` ⇒ this directive contributes no activity row. (The setter is `.activity({...})`;
125
- * this stored field is `activityDescriptor` to avoid a method/field name clash on `RequirableDirective`.)
123
+ * author time. `undefined` ⇒ codegen supplies the framework-owned baseline row from this directive's
124
+ * business name, declared effects and verified author/time; the change never disappears from the story.
125
+ * (The setter is `.activity({...})`; this stored field is `activityDescriptor` to avoid a method/field
126
+ * name clash on `RequirableDirective`.)
126
127
  */
127
128
  readonly activityDescriptor?: ActivityDescriptor;
128
129
  /**
@@ -258,19 +259,6 @@ export interface Directive<P = unknown> {
258
259
  * (declares no emits). Gate-side enforcement (emitted ⊆ declared) is a later step.
259
260
  */
260
261
  readonly declaredEmits: Record<string, { max?: number }>;
261
- /**
262
- * The directive's DECLARED CertifiedReads (survival Constraint 5; `certified_read.md`
263
- * §"Tenant authoring"): the profiled, named write-path reads its `decide()` consults — the
264
- * `reads:{ name: q.someProfiledRead(args) }` shape, keyed by the LOCAL binding name. Each
265
- * value is a profile-conforming {@link CertifiedReadDecl} (compiled through SQL-profile-v0 at
266
- * `certifiedRead(...).sql(...)`, so an under-specified read is refused at COMPILE). The
267
- * declared `query_id`s LAND IN THE CERTIFIED MANIFEST (`manifest.ts`), so the ONE write-path
268
- * gate (`executor::admit` step 5.6) DERIVES the expected declared-read set and REFUSES any
269
- * admit where a declared read produced no witness (the vacuous-pass close). Defaults to `{}`
270
- * (declares no write-path reads) — OMITTED ENTIRELY from the manifest when empty, so a
271
- * read-free directive's canonical manifest is byte-identical to before this existed.
272
- */
273
- readonly declaredCertifiedReads: Record<string, CertifiedReadDecl>;
274
262
  /**
275
263
  * The directive's DECLARED cross-workspace relations (`cross_workspace.md` §2): the relations
276
264
  * a cross-workspace PR proposing THIS directive is adjudicated against. The TARGET domain
@@ -480,19 +468,6 @@ export interface RequirableDirective<P> extends Directive<P> {
480
468
  * var / missing payload field refuse). Non-law, additive; a directive that never calls it is byte-identical.
481
469
  */
482
470
  activity(input: ActivityInput): RequirableDirective<P>;
483
- /**
484
- * Declare the directive's CertifiedReads (survival Constraint 5; `certified_read.md`
485
- * §"Tenant authoring") — the profiled, named write-path reads its `decide()` consults, the
486
- * `reads:{ name: q.someProfiledRead(args) }` shape. Each value is a {@link CertifiedReadDecl}
487
- * (built via `certifiedRead("id").sql("…", {multiRow,uniqueTieBreakers})`, which refuses an
488
- * under-specified read at COMPILE through SQL-profile-v0). The declared `query_id`s LAND IN
489
- * THE CERTIFIED MANIFEST, so the ONE write-path gate DERIVES the required-read set and refuses
490
- * any admit where a declared read produced no witness (the vacuous-pass close). Callable
491
- * multiple times to accumulate (a repeated local name's later decl wins). Purely additive +
492
- * non-destructive — a directive that never calls this declares no write-path reads (`{}`) and
493
- * is byte-identical in the manifest to before this existed. Returns a NEW requirable directive.
494
- */
495
- readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P>;
496
471
  /**
497
472
  * Declare a cross-workspace relation (`cross_workspace.md` §2) this directive (the TARGET of a
498
473
  * cross-workspace PR's `proposes`) is adjudicated against — built via
@@ -697,7 +672,6 @@ class DirectivePlanStep<Id extends string, P> {
697
672
  declaredReads: [],
698
673
  declaredQueryReads: [],
699
674
  declaredEmits: {},
700
- declaredCertifiedReads: {},
701
675
  declaredRelations: [],
702
676
  declaredHostCapabilities: [],
703
677
  // `.withId(...)` — record the caller-supplied-id extractor (codegen/authoring hint). The `absent`
@@ -935,13 +909,6 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
935
909
  ];
936
910
  return makeRequirable({ ...base, activityDescriptor: normalizeActivity(base.id, input, base.payloadSchema, createdIds) });
937
911
  },
938
- readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P> {
939
- // Accumulate onto any prior declared CertifiedReads (a repeated local name's later decl
940
- // wins) — additive, non-destructive. The local binding NAME (the key) is what `decide`
941
- // reads via `reads.<name>`; the decl's `queryId` is the manifest-landed identity.
942
- const merged: Record<string, CertifiedReadDecl> = { ...base.declaredCertifiedReads, ...reads };
943
- return makeRequirable({ ...base, declaredCertifiedReads: merged });
944
- },
945
912
  declaresRelation(rel: RelationDecl): RequirableDirective<P> {
946
913
  // Accumulate onto any prior relations — additive, non-destructive. A relation lands in
947
914
  // the manifest keyed by its `proposes` directive (cross_workspace.md §3.2.1).
package/src/drivers.ts CHANGED
@@ -23,6 +23,7 @@ import type { WireDriver } from "./wire.js";
23
23
  */
24
24
  export type KernelDriverKind =
25
25
  | "Lww"
26
+ | "FirstWriterWins"
26
27
  // ── EXPLICIT TIME-AUTHORITY Lww drivers (the "whose clock?" axis) ──
27
28
  // Nomos has no servers, so "do we trust the clock?" is the WRONG question — the real
28
29
  // one is WHOSE clock orders THIS field, and a dev assigns it per field. `Lww` trusts
@@ -57,6 +58,9 @@ export interface Driver {
57
58
  }
58
59
 
59
60
  export const Lww: Driver = { kind: "Lww" };
61
+ /** Nomos's ordinary scalar conflict policy: an informed later edit replaces the
62
+ * value; concurrent edits converge on the earliest writer. */
63
+ export const FirstWriterWins: Driver = { kind: "FirstWriterWins" };
60
64
  export const AddWins: Driver = { kind: "AddWins" };
61
65
  export const Conflict: Driver = { kind: "Conflict" };
62
66
  export const MapOf = (inner: Driver): Driver => ({ kind: "MapOf", inner });
@@ -97,9 +101,10 @@ export const LastPosition: Driver = { kind: "LastPosition" };
97
101
  export const RemoveWins: Driver = { kind: "RemoveWins" };
98
102
  export const Counter: Driver = { kind: "Counter" };
99
103
 
100
- /** The Lww-class inner drivers `Surfaced` may wrap in v1 (wholesale-replace scalars —
101
- * the pending set holds competing SCALAR values a human picks between). */
104
+ /** The scalar inner drivers `Surfaced` may wrap in v1. The pending set preserves competing
105
+ * business versions until a resolver produces one merged result. */
102
106
  const SURFACED_INNER_KINDS: ReadonlySet<DriverKind> = new Set<DriverKind>([
107
+ "FirstWriterWins",
103
108
  "Lww",
104
109
  "LwwCausal",
105
110
  "LwwBy",
@@ -114,7 +119,8 @@ const SURFACED_INNER_KINDS: ReadonlySet<DriverKind> = new Set<DriverKind>([
114
119
  * competing values as a bounded pending-observation set (cap 8, deterministically
115
120
  * pruned) that a read surfaces as `{"__surfaced":{"pending":[{value, author,
116
121
  * occurredAt}, ...]}}` — ALWAYS 2+ competing observations (a single-observation
117
- * set read-collapses to the plain scalar) — so an app can show a "pick one" UI.
122
+ * set read-collapses to the plain scalar) — so an app can explain the competing versions
123
+ * and produce a merged business result.
118
124
  * Linear writes and
119
125
  * fast-forwards behave EXACTLY as the inner driver (`Surfaced(LwwBy(p))` keeps
120
126
  * `p`'s write filter); pending sets form ONLY under real concurrency.
@@ -139,7 +145,7 @@ export const Surfaced = (inner: Driver): Driver => {
139
145
  if (!SURFACED_INNER_KINDS.has(inner.kind)) {
140
146
  throw new Error(
141
147
  `Surfaced(${inner.kind}) is not supported — Surfaced wraps an Lww-class SCALAR inner driver only ` +
142
- `(Lww, LwwCausal, LwwBy) in v1. A pending set holds competing scalar values a human picks between; ` +
148
+ `(FirstWriterWins, Lww, LwwCausal, LwwBy) in v1. A pending set holds competing scalar versions until the business merges them; ` +
143
149
  `'${inner.kind}' ${
144
150
  inner.kind === "Surfaced"
145
151
  ? "is already Surfaced — nesting adds nothing"
@@ -160,6 +166,8 @@ const PALETTE_ONLY: ReadonlySet<DriverKind> = new Set<DriverKind>([]);
160
166
  /** Encode one Driver to its kernel wire shape, or throw if not yet in the kernel. */
161
167
  export function encodeDriver(d: Driver): WireDriver {
162
168
  switch (d.kind) {
169
+ case "FirstWriterWins":
170
+ return "FirstWriterWins";
163
171
  case "Lww":
164
172
  return "Lww";
165
173
  // ── EXPLICIT TIME-AUTHORITY Lww drivers (kernel `merge_field` arms) ──
@@ -81,6 +81,8 @@ import type { InvariantBody, InvariantEvidence, InvariantVerdict } from "./relat
81
81
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
82
82
  import type { QueryRow } from "./report.js";
83
83
  import { expandBusinessBirthExports } from "./business_birth.js";
84
+ import { withOwnedCustodyExports } from "./framework/owned_custody.js";
85
+ import { withBusinessCorrectionExports } from "./framework/business_corrections.js";
84
86
 
85
87
  /** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
86
88
  export type DomainModuleExports = Record<string, unknown>;
@@ -249,6 +251,8 @@ function mergeModules(mods: readonly DomainModuleExports[], domain: string): Dom
249
251
  let merged: DomainModuleExports = {};
250
252
  for (const m of mods) merged = { ...merged, ...m };
251
253
  merged = expandBusinessBirthExports(merged, domain);
254
+ merged = withOwnedCustodyExports(merged);
255
+ merged = withBusinessCorrectionExports(merged);
252
256
  // `capability()` bundles expand into discoverable entries here — the engine
253
257
  // plane's ONE merge point, so the registry sees the task aggregate + quartet
254
258
  // without per-piece re-exports (capability_exports.ts).
@@ -7,19 +7,21 @@
7
7
  * THE EVOLVE PREVIEW — `githolon check --against <prior>` (architecture/author_time_gate.md,
8
8
  * the `--against` row of check slice 3). The evolve gate is the kernel's #58 diff-by-stable-id
9
9
  * (executor/src/law58.rs::evolve_violation): a RENAME (same sid, new label) admits silently; a
10
- * true REMOVAL / RETYPE refuses TYPED unless the deploy JSON carries a `dispositions`
11
- * ({retired:[sid], retyped:{sid:why}, rebinds:{newSid:oldSid}}) acknowledging it in-history.
10
+ * true REMOVAL / merge-driver change refuses TYPED unless the deploy JSON carries a
11
+ * `dispositions` acknowledgement. A changed BUSINESS VALUE CONTRACT is stricter: it needs
12
+ * deterministic `.evolvesFrom(...)` coverage and cannot be waived by an acknowledgement.
12
13
  *
13
14
  * This module is the AUTHOR-TIME TWIN of that gate: given the PRIOR law and the CURRENT build,
14
15
  * it diffs them BY STABLE ID and pre-fires the gate's verdicts — with the EXACT generated
15
- * dispositions fragment (pre-filled with the real sids) inside the refusal's `fix`, so the dev
16
- * pastes it into their deploy JSON and the real deploy admits. It classifies:
16
+ * repair inside the refusal's `fix`, matching what the real gate will accept. It classifies:
17
17
  *
18
18
  * RENAME (note) — same sid, new label; admits silently, old rows read the new label.
19
19
  * ADDITIVE (note) — a new aggregate/field/directive (incl. an additive optional VO field);
20
20
  * the evolve gate admits it silently (vo_first_class.md slice 6).
21
21
  * REMOVAL (refuse) — a sid disappeared; the gate REFUSES without dispositions.retired:[sid].
22
- * RETYPE (refuse) — same sid, changed read-kind or merge-driver; needs dispositions.retyped.
22
+ * CONTRACT (refuse) — same sid, changed value language without a deterministic projection;
23
+ * needs `.evolvesFrom(...)`, never a disposition.
24
+ * DRIVER (refuse) — same sid, changed merge driver; needs dispositions.retyped.
23
25
  * PAYLOAD (warn) — a directive's payload shape changed; clients compiled against the old
24
26
  * law get typed 422s until regenerated (directives are not sid-tracked).
25
27
  *
@@ -44,7 +46,15 @@ export interface EvolveAggregate {
44
46
  readonly label: string;
45
47
  /** keyed by FIELD SID. `driver` is the merge-law JSON (from the lockfile/usda schemas)
46
48
  * when known — it lets the retype arm catch a driver-only change (same read kind). */
47
- readonly fields: Record<string, { sid: string; label: string; kind: string; driver?: string }>;
49
+ readonly fields: Record<string, {
50
+ sid: string;
51
+ label: string;
52
+ kind: string;
53
+ driver?: string;
54
+ contract?: string;
55
+ schema?: string;
56
+ evolutions?: readonly { from: string; to: string }[];
57
+ }>;
48
58
  }
49
59
 
50
60
  /** One domain's evolve surface: aggregates (sid-keyed) + directive payload shapes (name-keyed). */
@@ -87,6 +97,11 @@ function aggregatesFromStableIds(
87
97
  label: fLabel,
88
98
  kind: f.kind ?? "",
89
99
  ...(driver !== undefined ? { driver: JSON.stringify(driver) } : {}),
100
+ ...(f.contract !== undefined ? { contract: f.contract } : {}),
101
+ ...(f.schema !== undefined ? { schema: f.schema } : {}),
102
+ ...(f.evolutions !== undefined ? {
103
+ evolutions: f.evolutions.map((rule) => ({ from: rule.from, to: rule.to })),
104
+ } : {}),
90
105
  };
91
106
  }
92
107
  out[a.sid] = { sid: a.sid, label: aggLabel, fields };
@@ -188,11 +203,23 @@ export function evolveLawFromDeployJson(deploy: DeployJson): EvolveLaw {
188
203
  const usdDom = fromUsd[key];
189
204
  if (usdDom !== undefined) {
190
205
  for (const agg of Object.values(dom.aggregates)) {
191
- const schema = usdDom.schemas[agg.label];
192
- if (schema === undefined) continue;
206
+ const stableAggregate = Object.values(usdDom.stableIds.aggregates)
207
+ .find((candidate) => candidate.sid === agg.sid);
208
+ const stableFields = new Map(
209
+ Object.values(stableAggregate?.fields ?? {})
210
+ .map((field) => [field.sid, field] as const),
211
+ );
212
+ const schema = usdDom.schemas[agg.label]
213
+ ?? Object.entries(usdDom.schemas).find(([type]) => type.endsWith(`.${agg.label}`))?.[1];
193
214
  for (const f of Object.values(agg.fields)) {
194
- const driver = schema[f.label];
215
+ const driver = schema?.[f.label];
195
216
  if (driver !== undefined) (f as { driver?: string }).driver = JSON.stringify(driver);
217
+ const stable = stableFields.get(f.sid);
218
+ if (stable?.contract !== undefined) f.contract = stable.contract;
219
+ if (stable?.schema !== undefined) f.schema = stable.schema;
220
+ if (stable?.evolutions !== undefined) {
221
+ f.evolutions = stable.evolutions.map((rule) => ({ from: rule.from, to: rule.to }));
222
+ }
196
223
  }
197
224
  }
198
225
  }
@@ -274,6 +301,28 @@ function ensureDisp(map: Record<string, EvolveDispositionsJson>, domain: string)
274
301
  return (map[domain] ??= {});
275
302
  }
276
303
 
304
+ /** Mirror executor::law58::evolution_reaches: one deterministic route per source,
305
+ * accumulated across supported releases, must terminate at the current schema. */
306
+ function evolutionReaches(
307
+ field: EvolveAggregate["fields"][string],
308
+ source: string | undefined,
309
+ target: string | undefined,
310
+ ): boolean {
311
+ if (source === undefined || target === undefined) return false;
312
+ if (source === target) return true;
313
+ const bySource = new Map((field.evolutions ?? []).map((rule) => [rule.from, rule.to]));
314
+ const seen = new Set<string>();
315
+ let cursor = source;
316
+ while (cursor !== target) {
317
+ if (seen.has(cursor)) return false;
318
+ seen.add(cursor);
319
+ const next = bySource.get(cursor);
320
+ if (next === undefined) return false;
321
+ cursor = next;
322
+ }
323
+ return true;
324
+ }
325
+
277
326
  /** Diff one domain's aggregates/fields/directives BY STABLE ID and append findings + dispositions. */
278
327
  function diffDomain(
279
328
  domain: string,
@@ -332,10 +381,22 @@ function diffDomain(
332
381
  const kindChanged = oldF.kind !== "" && newF.kind !== "" && oldF.kind !== newF.kind;
333
382
  const driverChanged =
334
383
  oldF.driver !== undefined && newF.driver !== undefined && oldF.driver !== newF.driver;
335
- if (kindChanged || driverChanged) {
384
+ const contractChanged =
385
+ oldF.contract !== undefined && newF.contract !== undefined && oldF.contract !== newF.contract;
386
+ const contractCovered = contractChanged && evolutionReaches(newF, oldF.schema, newF.schema);
387
+ if (contractChanged && !contractCovered) {
388
+ findings.push({
389
+ domain,
390
+ level: "refuse",
391
+ what: `field '${oldAgg.label}.${oldF.label}' (stable id ${fSid}) changes its business value contract without deterministic evolution coverage`,
392
+ fix: `declare .evolvesFrom(oldField) for a compatible widening or .evolvesFrom(oldField, value => nextValue) for an exhaustive finite business mapping; dispositions.retyped cannot waive this`,
393
+ });
394
+ continue;
395
+ }
396
+ if ((kindChanged && !contractCovered) || driverChanged) {
336
397
  const d = ensureDisp(disp, domain);
337
398
  (d.retyped ??= {})[fSid] = "<why goes here>";
338
- const how = kindChanged
399
+ const how = kindChanged && !contractCovered
339
400
  ? `read kind ${oldF.kind} → ${newF.kind}`
340
401
  : `merge driver ${oldF.driver} → ${newF.driver}`;
341
402
  findings.push({