@abelxiaoxing/cadence 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +130 -8
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/config/plan-draft.quick.example.json +78 -0
  8. package/package.json +10 -4
  9. package/prompts/abel-design.md +101 -44
  10. package/prompts/abel-implement.md +9 -0
  11. package/src/activity-contracts.ts +82 -0
  12. package/src/candidate-artifact.ts +143 -0
  13. package/src/candidate-patch.ts +31 -11
  14. package/src/candidate-workspace.ts +74 -0
  15. package/src/change-contract-schema.ts +191 -0
  16. package/src/change-contract.ts +5 -0
  17. package/src/change-verification.ts +920 -0
  18. package/src/child-budget.ts +58 -0
  19. package/src/child-model.ts +142 -0
  20. package/src/child-session.ts +289 -326
  21. package/src/contracts.ts +200 -76
  22. package/src/delivery-compiler.ts +105 -120
  23. package/src/delivery-revision.ts +42 -0
  24. package/src/design-control.ts +113 -4
  25. package/src/design-diagnostics.ts +150 -0
  26. package/src/durable-contracts.ts +213 -0
  27. package/src/durable-workflow.ts +148 -3570
  28. package/src/evidence-draft.ts +122 -0
  29. package/src/execution-profile.ts +38 -0
  30. package/src/implement-graph.ts +3 -0
  31. package/src/implement-plan.ts +70 -0
  32. package/src/index.ts +59 -522
  33. package/src/isolation-backend.ts +62 -40
  34. package/src/model-source.ts +22 -0
  35. package/src/operator-cli-entry.mjs +24 -0
  36. package/src/operator-cli.mjs +1421 -0
  37. package/src/operator-tools.ts +249 -0
  38. package/src/package-candidate.ts +227 -0
  39. package/src/package-verification.ts +86 -8
  40. package/src/package-workflow.ts +291 -0
  41. package/src/packet-runtime.ts +27 -48
  42. package/src/parent-provider.ts +86 -198
  43. package/src/phase-execution.ts +2248 -0
  44. package/src/pi-adapter.ts +19 -0
  45. package/src/plan-draft-summary.ts +149 -0
  46. package/src/plan-draft.ts +493 -0
  47. package/src/single-task-draft.ts +142 -0
  48. package/src/subagent-activity.ts +32 -66
  49. package/src/submit-tool.ts +20 -59
  50. package/src/transport-budget.ts +52 -0
  51. package/src/verification-capability.ts +5 -3
  52. package/src/verification-diagnostics.ts +76 -0
  53. package/src/verification-environment-io.ts +280 -0
  54. package/src/verification-environment.ts +56 -154
  55. package/src/verification-scan.ts +64 -0
  56. package/src/worker-broker.ts +39 -57
  57. package/src/workflow-policy.ts +14 -53
  58. package/src/workflow-recovery-policy.ts +95 -0
  59. package/src/workflow-scheduling.ts +105 -0
  60. package/src/workflow-state-machine.ts +105 -497
  61. package/src/workflow-status.ts +384 -0
  62. package/src/workspace-io-entry.mjs +19 -1
  63. package/src/workspace-io-worker.mjs +275 -50
  64. package/src/workspace-io.ts +6 -1
  65. package/src/empty-resource-loader.ts +0 -38
  66. package/src/parent-payload-bridge.ts +0 -523
package/src/contracts.ts CHANGED
@@ -52,6 +52,14 @@ export const LIMITS = {
52
52
  maxCompleteResultBytes: 64 * 1024,
53
53
  } as const;
54
54
 
