@tiangong-ai/cli 0.0.35 → 0.0.37

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 (47) hide show
  1. package/AGENTS.md +16 -6
  2. package/README.md +128 -10
  3. package/dist/research/orchestration.js +334 -29
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/workspace/acquisition-routes.d.ts +14 -0
  6. package/dist/research/workspace/acquisition-routes.js +45 -0
  7. package/dist/research/workspace/acquisition-routes.js.map +1 -0
  8. package/dist/research/workspace/audit-bundle.d.ts +44 -0
  9. package/dist/research/workspace/audit-bundle.js +357 -0
  10. package/dist/research/workspace/audit-bundle.js.map +1 -0
  11. package/dist/research/workspace/broker.js +54 -10
  12. package/dist/research/workspace/broker.js.map +1 -1
  13. package/dist/research/workspace/downloads.d.ts +6 -0
  14. package/dist/research/workspace/downloads.js +32 -8
  15. package/dist/research/workspace/downloads.js.map +1 -1
  16. package/dist/research/workspace/evidence-exhaustion.d.ts +45 -0
  17. package/dist/research/workspace/evidence-exhaustion.js +365 -0
  18. package/dist/research/workspace/evidence-exhaustion.js.map +1 -0
  19. package/dist/research/workspace/native-activity.d.ts +6 -0
  20. package/dist/research/workspace/native-activity.js +24 -7
  21. package/dist/research/workspace/native-activity.js.map +1 -1
  22. package/dist/research/workspace/preflight.d.ts +34 -2
  23. package/dist/research/workspace/preflight.js +161 -1
  24. package/dist/research/workspace/preflight.js.map +1 -1
  25. package/dist/research/workspace/projects.d.ts +28 -4
  26. package/dist/research/workspace/projects.js +301 -12
  27. package/dist/research/workspace/projects.js.map +1 -1
  28. package/dist/research/workspace/publication-workflow.js +2 -0
  29. package/dist/research/workspace/publication-workflow.js.map +1 -1
  30. package/dist/research/workspace/runtime.d.ts +151 -3
  31. package/dist/research/workspace/runtime.js +180 -6
  32. package/dist/research/workspace/runtime.js.map +1 -1
  33. package/dist/research/workspace/sanitization.js +9 -3
  34. package/dist/research/workspace/sanitization.js.map +1 -1
  35. package/dist/research/workspace/scientific-design.d.ts +359 -0
  36. package/dist/research/workspace/scientific-design.js +2021 -0
  37. package/dist/research/workspace/scientific-design.js.map +1 -0
  38. package/dist/research/workspace/scientific-review.d.ts +101 -0
  39. package/dist/research/workspace/scientific-review.js +1167 -0
  40. package/dist/research/workspace/scientific-review.js.map +1 -0
  41. package/dist/research/workspace/setup-catalog.js +2 -2
  42. package/dist/research/workspace/setup.js +12 -3
  43. package/dist/research/workspace/setup.js.map +1 -1
  44. package/dist/research/workspace/types.d.ts +54 -0
  45. package/dist/research/workspace/workspace.js +25 -7
  46. package/dist/research/workspace/workspace.js.map +1 -1
  47. package/package.json +2 -1
