@gate-forge/core 0.7.1 → 0.8.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 (141) hide show
  1. package/dist/classifier/bind.d.ts +2 -0
  2. package/dist/classifier/bind.d.ts.map +1 -1
  3. package/dist/classifier/bind.js +8 -0
  4. package/dist/classifier/bind.js.map +1 -1
  5. package/dist/classifier/classify.d.ts +2 -0
  6. package/dist/classifier/classify.d.ts.map +1 -1
  7. package/dist/classifier/classify.js +127 -2
  8. package/dist/classifier/classify.js.map +1 -1
  9. package/dist/classifier/index.d.ts +2 -2
  10. package/dist/classifier/index.d.ts.map +1 -1
  11. package/dist/classifier/index.js +1 -1
  12. package/dist/classifier/index.js.map +1 -1
  13. package/dist/classifier/schema.d.ts +54 -0
  14. package/dist/classifier/schema.d.ts.map +1 -1
  15. package/dist/classifier/schema.js +17 -0
  16. package/dist/classifier/schema.js.map +1 -1
  17. package/dist/config/index.d.ts +173 -0
  18. package/dist/config/index.d.ts.map +1 -1
  19. package/dist/config/index.js +296 -1
  20. package/dist/config/index.js.map +1 -1
  21. package/dist/graph/schema.d.ts +34 -0
  22. package/dist/graph/schema.d.ts.map +1 -1
  23. package/dist/index.d.ts +80 -10
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +58 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/mapping/resolve.d.ts +22 -1
  28. package/dist/mapping/resolve.d.ts.map +1 -1
  29. package/dist/mapping/resolve.js +71 -25
  30. package/dist/mapping/resolve.js.map +1 -1
  31. package/dist/policy/evaluate.d.ts +30 -0
  32. package/dist/policy/evaluate.d.ts.map +1 -1
  33. package/dist/policy/evaluate.js +22 -12
  34. package/dist/policy/evaluate.js.map +1 -1
  35. package/dist/quarantine/index.d.ts +82 -0
  36. package/dist/quarantine/index.d.ts.map +1 -0
  37. package/dist/quarantine/index.js +194 -0
  38. package/dist/quarantine/index.js.map +1 -0
  39. package/dist/receipt/index.d.ts +65 -1
  40. package/dist/receipt/index.d.ts.map +1 -1
  41. package/dist/receipt/index.js +115 -1
  42. package/dist/receipt/index.js.map +1 -1
  43. package/dist/report/human-message.d.ts +33 -0
  44. package/dist/report/human-message.d.ts.map +1 -0
  45. package/dist/report/human-message.js +33 -0
  46. package/dist/report/human-message.js.map +1 -0
  47. package/dist/report/index.d.ts +183 -9
  48. package/dist/report/index.d.ts.map +1 -1
  49. package/dist/report/index.js +296 -22
  50. package/dist/report/index.js.map +1 -1
  51. package/dist/schemas/adoption.d.ts +2 -0
  52. package/dist/schemas/adoption.d.ts.map +1 -1
  53. package/dist/schemas/adoption.js +6 -0
  54. package/dist/schemas/adoption.js.map +1 -1
  55. package/dist/schemas/behavior-catalog.d.ts +12 -0
  56. package/dist/schemas/behavior-catalog.d.ts.map +1 -1
  57. package/dist/schemas/behavior-evidence.d.ts +40 -0
  58. package/dist/schemas/behavior-evidence.d.ts.map +1 -1
  59. package/dist/schemas/behavior-evidence.js +9 -0
  60. package/dist/schemas/behavior-evidence.js.map +1 -1
  61. package/dist/schemas/behavior-policy.d.ts +26 -0
  62. package/dist/schemas/behavior-policy.d.ts.map +1 -1
  63. package/dist/schemas/behavior-policy.js +37 -1
  64. package/dist/schemas/behavior-policy.js.map +1 -1
  65. package/dist/schemas/execution-result.d.ts +17 -5
  66. package/dist/schemas/execution-result.d.ts.map +1 -1
  67. package/dist/schemas/execution-result.js +57 -2
  68. package/dist/schemas/execution-result.js.map +1 -1
  69. package/dist/schemas/gate-receipt.d.ts +25 -2
  70. package/dist/schemas/gate-receipt.d.ts.map +1 -1
  71. package/dist/schemas/gate-receipt.js +181 -2
  72. package/dist/schemas/gate-receipt.js.map +1 -1
  73. package/dist/schemas/index.d.ts +2 -0
  74. package/dist/schemas/index.d.ts.map +1 -1
  75. package/dist/schemas/index.js +2 -0
  76. package/dist/schemas/index.js.map +1 -1
  77. package/dist/schemas/policy.d.ts +19 -0
  78. package/dist/schemas/policy.d.ts.map +1 -1
  79. package/dist/schemas/policy.js +36 -0
  80. package/dist/schemas/policy.js.map +1 -1
  81. package/dist/schemas/quarantine.d.ts +25 -0
  82. package/dist/schemas/quarantine.d.ts.map +1 -0
  83. package/dist/schemas/quarantine.js +34 -0
  84. package/dist/schemas/quarantine.js.map +1 -0
  85. package/dist/schemas/queue-observer.d.ts +195 -0
  86. package/dist/schemas/queue-observer.d.ts.map +1 -0
  87. package/dist/schemas/queue-observer.js +190 -0
  88. package/dist/schemas/queue-observer.js.map +1 -0
  89. package/dist/schemas/run-record.d.ts +57 -0
  90. package/dist/schemas/run-record.d.ts.map +1 -0
  91. package/dist/schemas/run-record.js +108 -0
  92. package/dist/schemas/run-record.js.map +1 -0
  93. package/dist/schemas/runtime-config.d.ts +67 -0
  94. package/dist/schemas/runtime-config.d.ts.map +1 -1
  95. package/dist/schemas/runtime-config.js +75 -1
  96. package/dist/schemas/runtime-config.js.map +1 -1
  97. package/dist/schemas/signature-profile.d.ts +99 -0
  98. package/dist/schemas/signature-profile.d.ts.map +1 -0
  99. package/dist/schemas/signature-profile.js +197 -0
  100. package/dist/schemas/signature-profile.js.map +1 -0
  101. package/dist/schemas/test-map.d.ts +4 -0
  102. package/dist/schemas/test-map.d.ts.map +1 -1
  103. package/dist/schemas/test-map.js +12 -0
  104. package/dist/schemas/test-map.js.map +1 -1
  105. package/dist/schemas/verdict.d.ts +19 -4
  106. package/dist/schemas/verdict.d.ts.map +1 -1
  107. package/dist/schemas/verdict.js +38 -4
  108. package/dist/schemas/verdict.js.map +1 -1
  109. package/dist/strictness.d.ts +102 -0
  110. package/dist/strictness.d.ts.map +1 -0
  111. package/dist/strictness.js +92 -0
  112. package/dist/strictness.js.map +1 -0
  113. package/dist/supervision/index.js +6 -6
  114. package/dist/supervision/index.js.map +1 -1
  115. package/dist/twin-paths.d.ts +125 -0
  116. package/dist/twin-paths.d.ts.map +1 -0
  117. package/dist/twin-paths.js +233 -0
  118. package/dist/twin-paths.js.map +1 -0
  119. package/dist/verdict/behavior.d.ts.map +1 -1
  120. package/dist/verdict/behavior.js +284 -10
  121. package/dist/verdict/behavior.js.map +1 -1
  122. package/dist/verdict/cause.d.ts.map +1 -1
  123. package/dist/verdict/cause.js +18 -0
  124. package/dist/verdict/cause.js.map +1 -1
  125. package/dist/verdict/evaluate.d.ts +54 -0
  126. package/dist/verdict/evaluate.d.ts.map +1 -1
  127. package/dist/verdict/evaluate.js +278 -12
  128. package/dist/verdict/evaluate.js.map +1 -1
  129. package/dist/verdict/index.d.ts +3 -3
  130. package/dist/verdict/index.d.ts.map +1 -1
  131. package/dist/verdict/index.js +3 -3
  132. package/dist/verdict/index.js.map +1 -1
  133. package/dist/verdict/pack-verifiers.d.ts +17 -3
  134. package/dist/verdict/pack-verifiers.d.ts.map +1 -1
  135. package/dist/verdict/pack-verifiers.js +47 -8
  136. package/dist/verdict/pack-verifiers.js.map +1 -1
  137. package/dist/verdict/registry.d.ts +19 -0
  138. package/dist/verdict/registry.d.ts.map +1 -1
  139. package/dist/verdict/registry.js +21 -0
  140. package/dist/verdict/registry.js.map +1 -1
  141. package/package.json +1 -1
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Twin path coverage (E64): the pure half of "did the raw test and its
3
+ * witnessed twin actually exercise the same requests?"
4
+ *
5
+ * The reported bug is not a red test. It is a GREEN one: a raw test and
6
+ * its witnessed twin shared a helper whose parameter defaults sent them
7
+ * down different paths — the list call carried `?tab=all` in one and
8
+ * `?tab=open` in the other — so "green three times" proved nothing
9
+ * about the path the witnessed twin covered, and nobody could say so
10
+ * from the run.
11
+ *
12
+ * This module turns two sets of observed requests into the honest
13
+ * question: which request shapes did one twin exercise that the other
14
+ * never did? The observation itself lives engine-side (the proxy); the
15
+ * comparison, the shape vocabulary and the finding text live here, in
16
+ * core, because they are domain logic and must be testable without a
17
+ * browser.
18
+ *
19
+ * What a shape may contain is deliberately narrow. A shape is a method,
20
+ * a route TEMPLATE (never a concrete id) and the values of an
21
+ * owner-declared query-key allowlist. No body, no header, no cookie, no
22
+ * non-allowlisted value: a shape list is something an owner pastes into
23
+ * a bug, so it must be impossible for it to carry a secret.
24
+ */
25
+ import { compareStrings } from './graph/util.js';
26
+ /** Segments that identify nothing: an id is an id wherever it appears. */
27
+ const IDENTIFIER_SEGMENT = /^(\d+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
28
+ /** Splits a request target into its path and its decoded query pairs. */
29
+ function splitTarget(url) {
30
+ const cut = url.search(/[?#]/);
31
+ const path = cut === -1 ? url : url.slice(0, cut);
32
+ const search = cut === -1 ? '' : url.slice(cut + 1).split('#')[0] ?? '';
33
+ const query = search === ''
34
+ ? []
35
+ : search.split('&').map((pair) => {
36
+ const equals = pair.indexOf('=');
37
+ return equals === -1
38
+ ? [pair, '']
39
+ : [pair.slice(0, equals), decodeURIComponent(pair.slice(equals + 1).replace(/\+/g, ' '))];
40
+ });
41
+ return { path: path === '' ? '/' : path, query };
42
+ }
43
+ /**
44
+ * Rewrites a concrete path to a template: every numeric or UUID segment
45
+ * becomes `{}`. Used when the run has no compiled route inventory to
46
+ * resolve against, so a shape still never carries an identifier.
47
+ */
48
+ function templatizePath(path) {
49
+ return path
50
+ .split('/')
51
+ .map((segment) => (IDENTIFIER_SEGMENT.test(segment) ? '{}' : segment))
52
+ .join('/');
53
+ }
54
+ /**
55
+ * Resolves a concrete path against the compiled route inventory: the
56
+ * first template whose segment count and static segments match wins. A
57
+ * run with no inventory (or no match) falls back to the identifier-
58
+ * blind template, never to the raw path.
59
+ */
60
+ function routeOf(path, inventory) {
61
+ const segments = path.split('/');
62
+ for (const template of inventory?.templates ?? []) {
63
+ const parts = template.split('/');
64
+ if (parts.length !== segments.length)
65
+ continue;
66
+ const matches = parts.every((part, index) => part === '{}' || part === segments[index]);
67
+ if (matches)
68
+ return template;
69
+ }
70
+ return templatizePath(path);
71
+ }
72
+ /**
73
+ * Turns one observed request into its comparable shape.
74
+ *
75
+ * Args:
76
+ * request: the method and target the proxy observed.
77
+ * options: the run's route inventory and owner-declared query keys.
78
+ *
79
+ * Returns:
80
+ * TwinShape: method + route template + allowlisted query values.
81
+ */
82
+ export function twinShapeOf(request, options = {}) {
83
+ const { path, query } = splitTarget(request.url);
84
+ const allowlist = new Set(options.queryKeys ?? []);
85
+ const values = {};
86
+ for (const [key, value] of [...query].sort((left, right) => compareStrings(left[0], right[0]))) {
87
+ // The KEY is always recorded; the VALUE only when the owner named
88
+ // this key. A non-allowlisted value cannot reach a shape, and a
89
+ // shape is what every report and state document carries.
90
+ if (allowlist.has(key))
91
+ values[key] = value;
92
+ }
93
+ return {
94
+ method: request.method.toUpperCase(),
95
+ route: routeOf(path, options.inventory),
96
+ ...(Object.keys(values).length === 0 ? {} : { query: values }),
97
+ };
98
+ }
99
+ /** The stable comparable identity of a shape. */
100
+ function shapeKey(shape) {
101
+ const query = Object.keys(shape.query ?? {})
102
+ .sort(compareStrings)
103
+ .map((key) => `${key}=${shape.query[key]}`)
104
+ .join('&');
105
+ return `${shape.method} ${shape.route}${query === '' ? '' : `?${query}`}`;
106
+ }
107
+ /** Renders one shape for a human, naming exactly what it carries. */
108
+ function describeShape(shape) {
109
+ const query = Object.keys(shape.query ?? {});
110
+ if (query.length === 0)
111
+ return `${shape.method} ${shape.route}`;
112
+ return `${shape.method} ${shape.route}?${query
113
+ .sort(compareStrings)
114
+ .map((key) => `${key}=${shape.query[key]}`)
115
+ .join('&')}`;
116
+ }
117
+ /**
118
+ * Compares two twins' shapes and returns both directions of the
119
+ * divergence: what the witnessed twin did that the raw twin never did,
120
+ * and what the raw twin did that the witnessed twin never did. A shape
121
+ * both exercised is not reported — twins that agree are the healthy
122
+ * case and must stay silent.
123
+ *
124
+ * Args:
125
+ * witnessed: the witnessed twin's shapes.
126
+ * raw: the raw twin's shapes.
127
+ *
128
+ * Returns:
129
+ * TwinDivergence[]: every one-sided shape, witnessed-side first,
130
+ * deterministic in shape identity.
131
+ */
132
+ export function twinPathDivergence(witnessed, raw) {
133
+ const rawKeys = new Set(raw.shapes.map(shapeKey));
134
+ const witnessedKeys = new Set(witnessed.shapes.map(shapeKey));
135
+ const oneSided = (shapes, other, presentIn, missingFrom) => shapes
136
+ .filter((shape) => !other.has(shapeKey(shape)))
137
+ .map((shape) => ({ presentIn, missingFrom, shape }));
138
+ return [
139
+ ...oneSided(witnessed.shapes, rawKeys, witnessed.logicalKey, raw.logicalKey),
140
+ ...oneSided(raw.shapes, witnessedKeys, raw.logicalKey, witnessed.logicalKey),
141
+ ];
142
+ }
143
+ /** The finding text a divergence produces, naming BOTH tests and the shape. */
144
+ export function twinDivergenceDetail(divergence) {
145
+ return (`${describeShape(divergence.shape)} is exercised by '${divergence.presentIn}' and never by ` +
146
+ `'${divergence.missingFrom}' — the twins do not cover the same request path`);
147
+ }
148
+ /**
149
+ * The tag a title carries to declare itself the WITNESSED twin. It is
150
+ * part of the title convention, not a gate: the link is a naming
151
+ * convention an owner follows, and the engine only reads it when the
152
+ * owner switched twin coverage on.
153
+ */
154
+ export const WITNESSED_TITLE_TAG = '[witnessed]';
155
+ /**
156
+ * Resolves the raw/witnessed pairs a run should compare.
157
+ *
158
+ * Two rules, in that order, and never a guess:
159
+ * - `twinOf`: the explicit test-map declaration — the only link that
160
+ * survives a rename of either test, because it names logical keys.
161
+ * - the title convention: a witnessed title that carries
162
+ * {@link WITNESSED_TITLE_TAG} links to the untagged title with the
163
+ * same stem: `X raw`, else the one `X raw: <description>`, else a
164
+ * bare `X`.
165
+ *
166
+ * A link is only returned when BOTH tests exist in the catalog: a
167
+ * dangling `twinOf` or a title with no partner names nothing, because
168
+ * a pair with one missing side has no shapes to compare and a finding
169
+ * about it would be a ghost.
170
+ *
171
+ * Args:
172
+ * candidates: every test the catalog knows (logical key + title).
173
+ * twinOf: explicit `witnessed -> raw` logical-key declarations.
174
+ *
175
+ * Returns:
176
+ * TwinLink[]: every resolvable pair, deterministic in witnessed key.
177
+ */
178
+ export function twinLinksFor(candidates, twinOf = {}) {
179
+ const known = new Set(candidates.map((candidate) => candidate.logicalKey));
180
+ const links = new Map();
181
+ for (const witnessed of [...candidates].sort((left, right) => compareStrings(left.logicalKey, right.logicalKey))) {
182
+ const declared = twinOf[witnessed.logicalKey];
183
+ if (declared !== undefined && known.has(declared) && declared !== witnessed.logicalKey) {
184
+ links.set(witnessed.logicalKey, { witnessed: witnessed.logicalKey, raw: declared, source: 'test-map' });
185
+ continue;
186
+ }
187
+ const tagIndex = witnessed.title.lastIndexOf(WITNESSED_TITLE_TAG);
188
+ if (tagIndex === -1)
189
+ continue;
190
+ const stem = witnessed.title.slice(0, tagIndex).trim();
191
+ if (stem === '')
192
+ continue;
193
+ // `X [witnessed]` pairs with `X raw` first, then with the ONE
194
+ // `X raw: <description>`, then with a bare `X`: the explicit raw twin
195
+ // is the owner's clearer statement, and a same-title bare match is
196
+ // only taken when nothing else claims it.
197
+ const untagged = candidates.filter((candidate) => candidate.logicalKey !== witnessed.logicalKey && !candidate.title.includes(WITNESSED_TITLE_TAG));
198
+ const described = untagged.filter((candidate) => candidate.title.startsWith(`${stem} raw:`));
199
+ // A described partner is only unambiguous when the label names ONE
200
+ // test on each side: two witnessed tests under one label (or two
201
+ // described raw ones) leave the pairing to the owner (`twinOf`) —
202
+ // never a guess, and never a fallback to a bare title.
203
+ const witnessedUnderLabel = candidates.filter((candidate) => {
204
+ const index = candidate.title.lastIndexOf(WITNESSED_TITLE_TAG);
205
+ return index !== -1 && candidate.title.slice(0, index).trim() === stem;
206
+ }).length;
207
+ const describedPartner = described.length === 1 && witnessedUnderLabel === 1 ? described[0] : undefined;
208
+ const partner = untagged.find((candidate) => candidate.title === `${stem} raw`) ?? describedPartner;
209
+ if (partner === undefined && described.length > 0)
210
+ continue;
211
+ const bare = partner ??
212
+ candidates.find((candidate) => candidate.logicalKey !== witnessed.logicalKey &&
213
+ !candidate.title.includes(WITNESSED_TITLE_TAG) &&
214
+ candidate.title === stem);
215
+ if (partner === undefined && bare !== undefined) {
216
+ // A bare `X` may also be the stem of another witnessed title's
217
+ // partner; the first witnessed key (sorted) owns it, so the same
218
+ // raw test is never reported as the twin of two witnessed ones.
219
+ const alreadyLinked = [...links.values()].some((link) => link.raw === bare.logicalKey);
220
+ if (alreadyLinked)
221
+ continue;
222
+ }
223
+ if (bare !== undefined) {
224
+ links.set(witnessed.logicalKey, {
225
+ witnessed: witnessed.logicalKey,
226
+ raw: bare.logicalKey,
227
+ source: 'title',
228
+ });
229
+ }
230
+ }
231
+ return [...links.values()].sort((left, right) => compareStrings(left.witnessed, right.witnessed));
232
+ }
233
+ //# sourceMappingURL=twin-paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-paths.js","sourceRoot":"","sources":["../src/twin-paths.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/verdict/behavior.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,eAAe,EAAwB,MAAM,gCAAgC,CAAC;AAO5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAGpD,CAAC;AAEH,2EAA2E;AAC3E,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAM7C,CAAC;AAEH,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAMnD,CAAC;AAEH,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAMjD,CAAC;AAEH,0EAA0E;AAC1E,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAM7C,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAMhD,CAAC;AAEH,2DAA2D;AAC3D,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAOtD,CAAC;AAEH,qFAAqF;AACrF,MAAM,WAAW,yBAAyB;IACxC,oEAAoE;IACpE,OAAO,EAAE,eAAe,CAAC;IACzB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED,yEAAyE;AACzE,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,OAAO,EAAE,eAAe,CAAC;IACzB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,yEAAyE;IACzE,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,yCAAyC;AACzC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1D;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAE9D;AAopBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACvC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,UAAU,EAAE,SAAS,kBAAkB,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;CAC9D,GAAG,mBAAmB,CA8BtB"}
1
+ {"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/verdict/behavior.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,eAAe,EAAwB,MAAM,gCAAgC,CAAC;AAO5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAGpD,CAAC;AAEH,2EAA2E;AAC3E,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAM7C,CAAC;AAEH,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAMnD,CAAC;AAEH,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAMjD,CAAC;AAEH,0EAA0E;AAC1E,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAM7C,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAMhD,CAAC;AAEH,2DAA2D;AAC3D,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAOtD,CAAC;AAEH,qFAAqF;AACrF,MAAM,WAAW,yBAAyB;IACxC,oEAAoE;IACpE,OAAO,EAAE,eAAe,CAAC;IACzB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED,yEAAyE;AACzE,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,OAAO,EAAE,eAAe,CAAC;IACzB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,yEAAyE;IACzE,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,yCAAyC;AACzC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1D;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAE9D;AA2pBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACvC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,UAAU,EAAE,SAAS,kBAAkB,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;CAC9D,GAAG,mBAAmB,CA8BtB"}
@@ -21,9 +21,14 @@
21
21
  * - declarative bulk/import exact-set comparison (missing, extra, or
22
22
  * duplicate entities fail).
23
23
  *
24
- * Phase 5 grades `request` actions over the `engine-http` channel.
25
- * `surface` (Phase 6), `deliver`, and `sequence` (Phase 8) actions, and
26
- * non-HTTP channels, block with an explicit phase cause — never a pass.
24
+ * `request` cases grade over the `engine-http` channel, `surface` cases
25
+ * over `engine-browser` (the Phase 6 browser driver), and `deliver`
26
+ * cases over `engine-task`: the engine
27
+ * produces the delivery, reads the queue back until every job settled,
28
+ * and seals that read — `attempts` rules grade from it, and a delivery
29
+ * case without one settles nothing about the background job.
30
+ * `sequence` actions and non-HTTP channels still block with an
31
+ * explicit phase cause — never a pass.
27
32
  */
28
33
  import { canonicalJson, sha256Canonical } from '../canonical-json.js';
29
34
  import { compareStrings } from '../graph/util.js';
@@ -301,13 +306,18 @@ function scopeDelta(before, after) {
301
306
  }
302
307
  return { added, removed, changed };
303
308
  }
309
+ /** Whether a declared state rule is an `attempts` rule (graded only over a queue observation). */
310
+ function isAttemptsRule(rule) {
311
+ return typeof rule === 'object' && rule !== null && 'kind' in rule && rule.kind === 'attempts';
312
+ }
304
313
  /** Grades one state rule; records explained identities for atomicity. */
305
314
  function gradeStateRule(obligationId, rule, before, after, valueCtx, explained, scopeKeyOf) {
306
315
  const kind = rule['kind'];
307
316
  if (kind === 'attempts') {
308
317
  return {
309
318
  ok: false,
310
- reason: `'${obligationId}': 'attempts' rules require the Phase 8 delivery trace — blocked, never satisfied`,
319
+ reason: `'${obligationId}': 'attempts' rules are graded over the engine's own queue ` +
320
+ "observation, not a scope snapshot — declare the case on the 'engine-task' channel; blocked, never satisfied",
311
321
  };
312
322
  }
313
323
  if (typeof rule['scope'] !== 'string') {
@@ -772,6 +782,9 @@ function gradeRequiredCase(obligation, caseId, records, context, httpRoutes) {
772
782
  recordIds: [],
773
783
  };
774
784
  }
785
+ if (action['kind'] === 'deliver') {
786
+ return gradeTaskCase(obligation, compiled, caseId, records, context);
787
+ }
775
788
  if (action['kind'] !== 'request' && action['kind'] !== 'surface') {
776
789
  return {
777
790
  status: 'missing',
@@ -924,19 +937,17 @@ function gradeRequiredCase(obligation, caseId, records, context, httpRoutes) {
924
937
  // forbidden change in one case result.
925
938
  const explained = new Map();
926
939
  const scopeKeyOf = (effectId) => compiled.effects.find((effect) => effect.id === effectId)?.scope ?? null;
927
- let sawAttemptsRule = false;
928
940
  for (const rule of compiled.definition.expect.state) {
929
- if (rule.kind === 'attempts')
930
- sawAttemptsRule = true;
931
941
  const graded = gradeStateRule(obligationId, rule, beforeIndex, afterIndex, valueCtx, explained, scopeKeyOf);
932
942
  if (!graded.ok) {
933
- if (graded.reason.includes('Phase 8')) {
943
+ // No queue observation exists on this channel: the evidence is
944
+ // absent (fail closed), not contradicted.
945
+ if (isAttemptsRule(rule) || graded.reason.includes('Phase 8')) {
934
946
  return { status: 'missing', reason: graded.reason, recordIds: [recordId] };
935
947
  }
936
948
  return { status: 'invalid', reason: `${graded.reason} (BEHAVIOR_EFFECT_MISMATCH)`, recordIds: [recordId] };
937
949
  }
938
950
  }
939
- void sawAttemptsRule;
940
951
  const declaredScopes = new Set(compiled.effects.map((effect) => effect.scope));
941
952
  // Read contracts additionally require zero delta on every scope.
942
953
  if (obligation.contract === 'http:read-result-verified') {
@@ -1201,7 +1212,7 @@ function gradeSurfaceCase(obligation, compiled, caseId, records, context) {
1201
1212
  for (const rule of compiled.definition.expect.state) {
1202
1213
  const graded = gradeStateRule(obligationId, rule, beforeIndex, afterIndex, valueCtx, explained, scopeKeyOf);
1203
1214
  if (!graded.ok) {
1204
- if (graded.reason.includes('Phase 8')) {
1215
+ if (isAttemptsRule(rule) || graded.reason.includes('Phase 8')) {
1205
1216
  return { status: 'missing', reason: graded.reason, recordIds: [recordId] };
1206
1217
  }
1207
1218
  return { status: 'invalid', reason: `${graded.reason} (BEHAVIOR_EFFECT_MISMATCH)`, recordIds: [recordId] };
@@ -1233,6 +1244,269 @@ function gradeSurfaceCase(obligation, compiled, caseId, records, context) {
1233
1244
  }
1234
1245
  return { status: 'satisfied', recordIds: [recordId] };
1235
1246
  }
1247
+ /**
1248
+ * Grades one `attempts` rule over the engine's OWN read of the queue.
1249
+ * Nothing here consults the test: the
1250
+ * sealed observation carries the delivery identities the engine
1251
+ * stamped, the per-job attempt counts the queue reported, and the
1252
+ * transition timeline the engine sampled.
1253
+ *
1254
+ * The claims it settles:
1255
+ * - `terminal: succeeded` — every produced job reached the queue's
1256
+ * completed state (a job still waiting, delayed, active or paused is
1257
+ * not a success, it is an unsettled delivery);
1258
+ * - `terminal: failed` — every job reached the queue's failed state;
1259
+ * - `terminal: rejected` — every job failed on its FIRST attempt (the
1260
+ * error class the policy declares non-retryable);
1261
+ * - `count` — the declared attempt bound: no job may exceed it and the
1262
+ * queue's own declared bound may not exceed it either;
1263
+ * - `minAttempts` (optional) — every job must have USED at least that
1264
+ * many attempts, so "retries up to N" cannot be satisfied by a queue
1265
+ * that never retried anything;
1266
+ * - `recoveredFromStall` (optional) — the timeline must show a lost
1267
+ * worker being reclaimed: a job sampled `active` and later observed
1268
+ * non-terminal with UNCHANGED attempts and no failure reason (an
1269
+ * error retry raises both), and the delivery still settled.
1270
+ *
1271
+ * Args:
1272
+ obligationId: the obligation being graded (diagnostics).
1273
+ rule: the compiled `attempts` rule.
1274
+ * action: the case's compiled `deliver` action.
1275
+ observation: the sealed engine queue observation.
1276
+
1277
+ * Returns:
1278
+ AttemptsGrade: pass, or the verdict polarity plus the reason.
1279
+ */
1280
+ function gradeAttemptsRule(obligationId, rule, action, observation) {
1281
+ const fail = (reason) => ({ ok: false, reason, status: 'invalid' });
1282
+ const declaredResource = rule['resourceId'];
1283
+ if (declaredResource !== action['resourceId']) {
1284
+ return fail(`'${obligationId}': the 'attempts' rule names resource '${String(declaredResource)}' but the case ` +
1285
+ `delivers to '${String(action['resourceId'])}' (BEHAVIOR_BINDING_MISMATCH)`);
1286
+ }
1287
+ if (observation.complete !== true) {
1288
+ return fail(`'${obligationId}': the engine read queue '${observation.queue}' for ${String(observation.waitedMs)} ms and ` +
1289
+ 'the delivery never reached a terminal state — a stuck background job is not a terminal outcome (BEHAVIOR_EFFECT_MISMATCH)');
1290
+ }
1291
+ const declaredDeliveries = action['count'];
1292
+ if (observation.deliveryIds.length !== declaredDeliveries) {
1293
+ return fail(`'${obligationId}': the engine produced ${String(declaredDeliveries)} deliveries but sealed ` +
1294
+ `${String(observation.deliveryIds.length)} (BEHAVIOR_BINDING_MISMATCH)`);
1295
+ }
1296
+ if (observation.jobs.length !== observation.deliveryIds.length) {
1297
+ return fail(`'${obligationId}': the queue holds ${String(observation.jobs.length)} job(s) for ` +
1298
+ `${String(observation.deliveryIds.length)} engine deliveries — a duplicate or an extra delivery is ambiguity, not proof (BEHAVIOR_BINDING_MISMATCH)`);
1299
+ }
1300
+ const produced = new Set(observation.deliveryIds);
1301
+ const terminal = rule['terminal'];
1302
+ const bound = rule['count'];
1303
+ const floor = rule['minAttempts'];
1304
+ for (const job of observation.jobs) {
1305
+ if (job.deliveryId === null || !produced.has(job.deliveryId)) {
1306
+ return fail(`'${obligationId}': queue job '${job.jobId}' carries no engine delivery identity ` +
1307
+ `(read as '${String(job.deliveryId)}') — the engine only grades deliveries it produced itself (BEHAVIOR_BINDING_MISMATCH)`);
1308
+ }
1309
+ if (terminal === 'succeeded' && job.state !== 'completed') {
1310
+ return fail(`'${obligationId}': job '${job.jobId}' ended in state '${job.state}', not 'completed' — the queue itself ` +
1311
+ 'says the delivery did not succeed (BEHAVIOR_EFFECT_MISMATCH)');
1312
+ }
1313
+ if (terminal === 'failed' && job.state !== 'failed') {
1314
+ return fail(`'${obligationId}': job '${job.jobId}' ended in state '${job.state}', not 'failed' (BEHAVIOR_EFFECT_MISMATCH)`);
1315
+ }
1316
+ if (terminal === 'rejected' && (job.state !== 'failed' || job.attemptsMade !== 1)) {
1317
+ return fail(`'${obligationId}': job '${job.jobId}' is declared terminal-on-error but the queue reports state ` +
1318
+ `'${job.state}' after ${String(job.attemptsMade)} attempt(s) — the error class was retried (BEHAVIOR_EFFECT_MISMATCH)`);
1319
+ }
1320
+ if (job.maxAttempts === null) {
1321
+ return fail(`'${obligationId}': job '${job.jobId}' declares no attempt bound in the queue, so a retry bound ` +
1322
+ `of ${String(bound)} cannot be proven (OBSERVATION_SCOPE_INCOMPLETE)`);
1323
+ }
1324
+ if (job.maxAttempts > bound) {
1325
+ return fail(`'${obligationId}': job '${job.jobId}' declares an attempt bound of ${String(job.maxAttempts)}, above the ` +
1326
+ `declared ${String(bound)} (BEHAVIOR_BINDING_MISMATCH)`);
1327
+ }
1328
+ if (job.attemptsMade > bound) {
1329
+ return fail(`'${obligationId}': job '${job.jobId}' ran ${String(job.attemptsMade)} attempts, above the declared bound of ` +
1330
+ `${String(bound)} (BEHAVIOR_EFFECT_MISMATCH)`);
1331
+ }
1332
+ if (typeof floor === 'number' && job.attemptsMade < floor) {
1333
+ return fail(`'${obligationId}': job '${job.jobId}' ran ${String(job.attemptsMade)} attempt(s) — the declared rule ` +
1334
+ `requires at least ${String(floor)}, so the retry path was never exercised (BEHAVIOR_EFFECT_MISMATCH)`);
1335
+ }
1336
+ }
1337
+ if (rule['recoveredFromStall'] === true) {
1338
+ // A lost-worker reclaim, as the engine's own timeline saw it: the
1339
+ // same job was handed out AGAIN with UNCHANGED attempts and no
1340
+ // failure reason recorded between the two hand-outs (an error retry
1341
+ // raises attemptsMade and sets a reason before the re-queue). A
1342
+ // reclaim back onto the wait list is accepted too, for a queue that
1343
+ // parked the job before another worker took it.
1344
+ const reclaims = new Set();
1345
+ const activeAt = new Map();
1346
+ for (const sample of observation.samples) {
1347
+ if (sample.failedReason !== null)
1348
+ continue;
1349
+ if (sample.state === 'active') {
1350
+ const key = `${sample.jobId}|${String(sample.attemptsMade)}`;
1351
+ if (activeAt.has(key))
1352
+ reclaims.add(sample.jobId);
1353
+ else
1354
+ activeAt.set(key, sample.atMs);
1355
+ continue;
1356
+ }
1357
+ if (sample.state !== 'waiting' && sample.state !== 'delayed')
1358
+ continue;
1359
+ const startedAt = activeAt.get(`${sample.jobId}|${String(sample.attemptsMade)}`);
1360
+ if (startedAt !== undefined && sample.atMs > startedAt)
1361
+ reclaims.add(sample.jobId);
1362
+ }
1363
+ const settled = observation.jobs.filter((job) => reclaims.has(job.jobId));
1364
+ if (settled.length === 0) {
1365
+ return fail(`'${obligationId}': no job was observed being reclaimed from a lost worker (handed out again with ` +
1366
+ 'unchanged attempts and no failure reason) — nothing proves the queue recovers a killed worker ' +
1367
+ '(BEHAVIOR_EFFECT_MISMATCH)');
1368
+ }
1369
+ }
1370
+ return { ok: true };
1371
+ }
1372
+ /**
1373
+ * Grades one task-driven required case:
1374
+ * the engine produced the delivery itself, read the queue back until
1375
+ * every job settled, and sealed that read. Proof is the sealed
1376
+ * observation plus the declared state effects — never the suite's
1377
+ * account of what the job did. HTTP attempts and browser observations
1378
+ * are rejected outright: a task claim proved by a request would be a
1379
+ * different (transport) claim.
1380
+ *
1381
+ * @param obligation the obligation being graded.
1382
+ * @param compiled the compiled case from the trusted catalog.
1383
+ * @param caseId the compiled case id.
1384
+ * @param records the run's ledger records.
1385
+ * @param context the trusted grading context.
1386
+ *
1387
+ * @returns the case outcome.
1388
+ */
1389
+ function gradeTaskCase(obligation, compiled, caseId, records, context) {
1390
+ const obligationId = obligation.id;
1391
+ if (compiled.definition.channel !== 'engine-task') {
1392
+ return {
1393
+ status: 'missing',
1394
+ reason: `'${obligationId}': delivery case '${compiled.definition.id}' requires the ` +
1395
+ `'${compiled.definition.channel}' channel — a delivery is only provable engine-side`,
1396
+ recordIds: [],
1397
+ };
1398
+ }
1399
+ const matched = matchSingleCaseRecord(obligation, compiled, caseId, records, context);
1400
+ if (matched.kind === 'outcome')
1401
+ return matched.outcome;
1402
+ const { recordId, payload } = matched;
1403
+ const bound = validateCaseBindings(obligation, compiled, caseId, recordId, payload, context);
1404
+ if (bound.kind === 'outcome')
1405
+ return bound.outcome;
1406
+ if (payload.attempts.length > 0 || payload.browserObservation !== undefined) {
1407
+ return {
1408
+ status: 'invalid',
1409
+ reason: `'${obligationId}': delivery case record '${recordId}' carries transport evidence — a background ` +
1410
+ 'delivery is proved by the queue state the engine read, not by an HTTP attempt (BEHAVIOR_BINDING_MISMATCH)',
1411
+ recordIds: [recordId],
1412
+ };
1413
+ }
1414
+ const observation = payload.queueObservation;
1415
+ if (observation === undefined) {
1416
+ return {
1417
+ status: 'missing',
1418
+ reason: `'${obligationId}': delivery case '${compiled.definition.id}' sealed no queue observation — the ` +
1419
+ "engine's own queue observer produces that evidence",
1420
+ recordIds: [recordId],
1421
+ };
1422
+ }
1423
+ const beforeBuilt = indexSnapshots(payload.before, compiled);
1424
+ const afterBuilt = indexSnapshots(payload.after, compiled);
1425
+ if (!beforeBuilt.ok || !afterBuilt.ok) {
1426
+ return {
1427
+ status: 'invalid',
1428
+ reason: `'${obligationId}': ${(beforeBuilt.ok ? afterBuilt : beforeBuilt).reason}`,
1429
+ recordIds: [recordId],
1430
+ };
1431
+ }
1432
+ const valueCtx = {
1433
+ fixtureValues: payload.fixtureValues,
1434
+ observation: null,
1435
+ before: beforeBuilt.index,
1436
+ };
1437
+ const beforeIndex = beforeBuilt.index;
1438
+ const afterIndex = afterBuilt.index;
1439
+ const action = compiled.definition.action;
1440
+ // The engine produced the delivery: the sealed submitted values and
1441
+ // the queue jobs must agree on the identity the engine stamped.
1442
+ const submitted = payload.submittedValues;
1443
+ if (typeof submitted !== 'object' || submitted === null || Array.isArray(submitted)) {
1444
+ return {
1445
+ status: 'invalid',
1446
+ reason: `'${obligationId}': case record '${recordId}' sealed no delivery identity (BEHAVIOR_BINDING_MISMATCH)`,
1447
+ recordIds: [recordId],
1448
+ };
1449
+ }
1450
+ const stampedKey = submitted['idempotencyKey'];
1451
+ const mismatchedKey = observation.jobs.find((job) => typeof stampedKey === 'string' && job.idempotencyKey !== stampedKey);
1452
+ if (mismatchedKey !== undefined) {
1453
+ return {
1454
+ status: 'invalid',
1455
+ reason: `'${obligationId}': queue job '${mismatchedKey.jobId}' carries idempotency key ` +
1456
+ `'${mismatchedKey.idempotencyKey}', not the engine-stamped '${String(stampedKey)}' (BEHAVIOR_BINDING_MISMATCH)`,
1457
+ recordIds: [recordId],
1458
+ };
1459
+ }
1460
+ const explained = new Map();
1461
+ const scopeKeyOf = (effectId) => compiled.effects.find((effect) => effect.id === effectId)?.scope ?? null;
1462
+ let sawAttemptsRule = false;
1463
+ for (const rule of compiled.definition.expect.state) {
1464
+ if (rule.kind === 'attempts') {
1465
+ sawAttemptsRule = true;
1466
+ const graded = gradeAttemptsRule(obligationId, rule, action, observation);
1467
+ if (!graded.ok) {
1468
+ return { status: graded.status, reason: graded.reason, recordIds: [recordId] };
1469
+ }
1470
+ continue;
1471
+ }
1472
+ const graded = gradeStateRule(obligationId, rule, beforeIndex, afterIndex, valueCtx, explained, scopeKeyOf);
1473
+ if (!graded.ok) {
1474
+ return { status: 'invalid', reason: `${graded.reason} (BEHAVIOR_EFFECT_MISMATCH)`, recordIds: [recordId] };
1475
+ }
1476
+ }
1477
+ if (!sawAttemptsRule) {
1478
+ return {
1479
+ status: 'invalid',
1480
+ reason: `'${obligationId}': delivery case '${compiled.definition.id}' declares no 'attempts' rule — without one ` +
1481
+ 'the queue read settles nothing about the background job (BEHAVIOR_BINDING_MISMATCH)',
1482
+ recordIds: [recordId],
1483
+ };
1484
+ }
1485
+ for (const scope of new Set(compiled.effects.map((effect) => effect.scope))) {
1486
+ const beforeScope = beforeIndex.get(scope);
1487
+ const afterScope = afterIndex.get(scope);
1488
+ if (beforeScope === undefined || afterScope === undefined)
1489
+ continue;
1490
+ const ledger = explained.get(scope);
1491
+ if (ledger?.complete.has(scope) === true)
1492
+ continue;
1493
+ const delta = scopeDelta(beforeScope, afterScope);
1494
+ const unexplained = [
1495
+ ...delta.added.filter((key) => !ledger?.added.has(key)),
1496
+ ...delta.removed.filter((key) => !ledger?.removed.has(key)),
1497
+ ...delta.changed.filter((key) => !ledger?.changed.has(key)),
1498
+ ];
1499
+ if (unexplained.length > 0) {
1500
+ return {
1501
+ status: 'invalid',
1502
+ reason: `'${obligationId}': scope '${scope}' has ${String(unexplained.length)} unexplained change(s) ` +
1503
+ 'beside the declared effect — a correct delivery plus a wrong secondary effect still fails (BEHAVIOR_UNEXPECTED_EFFECT)',
1504
+ recordIds: [recordId],
1505
+ };
1506
+ }
1507
+ }
1508
+ return { status: 'satisfied', recordIds: [recordId] };
1509
+ }
1236
1510
  /**
1237
1511
  * Checks declared route/query/body identity selectors: the observed
1238
1512
  * concrete path must equal the template filled with resolved params,