@aventara/core 0.1.0-pilot.4 → 1.0.0-rc.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 (108) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/README.md +100 -3
  3. package/dist/adapters/adapter-model.d.ts +8 -0
  4. package/dist/adapters/adapter.interface.d.ts +6 -2
  5. package/dist/compiler/contract-compiler-diagnostic.d.ts +6 -0
  6. package/dist/compiler/contract.compiler.js +67 -0
  7. package/dist/config/config.resolver.d.ts +3 -0
  8. package/dist/config/config.resolver.js +1 -0
  9. package/dist/config/config.validator.js +6 -0
  10. package/dist/config/define-fields.d.ts +10 -3
  11. package/dist/config/framework-config.d.ts +4 -1
  12. package/dist/config/framework-fields-config.d.ts +6 -0
  13. package/dist/contracts/client-contract-structure.validator.d.ts +21 -1
  14. package/dist/contracts/client-contract-structure.validator.js +99 -2
  15. package/dist/contracts/contract-canonicalizer.js +32 -0
  16. package/dist/contracts/contract.d.ts +7 -0
  17. package/dist/contracts/field.d.ts +10 -0
  18. package/dist/contracts/json-shape.d.ts +65 -0
  19. package/dist/contracts/json-shape.js +1 -0
  20. package/dist/contracts/reference-write-limit.d.ts +7 -0
  21. package/dist/contracts/reference-write-limit.js +1 -0
  22. package/dist/contracts/scalar-value-type.d.ts +12 -1
  23. package/dist/framework/contract-view.d.ts +126 -21
  24. package/dist/framework/framework.d.ts +5 -3
  25. package/dist/framework/framework.factory.js +56 -13
  26. package/dist/index.d.ts +8 -40
  27. package/dist/index.js +0 -20
  28. package/dist/internal.d.ts +35 -0
  29. package/dist/internal.js +16 -0
  30. package/dist/json/avz.d.ts +96 -0
  31. package/dist/json/avz.js +212 -0
  32. package/dist/json/index.d.ts +6 -0
  33. package/dist/json/index.js +1 -0
  34. package/dist/json/json-shape.registry.d.ts +22 -0
  35. package/dist/json/json-shape.registry.js +16 -0
  36. package/dist/json/zod-issue.translator.d.ts +51 -0
  37. package/dist/json/zod-issue.translator.js +142 -0
  38. package/dist/operations/operation-arguments.d.ts +151 -101
  39. package/dist/operations/operation-identity.d.ts +2 -0
  40. package/dist/operations/operation-identity.js +3 -0
  41. package/dist/operations/operation-validator-core.d.ts +1 -1
  42. package/dist/operations/operation-validator-core.js +28 -10
  43. package/dist/operations/operation-validator.d.ts +20 -5
  44. package/dist/operations/operation-validator.js +26 -10
  45. package/dist/operations/projection-scope.resolver.d.ts +1 -1
  46. package/dist/operations/structural-limits.walker.d.ts +19 -0
  47. package/dist/operations/structural-limits.walker.js +344 -0
  48. package/dist/protocol/absent-route.responder.d.ts +2 -1
  49. package/dist/protocol/absent-route.responder.js +32 -6
  50. package/dist/protocol/http-request.decoder.d.ts +6 -0
  51. package/dist/protocol/http-request.decoder.js +11 -3
  52. package/dist/protocol/protocol.binder.d.ts +4 -2
  53. package/dist/protocol/protocol.binder.js +3 -1
  54. package/dist/protocol/protocol.handler.d.ts +6 -2
  55. package/dist/protocol/protocol.handler.js +14 -4
  56. package/dist/protocol/unmounted-route.error.d.ts +19 -0
  57. package/dist/protocol/unmounted-route.error.js +10 -0
  58. package/dist/runtime/caller-data.copier.d.ts +59 -0
  59. package/dist/runtime/caller-data.copier.js +470 -0
  60. package/dist/runtime/caller-data.door.d.ts +56 -0
  61. package/dist/runtime/caller-data.door.js +65 -0
  62. package/dist/runtime/computed-read-runtime.js +3 -10
  63. package/dist/runtime/computed-value.error.d.ts +13 -0
  64. package/dist/runtime/computed-value.error.js +9 -0
  65. package/dist/runtime/create-requirement.d.ts +43 -7
  66. package/dist/runtime/create-requirement.js +89 -9
  67. package/dist/runtime/diagnostics-reporter.d.ts +20 -4
  68. package/dist/runtime/diagnostics-reporter.js +11 -4
  69. package/dist/runtime/error-boundary.d.ts +8 -0
  70. package/dist/runtime/error-boundary.js +65 -35
  71. package/dist/runtime/field-behavior.d.ts +2 -2
  72. package/dist/runtime/framework-error.d.ts +12 -0
  73. package/dist/runtime/misuse-reporter.d.ts +32 -0
  74. package/dist/runtime/misuse-reporter.js +19 -0
  75. package/dist/runtime/mutation-planner.js +13 -11
  76. package/dist/runtime/operation-request.d.ts +4 -11
  77. package/dist/runtime/operation-runtime.d.ts +14 -7
  78. package/dist/runtime/operation-runtime.js +37 -18
  79. package/dist/runtime/outcome-mapper.js +12 -2
  80. package/dist/runtime/output-validator.d.ts +10 -0
  81. package/dist/runtime/output-validator.js +11 -0
  82. package/dist/runtime/owned-execution.registry.d.ts +13 -0
  83. package/dist/runtime/owned-execution.registry.js +7 -0
  84. package/dist/runtime/pipeline.d.ts +19 -7
  85. package/dist/runtime/referential-action.d.ts +7 -0
  86. package/dist/runtime/request-envelope.validator.d.ts +5 -0
  87. package/dist/runtime/request-envelope.validator.js +118 -0
  88. package/dist/runtime/scalar-runtime.d.ts +30 -2
  89. package/dist/runtime/scalar-runtime.js +162 -79
  90. package/dist/transactions/transaction-plan-assembler.d.ts +5 -1
  91. package/dist/transactions/transaction-plan-assembler.js +74 -75
  92. package/dist/transactions/transaction-plan-validator.d.ts +30 -7
  93. package/dist/transactions/transaction-plan-validator.js +204 -153
  94. package/dist/transactions/transaction-reference-collector.d.ts +30 -0
  95. package/dist/transactions/transaction-reference-collector.js +204 -0
  96. package/dist/transactions/transaction-reference-resolver.js +10 -5
  97. package/dist/transactions/transaction-reference-validator.js +24 -17
  98. package/dist/transactions/transaction-reference.d.ts +23 -51
  99. package/dist/transactions/transaction-request.d.ts +3 -3
  100. package/dist/transactions/transaction-response.d.ts +6 -1
  101. package/dist/transactions/transaction-response.js +24 -1
  102. package/dist/transactions/transaction-runtime.d.ts +4 -2
  103. package/dist/transactions/transaction-runtime.js +4 -3
  104. package/package.json +17 -3
  105. package/dist/operations/operation-boolean-nodes.d.ts +0 -11
  106. package/dist/operations/operation-boolean-nodes.js +0 -283
  107. package/dist/operations/operation-nesting.d.ts +0 -8
  108. package/dist/operations/operation-nesting.js +0 -307