55
+ /** Shared collection bounds for runtime verification and author-facing schemas. */
56
+ export const VERIFICATION_LIMITS = Object.freeze({
57
+ minSteps: 1,
58
+ maxSteps: 8,
59
+ minTestFiles: 1,
60
+ maxTestFiles: 64,
61
+ });
62
+
55
63
  export const ARTIFACT_FAILURE_CODES = [
56
64
  "agents-impact-mismatch",
57
65
  "agents-target-mismatch",
@@ -109,6 +117,7 @@ export const STALE_FAILURE_CODES = [
109
117
  "stale-snapshot",
110
118
  ] as const;
111
119
  export const ENVIRONMENT_FAILURE_CODES = [
120
+ "child-provider-authentication-failed",
112
121
  "bubblewrap-launch-failed",
113
122
  "bubblewrap-or-dependency-unavailable",
114
123
  "bun-executable-unavailable",
@@ -117,7 +126,6 @@ export const ENVIRONMENT_FAILURE_CODES = [
117
126
  "checkout-failed",
118
127
  "clone-alternates",
119
128
  "clone-failed",
120
- "child-session-create-failed",
121
129
  "dependency-path-unsafe",
122
130
  "git-apply-check-unavailable",
123
131
  "git-apply-failed",
@@ -125,11 +133,6 @@ export const ENVIRONMENT_FAILURE_CODES = [
125
133
  "git-ignore-check-failed",
126
134
  "git-index-unavailable",
127
135
  "invalid-subagent-endpoint",
128
- "parent-bridge-capture-not-ready",
129
- "parent-bridge-generation-invalidated",
130
- "parent-bridge-model-key-mismatch",
131
- "parent-bridge-provider-not-installed",
132
- "parent-bridge-session-unavailable",
133
136
  "root-unavailable",
134
137
  "sandbox-runtime-unavailable",
135
138
  ] as const;
@@ -155,6 +158,10 @@ export const VERIFICATION_ADAPTER_CODES = [
155
158
  "verification-config-unsafe",
156
159
  ] as const;
157
160
  export const CHILD_TRANSPORT_CODES = [
161
+ "child-provider-rate-limited",
162
+ "first-progress-timeout",
163
+ "stream-idle-timeout",
164
+ "attempt-timeout",
158
165
  "child-no-final-assistant",
159
166
  "child-provider-stream-aborted",
160
167
  "child-provider-stream-error",
@@ -237,6 +244,10 @@ export type CandidateFailure =
237
244
  | { kind: "result-limit"; limitBytes: number };
238
245
 
239
246
  export type ChildFailure =
247
+ | {
248
+ kind: "execution-limit";
249
+ code: "child-turn-limit" | "child-context-limit";
250
+ }
240
251
  | CandidateFailure
241
252
  | {
242
253
  kind: "transport";
@@ -252,6 +263,17 @@ export type VerificationClassification =
252
263
  | "expected-green"
253
264
  | "expected-refactor";
254
265
 
266
+ export interface VerificationFailureSummary {
267
+ verificationId: string;
268
+ code: string;
269
+ exitCode?: number;
270
+ failures: string[];
271
+ stdout: string;
272
+ stderr: string;
273
+ truncated: boolean;
274
+ nextStep: string;
275
+ }
276
+
255
277
  /** Runtime-owned evidence; unavailable checks never become product baselines. */
256
278
  export interface VerificationEvidence {
257
279
  id: string;
@@ -260,6 +282,7 @@ export interface VerificationEvidence {
260
282
  tests?: number;
261
283
  failureIdentities: string[];
262
284
  policy: "report-file-v3";
285
+ attributionReliable?: boolean;
263
286
  }
264
287
  export type VerificationObservation =
265
288
  | { kind: "accepted"; evidence: VerificationEvidence }
@@ -267,12 +290,14 @@ export type VerificationObservation =
267
290
  kind: "rejected";
268
291
  code: "red-not-witnessed" | "verification-rejected";
269
292
  evidence: VerificationEvidence;
293
+ diagnostic?: VerificationFailureSummary;
270
294
  }
271
295
  | {
272
296
  kind: "unavailable";
273
297
  category: "environment" | "adapter" | "resource";
274
298
  code: string;
275
299
  verificationId: string;
300
+ diagnostic?: VerificationFailureSummary;
276
301
  }
277
302
  | { kind: "cancelled" };
278
303
 
@@ -437,6 +462,9 @@ type ImplementGraphDiagnosticDetails = Partial<{
437
462
  producerTaskId: string;
438
463
  producerPhase: ImplementationPhase;
439
464
  dependencyTaskId: string;
465
+ field: string;
466
+ expectedPaths: string[];
467
+ actualPaths: string[];
440
468
  }>;
441
469
 
442
470
  export type ImplementGraphReadinessDiagnostic =
@@ -478,6 +506,10 @@ export interface PacketEnvelope {
478
506
  output: "evidence" | "diff";
479
507
  }
480
508
 
509
+ // JSON-schema counterpart for model-visible path arguments; keep its behavior
510
+ // checked against isValidRelativePath rather than hiding constraints in prose.
511
+ export const RELATIVE_PATH_PATTERN = String.raw`^(?:\.|(?!/)(?![a-zA-Z]:/)(?!\./)(?![\s\S]*\.\.)(?![\s\S]*[\\\u0000])(?![\s\S]*//)(?![\s\S]*/\.(?:/|$))(?![\s\S]*/$)[\s\S]+)$`;
512
+
481
513
  export function isValidRelativePath(p: unknown): p is string {
482
514
  return (
483
515
  typeof p === "string" &&
@@ -655,8 +687,8 @@ function validateAtomicVerification(
655
687
  validateExecutableRunner(contract.runner) &&
656
688
  validateVitestRunner(executableRunner) &&
657
689
  Array.isArray(contract.testFiles) &&
658
- contract.testFiles.length > 0 &&
659
- contract.testFiles.length <= 64 &&
690
+ contract.testFiles.length >= VERIFICATION_LIMITS.minTestFiles &&
691
+ contract.testFiles.length <= VERIFICATION_LIMITS.maxTestFiles &&
660
692
  contract.testFiles.every(isValidRelativePath) &&
661
693
  new Set(contract.testFiles).size === contract.testFiles.length &&
662
694
  validVerificationArgs(contract.args) &&
@@ -724,8 +756,8 @@ function validateStructuredVerification(
724
756
  String(contract.classification),
725
757
  ) ||
726
758
  !Array.isArray(contract.steps) ||
727
- contract.steps.length === 0 ||
728
- contract.steps.length > 8 ||
759
+ contract.steps.length < VERIFICATION_LIMITS.minSteps ||
760
+ contract.steps.length > VERIFICATION_LIMITS.maxSteps ||
729
761
  !contract.steps.every((step) => validateAtomicVerification(step))
730
762
  ) {
731
763
  return false;
@@ -1198,6 +1230,9 @@ export type ImplementGraphContractDiagnostic =
1198
1230
  field?: string;
1199
1231
  category?: string;
1200
1232
  outputId?: string;
1233
+ path?: string;
1234
+ expectedPaths?: string[];
1235
+ actualPaths?: string[];
1201
1236
  }
1202
1237
  | {
1203
1238
  code: "invalid-output-path";
@@ -1456,6 +1491,26 @@ function taskBoundaryContractDiagnostic(
1456
1491
  snapshot,
1457
1492
  );
1458
1493
  if (impactReason !== null) {
1494
+ if (impactReason === "current-task related test is outside the write set") {
1495
+ const related = (value.impactClosure as ImpactClosureContract)
1496
+ .relatedTests;
1497
+ const index = related.findIndex(
1498
+ (test) =>
1499
+ test.disposition === "current-task" &&
1500
+ !declared.write.includes(test.path),
1501
+ );
1502
+ const test = related[index];
1503
+ if (test)
1504
+ return {
1505
+ ...diagnostic(
1506
+ `impactClosure.relatedTests.${index}.disposition`,
1507
+ "current-task-outside-write-set",
1508
+ ),
1509
+ path: test.path,
1510
+ expectedPaths: [...declared.write].sort(),
1511
+ actualPaths: [test.path],
1512
+ };
1513
+ }
1459
1514
  return diagnostic("impactClosure", impactClosureCategory(impactReason));
1460
1515
  }
1461
1516
  return validateTaskBoundary(value, snapshot)
@@ -1469,6 +1524,7 @@ export function validateImplementGraphBoundary(value: unknown):
1469
1524
  ok: false;
1470
1525
  reason: string;
1471
1526
  diagnostic: ImplementGraphContractDiagnostic;
1527
+ diagnostics?: ImplementGraphContractDiagnostic[];
1472
1528
  } {
1473
1529
  if (
1474
1530
  !hasExactKeys(value, ["changeId", "tasks", "outputs"]) ||
@@ -1478,29 +1534,33 @@ export function validateImplementGraphBoundary(value: unknown):
1478
1534
  value.tasks.length > 128 ||
1479
1535
  !Array.isArray(value.outputs) ||
1480
1536
  value.outputs.length > 512
1481
- ) {
1537
+ )
1482
1538
  return {
1483
1539
  ok: false,
1484
1540
  reason: "invalid Implement graph boundary",
1485
1541
  diagnostic: { code: "invalid-implement-graph" },
1486
1542
  };
1487
- }
1488
1543
 
1544
+ // Collect independent task/output failures without admitting a partial graph.
1545
+ const failures: Array<{
1546
+ reason: string;
1547
+ diagnostic: ImplementGraphContractDiagnostic;
1548
+ }> = [];
1489
1549
  for (const candidate of value.tasks) {
1490
1550
  if (
1491
1551
  !candidate ||
1492
1552
  typeof candidate !== "object" ||
1493
1553
  Array.isArray(candidate)
1494
1554
  ) {
1495
- return {
1496
- ok: false,
1555
+ failures.push({
1497
1556
  reason: "invalid Implement task boundary",
1498
1557
  diagnostic: {
1499
1558
  code: "invalid-implement-graph",
1500
1559
  field: "tasks",
1501
1560
  category: "shape",
1502
1561
  },
1503
- };
1562
+ });
1563
+ continue;
1504
1564
  }
1505
1565
  const task = candidate as Record<string, unknown>;
1506
1566
  const diagnostic = Object.hasOwn(task, "changeId")
@@ -1513,31 +1573,27 @@ export function validateImplementGraphBoundary(value: unknown):
1513
1573
  { changeId: value.changeId, ...task },
1514
1574
  undefined,
1515
1575
  );
1516
- if (diagnostic) {
1517
- return {
1518
- ok: false,
1576
+ if (diagnostic)
1577
+ failures.push({
1519
1578
  reason: "invalid Implement task boundary",
1520
1579
  diagnostic: { code: "invalid-implement-graph", ...diagnostic },
1521
- };
1522
- }
1580
+ });
1523
1581
  }
1524
-
1525
1582
  for (const candidate of value.outputs) {
1526
1583
  if (
1527
1584
  !candidate ||
1528
1585
  typeof candidate !== "object" ||
1529
1586
  Array.isArray(candidate)
1530
1587
  ) {
1531
- return {
1532
- ok: false,
1588
+ failures.push({
1533
1589
  reason: "invalid Implement graph output",
1534
1590
  diagnostic: { code: "invalid-implement-graph" },
1535
- };
1591
+ });
1592
+ continue;
1536
1593
  }
1537
1594
  const output = candidate as Record<string, unknown>;
1538
1595
  if (!isValidRelativePath(output.path)) {
1539
- return {
1540
- ok: false,
1596
+ failures.push({
1541
1597
  reason: "invalid Implement graph output path",
1542
1598
  diagnostic: {
1543
1599
  code: "invalid-output-path",
@@ -1545,7 +1601,8 @@ export function validateImplementGraphBoundary(value: unknown):
1545
1601
  field: "outputs.path",
1546
1602
  category: "path",
1547
1603
  },
1548
- };
1604
+ });
1605
+ continue;
1549
1606
  }
1550
1607
  if (
1551
1608
  !hasExactKeys(output, ["id", "path", "producer", "postcondition"]) ||
@@ -1557,8 +1614,7 @@ export function validateImplementGraphBoundary(value: unknown):
1557
1614
  ) ||
1558
1615
  output.postcondition !== "regular-file"
1559
1616
  ) {
1560
- return {
1561
- ok: false,
1617
+ failures.push({
1562
1618
  reason: "invalid Implement graph output",
1563
1619
  diagnostic: {
1564
1620
  code: "invalid-implement-graph",
@@ -1566,10 +1622,18 @@ export function validateImplementGraphBoundary(value: unknown):
1566
1622
  field: "outputs",
1567
1623
  category: "shape",
1568
1624
  },
1569
- };
1625
+ });
1570
1626
  }
1571
1627
  }
1572
-
1628
+ const first = failures[0];
1629
+ if (first)
1630
+ return {
1631
+ ok: false,
1632
+ ...first,
1633
+ ...(failures.length > 1
1634
+ ? { diagnostics: failures.map((failure) => failure.diagnostic) }
1635
+ : {}),
1636
+ };
1573
1637
  return {
1574
1638
  ok: true,
1575
1639
  value: structuredClone(value) as unknown as ImplementGraphBoundary,
@@ -1976,6 +2040,24 @@ export interface DiffResult {
1976
2040
  contractCompliant: boolean;
1977
2041
  }
1978
2042
 
2043
+ // Diagnostics contain only code-owned field names, never unexpected keys or values.
2044
+ function evidenceFieldsFailure(
2045
+ value: unknown,
2046
+ fields: readonly string[],
2047
+ location: string,
2048
+ ): { ok: false; reason: string } {
2049
+ if (!value || typeof value !== "object" || Array.isArray(value))
2050
+ return { ok: false, reason: `${location}: expected an object` };
2051
+ const missing = fields.filter((field) => !Object.hasOwn(value, field));
2052
+ return {
2053
+ ok: false,
2054
+ reason:
2055
+ missing.length > 0
2056
+ ? `${location}: missing fields ${missing.join(", ")}`
2057
+ : `${location}: unexpected fields; allowed fields ${fields.join(", ")}`,
2058
+ };
2059
+ }
2060
+
1979
2061
  export function validateEvidenceResult(value: unknown): {
1980
2062
  ok: boolean;
1981
2063
  reason?: string;
@@ -1999,7 +2081,7 @@ export function validateEvidenceResult(value: unknown): {
1999
2081
  "hints",
2000
2082
  ] as const;
2001
2083
  if (!hasExactKeys(r, fields)) {
2002
- return { ok: false, reason: "invalid evidence result fields" };
2084
+ return evidenceFieldsFailure(r, fields, "result");
2003
2085
  }
2004
2086
  if (!validIdentifier(r.id) || typeof r.role !== "string")
2005
2087
  return { ok: false, reason: "invalid identity" };
@@ -2019,25 +2101,38 @@ export function validateEvidenceResult(value: unknown): {
2019
2101
  return { ok: false, reason: `invalid evidence field: ${field}` };
2020
2102
  }
2021
2103
  }
2022
- if (
2023
- !Array.isArray(r.citations) ||
2024
- r.citations.some(
2025
- (citation) =>
2026
- !hasExactKeys(citation, ["path", "lines"]) ||
2027
- !isValidRelativePath(citation.path) ||
2028
- typeof citation.lines !== "string",
2029
- )
2030
- ) {
2031
- return { ok: false, reason: "invalid evidence citations" };
2032
- }
2033
- if (
2034
- !hasExactKeys(r.hints, ["writeSet", "verification", "agentsImpact"]) ||
2035
- !validatePathSet(r.hints.writeSet) ||
2036
- typeof r.hints.verification !== "string" ||
2037
- !(AGENTS_IMPACTS as readonly unknown[]).includes(r.hints.agentsImpact)
2038
- ) {
2039
- return { ok: false, reason: "invalid evidence hints" };
2040
- }
2104
+ if (!Array.isArray(r.citations))
2105
+ return { ok: false, reason: "citations: expected an array" };
2106
+ for (const [index, citation] of r.citations.entries()) {
2107
+ const field = `citations[${index}]`;
2108
+ if (!hasExactKeys(citation, ["path", "lines"]))
2109
+ return evidenceFieldsFailure(citation, ["path", "lines"], field);
2110
+ if (!isValidRelativePath(citation.path))
2111
+ return {
2112
+ ok: false,
2113
+ reason: `${field}.path: expected a canonical relative path`,
2114
+ };
2115
+ if (typeof citation.lines !== "string")
2116
+ return { ok: false, reason: `${field}.lines: expected a string` };
2117
+ }
2118
+ if (!hasExactKeys(r.hints, ["writeSet", "verification", "agentsImpact"]))
2119
+ return evidenceFieldsFailure(
2120
+ r.hints,
2121
+ ["writeSet", "verification", "agentsImpact"],
2122
+ "hints",
2123
+ );
2124
+ if (!validatePathSet(r.hints.writeSet))
2125
+ return {
2126
+ ok: false,
2127
+ reason: "hints.writeSet: expected unique canonical relative paths",
2128
+ };
2129
+ if (typeof r.hints.verification !== "string")
2130
+ return { ok: false, reason: "hints.verification: expected a string" };
2131
+ if (!(AGENTS_IMPACTS as readonly unknown[]).includes(r.hints.agentsImpact))
2132
+ return {
2133
+ ok: false,
2134
+ reason: `hints.agentsImpact: expected one of ${AGENTS_IMPACTS.join(", ")}`,
2135
+ };
2041
2136
  const serialized = JSON.stringify(r);
2042
2137
  if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
2043
2138
  return {
@@ -2078,36 +2173,65 @@ function validateDesignEvidenceResult(r: Record<string, unknown>): {
2078
2173
  "success_criteria_hints",
2079
2174
  ] as const;
2080
2175
  if (!hasExactKeys(r, fields)) {
2081
- return { ok: false, reason: "invalid Design evidence result fields" };
2082
- }
2083
- if (
2084
- !validIdentifier(r.id) ||
2085
- r.packet_id !== r.id ||
2086
- !isValidRelativePath(r.module_name)
2087
- ) {
2088
- return { ok: false, reason: "invalid Design evidence identity" };
2176
+ return evidenceFieldsFailure(r, fields, "result");
2089
2177
  }
2178
+ if (!validIdentifier(r.id))
2179
+ return { ok: false, reason: "id: expected a valid identifier" };
2180
+ if (r.packet_id !== r.id)
2181
+ return { ok: false, reason: "packet_id: must equal id" };
2182
+ if (!isValidRelativePath(r.module_name))
2183
+ return {
2184
+ ok: false,
2185
+ reason: "module_name: expected a canonical relative path or module slug",
2186
+ };
2090
2187
  if (!validatePathSet(r.scope) || r.scope.length === 0) {
2091
- return { ok: false, reason: "invalid Design evidence scope" };
2188
+ return {
2189
+ ok: false,
2190
+ reason:
2191
+ "scope: expected a nonempty set of unique canonical relative paths",
2192
+ };
2092
2193
  }
2093
- if (!validatePathSet(r.files_read) || !validatePathSet(r.write_set_hints)) {
2094
- return { ok: false, reason: "invalid Design evidence path set" };
2194
+ for (const field of ["files_read", "write_set_hints"] as const) {
2195
+ if (!validatePathSet(r[field]))
2196
+ return {
2197
+ ok: false,
2198
+ reason: `${field}: expected unique canonical relative paths`,
2199
+ };
2095
2200
  }
2096
- if (
2097
- !Array.isArray(r.evidence) ||
2098
- r.evidence.some(
2099
- (entry) =>
2100
- !hasExactKeys(entry, ["claim", "path", "line_start", "line_end"]) ||
2101
- typeof entry.claim !== "string" ||
2102
- entry.claim.length === 0 ||
2103
- !isValidRelativePath(entry.path) ||
2104
- !Number.isSafeInteger(entry.line_start) ||
2105
- !Number.isSafeInteger(entry.line_end) ||
2106
- (entry.line_start as number) < 1 ||
2107
- (entry.line_end as number) < (entry.line_start as number),
2201
+ if (!Array.isArray(r.evidence))
2202
+ return { ok: false, reason: "evidence: expected an array" };
2203
+ for (const [index, entry] of r.evidence.entries()) {
2204
+ const field = `evidence[${index}]`;
2205
+ if (!hasExactKeys(entry, ["claim", "path", "line_start", "line_end"])) {
2206
+ return evidenceFieldsFailure(
2207
+ entry,
2208
+ ["claim", "path", "line_start", "line_end"],
2209
+ field,
2210
+ );
2211
+ }
2212
+ if (typeof entry.claim !== "string" || entry.claim.length === 0)
2213
+ return { ok: false, reason: `${field}.claim: expected nonempty text` };
2214
+ if (!isValidRelativePath(entry.path))
2215
+ return {
2216
+ ok: false,
2217
+ reason: `${field}.path: expected a canonical relative path`,
2218
+ };
2219
+ if (
2220
+ !Number.isSafeInteger(entry.line_start) ||
2221
+ (entry.line_start as number) < 1
2108
2222
  )
2109
- ) {
2110
- return { ok: false, reason: "invalid Design evidence citations" };
2223
+ return {
2224
+ ok: false,
2225
+ reason: `${field}.line_start: expected a positive safe integer`,
2226
+ };
2227
+ if (
2228
+ !Number.isSafeInteger(entry.line_end) ||
2229
+ (entry.line_end as number) < (entry.line_start as number)
2230
+ )
2231
+ return {
2232
+ ok: false,
2233
+ reason: `${field}.line_end: expected a safe integer >= line_start`,
2234
+ };
2111
2235
  }
2112
2236
  for (const field of [
2113
2237
  "existing_structures",