@@ -0,0 +1,2021 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { lstat, readFile } from "node:fs/promises";
3
+ import { isAbsolute } from "node:path";
4
+ import { CliError } from "../../errors.js";
5
+ import { sanitizeResearchValue } from "./sanitization.js";
6
+ import { sha256Text } from "./storage.js";
7
+ const MAX_SCIENTIFIC_DESIGN_BYTES = 2 * 1024 * 1024;
8
+ const IDENTIFIER = "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$";
9
+ const nonEmptyString = { type: "string", minLength: 1 };
10
+ const stringArray = {
11
+ type: "array",
12
+ items: nonEmptyString,
13
+ uniqueItems: true,
14
+ };
15
+ const identifierArray = {
16
+ type: "array",
17
+ items: { type: "string", pattern: IDENTIFIER },
18
+ uniqueItems: true,
19
+ };
20
+ const studyKinds = [
21
+ "causal-empirical",
22
+ "observational-empirical",
23
+ "predictive-model",
24
+ "mechanism-model",
25
+ "cross-model-comparison",
26
+ "scenario-analysis",
27
+ "material-flow-accounting",
28
+ "systematic-synthesis",
29
+ "methods-data-resource",
30
+ ];
31
+ const resultClasses = [
32
+ "causal-estimate",
33
+ "observational-estimate",
34
+ "validated-forecast",
35
+ "model-output",
36
+ "scenario-output",
37
+ "accounting-output",
38
+ "systematic-synthesis",
39
+ "method-performance",
40
+ ];
41
+ export function scientificDesignSchema() {
42
+ const endpointSchema = {
43
+ type: "object",
44
+ additionalProperties: false,
45
+ required: [
46
+ "id",
47
+ "label",
48
+ "physicalConstruct",
49
+ "scale",
50
+ "unit",
51
+ "timeBasis",
52
+ "modelStructureId",
53
+ "truthRole",
54
+ ],
55
+ properties: {
56
+ id: { type: "string", pattern: IDENTIFIER },
57
+ label: nonEmptyString,
58
+ physicalConstruct: nonEmptyString,
59
+ scale: nonEmptyString,
60
+ unit: nonEmptyString,
61
+ timeBasis: nonEmptyString,
62
+ modelStructureId: { type: ["string", "null"], pattern: IDENTIFIER },
63
+ truthRole: {
64
+ type: "string",
65
+ enum: [
66
+ "field-observation",
67
+ "experimental-reference",
68
+ "engineering-model",
69
+ "proxy",
70
+ "scenario-output",
71
+ "accounting-output",
72
+ ],
73
+ },
74
+ },
75
+ };
76
+ return {
77
+ $schema: "https://json-schema.org/draft/2020-12/schema",
78
+ $id: "https://schemas.tiangong.ai/research/scientific-design-v1.json",
79
+ title: "Tiangong top-journal scientific design contract",
80
+ type: "object",
81
+ additionalProperties: false,
82
+ required: [
83
+ "schemaVersion",
84
+ "projectId",
85
+ "workingTitle",
86
+ "identity",
87
+ "policyRuleDispositions",
88
+ "estimands",
89
+ "claims",
90
+ "edges",
91
+ "endpoints",
92
+ "comparisons",
93
+ "quantities",
94
+ "validationPlans",
95
+ "thresholds",
96
+ "evidenceRoles",
97
+ "acquisitionPlan",
98
+ "knownGaps",
99
+ "uncertaintyParameters",
100
+ "uncertaintyGroups",
101
+ "factors",
102
+ "baselinePlan",
103
+ "contextPlan",
104
+ ],
105
+ properties: {
106
+ schemaVersion: { type: "integer", const: 1 },
107
+ projectId: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{2,63}$" },
108
+ workingTitle: { type: "string", minLength: 8 },
109
+ identity: {
110
+ type: "object",
111
+ additionalProperties: false,
112
+ required: [
113
+ "centralStudyKind",
114
+ "contributionStatement",
115
+ "components",
116
+ "modelStructures",
117
+ "allowedClaimVerbs",
118
+ "targetJournals",
119
+ ],
120
+ properties: {
121
+ centralStudyKind: { type: "string", enum: studyKinds },
122
+ contributionStatement: nonEmptyString,
123
+ components: {
124
+ type: "array",
125
+ minItems: 1,
126
+ items: {
127
+ type: "object",
128
+ additionalProperties: false,
129
+ required: ["kind", "role", "purpose", "bridgeEdgeIds"],
130
+ properties: {
131
+ kind: { type: "string", enum: studyKinds },
132
+ role: { type: "string", enum: ["central", "supporting", "contextual"] },
133
+ purpose: nonEmptyString,
134
+ bridgeEdgeIds: identifierArray,
135
+ },
136
+ },
137
+ },
138
+ modelStructures: {
139
+ type: "array",
140
+ items: {
141
+ type: "object",
142
+ additionalProperties: false,
143
+ required: [
144
+ "id",
145
+ "label",
146
+ "family",
147
+ "version",
148
+ "equationSet",
149
+ "coefficientSet",
150
+ "implementationArtifactSha256",
151
+ "implementationArtifactLocator",
152
+ "implementationEntrypoint",
153
+ "implementationStatus",
154
+ "implementationFreezeBeforeGate",
155
+ "environmentLockSha256",
156
+ "environmentLockLocator",
157
+ "environmentLockStatus",
158
+ "environmentLockFreezeBeforeGate",
159
+ "artifactHashBasis",
160
+ "baselineRole",
161
+ "baselineSelectionJustification",
162
+ "sourceEvidenceRoleIds",
163
+ ],
164
+ properties: {
165
+ id: { type: "string", pattern: IDENTIFIER },
166
+ label: nonEmptyString,
167
+ family: nonEmptyString,
168
+ version: nonEmptyString,
169
+ equationSet: nonEmptyString,
170
+ coefficientSet: nonEmptyString,
171
+ implementationArtifactSha256: {
172
+ type: "string",
173
+ pattern: "^[a-f0-9]{64}$",
174
+ },
175
+ implementationArtifactLocator: {
176
+ type: "string",
177
+ pattern: "^(?:projects/[a-z0-9][a-z0-9-]{2,63}|lineage/objects)/[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$",
178
+ },
179
+ implementationEntrypoint: nonEmptyString,
180
+ implementationStatus: {
181
+ type: "string",
182
+ enum: ["executable-frozen", "pending-source-acquisition"],
183
+ },
184
+ implementationFreezeBeforeGate: {
185
+ type: "string",
186
+ enum: ["research-design", "evidence-construct", "pilot-methods"],
187
+ },
188
+ environmentLockSha256: { type: "string", pattern: "^[a-f0-9]{64}$" },
189
+ environmentLockLocator: {
190
+ type: "string",
191
+ pattern: "^(?:projects/[a-z0-9][a-z0-9-]{2,63}|lineage/objects)/[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$",
192
+ },
193
+ environmentLockStatus: {
194
+ type: "string",
195
+ enum: ["exact-frozen", "pending-runtime-lock"],
196
+ },
197
+ environmentLockFreezeBeforeGate: {
198
+ type: "string",
199
+ enum: ["research-design", "evidence-construct", "pilot-methods"],
200
+ },
201
+ artifactHashBasis: { const: "raw-file-bytes" },
202
+ baselineRole: {
203
+ type: "string",
204
+ enum: ["candidate", "reference", "strongest-available"],
205
+ },
206
+ baselineSelectionJustification: nonEmptyString,
207
+ sourceEvidenceRoleIds: identifierArray,
208
+ },
209
+ },
210
+ },
211
+ allowedClaimVerbs: stringArray,
212
+ targetJournals: {
213
+ type: "object",
214
+ additionalProperties: false,
215
+ required: ["primary", "alternate", "fallback", "approvalStatus"],
216
+ properties: {
217
+ primary: nonEmptyString,
218
+ alternate: nonEmptyString,
219
+ fallback: nonEmptyString,
220
+ approvalStatus: { type: "string", enum: ["candidate-only", "policy-approved"] },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ policyRuleDispositions: {
226
+ type: "array",
227
+ items: {
228
+ type: "object",
229
+ additionalProperties: false,
230
+ required: [
231
+ "ruleId",
232
+ "status",
233
+ "dueGate",
234
+ "rationale",
235
+ "claimIds",
236
+ "evidenceRoleIds",
237
+ "validationPlanIds",
238
+ "knownGapIds",
239
+ "uncertaintyParameterIds",
240
+ "modelStructureIds",
241
+ ],
242
+ properties: {
243
+ ruleId: { type: "string", pattern: IDENTIFIER },
244
+ status: {
245
+ type: "string",
246
+ enum: ["satisfied-by-design", "planned", "scope-limited", "incompatible"],
247
+ },
248
+ dueGate: {
249
+ type: "string",
250
+ enum: [
251
+ "research-design",
252
+ "evidence-construct",
253
+ "pilot-methods",
254
+ "publication-freeze",
255
+ ],
256
+ },
257
+ rationale: nonEmptyString,
258
+ claimIds: identifierArray,
259
+ evidenceRoleIds: identifierArray,
260
+ validationPlanIds: identifierArray,
261
+ knownGapIds: identifierArray,
262
+ uncertaintyParameterIds: identifierArray,
263
+ modelStructureIds: identifierArray,
264
+ },
265
+ },
266
+ },
267
+ estimands: {
268
+ type: "array",
269
+ minItems: 1,
270
+ items: {
271
+ type: "object",
272
+ additionalProperties: false,
273
+ required: [
274
+ "id",
275
+ "role",
276
+ "population",
277
+ "analysisUnit",
278
+ "exposure",
279
+ "comparator",
280
+ "outcome",
281
+ "spatialScale",
282
+ "timeHorizon",
283
+ "resultClass",
284
+ ],
285
+ properties: {
286
+ id: { type: "string", pattern: IDENTIFIER },
287
+ role: { type: "string", enum: ["central", "supporting", "contextual"] },
288
+ population: nonEmptyString,
289
+ analysisUnit: nonEmptyString,
290
+ exposure: nonEmptyString,
291
+ comparator: nonEmptyString,
292
+ outcome: nonEmptyString,
293
+ spatialScale: nonEmptyString,
294
+ timeHorizon: nonEmptyString,
295
+ resultClass: { type: "string", enum: resultClasses },
296
+ },
297
+ },
298
+ },
299
+ claims: {
300
+ type: "array",
301
+ minItems: 1,
302
+ items: {
303
+ type: "object",
304
+ additionalProperties: false,
305
+ required: [
306
+ "id",
307
+ "role",
308
+ "statement",
309
+ "resultClass",
310
+ "edgeIds",
311
+ "endpointIds",
312
+ "comparisonIds",
313
+ "estimandIds",
314
+ "quantityIds",
315
+ "evidenceRoleIds",
316
+ "hypothesisMode",
317
+ "nullOutcomeStatement",
318
+ ],
319
+ properties: {
320
+ id: { type: "string", pattern: IDENTIFIER },
321
+ role: { type: "string", enum: ["central", "supporting", "contextual"] },
322
+ statement: nonEmptyString,
323
+ resultClass: { type: "string", enum: resultClasses },
324
+ edgeIds: identifierArray,
325
+ endpointIds: identifierArray,
326
+ comparisonIds: identifierArray,
327
+ estimandIds: identifierArray,
328
+ quantityIds: identifierArray,
329
+ evidenceRoleIds: identifierArray,
330
+ hypothesisMode: {
331
+ type: "string",
332
+ enum: ["two-sided", "directional", "descriptive", "not-applicable"],
333
+ },
334
+ nullOutcomeStatement: { type: ["string", "null"] },
335
+ },
336
+ },
337
+ },
338
+ edges: {
339
+ type: "array",
340
+ minItems: 1,
341
+ items: {
342
+ type: "object",
343
+ additionalProperties: false,
344
+ required: [
345
+ "id",
346
+ "role",
347
+ "fromConstruct",
348
+ "toConstruct",
349
+ "evidenceMode",
350
+ "requiredJoinKeys",
351
+ "temporalAlignment",
352
+ "spatialAlignment",
353
+ "fromModelStructureIds",
354
+ "toModelStructureIds",
355
+ "fromEndpointIds",
356
+ "toEndpointIds",
357
+ "operatorId",
358
+ "operatorDefinition",
359
+ "aggregationRule",
360
+ "scaleReconciliation",
361
+ "quantityIds",
362
+ "uncertaintyParameterIds",
363
+ "sameSectionRequired",
364
+ "sameEventRequired",
365
+ "status",
366
+ "blockingReason",
367
+ ],
368
+ properties: {
369
+ id: { type: "string", pattern: IDENTIFIER },
370
+ role: { type: "string", enum: ["central", "supporting", "contextual"] },
371
+ fromConstruct: nonEmptyString,
372
+ toConstruct: nonEmptyString,
373
+ evidenceMode: {
374
+ type: "string",
375
+ enum: ["direct-observation", "model-bridge", "accounting-bridge", "assumption-only"],
376
+ },
377
+ requiredJoinKeys: identifierArray,
378
+ temporalAlignment: nonEmptyString,
379
+ spatialAlignment: nonEmptyString,
380
+ fromModelStructureIds: identifierArray,
381
+ toModelStructureIds: identifierArray,
382
+ fromEndpointIds: identifierArray,
383
+ toEndpointIds: identifierArray,
384
+ operatorId: { type: "string", pattern: IDENTIFIER },
385
+ operatorDefinition: nonEmptyString,
386
+ aggregationRule: nonEmptyString,
387
+ scaleReconciliation: nonEmptyString,
388
+ quantityIds: identifierArray,
389
+ uncertaintyParameterIds: identifierArray,
390
+ sameSectionRequired: { type: "boolean" },
391
+ sameEventRequired: { type: "boolean" },
392
+ status: { type: "string", enum: ["planned", "constructible", "blocked"] },
393
+ blockingReason: { type: ["string", "null"] },
394
+ },
395
+ },
396
+ },
397
+ endpoints: { type: "array", minItems: 1, items: endpointSchema },
398
+ comparisons: {
399
+ type: "array",
400
+ items: {
401
+ type: "object",
402
+ additionalProperties: false,
403
+ required: [
404
+ "id",
405
+ "leftEndpointId",
406
+ "rightEndpointId",
407
+ "operation",
408
+ "axis",
409
+ "quantityIds",
410
+ "thresholdIds",
411
+ "decisionRule",
412
+ "reportingLevel",
413
+ "truthEndpointId",
414
+ ],
415
+ properties: {
416
+ id: { type: "string", pattern: IDENTIFIER },
417
+ leftEndpointId: { type: "string", pattern: IDENTIFIER },
418
+ rightEndpointId: { type: "string", pattern: IDENTIFIER },
419
+ operation: {
420
+ type: "string",
421
+ enum: [
422
+ "error",
423
+ "accuracy",
424
+ "validation",
425
+ "agreement",
426
+ "discrepancy",
427
+ "ranking",
428
+ "qualitative-boundary",
429
+ ],
430
+ },
431
+ axis: {
432
+ type: "string",
433
+ enum: [
434
+ "same-endpoint-cross-model",
435
+ "model-to-observation",
436
+ "decision-consequence",
437
+ "qualitative-boundary",
438
+ ],
439
+ },
440
+ quantityIds: identifierArray,
441
+ thresholdIds: identifierArray,
442
+ decisionRule: nonEmptyString,
443
+ reportingLevel: nonEmptyString,
444
+ truthEndpointId: { type: ["string", "null"], pattern: IDENTIFIER },
445
+ },
446
+ },
447
+ },
448
+ quantities: {
449
+ type: "array",
450
+ items: {
451
+ type: "object",
452
+ additionalProperties: false,
453
+ required: [
454
+ "id",
455
+ "label",
456
+ "quantityType",
457
+ "unit",
458
+ "numeratorType",
459
+ "denominatorType",
460
+ "denominatorDescription",
461
+ "normalizationMode",
462
+ "normalizationJustification",
463
+ "valueMode",
464
+ "uncertaintyParameterIds",
465
+ "spatialScope",
466
+ "temporalScope",
467
+ "allowedTerms",
468
+ "prohibitedTerms",
469
+ ],
470
+ properties: {
471
+ id: { type: "string", pattern: IDENTIFIER },
472
+ label: nonEmptyString,
473
+ quantityType: {
474
+ type: "string",
475
+ enum: ["share", "material", "rate", "count", "index", "other"],
476
+ },
477
+ unit: nonEmptyString,
478
+ numeratorType: nonEmptyString,
479
+ denominatorType: { type: "string", pattern: IDENTIFIER },
480
+ denominatorDescription: nonEmptyString,
481
+ normalizationMode: {
482
+ type: "string",
483
+ enum: ["symmetric", "directional-convention", "not-applicable"],
484
+ },
485
+ normalizationJustification: nonEmptyString,
486
+ valueMode: {
487
+ type: "string",
488
+ enum: ["signed", "absolute", "nonnegative", "categorical"],
489
+ },
490
+ uncertaintyParameterIds: identifierArray,
491
+ spatialScope: nonEmptyString,
492
+ temporalScope: nonEmptyString,
493
+ allowedTerms: stringArray,
494
+ prohibitedTerms: stringArray,
495
+ },
496
+ },
497
+ },
498
+ validationPlans: {
499
+ type: "array",
500
+ items: {
501
+ type: "object",
502
+ additionalProperties: false,
503
+ required: [
504
+ "id",
505
+ "claimIds",
506
+ "role",
507
+ "parameterDatasetIds",
508
+ "comparisonDatasetIds",
509
+ "datasetRoles",
510
+ "factorIds",
511
+ "exposureIdentifierAvailable",
512
+ "independentDataGeneratingProcess",
513
+ "outcomeBlind",
514
+ "originalUnitCount",
515
+ "independentClusterCount",
516
+ "effectiveIndependentUnits",
517
+ "originalUnitDefinition",
518
+ "independentClusterDefinition",
519
+ "nestingRule",
520
+ "reportingUnitDefinition",
521
+ "clusterKeyIds",
522
+ "independenceJustification",
523
+ "resamplingUnit",
524
+ "resamplingIterations",
525
+ "resamplingMethod",
526
+ "resamplingStateSpaceSize",
527
+ "reportingPrecision",
528
+ "minimumDetectableDifference",
529
+ "independentValidation",
530
+ "status",
531
+ "blockingReason",
532
+ ],
533
+ properties: {
534
+ id: { type: "string", pattern: IDENTIFIER },
535
+ claimIds: identifierArray,
536
+ role: {
537
+ type: "string",
538
+ enum: [
539
+ "internal-holdout",
540
+ "temporal-holdout",
541
+ "section-holdout",
542
+ "external-dgp",
543
+ "cross-model-reference",
544
+ "background-constraint",
545
+ "not-applicable",
546
+ ],
547
+ },
548
+ parameterDatasetIds: identifierArray,
549
+ comparisonDatasetIds: identifierArray,
550
+ datasetRoles: {
551
+ type: "array",
552
+ items: {
553
+ type: "object",
554
+ additionalProperties: false,
555
+ required: ["datasetId", "role", "sharedUpstreamIds", "justification"],
556
+ properties: {
557
+ datasetId: { type: "string", pattern: IDENTIFIER },
558
+ role: {
559
+ type: "string",
560
+ enum: [
561
+ "parameter-source",
562
+ "endpoint-definition-source",
563
+ "non-independent-cross-check",
564
+ "independent-validation",
565
+ "background-context",
566
+ ],
567
+ },
568
+ sharedUpstreamIds: identifierArray,
569
+ justification: nonEmptyString,
570
+ },
571
+ },
572
+ },
573
+ factorIds: identifierArray,
574
+ exposureIdentifierAvailable: { type: "boolean" },
575
+ independentDataGeneratingProcess: { type: "boolean" },
576
+ outcomeBlind: { type: "boolean" },
577
+ originalUnitCount: { type: "integer", minimum: 0 },
578
+ independentClusterCount: { type: "integer", minimum: 0 },
579
+ effectiveIndependentUnits: { type: "number", minimum: 0 },
580
+ originalUnitDefinition: nonEmptyString,
581
+ independentClusterDefinition: nonEmptyString,
582
+ nestingRule: nonEmptyString,
583
+ reportingUnitDefinition: nonEmptyString,
584
+ clusterKeyIds: identifierArray,
585
+ independenceJustification: nonEmptyString,
586
+ resamplingUnit: nonEmptyString,
587
+ resamplingIterations: { type: "integer", minimum: 0 },
588
+ resamplingMethod: {
589
+ type: "string",
590
+ enum: ["exact-enumeration", "cluster-bootstrap", "none"],
591
+ },
592
+ resamplingStateSpaceSize: { type: "integer", minimum: 0 },
593
+ reportingPrecision: nonEmptyString,
594
+ minimumDetectableDifference: { type: ["string", "null"] },
595
+ independentValidation: {
596
+ type: "object",
597
+ additionalProperties: false,
598
+ required: ["status", "gapId", "justification"],
599
+ properties: {
600
+ status: {
601
+ type: "string",
602
+ enum: ["available", "planned", "unavailable-scope-bounded", "not-required"],
603
+ },
604
+ gapId: { type: ["string", "null"], pattern: IDENTIFIER },
605
+ justification: nonEmptyString,
606
+ },
607
+ },
608
+ status: { type: "string", enum: ["planned", "feasible", "impossible"] },
609
+ blockingReason: { type: ["string", "null"] },
610
+ },
611
+ },
612
+ },
613
+ thresholds: {
614
+ type: "array",
615
+ items: {
616
+ type: "object",
617
+ additionalProperties: false,
618
+ required: [
619
+ "id",
620
+ "claimId",
621
+ "quantityId",
622
+ "type",
623
+ "reportedAs",
624
+ "criterion",
625
+ "criterionQuantityIds",
626
+ "numericValue",
627
+ "unit",
628
+ "direction",
629
+ "basis",
630
+ "basisJustification",
631
+ "stabilityMode",
632
+ "stabilityQuantityId",
633
+ "stabilityRule",
634
+ "sensitivityReportingRule",
635
+ "assumptionIds",
636
+ "sensitivityParameterIds",
637
+ ],
638
+ properties: {
639
+ id: { type: "string", pattern: IDENTIFIER },
640
+ claimId: { type: "string", pattern: IDENTIFIER },
641
+ quantityId: { type: "string", pattern: IDENTIFIER },
642
+ type: { type: "string", enum: ["analytic", "scenario", "estimated", "policy-trigger"] },
643
+ reportedAs: {
644
+ type: "string",
645
+ enum: [
646
+ "analytic-threshold",
647
+ "scenario-threshold",
648
+ "estimated-threshold",
649
+ "policy-trigger",
650
+ ],
651
+ },
652
+ criterion: nonEmptyString,
653
+ criterionQuantityIds: identifierArray,
654
+ numericValue: { type: ["number", "null"] },
655
+ unit: nonEmptyString,
656
+ direction: {
657
+ type: "string",
658
+ enum: ["above", "below", "outside", "equal", "categorical"],
659
+ },
660
+ basis: {
661
+ type: "string",
662
+ enum: ["reporting-convention", "domain-standard", "decision-consequence", "policy"],
663
+ },
664
+ basisJustification: nonEmptyString,
665
+ stabilityMode: {
666
+ type: "string",
667
+ enum: ["sign", "classification", "range", "none"],
668
+ },
669
+ stabilityQuantityId: { type: ["string", "null"], pattern: IDENTIFIER },
670
+ stabilityRule: nonEmptyString,
671
+ sensitivityReportingRule: nonEmptyString,
672
+ assumptionIds: identifierArray,
673
+ sensitivityParameterIds: identifierArray,
674
+ },
675
+ },
676
+ },
677
+ evidenceRoles: {
678
+ type: "array",
679
+ minItems: 1,
680
+ items: {
681
+ type: "object",
682
+ additionalProperties: false,
683
+ required: [
684
+ "id",
685
+ "role",
686
+ "claimIds",
687
+ "coverageDimensionIds",
688
+ "sourceTypeRequirements",
689
+ "peerReviewedRequired",
690
+ "required",
691
+ "minimumFullText",
692
+ "minimumIndependentSources",
693
+ "minimumDatedSources",
694
+ ],
695
+ properties: {
696
+ id: { type: "string", pattern: IDENTIFIER },
697
+ role: {
698
+ type: "string",
699
+ enum: [
700
+ "central-model-source",
701
+ "central-data-documentation",
702
+ "closest-prior-work",
703
+ "counterevidence",
704
+ "method-identification",
705
+ "material-conversion",
706
+ "overlay-rule",
707
+ "cross-model-validation",
708
+ "pavement-context",
709
+ "limitation-boundary",
710
+ "target-journal-recent-work",
711
+ "background",
712
+ ],
713
+ },
714
+ claimIds: identifierArray,
715
+ coverageDimensionIds: identifierArray,
716
+ sourceTypeRequirements: identifierArray,
717
+ peerReviewedRequired: { type: "boolean" },
718
+ required: { type: "boolean" },
719
+ minimumFullText: { type: "integer", minimum: 0 },
720
+ minimumIndependentSources: { type: "integer", minimum: 0 },
721
+ minimumDatedSources: { type: "integer", minimum: 0 },
722
+ },
723
+ },
724
+ },
725
+ acquisitionPlan: {
726
+ type: "object",
727
+ additionalProperties: false,
728
+ required: ["routes", "stopPolicy"],
729
+ properties: {
730
+ routes: {
731
+ type: "array",
732
+ minItems: 1,
733
+ items: {
734
+ type: "object",
735
+ additionalProperties: false,
736
+ required: [
737
+ "id",
738
+ "evidenceRoleIds",
739
+ "routeClass",
740
+ "executor",
741
+ "required",
742
+ "capabilityId",
743
+ "activityKind",
744
+ "activityChannel",
745
+ "downloadBackends",
746
+ "accessMode",
747
+ "rationale",
748
+ ],
749
+ properties: {
750
+ id: { type: "string", pattern: IDENTIFIER },
751
+ evidenceRoleIds: {
752
+ ...identifierArray,
753
+ minItems: 1,
754
+ },
755
+ routeClass: {
756
+ type: "string",
757
+ enum: [
758
+ "broker-capability",
759
+ "native-discovery",
760
+ "open-access-download",
761
+ "authorized-browser",
762
+ "licensed-resource",
763
+ "owner-provided-resource",
764
+ "external-data-request",
765
+ "field-data-collection",
766
+ ],
767
+ },
768
+ executor: { type: "string", enum: ["agent", "user", "external-party"] },
769
+ required: { type: "boolean" },
770
+ capabilityId: { type: ["string", "null"], pattern: IDENTIFIER },
771
+ activityKind: {
772
+ type: ["string", "null"],
773
+ enum: [
774
+ "web-search",
775
+ "database-search",
776
+ "browser-navigation",
777
+ "download",
778
+ "file-inspection",
779
+ null,
780
+ ],
781
+ },
782
+ activityChannel: { type: ["string", "null"], pattern: IDENTIFIER },
783
+ downloadBackends: {
784
+ type: "array",
785
+ uniqueItems: true,
786
+ items: {
787
+ type: "string",
788
+ enum: [
789
+ "native-browser",
790
+ "chrome",
791
+ "cloakbrowser",
792
+ "skill-adapter",
793
+ "direct-http",
794
+ ],
795
+ },
796
+ },
797
+ accessMode: {
798
+ type: "string",
799
+ enum: [
800
+ "open-public",
801
+ "owner-authorized",
802
+ "user-authorization-required",
803
+ "purchase-or-subscription",
804
+ "external-request",
805
+ ],
806
+ },
807
+ rationale: { type: "string", minLength: 8, maxLength: 2_000 },
808
+ },
809
+ },
810
+ },
811
+ stopPolicy: {
812
+ type: "object",
813
+ additionalProperties: false,
814
+ required: [
815
+ "allAgentRoutesExhaustedBeforeHandoff",
816
+ "unresolvedRequiredEvidenceRoleBlocksDownstream",
817
+ "prohibitUnreviewedSubstitution",
818
+ ],
819
+ properties: {
820
+ allAgentRoutesExhaustedBeforeHandoff: { type: "boolean" },
821
+ unresolvedRequiredEvidenceRoleBlocksDownstream: { type: "boolean" },
822
+ prohibitUnreviewedSubstitution: { type: "boolean" },
823
+ },
824
+ },
825
+ },
826
+ },
827
+ knownGaps: {
828
+ type: "array",
829
+ items: {
830
+ type: "object",
831
+ additionalProperties: false,
832
+ required: [
833
+ "id",
834
+ "description",
835
+ "sourceProjectId",
836
+ "sourceArtifacts",
837
+ "lineageStatus",
838
+ "disposition",
839
+ "evidenceRefs",
840
+ ],
841
+ properties: {
842
+ id: { type: "string", pattern: IDENTIFIER },
843
+ description: nonEmptyString,
844
+ sourceProjectId: { type: ["string", "null"], pattern: "^[a-z0-9][a-z0-9-]{2,63}$" },
845
+ sourceArtifacts: {
846
+ type: "array",
847
+ items: {
848
+ type: "object",
849
+ additionalProperties: false,
850
+ required: ["sha256", "objectLocator", "hashBasis", "kind"],
851
+ properties: {
852
+ sha256: { type: "string", pattern: "^[a-f0-9]{64}$" },
853
+ objectLocator: {
854
+ type: "string",
855
+ pattern: "^(?:projects/[a-z0-9][a-z0-9-]{2,63}|lineage/objects)/[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$",
856
+ },
857
+ hashBasis: { const: "raw-file-bytes" },
858
+ kind: {
859
+ type: "string",
860
+ enum: [
861
+ "scientific-review-packet",
862
+ "scientific-review",
863
+ "evidence-snapshot",
864
+ "publication-assessment",
865
+ "owner-attestation",
866
+ ],
867
+ },
868
+ },
869
+ },
870
+ },
871
+ lineageStatus: {
872
+ type: "string",
873
+ enum: ["verified", "owner-attested", "unverified"],
874
+ },
875
+ disposition: {
876
+ type: "string",
877
+ enum: ["unresolved", "closed", "scope-narrowed", "user-handoff", "external-handoff"],
878
+ },
879
+ evidenceRefs: {
880
+ type: "array",
881
+ items: {
882
+ type: "object",
883
+ additionalProperties: false,
884
+ required: ["kind", "id"],
885
+ properties: {
886
+ kind: {
887
+ type: "string",
888
+ enum: ["claim", "quantity", "validation-plan", "edge", "evidence-role"],
889
+ },
890
+ id: { type: "string", pattern: IDENTIFIER },
891
+ },
892
+ },
893
+ },
894
+ },
895
+ },
896
+ },
897
+ uncertaintyParameters: {
898
+ type: "array",
899
+ items: {
900
+ type: "object",
901
+ additionalProperties: false,
902
+ required: [
903
+ "id",
904
+ "label",
905
+ "distributionOrRange",
906
+ "states",
907
+ "stateValueType",
908
+ "stateValueStatus",
909
+ "freezeBeforeGate",
910
+ "factorIds",
911
+ "applicationPoint",
912
+ "compositionRule",
913
+ "preservesFactorLevelIdentity",
914
+ "sourceEvidenceRoleIds",
915
+ "quantityIds",
916
+ ],
917
+ properties: {
918
+ id: { type: "string", pattern: IDENTIFIER },
919
+ label: nonEmptyString,
920
+ distributionOrRange: nonEmptyString,
921
+ states: {
922
+ type: "array",
923
+ minItems: 1,
924
+ items: {
925
+ type: "object",
926
+ additionalProperties: false,
927
+ required: ["id", "label", "value", "unit"],
928
+ properties: {
929
+ id: { type: "string", pattern: IDENTIFIER },
930
+ label: nonEmptyString,
931
+ value: nonEmptyString,
932
+ unit: nonEmptyString,
933
+ },
934
+ },
935
+ },
936
+ stateValueType: { type: "string", enum: ["numeric", "categorical"] },
937
+ stateValueStatus: {
938
+ type: "string",
939
+ enum: ["frozen", "pending-source-acquisition"],
940
+ },
941
+ freezeBeforeGate: {
942
+ type: "string",
943
+ enum: ["research-design", "evidence-construct", "pilot-methods"],
944
+ },
945
+ factorIds: identifierArray,
946
+ applicationPoint: nonEmptyString,
947
+ compositionRule: nonEmptyString,
948
+ preservesFactorLevelIdentity: { type: "boolean" },
949
+ sourceEvidenceRoleIds: identifierArray,
950
+ quantityIds: identifierArray,
951
+ },
952
+ },
953
+ },
954
+ uncertaintyGroups: {
955
+ type: "array",
956
+ items: {
957
+ type: "object",
958
+ additionalProperties: false,
959
+ required: [
960
+ "id",
961
+ "parameterIds",
962
+ "combinationMode",
963
+ "jointStateIds",
964
+ "jointStateBindings",
965
+ "applicationRule",
966
+ "sharedAcrossModelStructureIds",
967
+ ],
968
+ properties: {
969
+ id: { type: "string", pattern: IDENTIFIER },
970
+ parameterIds: identifierArray,
971
+ combinationMode: {
972
+ type: "string",
973
+ enum: ["one-at-a-time", "full-factorial", "explicit-joint-states"],
974
+ },
975
+ jointStateIds: identifierArray,
976
+ jointStateBindings: {
977
+ type: "array",
978
+ minItems: 1,
979
+ items: {
980
+ type: "object",
981
+ additionalProperties: false,
982
+ required: ["jointStateId", "parameterStateIds"],
983
+ properties: {
984
+ jointStateId: { type: "string", pattern: IDENTIFIER },
985
+ parameterStateIds: identifierArray,
986
+ },
987
+ },
988
+ },
989
+ applicationRule: nonEmptyString,
990
+ sharedAcrossModelStructureIds: identifierArray,
991
+ },
992
+ },
993
+ },
994
+ factors: {
995
+ type: "array",
996
+ items: {
997
+ type: "object",
998
+ additionalProperties: false,
999
+ required: [
1000
+ "id",
1001
+ "label",
1002
+ "role",
1003
+ "observationalStatus",
1004
+ "applicabilityBoundary",
1005
+ "evidenceRoleIds",
1006
+ "levels",
1007
+ ],
1008
+ properties: {
1009
+ id: { type: "string", pattern: IDENTIFIER },
1010
+ label: nonEmptyString,
1011
+ role: { type: "string", enum: ["exposure", "blocking", "scenario"] },
1012
+ observationalStatus: {
1013
+ type: "string",
1014
+ enum: ["observed", "modeled-only", "externally-defined"],
1015
+ },
1016
+ applicabilityBoundary: nonEmptyString,
1017
+ evidenceRoleIds: identifierArray,
1018
+ levels: {
1019
+ type: "array",
1020
+ minItems: 1,
1021
+ items: {
1022
+ type: "object",
1023
+ additionalProperties: false,
1024
+ required: ["id", "label", "definition", "attributes"],
1025
+ properties: {
1026
+ id: { type: "string", pattern: IDENTIFIER },
1027
+ label: nonEmptyString,
1028
+ definition: nonEmptyString,
1029
+ attributes: {
1030
+ type: "array",
1031
+ items: {
1032
+ type: "object",
1033
+ additionalProperties: false,
1034
+ required: ["id", "label", "value", "unit"],
1035
+ properties: {
1036
+ id: { type: "string", pattern: IDENTIFIER },
1037
+ label: nonEmptyString,
1038
+ value: nonEmptyString,
1039
+ unit: nonEmptyString,
1040
+ },
1041
+ },
1042
+ },
1043
+ },
1044
+ },
1045
+ },
1046
+ },
1047
+ },
1048
+ },
1049
+ baselinePlan: {
1050
+ type: "object",
1051
+ additionalProperties: false,
1052
+ required: [
1053
+ "sameInputInformation",
1054
+ "comparableCalibrationBudget",
1055
+ "sameEndpoint",
1056
+ "frozenBeforeAnalysis",
1057
+ "decisionLossMetrics",
1058
+ ],
1059
+ properties: {
1060
+ sameInputInformation: { type: "boolean" },
1061
+ comparableCalibrationBudget: { type: "boolean" },
1062
+ sameEndpoint: { type: "boolean" },
1063
+ frozenBeforeAnalysis: { type: "boolean" },
1064
+ decisionLossMetrics: {
1065
+ type: "array",
1066
+ items: {
1067
+ type: "object",
1068
+ additionalProperties: false,
1069
+ required: [
1070
+ "id",
1071
+ "label",
1072
+ "comparisonIds",
1073
+ "quantityIds",
1074
+ "decisionRule",
1075
+ "reportingLevel",
1076
+ ],
1077
+ properties: {
1078
+ id: { type: "string", pattern: IDENTIFIER },
1079
+ label: nonEmptyString,
1080
+ comparisonIds: identifierArray,
1081
+ quantityIds: identifierArray,
1082
+ decisionRule: nonEmptyString,
1083
+ reportingLevel: nonEmptyString,
1084
+ },
1085
+ },
1086
+ },
1087
+ },
1088
+ },
1089
+ contextPlan: {
1090
+ type: "object",
1091
+ additionalProperties: false,
1092
+ required: [
1093
+ "maxEstimatedTokens",
1094
+ "estimatedTokens",
1095
+ "claimCriticalCapsuleIds",
1096
+ "backgroundIndexOnly",
1097
+ "centralEvidenceFits",
1098
+ ],
1099
+ properties: {
1100
+ maxEstimatedTokens: { type: "integer", minimum: 1 },
1101
+ estimatedTokens: { type: "integer", minimum: 0 },
1102
+ claimCriticalCapsuleIds: identifierArray,
1103
+ backgroundIndexOnly: { type: "boolean" },
1104
+ centralEvidenceFits: { type: "boolean" },
1105
+ },
1106
+ },
1107
+ },
1108
+ };
1109
+ }
1110
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
1111
+ let designValidator = null;
1112
+ export function parseScientificDesign(value) {
1113
+ const validate = designValidator ?? (designValidator = ajv.compile(scientificDesignSchema()));
1114
+ if (!validate(value)) {
1115
+ throw new CliError("Scientific design does not match the authoritative schema.", {
1116
+ code: "RESEARCH_SCIENTIFIC_DESIGN_INVALID",
1117
+ exitCode: 2,
1118
+ details: sanitizeResearchValue({ validation: formatValidationErrors(validate.errors) }),
1119
+ });
1120
+ }
1121
+ const design = value;
1122
+ assertUniqueIds(design);
1123
+ assertReferences(design);
1124
+ return design;
1125
+ }
1126
+ export async function readAndVerifyScientificDesign(path, expectedProjectId) {
1127
+ if (!isAbsolute(path)) {
1128
+ throw scientificDesignPathError("Scientific design path must be absolute.");
1129
+ }
1130
+ const info = await lstat(path).catch(() => undefined);
1131
+ if (!info || !info.isFile() || info.isSymbolicLink()) {
1132
+ throw scientificDesignPathError("Scientific design path must be an existing regular file and cannot be a symbolic link.");
1133
+ }
1134
+ if (info.size > MAX_SCIENTIFIC_DESIGN_BYTES) {
1135
+ throw scientificDesignPathError(`Scientific design exceeds the ${MAX_SCIENTIFIC_DESIGN_BYTES}-byte limit.`);
1136
+ }
1137
+ let raw;
1138
+ try {
1139
+ raw = JSON.parse(await readFile(path, "utf8"));
1140
+ }
1141
+ catch {
1142
+ throw new CliError("Scientific design is not valid JSON.", {
1143
+ code: "RESEARCH_SCIENTIFIC_DESIGN_INVALID",
1144
+ exitCode: 2,
1145
+ });
1146
+ }
1147
+ const contract = parseScientificDesign(raw);
1148
+ if (expectedProjectId && contract.projectId !== expectedProjectId) {
1149
+ throw new CliError("Scientific design projectId does not match the target project.", {
1150
+ code: "RESEARCH_SCIENTIFIC_DESIGN_PROJECT_MISMATCH",
1151
+ exitCode: 2,
1152
+ details: { expectedProjectId, actualProjectId: contract.projectId },
1153
+ });
1154
+ }
1155
+ const normalized = `${JSON.stringify(contract, null, 2)}\n`;
1156
+ return {
1157
+ schemaVersion: 1,
1158
+ contract,
1159
+ sha256: sha256Text(normalized),
1160
+ bytes: Buffer.byteLength(normalized, "utf8"),
1161
+ };
1162
+ }
1163
+ export function evaluateScientificDesign(design) {
1164
+ const issues = new Map();
1165
+ const add = (code, message, objectIds = []) => {
1166
+ if (!issues.has(code)) {
1167
+ issues.set(code, { code, severity: "blocking", message, objectIds });
1168
+ }
1169
+ };
1170
+ const endpointById = new Map(design.endpoints.map((endpoint) => [endpoint.id, endpoint]));
1171
+ const quantityById = new Map(design.quantities.map((quantity) => [quantity.id, quantity]));
1172
+ const edgeById = new Map(design.edges.map((edge) => [edge.id, edge]));
1173
+ const uncertaintyParameterById = new Map(design.uncertaintyParameters.map((parameter) => [parameter.id, parameter]));
1174
+ const evidenceRoleById = new Map(design.evidenceRoles.map((role) => [role.id, role]));
1175
+ const factorById = new Map(design.factors.map((factor) => [factor.id, factor]));
1176
+ const centralClaimIds = new Set(design.claims.filter((claim) => claim.role === "central").map((claim) => claim.id));
1177
+ const centralComparisonIds = new Set(design.claims
1178
+ .filter((claim) => claim.role === "central")
1179
+ .flatMap((claim) => claim.comparisonIds));
1180
+ const modelStructureById = new Map(design.identity.modelStructures.map((model) => [model.id, model]));
1181
+ if (design.identity.centralStudyKind === "cross-model-comparison" &&
1182
+ modelStructureById.size < 2) {
1183
+ add("CROSS_MODEL_STRUCTURES_INSUFFICIENT", "A central cross-model study must freeze at least two explicitly identified model structures.", [...modelStructureById.keys()]);
1184
+ }
1185
+ const incompletelyBoundModels = design.identity.modelStructures.filter((model) => !model.equationSet.trim() ||
1186
+ !model.coefficientSet.trim() ||
1187
+ !isReviewableDigest(model.implementationArtifactSha256) ||
1188
+ !model.implementationArtifactLocator.trim() ||
1189
+ !model.implementationEntrypoint.trim() ||
1190
+ !isReviewableDigest(model.environmentLockSha256) ||
1191
+ !model.environmentLockLocator.trim() ||
1192
+ model.artifactHashBasis !== "raw-file-bytes" ||
1193
+ !model.baselineSelectionJustification.trim());
1194
+ const hasReviewableReference = design.identity.modelStructures.some((model) => ["reference", "strongest-available"].includes(model.baselineRole));
1195
+ if (incompletelyBoundModels.length ||
1196
+ (design.identity.centralStudyKind === "cross-model-comparison" && !hasReviewableReference)) {
1197
+ add("MODEL_IMPLEMENTATION_BINDING_MISSING", "Every model must freeze its equations, coefficients, implementation artifact, environment lock, and baseline-selection rationale; a cross-model design must identify a reviewable reference or strongest available baseline.", incompletelyBoundModels.length
1198
+ ? incompletelyBoundModels.map((model) => model.id)
1199
+ : [...modelStructureById.keys()]);
1200
+ }
1201
+ const modelsWithoutRetrievableObjects = design.identity.modelStructures.filter((model) => !model.implementationArtifactLocator.trim() ||
1202
+ !model.environmentLockLocator.trim() ||
1203
+ model.artifactHashBasis !== "raw-file-bytes");
1204
+ if (modelsWithoutRetrievableObjects.length) {
1205
+ add("MODEL_ARTIFACT_OBJECT_UNBOUND", "Every frozen model implementation and environment lock must bind a safe retrievable object locator whose digest is explicitly defined over raw file bytes.", modelsWithoutRetrievableObjects.map((model) => model.id));
1206
+ }
1207
+ const invalidModelFreezePlans = design.identity.modelStructures.filter((model) => (model.implementationStatus === "executable-frozen" &&
1208
+ model.implementationFreezeBeforeGate !== "research-design") ||
1209
+ (model.implementationStatus === "pending-source-acquisition" &&
1210
+ model.implementationFreezeBeforeGate === "research-design") ||
1211
+ (model.environmentLockStatus === "exact-frozen" &&
1212
+ model.environmentLockFreezeBeforeGate !== "research-design") ||
1213
+ (model.environmentLockStatus === "pending-runtime-lock" &&
1214
+ model.environmentLockFreezeBeforeGate === "research-design"));
1215
+ if (invalidModelFreezePlans.length) {
1216
+ add("MODEL_FREEZE_PLAN_INVALID", "Frozen model implementations and environment locks must be frozen at research design; pending model objects must declare a later early-review gate before which a new authoritative generation will freeze them.", invalidModelFreezePlans.map((model) => model.id));
1217
+ }
1218
+ const modelFreezePolicyBindingsMissing = design.identity.modelStructures.filter((model) => {
1219
+ const pendingGates = [
1220
+ ...(model.implementationStatus === "pending-source-acquisition"
1221
+ ? [model.implementationFreezeBeforeGate]
1222
+ : []),
1223
+ ...(model.environmentLockStatus === "pending-runtime-lock"
1224
+ ? [model.environmentLockFreezeBeforeGate]
1225
+ : []),
1226
+ ];
1227
+ return pendingGates.some((dueGate) => !design.policyRuleDispositions.some((disposition) => disposition.status === "planned" &&
1228
+ disposition.dueGate === dueGate &&
1229
+ disposition.modelStructureIds.includes(model.id)));
1230
+ });
1231
+ if (modelFreezePolicyBindingsMissing.length) {
1232
+ add("MODEL_FREEZE_POLICY_BINDING_MISSING", "Every pending model implementation or environment lock must bind to a planned Research Policy disposition due at the same early-review gate.", modelFreezePolicyBindingsMissing.map((model) => model.id));
1233
+ }
1234
+ const disconnectedComponents = design.identity.components.filter((component) => component.bridgeEdgeIds.length > 1 &&
1235
+ !bridgeEdgesConnected(component.bridgeEdgeIds, edgeById));
1236
+ if (disconnectedComponents.length) {
1237
+ add("COMPONENT_BRIDGE_GRAPH_DISCONNECTED", "Every multi-edge study component must declare one endpoint-connected executable bridge graph from its inputs to its claimed consequences.", disconnectedComponents.flatMap((component) => component.bridgeEdgeIds));
1238
+ }
1239
+ const unidentifiedModelEndpoints = design.endpoints.filter((endpoint) => endpoint.truthRole === "engineering-model" && !endpoint.modelStructureId);
1240
+ if (unidentifiedModelEndpoints.length) {
1241
+ add("MODEL_ENDPOINT_IDENTITY_MISSING", "Every engineering-model endpoint must bind one frozen model structure.", unidentifiedModelEndpoints.map((endpoint) => endpoint.id));
1242
+ }
1243
+ const observationalTruthRoles = new Set(["field-observation", "experimental-reference"]);
1244
+ const truthOperations = new Set(["error", "accuracy", "validation"]);
1245
+ const crossModelOperations = new Set(["agreement", "discrepancy", "ranking"]);
1246
+ for (const comparison of design.comparisons) {
1247
+ const left = endpointById.get(comparison.leftEndpointId);
1248
+ const right = endpointById.get(comparison.rightEndpointId);
1249
+ if (comparison.axis === "same-endpoint-cross-model" &&
1250
+ (!left.modelStructureId ||
1251
+ !right.modelStructureId ||
1252
+ left.modelStructureId === right.modelStructureId ||
1253
+ !endpointSignaturesMatch(left, right))) {
1254
+ add("CROSS_MODEL_COMPARISON_INVALID", "A same-endpoint cross-model comparison requires compatible endpoint signatures bound to two different frozen model structures.", [comparison.id, left.id, right.id]);
1255
+ }
1256
+ else if (crossModelOperations.has(comparison.operation) &&
1257
+ !endpointSignaturesMatch(left, right)) {
1258
+ add("CROSS_MODEL_COMPARISON_INVALID", "Cross-model agreement, discrepancy, and ranking require the same physical construct, scale, unit, and time basis.", [comparison.id, left.id, right.id]);
1259
+ }
1260
+ if (!truthOperations.has(comparison.operation))
1261
+ continue;
1262
+ const truth = comparison.truthEndpointId
1263
+ ? endpointById.get(comparison.truthEndpointId)
1264
+ : undefined;
1265
+ if (!truth || !observationalTruthRoles.has(truth.truthRole)) {
1266
+ add("ENDPOINT_TRUTH_ROLE_INVALID", "Error, accuracy, and validation claims require an observational or experimental truth endpoint.", [comparison.id]);
1267
+ }
1268
+ if (!endpointSignaturesMatch(left, right)) {
1269
+ add("ENDPOINT_COMPARISON_INCOMPATIBLE", "Error, accuracy, and validation operations require compatible physical endpoints, scales, units, and time bases.", [comparison.id, left.id, right.id]);
1270
+ }
1271
+ }
1272
+ const undeclaredCentralDecisionRules = design.comparisons.filter((comparison) => centralComparisonIds.has(comparison.id) &&
1273
+ (!comparison.decisionRule.trim() ||
1274
+ !comparison.reportingLevel.trim() ||
1275
+ (["agreement", "discrepancy"].includes(comparison.operation) &&
1276
+ comparison.thresholdIds.length === 0)));
1277
+ if (undeclaredCentralDecisionRules.length) {
1278
+ add("CENTRAL_DECISION_RULE_UNDECLARED", "Every central comparison must freeze an executable decision rule and reporting level; agreement or discrepancy decisions must also bind a declared threshold.", undeclaredCentralDecisionRules.map((comparison) => comparison.id));
1279
+ }
1280
+ const unexecutableCentralEdges = design.edges.filter((edge) => {
1281
+ if (edge.role !== "central")
1282
+ return false;
1283
+ if (edge.fromEndpointIds.length === 0 ||
1284
+ edge.toEndpointIds.length === 0 ||
1285
+ !edge.operatorId.trim() ||
1286
+ !edge.operatorDefinition.trim()) {
1287
+ return true;
1288
+ }
1289
+ const sourceModels = new Set(edge.fromEndpointIds
1290
+ .map((endpointId) => endpointById.get(endpointId)?.modelStructureId)
1291
+ .filter((modelId) => Boolean(modelId)));
1292
+ return edge.fromModelStructureIds.some((modelId) => !sourceModels.has(modelId));
1293
+ });
1294
+ if (unexecutableCentralEdges.length) {
1295
+ add("CROSS_SCALE_OPERATOR_UNDECLARED", "Every central cross-scale edge must bind source and destination endpoints plus a frozen, executable accumulation or reconciliation operator.", unexecutableCentralEdges.map((edge) => edge.id));
1296
+ }
1297
+ for (const plan of design.validationPlans) {
1298
+ const declaredDatasets = [...plan.parameterDatasetIds, ...plan.comparisonDatasetIds];
1299
+ const roleCount = new Map();
1300
+ for (const role of plan.datasetRoles) {
1301
+ roleCount.set(role.datasetId, (roleCount.get(role.datasetId) ?? 0) + 1);
1302
+ }
1303
+ const datasetRolesInvalid = declaredDatasets.some((datasetId) => roleCount.get(datasetId) !== 1) ||
1304
+ plan.datasetRoles.some((role) => !declaredDatasets.includes(role.datasetId)) ||
1305
+ (plan.independentValidation.status === "unavailable-scope-bounded" &&
1306
+ plan.datasetRoles.some((role) => role.role === "independent-validation"));
1307
+ if (datasetRolesInvalid) {
1308
+ add("VALIDATION_DATASET_ROLE_UNDECLARED", "Every parameter and comparison dataset must have exactly one declared role, shared-upstream disclosure, and justification consistent with the independent-validation disposition.", [plan.id, ...declaredDatasets]);
1309
+ }
1310
+ if (design.identity.centralStudyKind === "cross-model-comparison" &&
1311
+ plan.claimIds.some((claimId) => centralClaimIds.has(claimId)) &&
1312
+ plan.originalUnitCount > plan.independentClusterCount) {
1313
+ const factors = plan.factorIds.map((factorId) => factorById.get(factorId));
1314
+ const levelProduct = factors.reduce((product, factor) => product * (factor?.levels.length ?? 0), 1);
1315
+ const repeatedLevels = plan.independentClusterCount > 0
1316
+ ? plan.originalUnitCount / plan.independentClusterCount
1317
+ : Number.NaN;
1318
+ if (factors.length === 0 ||
1319
+ factors.some((factor) => !factor) ||
1320
+ !Number.isInteger(repeatedLevels) ||
1321
+ levelProduct !== repeatedLevels) {
1322
+ add("CONFIGURATION_FACTOR_UNDECLARED", "A repeated cross-model design must bind a structured factor inventory whose frozen level product exactly explains the modeled configurations within each independent cluster.", [plan.id, ...plan.factorIds]);
1323
+ }
1324
+ }
1325
+ if (plan.claimIds.some((claimId) => centralClaimIds.has(claimId)) &&
1326
+ plan.originalUnitCount > plan.independentClusterCount &&
1327
+ (!plan.originalUnitDefinition.trim() ||
1328
+ !plan.independentClusterDefinition.trim() ||
1329
+ !plan.nestingRule.trim() ||
1330
+ !plan.reportingUnitDefinition.trim())) {
1331
+ add("CONFIGURATION_INVENTORY_UNDECLARED", "A repeated-measures central design must define original units, independent clusters, their nesting rule, and the level at which results are reported.", [plan.id]);
1332
+ }
1333
+ if (plan.independentClusterCount > 0 && plan.clusterKeyIds.length === 0) {
1334
+ add("INDEPENDENT_CLUSTER_KEY_MISSING", "Every nonzero independent-cluster count must declare the stable keys used to deduplicate repeated records.", [plan.id]);
1335
+ }
1336
+ if (["external-dgp", "internal-holdout", "temporal-holdout", "section-holdout"].includes(plan.role) &&
1337
+ !plan.exposureIdentifierAvailable) {
1338
+ add("TARGET_EXPOSURE_UNIDENTIFIABLE", "A validation plan cannot test the target exposure because the validation data do not identify it.", [plan.id]);
1339
+ }
1340
+ if (plan.role === "external-dgp" && !plan.independentDataGeneratingProcess) {
1341
+ add("VALIDATION_DGP_NOT_INDEPENDENT", "An external validation plan shares a data-generating process or upstream information with calibration.", [plan.id]);
1342
+ }
1343
+ if (plan.effectiveIndependentUnits > plan.independentClusterCount ||
1344
+ plan.independentClusterCount > plan.originalUnitCount) {
1345
+ add("EFFECTIVE_SAMPLE_SIZE_INFLATED", "Effective independent units cannot exceed independent clusters or original units.", [plan.id]);
1346
+ }
1347
+ if (plan.resamplingIterations > 0 &&
1348
+ plan.originalUnitCount > plan.independentClusterCount &&
1349
+ (plan.effectiveIndependentUnits > plan.independentClusterCount ||
1350
+ /^(cell|row|observation)$/i.test(plan.resamplingUnit))) {
1351
+ add("RESAMPLING_UNIT_INVALID", "Resampling must operate at the independent data-generating cluster, not a repeated cell or row.", [plan.id]);
1352
+ }
1353
+ const resamplingStateCount = bootstrapMultisetCount(plan.effectiveIndependentUnits);
1354
+ if (plan.resamplingIterations > 0 &&
1355
+ resamplingStateCount !== null &&
1356
+ plan.resamplingIterations > resamplingStateCount) {
1357
+ add("RESAMPLING_PRECISION_UNJUSTIFIED", "Requested resampling iterations exceed the distinct cluster-level bootstrap multisets available from the effective independent units.", [plan.id]);
1358
+ }
1359
+ if (plan.resamplingMethod === "exact-enumeration" &&
1360
+ (resamplingStateCount === null ||
1361
+ plan.resamplingStateSpaceSize !== resamplingStateCount ||
1362
+ plan.resamplingIterations !== resamplingStateCount)) {
1363
+ add("RESAMPLING_STATE_SPACE_INVALID", "Exact cluster-level resampling must report and enumerate the complete distinct multiset state space.", [plan.id]);
1364
+ }
1365
+ if (plan.independentValidation.status === "unavailable-scope-bounded" &&
1366
+ !plan.independentValidation.gapId) {
1367
+ add("INDEPENDENT_VALIDATION_DISPOSITION_MISSING", "Unavailable independent validation must bind an explicit scope-limiting known gap.", [plan.id]);
1368
+ }
1369
+ }
1370
+ const unboundCentralClaims = design.claims.filter((claim) => claim.role === "central" &&
1371
+ ["model-output", "scenario-output", "accounting-output"].includes(claim.resultClass) &&
1372
+ claim.quantityIds.length === 0);
1373
+ if (unboundCentralClaims.length) {
1374
+ add("CENTRAL_CLAIM_QUANTITY_UNBOUND", "Every central model, scenario, or accounting claim must bind at least one declared quantity.", unboundCentralClaims.map((claim) => claim.id));
1375
+ }
1376
+ const missingQuantityBridges = design.claims.flatMap((claim) => {
1377
+ const boundEdgeQuantityIds = new Set(claim.edgeIds.flatMap((edgeId) => edgeById.get(edgeId)?.quantityIds ?? []));
1378
+ return claim.quantityIds
1379
+ .filter((quantityId) => !boundEdgeQuantityIds.has(quantityId))
1380
+ .map((quantityId) => `${claim.id}:${quantityId}`);
1381
+ });
1382
+ if (missingQuantityBridges.length) {
1383
+ add("CLAIM_QUANTITY_BRIDGE_MISSING", "Every quantity bound to a claim must be carried by at least one executable edge used by that claim.", missingQuantityBridges);
1384
+ }
1385
+ const centralUncertaintyFailures = design.claims
1386
+ .filter((claim) => claim.role === "central" && claim.resultClass === "model-output")
1387
+ .flatMap((claim) => claim.quantityIds.flatMap((quantityId) => {
1388
+ const quantity = quantityById.get(quantityId);
1389
+ if (!quantity)
1390
+ return [];
1391
+ const quantityEdges = claim.edgeIds
1392
+ .map((edgeId) => edgeById.get(edgeId))
1393
+ .filter((edge) => Boolean(edge?.quantityIds.includes(quantityId)));
1394
+ const propagated = new Set(quantityEdges.flatMap((edge) => edge.uncertaintyParameterIds));
1395
+ return quantity.uncertaintyParameterIds.length === 0 ||
1396
+ quantityEdges.length === 0 ||
1397
+ quantity.uncertaintyParameterIds.some((parameterId) => !propagated.has(parameterId))
1398
+ ? [`${claim.id}:${quantity.id}`, ...quantityEdges.map((edge) => edge.id)]
1399
+ : [];
1400
+ }));
1401
+ if (centralUncertaintyFailures.length) {
1402
+ add("CENTRAL_UNCERTAINTY_PLAN_MISSING", "Every central model quantity must bind shared-input uncertainty and propagate it through a claim edge before comparison.", [...new Set(centralUncertaintyFailures)]);
1403
+ }
1404
+ const unjustifiedCentralNormalizations = design.claims
1405
+ .filter((claim) => claim.role === "central" && claim.resultClass === "model-output")
1406
+ .flatMap((claim) => claim.quantityIds)
1407
+ .map((quantityId) => quantityById.get(quantityId))
1408
+ .filter((quantity) => quantity?.normalizationMode === "directional-convention" &&
1409
+ !quantity.normalizationJustification.trim());
1410
+ if (unjustifiedCentralNormalizations.length) {
1411
+ add("CENTRAL_NORMALIZATION_UNJUSTIFIED", "A directional cross-model normalization must explain why one model is the denominator and how directionality affects interpretation.", unjustifiedCentralNormalizations.map((quantity) => quantity.id));
1412
+ }
1413
+ const unboundCentralEstimands = design.claims.filter((claim) => claim.role === "central" && claim.estimandIds.length === 0);
1414
+ if (unboundCentralEstimands.length) {
1415
+ add("CENTRAL_CLAIM_ESTIMAND_UNBOUND", "Every central claim must bind an explicit central estimand.", unboundCentralEstimands.map((claim) => claim.id));
1416
+ }
1417
+ const crossModelClaims = design.claims.filter((claim) => claim.role === "central" && claim.resultClass === "model-output");
1418
+ const outcomePresuppositions = crossModelClaims.filter((claim) => claim.comparisonIds.length === 0 ||
1419
+ claim.hypothesisMode !== "two-sided" ||
1420
+ !claim.nullOutcomeStatement?.trim());
1421
+ if (design.identity.centralStudyKind === "cross-model-comparison" &&
1422
+ outcomePresuppositions.length) {
1423
+ add("CLAIM_NULL_OUTCOME_UNPLANNED", "Central cross-model claims must bind explicit comparisons and a two-sided null or agreement outcome before results are inspected.", outcomePresuppositions.map((claim) => claim.id));
1424
+ }
1425
+ const centralEstimands = design.estimands.filter((estimand) => estimand.role === "central");
1426
+ if (centralEstimands.length !== 1) {
1427
+ add("CENTRAL_ESTIMAND_IDENTITY_INVALID", "A scientific design must declare exactly one central estimand; supporting scenario and accounting estimands remain separate.", centralEstimands.map((estimand) => estimand.id));
1428
+ }
1429
+ const claimText = [design.workingTitle, ...design.claims.map((claim) => claim.statement)].join("\n");
1430
+ for (const quantity of design.quantities) {
1431
+ const denominator = normalizeIdentifierTerm(quantity.denominatorType);
1432
+ if (denominator === normalizeIdentifierTerm(quantity.id) ||
1433
+ denominator === normalizeIdentifierTerm(quantity.label)) {
1434
+ add("QUANTITY_DENOMINATOR_SELF_REFERENCE", "A quantity denominator must identify the population or exposure base and cannot restate the quantity itself.", [quantity.id]);
1435
+ }
1436
+ if (quantity.quantityType === "material" && quantity.uncertaintyParameterIds.length === 0) {
1437
+ add("MATERIAL_UNCERTAINTY_UNPROPAGATED", "Material quantities must bind the declared uncertainty parameters propagated through their accounting bridge.", [quantity.id]);
1438
+ }
1439
+ if (quantity.quantityType === "material" && quantity.uncertaintyParameterIds.length > 0) {
1440
+ const claimEdgeIds = new Set(design.claims
1441
+ .filter((claim) => claim.quantityIds.includes(quantity.id))
1442
+ .flatMap((claim) => claim.edgeIds));
1443
+ const accountingEdges = design.edges.filter((edge) => claimEdgeIds.has(edge.id) && edge.evidenceMode === "accounting-bridge");
1444
+ const propagatedParameters = new Set(accountingEdges.flatMap((edge) => edge.uncertaintyParameterIds));
1445
+ if (accountingEdges.length === 0 ||
1446
+ quantity.uncertaintyParameterIds.some((parameterId) => !propagatedParameters.has(parameterId))) {
1447
+ add("ACCOUNTING_UNCERTAINTY_BRIDGE_INCOMPLETE", "Every material-quantity uncertainty parameter must propagate through an accounting bridge used by the bound claim.", [quantity.id, ...accountingEdges.map((edge) => edge.id)]);
1448
+ }
1449
+ }
1450
+ const prohibited = quantity.prohibitedTerms.filter((term) => includesTerm(claimText, term));
1451
+ if (prohibited.length) {
1452
+ add("QUANTITY_TERM_OVERCLAIM", "The working title or a claim uses a term explicitly prohibited by the declared quantity and denominator.", [quantity.id]);
1453
+ }
1454
+ }
1455
+ const thresholdLabels = {
1456
+ analytic: "analytic-threshold",
1457
+ scenario: "scenario-threshold",
1458
+ estimated: "estimated-threshold",
1459
+ "policy-trigger": "policy-trigger",
1460
+ };
1461
+ for (const threshold of design.thresholds) {
1462
+ const boundQuantity = quantityById.get(threshold.quantityId);
1463
+ if (threshold.reportedAs !== thresholdLabels[threshold.type]) {
1464
+ add("THRESHOLD_TYPE_MISMATCH", "A threshold must be reported using the same analytic, scenario, estimated, or policy type declared by its design.", [threshold.id]);
1465
+ }
1466
+ if (["analytic", "scenario"].includes(threshold.type) &&
1467
+ threshold.sensitivityParameterIds.length === 0) {
1468
+ add("THRESHOLD_SENSITIVITY_MISSING", "Analytic and scenario thresholds require predeclared sensitivity parameters.", [threshold.id]);
1469
+ }
1470
+ if (!boundQuantity || normalizeTerm(threshold.unit) !== normalizeTerm(boundQuantity.unit)) {
1471
+ add("THRESHOLD_QUANTITY_UNIT_MISMATCH", "A threshold numeric value and unit must bind the quantity it actually classifies; a scenario input fraction cannot carry an event-count threshold.", [threshold.id, threshold.quantityId]);
1472
+ }
1473
+ if (threshold.sensitivityParameterIds.length > 0 &&
1474
+ (!threshold.basisJustification.trim() ||
1475
+ !threshold.stabilityRule.trim() ||
1476
+ !threshold.sensitivityReportingRule.trim() ||
1477
+ threshold.criterionQuantityIds.length === 0 ||
1478
+ threshold.stabilityMode === "none")) {
1479
+ add("THRESHOLD_SENSITIVITY_RULE_UNDECLARED", "A sensitivity-dependent threshold must freeze its basis, criterion quantities, stability test, and the rule used to report all sensitivity states.", [threshold.id]);
1480
+ }
1481
+ if (threshold.stabilityMode === "sign") {
1482
+ const stabilityQuantity = threshold.stabilityQuantityId
1483
+ ? quantityById.get(threshold.stabilityQuantityId)
1484
+ : undefined;
1485
+ if (!stabilityQuantity || stabilityQuantity.valueMode !== "signed") {
1486
+ add("SIGNED_STABILITY_QUANTITY_MISSING", "A sign-stability decision must bind a signed quantity; an absolute discrepancy cannot determine direction.", [threshold.id, ...(threshold.stabilityQuantityId ? [threshold.stabilityQuantityId] : [])]);
1487
+ }
1488
+ }
1489
+ }
1490
+ const incompleteFactorCompositions = design.uncertaintyParameters.filter((parameter) => parameter.factorIds.length > 0 &&
1491
+ (!parameter.applicationPoint.trim() ||
1492
+ !parameter.compositionRule.trim() ||
1493
+ !parameter.preservesFactorLevelIdentity));
1494
+ if (incompleteFactorCompositions.length) {
1495
+ add("FACTOR_UNCERTAINTY_COMPOSITION_UNDECLARED", "An uncertainty parameter that acts on a frozen factor must declare where and how it composes with factor levels and must preserve each level's identity.", incompleteFactorCompositions.map((parameter) => parameter.id));
1496
+ }
1497
+ const invalidStateFreezePlans = design.uncertaintyParameters.filter((parameter) => (parameter.stateValueStatus === "frozen" &&
1498
+ parameter.freezeBeforeGate !== "research-design") ||
1499
+ (parameter.stateValueStatus === "pending-source-acquisition" &&
1500
+ parameter.freezeBeforeGate === "research-design"));
1501
+ if (invalidStateFreezePlans.length) {
1502
+ add("UNCERTAINTY_STATE_FREEZE_PLAN_INVALID", "Frozen uncertainty states must be frozen at research design; pending source-derived states must name a later early-review gate before which their exact values will be frozen in a new generation.", invalidStateFreezePlans.map((parameter) => parameter.id));
1503
+ }
1504
+ const nonNumericFrozenStates = design.uncertaintyParameters.filter((parameter) => parameter.stateValueStatus === "frozen" &&
1505
+ parameter.stateValueType === "numeric" &&
1506
+ parameter.states.some((state) => !Number.isFinite(Number(state.value))));
1507
+ if (nonNumericFrozenStates.length) {
1508
+ add("UNCERTAINTY_STATE_VALUES_NOT_FROZEN", "A frozen numeric uncertainty state must contain finite numeric values; source placeholders require a declared pending-source freeze plan and a new generation before the due gate.", nonNumericFrozenStates.map((parameter) => parameter.id));
1509
+ }
1510
+ const pendingFreezePolicyBindingsMissing = design.uncertaintyParameters.filter((parameter) => parameter.stateValueStatus === "pending-source-acquisition" &&
1511
+ !design.policyRuleDispositions.some((disposition) => disposition.status === "planned" &&
1512
+ disposition.dueGate === parameter.freezeBeforeGate &&
1513
+ disposition.uncertaintyParameterIds.includes(parameter.id)));
1514
+ if (pendingFreezePolicyBindingsMissing.length) {
1515
+ add("UNCERTAINTY_FREEZE_POLICY_BINDING_MISSING", "Every pending source-derived uncertainty state must bind to a planned Research Policy disposition due at the same early-review gate, so the future freeze is independently visible and mechanically enforceable.", pendingFreezePolicyBindingsMissing.map((parameter) => parameter.id));
1516
+ }
1517
+ const stateSpaceFailures = [];
1518
+ const jointStateBindingFailures = [];
1519
+ const coveredParameters = new Set();
1520
+ for (const group of design.uncertaintyGroups) {
1521
+ const parameters = group.parameterIds.map((parameterId) => uncertaintyParameterById.get(parameterId));
1522
+ parameters.forEach((parameter) => {
1523
+ if (parameter)
1524
+ coveredParameters.add(parameter.id);
1525
+ });
1526
+ const stateCounts = parameters.map((parameter) => parameter?.states.length ?? 0);
1527
+ const expectedJointStates = group.combinationMode === "one-at-a-time"
1528
+ ? 1 + stateCounts.reduce((sum, count) => sum + Math.max(0, count - 1), 0)
1529
+ : group.combinationMode === "full-factorial"
1530
+ ? stateCounts.reduce((product, count) => product * count, 1)
1531
+ : group.jointStateIds.length;
1532
+ if (parameters.length === 0 ||
1533
+ parameters.some((parameter) => !parameter || parameter.states.length === 0) ||
1534
+ !group.applicationRule.trim() ||
1535
+ group.jointStateIds.length === 0 ||
1536
+ group.jointStateIds.length !== expectedJointStates) {
1537
+ stateSpaceFailures.push(group.id);
1538
+ }
1539
+ const jointStateIds = new Set(group.jointStateIds);
1540
+ const bindingIds = group.jointStateBindings.map((binding) => binding.jointStateId);
1541
+ const bindingIdSet = new Set(bindingIds);
1542
+ const bindingsTraceable = group.jointStateBindings.every((binding) => {
1543
+ if (binding.parameterStateIds.length !== group.parameterIds.length)
1544
+ return false;
1545
+ const stateOwners = binding.parameterStateIds.map((stateId) => parameters.filter((parameter) => parameter?.states.some((state) => state.id === stateId)));
1546
+ return (stateOwners.every((owners) => owners.length === 1) &&
1547
+ parameters.every((parameter) => parameter &&
1548
+ binding.parameterStateIds.filter((stateId) => parameter.states.some((state) => state.id === stateId)).length === 1));
1549
+ });
1550
+ if (bindingIds.length !== group.jointStateIds.length ||
1551
+ bindingIdSet.size !== bindingIds.length ||
1552
+ jointStateIds.size !== group.jointStateIds.length ||
1553
+ [...jointStateIds].some((stateId) => !bindingIdSet.has(stateId)) ||
1554
+ !bindingsTraceable) {
1555
+ jointStateBindingFailures.push(group.id);
1556
+ }
1557
+ }
1558
+ const usedUncertaintyParameters = new Set(design.quantities.flatMap((quantity) => quantity.uncertaintyParameterIds));
1559
+ for (const parameterId of usedUncertaintyParameters) {
1560
+ const parameter = uncertaintyParameterById.get(parameterId);
1561
+ if (!parameter || parameter.states.length === 0 || !coveredParameters.has(parameterId)) {
1562
+ stateSpaceFailures.push(parameterId);
1563
+ }
1564
+ }
1565
+ const centralModelIds = new Set(design.edges
1566
+ .filter((edge) => edge.role === "central")
1567
+ .flatMap((edge) => edge.fromModelStructureIds));
1568
+ if (design.identity.centralStudyKind === "cross-model-comparison" &&
1569
+ centralModelIds.size > 1 &&
1570
+ !design.uncertaintyGroups.some((group) => [...centralModelIds].every((modelId) => group.sharedAcrossModelStructureIds.includes(modelId)))) {
1571
+ stateSpaceFailures.push(...centralModelIds);
1572
+ }
1573
+ if (stateSpaceFailures.length) {
1574
+ add("UNCERTAINTY_STATE_SPACE_UNDECLARED", "Every used uncertainty parameter must declare finite states and belong to a group with an exact combination rule, joint-state inventory, and shared-model application where required.", [...new Set(stateSpaceFailures)]);
1575
+ }
1576
+ if (jointStateBindingFailures.length) {
1577
+ add("UNCERTAINTY_JOINT_STATE_BINDING_INVALID", "Every joint sensitivity state must map exactly one declared parameter state from every parameter in its group, and the binding IDs must exactly match the reviewed joint-state inventory.", [...new Set(jointStateBindingFailures)]);
1578
+ }
1579
+ const closestWorkRoles = design.evidenceRoles.filter((role) => role.required && role.role === "closest-prior-work");
1580
+ if (closestWorkRoles.length === 0 ||
1581
+ closestWorkRoles.some((role) => role.minimumFullText < 1 || role.minimumIndependentSources < 1)) {
1582
+ add("CLOSEST_WORK_FULLTEXT_UNPLANNED", "A top-journal design must require full-text comparison with independent closest prior work.", closestWorkRoles.map((role) => role.id));
1583
+ }
1584
+ const incompleteEvidenceRoles = design.evidenceRoles.filter((role) => role.required &&
1585
+ (role.coverageDimensionIds.length === 0 || role.sourceTypeRequirements.length === 0));
1586
+ if (incompleteEvidenceRoles.length) {
1587
+ add("EVIDENCE_ROLE_COVERAGE_UNMAPPED", "Every required evidence role must map explicit research dimensions and source types.", incompleteEvidenceRoles.map((role) => role.id));
1588
+ }
1589
+ const requiredEvidenceRoleIds = design.evidenceRoles
1590
+ .filter((role) => role.required)
1591
+ .map((role) => role.id);
1592
+ const unmappedAcquisitionRoles = requiredEvidenceRoleIds.filter((roleId) => !design.acquisitionPlan.routes.some((route) => route.required && route.evidenceRoleIds.includes(roleId)) ||
1593
+ !design.acquisitionPlan.routes.some((route) => route.required && route.executor === "agent" && route.evidenceRoleIds.includes(roleId)));
1594
+ if (unmappedAcquisitionRoles.length) {
1595
+ add("EVIDENCE_ACQUISITION_ROUTE_UNMAPPED", "Every required evidence role must map to at least one required lawful route and one agent-executable route before paid, authorized, or external access is requested.", unmappedAcquisitionRoles);
1596
+ }
1597
+ const requiredEvidenceRoleIdSet = new Set(requiredEvidenceRoleIds);
1598
+ const optionalRelevantAgentRoutes = design.acquisitionPlan.routes.filter((route) => route.executor === "agent" &&
1599
+ !route.required &&
1600
+ route.evidenceRoleIds.some((roleId) => requiredEvidenceRoleIdSet.has(roleId)));
1601
+ if (optionalRelevantAgentRoutes.length) {
1602
+ add("EVIDENCE_ACQUISITION_AGENT_ROUTE_OPTIONAL", "Every declared agent-executable route for a required evidence role must be required so evidence exhaustion cannot skip a lawful planned method.", optionalRelevantAgentRoutes.map((route) => route.id));
1603
+ }
1604
+ const invalidAcquisitionRoutes = design.acquisitionPlan.routes.filter((route) => !validAcquisitionRoute(route));
1605
+ if (invalidAcquisitionRoutes.length) {
1606
+ add("EVIDENCE_ACQUISITION_ROUTE_INVALID", "Each acquisition route must bind one coherent executor, access mode, and immutable event selector for its route class.", invalidAcquisitionRoutes.map((route) => route.id));
1607
+ }
1608
+ if (!design.acquisitionPlan.stopPolicy.allAgentRoutesExhaustedBeforeHandoff ||
1609
+ !design.acquisitionPlan.stopPolicy.unresolvedRequiredEvidenceRoleBlocksDownstream ||
1610
+ !design.acquisitionPlan.stopPolicy.prohibitUnreviewedSubstitution) {
1611
+ add("EVIDENCE_EXHAUSTION_POLICY_INVALID", "The reviewed stop policy must require all plan-bound agent routes to be exhausted, block downstream work on unresolved required roles, and prohibit unreviewed substitute evidence.");
1612
+ }
1613
+ const inconsistentEvidenceBindings = design.claims.flatMap((claim) => {
1614
+ const requiredRoleIds = new Set(claim.quantityIds.flatMap((quantityId) => quantityById
1615
+ .get(quantityId)
1616
+ ?.uncertaintyParameterIds.flatMap((parameterId) => uncertaintyParameterById.get(parameterId)?.sourceEvidenceRoleIds ?? []) ?? []));
1617
+ return [...requiredRoleIds]
1618
+ .filter((roleId) => {
1619
+ const role = evidenceRoleById.get(roleId);
1620
+ return !claim.evidenceRoleIds.includes(roleId) || !role?.claimIds.includes(claim.id);
1621
+ })
1622
+ .map((roleId) => `${claim.id}:${roleId}`);
1623
+ });
1624
+ if (inconsistentEvidenceBindings.length) {
1625
+ add("EVIDENCE_ROLE_CLAIM_BINDING_INCONSISTENT", "Every evidence role supplying uncertainty for a claim quantity must be bound in both directions: from the claim to the role and from the role to the claim.", inconsistentEvidenceBindings);
1626
+ }
1627
+ const unresolvedGaps = design.knownGaps.filter((gap) => gap.disposition === "unresolved");
1628
+ if (unresolvedGaps.length) {
1629
+ add("KNOWN_BLOCKING_GAP_UNRESOLVED", "Inherited central gaps must be closed, scope-narrowed, or placed in an explicit handoff before research starts.", unresolvedGaps.map((gap) => gap.id));
1630
+ }
1631
+ const unverifiableInheritedGaps = design.knownGaps.filter((gap) => gap.sourceProjectId &&
1632
+ gap.disposition !== "unresolved" &&
1633
+ (gap.lineageStatus === "unverified" ||
1634
+ gap.sourceArtifacts.length === 0 ||
1635
+ gap.sourceArtifacts.some((artifact) => (!artifact.objectLocator.startsWith(`projects/${gap.sourceProjectId}/`) &&
1636
+ !artifact.objectLocator.startsWith("lineage/objects/")) ||
1637
+ (gap.lineageStatus === "verified" && artifact.kind === "owner-attestation")) ||
1638
+ (gap.lineageStatus === "owner-attested" &&
1639
+ !gap.sourceArtifacts.some((artifact) => artifact.kind === "owner-attestation"))));
1640
+ if (unverifiableInheritedGaps.length) {
1641
+ add("GAP_LINEAGE_UNVERIFIABLE", "A disposed inherited gap must retain a verified or owner-attested content-hash lineage.", unverifiableInheritedGaps.map((gap) => gap.id));
1642
+ }
1643
+ const placeholderGapDigests = design.knownGaps.filter((gap) => gap.sourceArtifacts.some((artifact) => /^([a-f0-9])\1{63}$/iu.test(artifact.sha256)));
1644
+ if (placeholderGapDigests.length) {
1645
+ add("GAP_LINEAGE_PLACEHOLDER_DIGEST", "Inherited-gap lineage must use a real content digest; repeated-character placeholder hashes are not reviewable provenance.", placeholderGapDigests.map((gap) => gap.id));
1646
+ }
1647
+ const baseline = design.baselinePlan;
1648
+ if (!baseline.sameInputInformation ||
1649
+ !baseline.comparableCalibrationBudget ||
1650
+ !baseline.sameEndpoint ||
1651
+ !baseline.frozenBeforeAnalysis ||
1652
+ baseline.decisionLossMetrics.length === 0) {
1653
+ add("BASELINE_FAIRNESS_UNRESOLVED", "The comparison baseline must use fair information, calibration, endpoint, freeze, and decision-loss rules.");
1654
+ }
1655
+ const unboundDecisionLossMetrics = baseline.decisionLossMetrics.filter((metric) => metric.comparisonIds.length === 0 ||
1656
+ metric.quantityIds.length === 0 ||
1657
+ !metric.decisionRule.trim() ||
1658
+ !metric.reportingLevel.trim());
1659
+ if (design.identity.centralStudyKind === "cross-model-comparison" &&
1660
+ (baseline.decisionLossMetrics.length === 0 || unboundDecisionLossMetrics.length > 0)) {
1661
+ add("DECISION_LOSS_METRIC_UNBOUND", "A cross-model design must freeze executable decision-loss metrics bound to concrete comparisons, quantities, rules, and reporting levels.", unboundDecisionLossMetrics.map((metric) => metric.id));
1662
+ }
1663
+ if (design.contextPlan.estimatedTokens > design.contextPlan.maxEstimatedTokens ||
1664
+ !design.contextPlan.centralEvidenceFits) {
1665
+ add("CONTEXT_PLAN_OVER_LIMIT", "The planned claim-critical context does not fit its reviewed token boundary.");
1666
+ }
1667
+ if (design.contextPlan.claimCriticalCapsuleIds.length === 0) {
1668
+ add("CLAIM_CRITICAL_CONTEXT_MISSING", "At least one claim-critical evidence capsule must be planned before high-cost stages.");
1669
+ }
1670
+ const coverageDimensionIds = new Set(design.evidenceRoles.flatMap((role) => role.coverageDimensionIds));
1671
+ const unmappedCapsules = design.contextPlan.claimCriticalCapsuleIds.filter((capsuleId) => !coverageDimensionIds.has(capsuleId));
1672
+ if (unmappedCapsules.length) {
1673
+ add("CLAIM_CRITICAL_CAPSULE_UNMAPPED", "Every claim-critical context capsule must map to a reviewed evidence-role coverage dimension.", unmappedCapsules);
1674
+ }
1675
+ const blockingEdges = design.edges.filter((edge) => edge.role === "central" && edge.status === "blocked");
1676
+ if (blockingEdges.length) {
1677
+ add("CENTRAL_CLAIM_EDGE_BLOCKED", "A central claim edge is explicitly blocked and must be closed or removed through approved scope narrowing.", blockingEdges.map((edge) => edge.id));
1678
+ }
1679
+ const issueList = [...issues.values()];
1680
+ const centralValidationPlans = design.validationPlans.filter((plan) => plan.claimIds.some((claimId) => design.claims.find((claim) => claim.id === claimId)?.role === "central"));
1681
+ return {
1682
+ schemaVersion: 1,
1683
+ projectId: design.projectId,
1684
+ centralStudyKind: design.identity.centralStudyKind,
1685
+ readyForDesignReview: issueList.every((issue) => issue.severity !== "blocking"),
1686
+ issues: issueList,
1687
+ issueCodes: issueList.map((issue) => issue.code),
1688
+ effectiveIndependentUnits: centralValidationPlans.reduce((minimum, plan) => Math.min(minimum, plan.effectiveIndependentUnits), centralValidationPlans.length ? Number.POSITIVE_INFINITY : 0),
1689
+ requiredEvidenceRoles: design.evidenceRoles.filter((role) => role.required).length,
1690
+ };
1691
+ }
1692
+ export function scientificDesignPolicyGaps(design, policy) {
1693
+ const gaps = [];
1694
+ const policyRules = [...new Set(policy.resolvedRules)];
1695
+ const dispositionCounts = new Map();
1696
+ for (const disposition of design.policyRuleDispositions) {
1697
+ dispositionCounts.set(disposition.ruleId, (dispositionCounts.get(disposition.ruleId) ?? 0) + 1);
1698
+ }
1699
+ for (const ruleId of policyRules) {
1700
+ const matches = design.policyRuleDispositions.filter((disposition) => disposition.ruleId === ruleId);
1701
+ if (matches.length === 0) {
1702
+ gaps.push(`policy-rule-disposition-missing:${ruleId}`);
1703
+ continue;
1704
+ }
1705
+ if (matches.length > 1) {
1706
+ gaps.push(`policy-rule-disposition-duplicate:${ruleId}`);
1707
+ continue;
1708
+ }
1709
+ const disposition = matches[0];
1710
+ if (disposition.status === "incompatible") {
1711
+ gaps.push(`policy-rule-incompatible:${ruleId}`);
1712
+ }
1713
+ if (disposition.status === "planned" && disposition.dueGate === "research-design") {
1714
+ gaps.push(`policy-rule-due-unresolved:${ruleId}`);
1715
+ }
1716
+ if (disposition.status === "scope-limited" && policy.targetJournal) {
1717
+ gaps.push(`policy-rule-scope-conflict:${ruleId}`);
1718
+ }
1719
+ if (disposition.status === "satisfied-by-design" &&
1720
+ disposition.claimIds.length === 0 &&
1721
+ disposition.evidenceRoleIds.length === 0 &&
1722
+ disposition.validationPlanIds.length === 0 &&
1723
+ disposition.uncertaintyParameterIds.length === 0 &&
1724
+ disposition.modelStructureIds.length === 0) {
1725
+ gaps.push(`policy-rule-binding-empty:${ruleId}`);
1726
+ }
1727
+ }
1728
+ for (const ruleId of dispositionCounts.keys()) {
1729
+ if (!policyRules.includes(ruleId))
1730
+ gaps.push(`policy-rule-disposition-unbound:${ruleId}`);
1731
+ }
1732
+ if (policyRules.includes("independent-validation-required")) {
1733
+ const disposition = design.policyRuleDispositions.find((candidate) => candidate.ruleId === "independent-validation-required");
1734
+ const centralClaimIds = new Set(design.claims.filter((claim) => claim.role === "central").map((claim) => claim.id));
1735
+ const centralPlans = design.validationPlans.filter((plan) => plan.claimIds.some((claimId) => centralClaimIds.has(claimId)));
1736
+ if (disposition?.status === "satisfied-by-design" &&
1737
+ centralPlans.some((plan) => plan.independentValidation.status !== "available" ||
1738
+ !plan.independentDataGeneratingProcess)) {
1739
+ gaps.push("policy-rule-status-mismatch:independent-validation-required");
1740
+ }
1741
+ }
1742
+ return gaps;
1743
+ }
1744
+ function bridgeEdgesConnected(edgeIds, edgeById) {
1745
+ const endpointSets = edgeIds.map((edgeId) => {
1746
+ const edge = edgeById.get(edgeId);
1747
+ return new Set([...(edge?.fromEndpointIds ?? []), ...(edge?.toEndpointIds ?? [])]);
1748
+ });
1749
+ if (endpointSets.some((endpoints) => endpoints.size === 0))
1750
+ return false;
1751
+ const reachedEdges = new Set([0]);
1752
+ const reachedEndpoints = new Set(endpointSets[0]);
1753
+ let changed = true;
1754
+ while (changed) {
1755
+ changed = false;
1756
+ endpointSets.forEach((endpoints, index) => {
1757
+ if (reachedEdges.has(index))
1758
+ return;
1759
+ if ([...endpoints].some((endpointId) => reachedEndpoints.has(endpointId))) {
1760
+ reachedEdges.add(index);
1761
+ endpoints.forEach((endpointId) => reachedEndpoints.add(endpointId));
1762
+ changed = true;
1763
+ }
1764
+ });
1765
+ }
1766
+ return reachedEdges.size === endpointSets.length;
1767
+ }
1768
+ function endpointSignaturesMatch(left, right) {
1769
+ return (normalizeTerm(left.physicalConstruct) === normalizeTerm(right.physicalConstruct) &&
1770
+ normalizeTerm(left.scale) === normalizeTerm(right.scale) &&
1771
+ normalizeTerm(left.unit) === normalizeTerm(right.unit) &&
1772
+ normalizeTerm(left.timeBasis) === normalizeTerm(right.timeBasis));
1773
+ }
1774
+ function includesTerm(value, term) {
1775
+ return normalizeTerm(value).includes(normalizeTerm(term));
1776
+ }
1777
+ function normalizeTerm(value) {
1778
+ return value.trim().toLocaleLowerCase("en-US").replace(/\s+/g, " ");
1779
+ }
1780
+ function normalizeIdentifierTerm(value) {
1781
+ return value
1782
+ .trim()
1783
+ .toLocaleLowerCase("en-US")
1784
+ .replace(/^quantity[-_:\s]+/u, "")
1785
+ .replace(/[^a-z0-9]+/gu, " ")
1786
+ .trim();
1787
+ }
1788
+ function isReviewableDigest(value) {
1789
+ return /^[a-f0-9]{64}$/u.test(value) && !/^([a-f0-9])\1{63}$/u.test(value);
1790
+ }
1791
+ function bootstrapMultisetCount(effectiveIndependentUnits) {
1792
+ if (!Number.isInteger(effectiveIndependentUnits) || effectiveIndependentUnits < 1)
1793
+ return null;
1794
+ if (effectiveIndependentUnits > 20)
1795
+ return null;
1796
+ const n = effectiveIndependentUnits;
1797
+ let result = 1;
1798
+ for (let index = 1; index <= n; index += 1) {
1799
+ result = (result * (n - 1 + index)) / index;
1800
+ }
1801
+ return Math.round(result);
1802
+ }
1803
+ function assertUniqueIds(design) {
1804
+ const collections = [
1805
+ ["policyRuleDispositions", design.policyRuleDispositions.map((item) => item.ruleId)],
1806
+ ["modelStructures", design.identity.modelStructures.map((item) => item.id)],
1807
+ ["estimands", design.estimands.map((item) => item.id)],
1808
+ ["claims", design.claims.map((item) => item.id)],
1809
+ ["edges", design.edges.map((item) => item.id)],
1810
+ ["endpoints", design.endpoints.map((item) => item.id)],
1811
+ ["comparisons", design.comparisons.map((item) => item.id)],
1812
+ ["quantities", design.quantities.map((item) => item.id)],
1813
+ ["validationPlans", design.validationPlans.map((item) => item.id)],
1814
+ ["thresholds", design.thresholds.map((item) => item.id)],
1815
+ ["evidenceRoles", design.evidenceRoles.map((item) => item.id)],
1816
+ ["acquisitionRoutes", design.acquisitionPlan.routes.map((item) => item.id)],
1817
+ ["knownGaps", design.knownGaps.map((item) => item.id)],
1818
+ ["uncertaintyParameters", design.uncertaintyParameters.map((item) => item.id)],
1819
+ ["uncertaintyGroups", design.uncertaintyGroups.map((item) => item.id)],
1820
+ ["factors", design.factors.map((item) => item.id)],
1821
+ ["decisionLossMetrics", design.baselinePlan.decisionLossMetrics.map((item) => item.id)],
1822
+ ];
1823
+ const duplicateCollections = collections
1824
+ .filter(([, ids]) => new Set(ids).size !== ids.length)
1825
+ .map(([name]) => name);
1826
+ if (duplicateCollections.length) {
1827
+ throw scientificDesignSemanticError(duplicateCollections.map((name) => `${name} contains duplicate identifiers`));
1828
+ }
1829
+ }
1830
+ function assertReferences(design) {
1831
+ const claims = new Set(design.claims.map((item) => item.id));
1832
+ const modelStructures = new Set(design.identity.modelStructures.map((item) => item.id));
1833
+ const estimands = new Set(design.estimands.map((item) => item.id));
1834
+ const edges = new Set(design.edges.map((item) => item.id));
1835
+ const endpoints = new Set(design.endpoints.map((item) => item.id));
1836
+ const comparisons = new Set(design.comparisons.map((item) => item.id));
1837
+ const quantities = new Set(design.quantities.map((item) => item.id));
1838
+ const validationPlans = new Set(design.validationPlans.map((item) => item.id));
1839
+ const thresholds = new Set(design.thresholds.map((item) => item.id));
1840
+ const evidenceRoles = new Set(design.evidenceRoles.map((item) => item.id));
1841
+ const knownGaps = new Set(design.knownGaps.map((item) => item.id));
1842
+ const uncertaintyParameters = new Set(design.uncertaintyParameters.map((item) => item.id));
1843
+ const factors = new Set(design.factors.map((item) => item.id));
1844
+ const failures = [];
1845
+ const requireIds = (path, ids, known) => {
1846
+ const missing = ids.filter((id) => !known.has(id));
1847
+ if (missing.length)
1848
+ failures.push(`${path} references unknown ids: ${missing.join(", ")}`);
1849
+ };
1850
+ for (const disposition of design.policyRuleDispositions) {
1851
+ requireIds(`policyRuleDispositions.${disposition.ruleId}.claimIds`, disposition.claimIds, claims);
1852
+ requireIds(`policyRuleDispositions.${disposition.ruleId}.evidenceRoleIds`, disposition.evidenceRoleIds, evidenceRoles);
1853
+ requireIds(`policyRuleDispositions.${disposition.ruleId}.validationPlanIds`, disposition.validationPlanIds, validationPlans);
1854
+ requireIds(`policyRuleDispositions.${disposition.ruleId}.knownGapIds`, disposition.knownGapIds, knownGaps);
1855
+ requireIds(`policyRuleDispositions.${disposition.ruleId}.uncertaintyParameterIds`, disposition.uncertaintyParameterIds, uncertaintyParameters);
1856
+ requireIds(`policyRuleDispositions.${disposition.ruleId}.modelStructureIds`, disposition.modelStructureIds, modelStructures);
1857
+ }
1858
+ for (const component of design.identity.components) {
1859
+ requireIds(`identity.components.${component.kind}.bridgeEdgeIds`, component.bridgeEdgeIds, edges);
1860
+ }
1861
+ for (const model of design.identity.modelStructures) {
1862
+ requireIds(`identity.modelStructures.${model.id}.sourceEvidenceRoleIds`, model.sourceEvidenceRoleIds, evidenceRoles);
1863
+ }
1864
+ const centralComponents = design.identity.components.filter((component) => component.role === "central");
1865
+ if (centralComponents.length !== 1 ||
1866
+ centralComponents[0]?.kind !== design.identity.centralStudyKind) {
1867
+ failures.push("identity must contain exactly one central component matching centralStudyKind");
1868
+ }
1869
+ for (const claim of design.claims) {
1870
+ requireIds(`claims.${claim.id}.edgeIds`, claim.edgeIds, edges);
1871
+ requireIds(`claims.${claim.id}.endpointIds`, claim.endpointIds, endpoints);
1872
+ requireIds(`claims.${claim.id}.comparisonIds`, claim.comparisonIds, comparisons);
1873
+ requireIds(`claims.${claim.id}.estimandIds`, claim.estimandIds, estimands);
1874
+ requireIds(`claims.${claim.id}.quantityIds`, claim.quantityIds, quantities);
1875
+ requireIds(`claims.${claim.id}.evidenceRoleIds`, claim.evidenceRoleIds, evidenceRoles);
1876
+ }
1877
+ for (const edge of design.edges) {
1878
+ requireIds(`edges.${edge.id}.fromModelStructureIds`, edge.fromModelStructureIds, modelStructures);
1879
+ requireIds(`edges.${edge.id}.toModelStructureIds`, edge.toModelStructureIds, modelStructures);
1880
+ requireIds(`edges.${edge.id}.uncertaintyParameterIds`, edge.uncertaintyParameterIds, uncertaintyParameters);
1881
+ requireIds(`edges.${edge.id}.quantityIds`, edge.quantityIds, quantities);
1882
+ requireIds(`edges.${edge.id}.fromEndpointIds`, edge.fromEndpointIds, endpoints);
1883
+ requireIds(`edges.${edge.id}.toEndpointIds`, edge.toEndpointIds, endpoints);
1884
+ }
1885
+ for (const endpoint of design.endpoints) {
1886
+ if (endpoint.modelStructureId) {
1887
+ requireIds(`endpoints.${endpoint.id}.modelStructureId`, [endpoint.modelStructureId], modelStructures);
1888
+ }
1889
+ }
1890
+ for (const comparison of design.comparisons) {
1891
+ requireIds(`comparisons.${comparison.id}.leftEndpointId`, [comparison.leftEndpointId], endpoints);
1892
+ requireIds(`comparisons.${comparison.id}.rightEndpointId`, [comparison.rightEndpointId], endpoints);
1893
+ if (comparison.truthEndpointId) {
1894
+ requireIds(`comparisons.${comparison.id}.truthEndpointId`, [comparison.truthEndpointId], endpoints);
1895
+ }
1896
+ requireIds(`comparisons.${comparison.id}.quantityIds`, comparison.quantityIds, quantities);
1897
+ requireIds(`comparisons.${comparison.id}.thresholdIds`, comparison.thresholdIds, thresholds);
1898
+ }
1899
+ for (const plan of design.validationPlans) {
1900
+ requireIds(`validationPlans.${plan.id}.claimIds`, plan.claimIds, claims);
1901
+ requireIds(`validationPlans.${plan.id}.factorIds`, plan.factorIds, factors);
1902
+ if (plan.independentValidation.gapId) {
1903
+ requireIds(`validationPlans.${plan.id}.independentValidation.gapId`, [plan.independentValidation.gapId], knownGaps);
1904
+ }
1905
+ }
1906
+ for (const threshold of design.thresholds) {
1907
+ requireIds(`thresholds.${threshold.id}.claimId`, [threshold.claimId], claims);
1908
+ requireIds(`thresholds.${threshold.id}.quantityId`, [threshold.quantityId], quantities);
1909
+ requireIds(`thresholds.${threshold.id}.criterionQuantityIds`, threshold.criterionQuantityIds, quantities);
1910
+ if (threshold.stabilityQuantityId) {
1911
+ requireIds(`thresholds.${threshold.id}.stabilityQuantityId`, [threshold.stabilityQuantityId], quantities);
1912
+ }
1913
+ requireIds(`thresholds.${threshold.id}.sensitivityParameterIds`, threshold.sensitivityParameterIds, uncertaintyParameters);
1914
+ }
1915
+ for (const role of design.evidenceRoles) {
1916
+ requireIds(`evidenceRoles.${role.id}.claimIds`, role.claimIds, claims);
1917
+ }
1918
+ for (const route of design.acquisitionPlan.routes) {
1919
+ requireIds(`acquisitionPlan.routes.${route.id}.evidenceRoleIds`, route.evidenceRoleIds, evidenceRoles);
1920
+ }
1921
+ for (const gap of design.knownGaps) {
1922
+ for (const evidenceRef of gap.evidenceRefs) {
1923
+ const known = evidenceRef.kind === "claim"
1924
+ ? claims
1925
+ : evidenceRef.kind === "quantity"
1926
+ ? quantities
1927
+ : evidenceRef.kind === "validation-plan"
1928
+ ? validationPlans
1929
+ : evidenceRef.kind === "edge"
1930
+ ? edges
1931
+ : evidenceRoles;
1932
+ requireIds(`knownGaps.${gap.id}.evidenceRefs.${evidenceRef.kind}`, [evidenceRef.id], known);
1933
+ }
1934
+ }
1935
+ for (const quantity of design.quantities) {
1936
+ requireIds(`quantities.${quantity.id}.uncertaintyParameterIds`, quantity.uncertaintyParameterIds, uncertaintyParameters);
1937
+ }
1938
+ for (const parameter of design.uncertaintyParameters) {
1939
+ requireIds(`uncertaintyParameters.${parameter.id}.sourceEvidenceRoleIds`, parameter.sourceEvidenceRoleIds, evidenceRoles);
1940
+ requireIds(`uncertaintyParameters.${parameter.id}.quantityIds`, parameter.quantityIds, quantities);
1941
+ requireIds(`uncertaintyParameters.${parameter.id}.factorIds`, parameter.factorIds, factors);
1942
+ }
1943
+ for (const group of design.uncertaintyGroups) {
1944
+ requireIds(`uncertaintyGroups.${group.id}.parameterIds`, group.parameterIds, uncertaintyParameters);
1945
+ requireIds(`uncertaintyGroups.${group.id}.sharedAcrossModelStructureIds`, group.sharedAcrossModelStructureIds, modelStructures);
1946
+ }
1947
+ for (const factor of design.factors) {
1948
+ requireIds(`factors.${factor.id}.evidenceRoleIds`, factor.evidenceRoleIds, evidenceRoles);
1949
+ }
1950
+ for (const metric of design.baselinePlan.decisionLossMetrics) {
1951
+ requireIds(`baselinePlan.decisionLossMetrics.${metric.id}.comparisonIds`, metric.comparisonIds, comparisons);
1952
+ requireIds(`baselinePlan.decisionLossMetrics.${metric.id}.quantityIds`, metric.quantityIds, quantities);
1953
+ }
1954
+ if (failures.length)
1955
+ throw scientificDesignSemanticError(failures);
1956
+ }
1957
+ function validAcquisitionRoute(route) {
1958
+ const hasCapability = route.capabilityId !== null;
1959
+ const hasActivity = route.activityKind !== null || route.activityChannel !== null;
1960
+ const hasCompleteActivitySelector = route.activityKind !== null && route.activityChannel !== null;
1961
+ const hasDownloads = route.downloadBackends.length > 0;
1962
+ if (route.routeClass === "broker-capability") {
1963
+ return (route.executor === "agent" &&
1964
+ hasCapability &&
1965
+ !hasActivity &&
1966
+ !hasDownloads &&
1967
+ ["open-public", "owner-authorized"].includes(route.accessMode));
1968
+ }
1969
+ if (route.routeClass === "native-discovery") {
1970
+ return (route.executor === "agent" &&
1971
+ !hasCapability &&
1972
+ hasCompleteActivitySelector &&
1973
+ !hasDownloads &&
1974
+ ["open-public", "owner-authorized"].includes(route.accessMode));
1975
+ }
1976
+ if (route.routeClass === "open-access-download") {
1977
+ return (route.executor === "agent" &&
1978
+ !hasCapability &&
1979
+ !hasActivity &&
1980
+ hasDownloads &&
1981
+ route.downloadBackends.every((backend) => ["skill-adapter", "direct-http"].includes(backend)) &&
1982
+ ["open-public", "owner-authorized"].includes(route.accessMode));
1983
+ }
1984
+ if (route.routeClass === "authorized-browser") {
1985
+ return (route.executor === "agent" &&
1986
+ !hasCapability &&
1987
+ !hasActivity &&
1988
+ hasDownloads &&
1989
+ route.downloadBackends.every((backend) => ["native-browser", "chrome", "cloakbrowser"].includes(backend)) &&
1990
+ ["owner-authorized", "user-authorization-required"].includes(route.accessMode));
1991
+ }
1992
+ if (hasCapability || hasActivity || hasDownloads)
1993
+ return false;
1994
+ if (route.routeClass === "licensed-resource") {
1995
+ return route.executor === "user" && route.accessMode === "purchase-or-subscription";
1996
+ }
1997
+ if (route.routeClass === "owner-provided-resource") {
1998
+ return route.executor === "user" && route.accessMode === "user-authorization-required";
1999
+ }
2000
+ return route.executor === "external-party" && route.accessMode === "external-request";
2001
+ }
2002
+ function scientificDesignSemanticError(validation) {
2003
+ return new CliError("Scientific design failed semantic validation.", {
2004
+ code: "RESEARCH_SCIENTIFIC_DESIGN_INVALID",
2005
+ exitCode: 2,
2006
+ details: sanitizeResearchValue({ validation }),
2007
+ });
2008
+ }
2009
+ function scientificDesignPathError(message) {
2010
+ return new CliError(message, {
2011
+ code: "RESEARCH_SCIENTIFIC_DESIGN_PATH_INVALID",
2012
+ exitCode: 2,
2013
+ });
2014
+ }
2015
+ function formatValidationErrors(errors) {
2016
+ return (errors ?? []).slice(0, 20).map((error) => {
2017
+ const location = error.instancePath || "/";
2018
+ return `${location} ${error.message ?? "is invalid"}`;
2019
+ });
2020
+ }
2021
+ //# sourceMappingURL=scientific-design.js.map