@@ -0,0 +1,65 @@
1
+ import { parseOperationIdentity } from "../operations/operation-identity.js";
2
+ import { validationIssue } from "../operations/validation-code.js";
3
+ import { CallerDataError, copyCallerData, } from "./caller-data.copier.js";
4
+ export function misuseIdentityOf(request) {
5
+ const origin = request.origin === "application" || request.origin === "client"
6
+ ? request.origin
7
+ : undefined;
8
+ const identity = typeof request.family === "string" && typeof request.variant === "string"
9
+ ? parseOperationIdentity(request.family, request.variant)
10
+ : undefined;
11
+ return {
12
+ ...(origin === undefined ? {} : { origin }),
13
+ ...(typeof request.resource === "string"
14
+ ? { resource: request.resource }
15
+ : {}),
16
+ ...(identity === undefined
17
+ ? {}
18
+ : { family: identity.family, variant: identity.variant }),
19
+ };
20
+ }
21
+ export function refuseCallerData(error, address, identity, misuse) {
22
+ const path = [...address, ...error.path];
23
+ misuse({ kind: error.kind, path, ...identity });
24
+ return validationIssue("V1001", path, error.message);
25
+ }
26
+ export function copyAtDoor(value, policy) {
27
+ try {
28
+ return { ok: true, value: copyCallerData(value, policy) };
29
+ }
30
+ catch (error) {
31
+ if (error instanceof CallerDataError) {
32
+ return { ok: false, error };
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ export function copyEnvelopeAtDoor(request, members) {
38
+ const shallow = copyAtDoor(request, {
39
+ keep: (value) => value !== request,
40
+ });
41
+ if (!shallow.ok ||
42
+ typeof shallow.value !== "object" ||
43
+ shallow.value === null ||
44
+ Array.isArray(shallow.value)) {
45
+ return shallow;
46
+ }
47
+ const envelope = shallow.value;
48
+ for (const member of members) {
49
+ if (!Object.hasOwn(envelope, member)) {
50
+ continue;
51
+ }
52
+ const copy = copyAtDoor(envelope[member]);
53
+ if (!copy.ok) {
54
+ return {
55
+ ok: false,
56
+ error: new CallerDataError(copy.error.kind, [
57
+ member,
58
+ ...copy.error.path,
59
+ ]),
60
+ };
61
+ }
62
+ envelope[member] = copy.value;
63
+ }
64
+ return { ok: true, value: envelope };
65
+ }
@@ -1,6 +1,5 @@
1
1
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
- import { FrameworkError } from "./framework-error.js";
3
- import { INTERNAL_FAILURE_MESSAGE } from "./operation-response.js";
2
+ import { ComputedValueError } from "./computed-value.error.js";
4
3
  import { normalizeScalarFieldValue } from "./scalar-runtime.js";
5
4
  function isRecord(value) {
6
5
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -274,12 +273,6 @@ export function planReadOperation(core, active, behaviors, request, args) {
274
273
  materialize: defaults.plan.computed.length > 0,
275
274
  };
276
275
  }
277
- function behaviorFailure(issues) {
278
- throw new FrameworkError("A3000", {
279
- message: INTERNAL_FAILURE_MESSAGE,
280
- issues,
281
- });
282
- }
283
276
  async function materializeRelation(active, behaviors, request, relation, value, contextFactory) {
284
277
  if (value === null || value === undefined) {
285
278
  return value;
@@ -308,7 +301,7 @@ async function materializeRecord(active, behaviors, request, plan, value, contex
308
301
  continue;
309
302
  }
310
303
  const computedValue = await behavior.compute({
311
- scope: request.scope,
304
+ origin: request.origin,
312
305
  resource: plan.resource,
313
306
  field: fieldName,
314
307
  value: record[fieldName],
@@ -320,7 +313,7 @@ async function materializeRecord(active, behaviors, request, plan, value, contex
320
313
  }
321
314
  const validation = normalizeScalarFieldValue(active, field, computedValue, "runtime", ["behaviors", plan.resource, fieldName, "read"]);
322
315
  if (!validation.ok) {
323
- behaviorFailure(validation.issues);
316
+ throw new ComputedValueError(plan.resource, fieldName, "read", validation.issues);
324
317
  }
325
318
  record[fieldName] = validation.value;
326
319
  }
@@ -0,0 +1,13 @@
1
+ import type { ValidationIssue } from "../operations/validation-code.js";
2
+ /**
3
+ * What the diagnostics channel receives when a computed value — a read or write
4
+ * behavior's return — is one its Contract refuses: the developer's broken code,
5
+ * never the caller's. It is thrown, not a deliberate outcome, so the framework's
6
+ * boundary answers the caller the generic `A3000` and reports this error, with the
7
+ * issues, exactly once.
8
+ */
9
+ export declare class ComputedValueError extends Error {
10
+ readonly issues: readonly ValidationIssue[];
11
+ readonly name = "ComputedValueError";
12
+ constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly ValidationIssue[]);
13
+ }
@@ -0,0 +1,9 @@
1
+ export class ComputedValueError extends Error {
2
+ issues;
3
+ name = "ComputedValueError";
4
+ constructor(resource, field, stage, issues) {
5
+ const [first] = issues;
6
+ super(`The ${stage} behavior of ${resource}.${field} returned a value its Contract refuses${first === undefined ? "." : ` (${first.code}: ${first.message})`}`);
7
+ this.issues = issues;
8
+ }
9
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Contract } from "../contracts/contract.js";
2
2
  import type { OperationIdentity } from "../operations/operation-identity.js";
3
3
  import type { ValidationIssue } from "../operations/validation-code.js";
4
+ import type { ReferentialActionTable } from "./referential-action.js";
4
5
  /**
5
6
  * Core's create-requirement vocabulary, and the check that reads it.
6
7
  *
@@ -22,6 +23,24 @@ import type { ValidationIssue } from "../operations/validation-code.js";
22
23
  * which reaches the caller as an internal failure. A missing value is the
23
24
  * caller's to fix, so it answers `A2004` with a `V1000` issue at the path where
24
25
  * the value belongs.
26
+ *
27
+ * **One value, two spellings.** A relation that holds a reference has scalar
28
+ * fields that hold it too — the Adapter states which, on the relation's
29
+ * referential edge (`ReferentialEdge.references`), for every such relation,
30
+ * required or optional. A write that sets both — `authorId` and
31
+ * `author: { $connect: … }` in one record, at create or update — names the
32
+ * reference twice. No store can honour both, and which one the caller meant is
33
+ * not the framework's to guess, so it is the caller's `A2004` too: a `V1012`
34
+ * issue at the relation, in every record the check walks, before any Adapter
35
+ * work. The same edge is what a required relation may instead be written
36
+ * through, so the pairing has one source.
37
+ *
38
+ * **One store, one spelling per record.** A store may take a record's
39
+ * references only one way at a time — every relation that holds one written as
40
+ * a relation, or every one through its fields — and state it as the Contract's
41
+ * `referenceWrites: "uniform"`. Under it, a record that writes a relation
42
+ * holding a reference beside another relation's reference field is the caller's
43
+ * `A2004` as well: a `V1012` issue at the relation write.
25
44
  */
26
45
  /** A relation edge, named by the Resource that holds it and its field there. */
27
46
  export type CreateRequirementEdge = {
@@ -34,9 +53,11 @@ export type CreateRequirementEdge = {
34
53
  * - `field` — a scalar with no default, generated value or other source: it
35
54
  * must be written.
36
55
  * - `relation` — a required to-one relation: it must be written (connected or
37
- * created), or every one of `orFields` must be — the scalar fields that hold
38
- * the reference. When the row is created **through** the relation's other end
39
- * (`inverse`), the parent supplies it, and it is not required.
56
+ * created), or every field that holds its reference must be — the
57
+ * `references` of the relation's referential edge, less any the store fills
58
+ * itself (`DEFAULTED` or `GENERATED_ON_CREATE`). When the row is created
59
+ * **through** the relation's other end (`inverse`), the parent supplies it,
60
+ * and it is not required.
40
61
  */
41
62
  export type CreateRequirement = {
42
63
  readonly kind: "field";
@@ -44,7 +65,6 @@ export type CreateRequirement = {
44
65
  } | {
45
66
  readonly kind: "relation";
46
67
  readonly relation: string;
47
- readonly orFields: readonly string[];
48
68
  readonly inverse?: CreateRequirementEdge;
49
69
  };
50
70
  /**
@@ -53,13 +73,29 @@ export type CreateRequirement = {
53
73
  */
54
74
  export type CreateRequirementTable = Readonly<Record<string, readonly CreateRequirement[]>>;
55
75
  type Path = readonly (string | number)[];
76
+ /**
77
+ * What the Adapter stated that the check reads: its create requirements, and
78
+ * its referential edges for the fields that hold each reference. Read once when
79
+ * the Framework is constructed.
80
+ */
81
+ export type RecordWriteFacts = {
82
+ readonly requirements: CreateRequirementTable;
83
+ readonly references: ReferentialActionTable;
84
+ };
85
+ /** The facts an Adapter stated, read off it once; an absent table states nothing. */
86
+ export declare function recordWriteFactsOf(adapter: {
87
+ readonly createRequirements?: CreateRequirementTable;
88
+ readonly referentialActions?: ReferentialActionTable;
89
+ }): RecordWriteFacts;
56
90
  /**
57
91
  * The `V1000` issues for every create position in one operation's arguments
58
92
  * that lacks a value the Adapter requires — `create.*`'s `data`, `upsert`'s
59
- * `create`, and every nested create under any write — in argument order.
60
- * Empty when nothing is missing. Total over malformed input.
93
+ * `create`, and every nested create under any write — and the `V1012` issues
94
+ * for every write record, create or update, that sets a relation and its
95
+ * reference both, in argument order. Empty when there are none. Total over
96
+ * malformed input.
61
97
  */
62
- export declare function missingCreateValues(contract: Contract, table: CreateRequirementTable, request: OperationIdentity & {
98
+ export declare function createRequirementIssues(contract: Contract, facts: RecordWriteFacts, request: OperationIdentity & {
63
99
  readonly resource: string;
64
100
  }, args: unknown, argumentsPath: Path): readonly ValidationIssue[];
65
101
  export {};
@@ -12,6 +12,23 @@ function requirementsOf(table, resource) {
12
12
  const listed = Object.hasOwn(table, resource) ? table[resource] : undefined;
13
13
  return Array.isArray(listed) ? listed : [];
14
14
  }
15
+ function edgesOf(table, resource) {
16
+ const listed = Object.hasOwn(table, resource) ? table[resource] : undefined;
17
+ return Array.isArray(listed) ? listed : [];
18
+ }
19
+ function referencesOf(edge) {
20
+ return Array.isArray(edge.references) ? edge.references : [];
21
+ }
22
+ function referenceFieldsOf(table, resource, relation) {
23
+ const edge = edgesOf(table, resource).find((candidate) => candidate.field === relation);
24
+ return edge === undefined ? [] : referencesOf(edge);
25
+ }
26
+ function storeSupplied(contract, resource, field) {
27
+ const found = lookupField(lookupResource(contract, resource), field);
28
+ return (found?.kind === "scalar" &&
29
+ (found.lifecycle.includes("DEFAULTED") ||
30
+ found.lifecycle.includes("GENERATED_ON_CREATE")));
31
+ }
15
32
  function creatableScalar(contract, resource, field) {
16
33
  const found = lookupField(lookupResource(contract, resource), field);
17
34
  return found?.kind === "scalar" && found.capabilities.create !== undefined;
@@ -26,7 +43,9 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
26
43
  return;
27
44
  }
28
45
  const { contract, issues } = context;
29
- for (const requirement of requirementsOf(context.table, resource)) {
46
+ const conflicted = referenceConflicts(context, resource, record, path);
47
+ mixedReferenceWrites(context, resource, record, path, conflicted);
48
+ for (const requirement of requirementsOf(context.requirements, resource)) {
30
49
  if (requirement.kind === "field") {
31
50
  if (!written(record, requirement.field) &&
32
51
  creatableScalar(contract, resource, requirement.field)) {
@@ -38,14 +57,15 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
38
57
  }
39
58
  continue;
40
59
  }
60
+ if (conflicted.has(requirement.relation)) {
61
+ continue;
62
+ }
41
63
  if (via !== undefined &&
42
64
  requirement.inverse?.resource === via.resource &&
43
65
  requirement.inverse.relation === via.relation) {
44
66
  continue;
45
67
  }
46
- const orFields = Array.isArray(requirement.orFields)
47
- ? requirement.orFields
48
- : [];
68
+ const orFields = referenceFieldsOf(context.references, resource, requirement.relation).filter((field) => !storeSupplied(contract, resource, field));
49
69
  if (written(record, requirement.relation) ||
50
70
  (orFields.length > 0 && orFields.every((field) => written(record, field)))) {
51
71
  continue;
@@ -69,6 +89,55 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
69
89
  }
70
90
  walkNestedCreates(context, resource, record, path);
71
91
  }
92
+ function referenceConflicts(context, resource, record, path) {
93
+ const conflicted = new Set();
94
+ for (const edge of edgesOf(context.references, resource)) {
95
+ if (!written(record, edge.field)) {
96
+ continue;
97
+ }
98
+ const fields = referencesOf(edge).filter((field) => written(record, field));
99
+ if (fields.length === 0) {
100
+ continue;
101
+ }
102
+ conflicted.add(edge.field);
103
+ context.issues.push({
104
+ code: "V1012",
105
+ path: [...path, edge.field],
106
+ message: `Set ${fields.map((field) => `"${field}"`).join(" and ")} or "${edge.field}", not both.`,
107
+ });
108
+ }
109
+ return conflicted;
110
+ }
111
+ function mixedReferenceWrites(context, resource, record, path, conflicted) {
112
+ if (context.contract.referenceWrites !== "uniform") {
113
+ return;
114
+ }
115
+ const edges = edgesOf(context.references, resource);
116
+ const referenceFields = [...new Set(edges.flatMap(referencesOf))];
117
+ for (const edge of edges) {
118
+ if (!written(record, edge.field) || conflicted.has(edge.field)) {
119
+ continue;
120
+ }
121
+ const fields = referenceFields.filter((field) => written(record, field));
122
+ if (fields.length === 0) {
123
+ continue;
124
+ }
125
+ const named = fields.map((field) => `"${field}"`).join(" and ");
126
+ context.issues.push({
127
+ code: "V1012",
128
+ path: [...path, edge.field],
129
+ message: `Use relation fields only, or reference fields only, in one record: ${named} ${fields.length === 1 ? "is a reference field" : "are reference fields"} and "${edge.field}" is a relation.`,
130
+ });
131
+ }
132
+ }
133
+ function checkUpdateRecord(context, resource, record, path) {
134
+ if (!isRecord(record)) {
135
+ return;
136
+ }
137
+ const conflicted = referenceConflicts(context, resource, record, path);
138
+ mixedReferenceWrites(context, resource, record, path, conflicted);
139
+ walkNestedCreates(context, resource, record, path);
140
+ }
72
141
  function walkNestedCreates(context, resource, record, path) {
73
142
  if (!isRecord(record)) {
74
143
  return;
@@ -100,18 +169,29 @@ function walkNestedCreates(context, resource, record, path) {
100
169
  checkCreateRecord(context, field.target, siteRecord, sitePath, NESTED_WRITE_SCOPE, via);
101
170
  }
102
171
  else {
103
- walkNestedCreates(context, field.target, siteRecord, sitePath);
172
+ checkUpdateRecord(context, field.target, siteRecord, sitePath);
104
173
  }
105
174
  }
106
175
  }
107
176
  }
108
177
  }
109
178
  }
110
- export function missingCreateValues(contract, table, request, args, argumentsPath) {
179
+ export function recordWriteFactsOf(adapter) {
180
+ return {
181
+ requirements: adapter.createRequirements ?? {},
182
+ references: adapter.referentialActions ?? {},
183
+ };
184
+ }
185
+ export function createRequirementIssues(contract, facts, request, args, argumentsPath) {
111
186
  if (!isRecord(args)) {
112
187
  return [];
113
188
  }
114
- const context = { contract, table, issues: [] };
189
+ const context = {
190
+ contract,
191
+ requirements: facts.requirements,
192
+ references: facts.references,
193
+ issues: [],
194
+ };
115
195
  const scope = resolveWriteScope(lookupResource(contract, request.resource), request);
116
196
  if (request.family === "create" && written(args, "data")) {
117
197
  const data = args.data;
@@ -126,13 +206,13 @@ export function missingCreateValues(contract, table, request, args, argumentsPat
126
206
  }
127
207
  if (request.family === "upsert") {
128
208
  checkCreateRecord(context, request.resource, args.create, [...argumentsPath, "create"], scope, undefined);
129
- walkNestedCreates(context, request.resource, args.update, [
209
+ checkUpdateRecord(context, request.resource, args.update, [
130
210
  ...argumentsPath,
131
211
  "update",
132
212
  ]);
133
213
  }
134
214
  if (request.family === "update") {
135
- walkNestedCreates(context, request.resource, args.data, [
215
+ checkUpdateRecord(context, request.resource, args.data, [
136
216
  ...argumentsPath,
137
217
  "data",
138
218
  ]);
@@ -5,19 +5,28 @@ import type { FrameworkOperationRequest } from "./operation-request.js";
5
5
  import type { OperationErrorCode } from "./operation-response.js";
6
6
  /**
7
7
  * One unplanned failure of an operation: the raw error, the code the caller
8
- * received, and the operation and request it happened in. Server-side only:
8
+ * received, and the operation and request it happened in — or a developer's
9
+ * deliberate internal failure, marked `deliberate`. Server-side only:
9
10
  * diagnostics are never sent to the caller.
10
11
  */
11
12
  export type OperationDiagnostic = {
12
13
  readonly kind: "operation";
13
14
  readonly error: unknown;
14
15
  readonly code: OperationErrorCode;
15
- readonly scope: RequestOrigin;
16
+ readonly origin: RequestOrigin;
16
17
  readonly resource: string;
17
18
  readonly family: OperationFamily;
18
19
  readonly variant: string;
19
20
  readonly requestId: string;
20
21
  readonly operation?: number;
22
+ /**
23
+ * `true` when `error` is a `FrameworkError` its thrower — a pipeline
24
+ * callback, a behavior, a filter or an Adapter — raised deliberately with an
25
+ * internal (`A3xxx`) code: the developer's own failure, whose message and
26
+ * issues the caller never receives. Absent for a failure the framework did not
27
+ * expect.
28
+ */
29
+ readonly deliberate?: true;
21
30
  };
22
31
  /**
23
32
  * One unplanned failure of a transaction: the raw error, the code the caller
@@ -27,9 +36,14 @@ export type TransactionDiagnostic = {
27
36
  readonly kind: "transaction";
28
37
  readonly error: unknown;
29
38
  readonly code: OperationErrorCode;
30
- readonly scope: RequestOrigin;
39
+ readonly origin: RequestOrigin;
31
40
  readonly requestId: string;
32
41
  readonly operation?: number;
42
+ /**
43
+ * Never present: only an operation diagnostic is marked `deliberate`. Declared
44
+ * so the mark can be read on any {@link FrameworkDiagnostic}.
45
+ */
46
+ readonly deliberate?: never;
33
47
  };
34
48
  /** One diagnostic: of an operation, or of a transaction as a whole. */
35
49
  export type FrameworkDiagnostic = OperationDiagnostic | TransactionDiagnostic;
@@ -43,7 +57,9 @@ export type FrameworkDiagnosticsSink = (diagnostic: FrameworkDiagnostic) => void
43
57
  * error and the code being emitted. Total by construction — it returns
44
58
  * before the sink has finished and swallows everything the sink does.
45
59
  */
46
- export type DiagnosticsReporter = (error: unknown, code: OperationErrorCode) => void;
60
+ export type DiagnosticsReporter = (error: unknown, code: OperationErrorCode, marks?: DiagnosticMarks) => void;
61
+ /** What a report states about its error beyond the code: today, whether it was deliberate. */
62
+ export type DiagnosticMarks = Pick<OperationDiagnostic, "deliberate">;
47
63
  /** The identifying half of a diagnostic, captured once per request. */
48
64
  type OperationDiagnosticIdentity = Omit<OperationDiagnostic, "error" | "code">;
49
65
  type TransactionDiagnosticIdentity = Omit<TransactionDiagnostic, "error" | "code">;
@@ -10,9 +10,16 @@ export function createReporter(sink, identity) {
10
10
  if (sink === undefined) {
11
11
  return IGNORE_DIAGNOSTIC;
12
12
  }
13
- return (error, code) => {
13
+ return (error, code, marks) => {
14
14
  try {
15
- const settled = sink({ ...identity, error, code });
15
+ const settled = sink(identity.kind === "operation"
16
+ ? {
17
+ ...identity,
18
+ error,
19
+ code,
20
+ ...(marks?.deliberate === true ? { deliberate: true } : {}),
21
+ }
22
+ : { ...identity, error, code });
16
23
  if (isThenable(settled)) {
17
24
  settled.then(undefined, () => undefined);
18
25
  }
@@ -36,7 +43,7 @@ export function createDiagnosticsReporter(sink, request, operation) {
36
43
  try {
37
44
  identity = {
38
45
  kind: "operation",
39
- scope: request.scope,
46
+ origin: request.origin,
40
47
  resource: request.resource,
41
48
  family: request.family,
42
49
  variant: request.variant,
@@ -54,7 +61,7 @@ export function createTransactionDiagnosticsChannel(sink, request) {
54
61
  try {
55
62
  identity = {
56
63
  kind: "transaction",
57
- scope: request.scope,
64
+ origin: request.origin,
58
65
  requestId: request.requestId,
59
66
  };
60
67
  }
@@ -20,6 +20,8 @@ export declare class AdapterBoundary {
20
20
  export declare const SERIALIZATION_FAILURE_MESSAGE = "Result serialization failed.";
21
21
  /** Where a standalone operation's arguments sit in a framework issue path. */
22
22
  export declare const OPERATION_ARGUMENTS_PATH: readonly (string | number)[];
23
+ /** Where one plan step sits in a framework issue path: its node in the request. */
24
+ export declare function planStepPath(index: number): readonly (string | number)[];
23
25
  /** Where one plan operation's arguments sit in a framework issue path. */
24
26
  export declare function planOperationArgumentsPath(index: number): readonly (string | number)[];
25
27
  /**
@@ -31,6 +33,12 @@ export type CallerArgumentsView = {
31
33
  readonly path: readonly (string | number)[];
32
34
  readonly arguments: unknown;
33
35
  };
36
+ /**
37
+ * The longest issue message a developer's code may send to the caller, in UTF-16
38
+ * code units. A longer one, or one with no visible character, is replaced by the
39
+ * framework's text for its code and reported to the diagnostics channel.
40
+ */
41
+ export declare const MAX_DEVELOPER_ISSUE_MESSAGE_LENGTH = 1000;
34
42
  /**
35
43
  * Final framework sanitization, shared by every boundary. A deliberate
36
44
  * {@link FrameworkError} carries its own stable outcome; anything else is