@intentius/chant-lexicon-cedar 0.44.8 → 0.44.10

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 (149) hide show
  1. package/README.md +106 -2
  2. package/dist/agentcore/embed.d.ts +189 -0
  3. package/dist/agentcore/embed.d.ts.map +1 -0
  4. package/dist/agentcore/enforcement.d.ts +76 -0
  5. package/dist/agentcore/enforcement.d.ts.map +1 -0
  6. package/dist/agentcore/scan.d.ts +46 -0
  7. package/dist/agentcore/scan.d.ts.map +1 -0
  8. package/dist/codegen/docs-dogwood.d.ts +21 -0
  9. package/dist/codegen/docs-dogwood.d.ts.map +1 -0
  10. package/dist/codegen/docs.d.ts.map +1 -1
  11. package/dist/codegen/package.d.ts.map +1 -1
  12. package/dist/config.d.ts +25 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/dogwood/cli.d.ts +247 -0
  15. package/dist/dogwood/cli.d.ts.map +1 -0
  16. package/dist/dogwood/event-schema.d.ts +161 -0
  17. package/dist/dogwood/event-schema.d.ts.map +1 -0
  18. package/dist/dogwood/index.d.ts +39 -0
  19. package/dist/dogwood/index.d.ts.map +1 -0
  20. package/dist/dogwood/macros.d.ts +96 -0
  21. package/dist/dogwood/macros.d.ts.map +1 -0
  22. package/dist/dogwood/policy.d.ts +120 -0
  23. package/dist/dogwood/policy.d.ts.map +1 -0
  24. package/dist/dogwood/replay-activity.d.ts +196 -0
  25. package/dist/dogwood/replay-activity.d.ts.map +1 -0
  26. package/dist/dogwood/replay-op.d.ts +165 -0
  27. package/dist/dogwood/replay-op.d.ts.map +1 -0
  28. package/dist/dogwood/scan.d.ts +109 -0
  29. package/dist/dogwood/scan.d.ts.map +1 -0
  30. package/dist/dogwood/serialize.d.ts +66 -0
  31. package/dist/dogwood/serialize.d.ts.map +1 -0
  32. package/dist/dogwood/temporal.d.ts +259 -0
  33. package/dist/dogwood/temporal.d.ts.map +1 -0
  34. package/dist/dogwood/trace.d.ts +215 -0
  35. package/dist/dogwood/trace.d.ts.map +1 -0
  36. package/dist/dogwood/upstream.d.ts +41 -0
  37. package/dist/dogwood/upstream.d.ts.map +1 -0
  38. package/dist/dogwood/window.d.ts +73 -0
  39. package/dist/dogwood/window.d.ts.map +1 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/integrity.json +15 -3
  43. package/dist/lint/audit-catalog.d.ts.map +1 -1
  44. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  45. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  46. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  47. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  48. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  49. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  50. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  51. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  52. package/dist/lint/post-synth/dwdc013.d.ts +33 -0
  53. package/dist/lint/post-synth/dwdc013.d.ts.map +1 -0
  54. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  55. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  56. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  57. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  58. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  59. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  60. package/dist/lint/post-synth/index.d.ts.map +1 -1
  61. package/dist/manifest.json +1 -1
  62. package/dist/okf/index.md +7 -0
  63. package/dist/okf/rules/DWDC010.md +11 -0
  64. package/dist/okf/rules/DWDC011.md +11 -0
  65. package/dist/okf/rules/DWDC012.md +11 -0
  66. package/dist/okf/rules/DWDC013.md +15 -0
  67. package/dist/okf/rules/DWDE010.md +11 -0
  68. package/dist/okf/rules/DWDE011.md +11 -0
  69. package/dist/okf/rules/DWDS010.md +11 -0
  70. package/dist/okf/types/Policy.md +1 -0
  71. package/dist/op/activities/index.d.ts +18 -0
  72. package/dist/op/activities/index.d.ts.map +1 -0
  73. package/dist/plugin.d.ts.map +1 -1
  74. package/dist/policy-text.d.ts +53 -0
  75. package/dist/policy-text.d.ts.map +1 -0
  76. package/dist/rules/dogwood-helpers.ts +139 -0
  77. package/dist/rules/dwdc010.ts +62 -0
  78. package/dist/rules/dwdc011.ts +61 -0
  79. package/dist/rules/dwdc012.ts +46 -0
  80. package/dist/rules/dwdc013.ts +64 -0
  81. package/dist/rules/dwde010.ts +130 -0
  82. package/dist/rules/dwde011.ts +108 -0
  83. package/dist/rules/dwds010.ts +46 -0
  84. package/dist/serializer.d.ts +10 -18
  85. package/dist/serializer.d.ts.map +1 -1
  86. package/dist/skills/chant-cedar-authoring.md +180 -0
  87. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  88. package/dist/skills/chant-cedar-dogwood.md +327 -0
  89. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  90. package/package.json +7 -2
  91. package/src/agentcore/embed.test.ts +254 -0
  92. package/src/agentcore/embed.ts +399 -0
  93. package/src/agentcore/enforcement.test.ts +43 -0
  94. package/src/agentcore/enforcement.ts +92 -0
  95. package/src/agentcore/scan.ts +119 -0
  96. package/src/codegen/docs-dogwood.ts +1119 -0
  97. package/src/codegen/docs.ts +66 -1
  98. package/src/codegen/package.ts +3 -2
  99. package/src/config.test.ts +12 -0
  100. package/src/config.ts +28 -0
  101. package/src/dogwood/cli.test.ts +513 -0
  102. package/src/dogwood/cli.ts +666 -0
  103. package/src/dogwood/event-schema.test.ts +218 -0
  104. package/src/dogwood/event-schema.ts +318 -0
  105. package/src/dogwood/index.ts +271 -0
  106. package/src/dogwood/macros.test.ts +104 -0
  107. package/src/dogwood/macros.ts +229 -0
  108. package/src/dogwood/policy.test.ts +94 -0
  109. package/src/dogwood/policy.ts +141 -0
  110. package/src/dogwood/replay-activity.test.ts +481 -0
  111. package/src/dogwood/replay-activity.ts +506 -0
  112. package/src/dogwood/replay-op.ts +242 -0
  113. package/src/dogwood/scan.ts +287 -0
  114. package/src/dogwood/serialize.test.ts +331 -0
  115. package/src/dogwood/serialize.ts +246 -0
  116. package/src/dogwood/temporal.test.ts +272 -0
  117. package/src/dogwood/temporal.ts +592 -0
  118. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  119. package/src/dogwood/testdata/default-macros.dw +23 -0
  120. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  121. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  122. package/src/dogwood/testdata/pinned.dwschema +31 -0
  123. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  124. package/src/dogwood/testdata/read-after-login.dw +17 -0
  125. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  126. package/src/dogwood/trace.test.ts +231 -0
  127. package/src/dogwood/trace.ts +471 -0
  128. package/src/dogwood/upstream.ts +41 -0
  129. package/src/dogwood/window.ts +124 -0
  130. package/src/index.ts +76 -0
  131. package/src/lint/audit-catalog.ts +64 -0
  132. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  133. package/src/lint/post-synth/dwd-post-synth.test.ts +374 -0
  134. package/src/lint/post-synth/dwdc010.ts +62 -0
  135. package/src/lint/post-synth/dwdc011.ts +61 -0
  136. package/src/lint/post-synth/dwdc012.ts +46 -0
  137. package/src/lint/post-synth/dwdc013.ts +64 -0
  138. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  139. package/src/lint/post-synth/dwde010.ts +130 -0
  140. package/src/lint/post-synth/dwde011.ts +108 -0
  141. package/src/lint/post-synth/dwds010.ts +46 -0
  142. package/src/lint/post-synth/index.ts +14 -0
  143. package/src/lint/post-synth/post-synth.test.ts +7 -3
  144. package/src/op/activities/index.ts +27 -0
  145. package/src/plugin.test.ts +3 -2
  146. package/src/plugin.ts +30 -0
  147. package/src/policy-text.ts +128 -0
  148. package/src/serializer.ts +71 -109
  149. package/src/skills/chant-cedar-dogwood.md +327 -0
@@ -0,0 +1,31 @@
1
+ // Adapted from dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0):
2
+ // dogwood-language/configuration/event-schemas/pinned.dwschema
3
+ // Upstream's shape; the bytes are what chant's typed builders emit.
4
+
5
+ // The default event-schema shape: request/response/error, each correlated to
6
+ // the deciding request's principal.
7
+
8
+ decision event <A>::request {
9
+ ...inputs(A),
10
+ pin callerPrincipal: principalType(A) = principal,
11
+ callerResource: resourceType(A),
12
+ requestId: String,
13
+ sessionId: String,
14
+ }
15
+
16
+ event <A>::response {
17
+ ...inputs(A),
18
+ ...outputs(A),
19
+ pin callerPrincipal: principalType(A) = principal,
20
+ callerResource: resourceType(A),
21
+ requestId: String,
22
+ sessionId: String,
23
+ }
24
+
25
+ event <A>::error {
26
+ ...inputs(A),
27
+ pin callerPrincipal: principalType(A) = principal,
28
+ callerResource: resourceType(A),
29
+ requestId: String,
30
+ sessionId: String,
31
+ }
@@ -0,0 +1,20 @@
1
+ // Adapted from dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0):
2
+ // dogwood-docs/examples/read_after_login/schema.cedarschema
3
+ // The action schema half — what `dogwood validate --policy-schema` reads.
4
+
5
+ namespace Drupe {
6
+ type LoginInput = { user: String };
7
+ type ReadInput = { user: String };
8
+ entity Gateway;
9
+ entity OAuthUser = { id: String } tags String;
10
+ action "Login" appliesTo {
11
+ principal: [OAuthUser],
12
+ resource: [Gateway],
13
+ context: { input: LoginInput }
14
+ };
15
+ action "Read" appliesTo {
16
+ principal: [OAuthUser],
17
+ resource: [Gateway],
18
+ context: { input: ReadInput }
19
+ };
20
+ }
@@ -0,0 +1,17 @@
1
+ // Adapted from dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0):
2
+ // dogwood-docs/examples/read_after_login/policy.dw
3
+ // The bundle the CLI-gated DWDE checks are exercised against (#1659).
4
+
5
+ // Permit Read only if the same user successfully logged in within the last
6
+ // hour. The `when temporal { … }` clause reads the accumulated event history;
7
+ // the `{ input.user: context.input.user }` pin correlates the past login's
8
+ // user with the current request's user.
9
+ @id("read_after_login")
10
+ permit (
11
+ principal,
12
+ action == Drupe::Action::"Read",
13
+ resource
14
+ )
15
+ when temporal {
16
+ formerly within 1h Drupe::Action::"Login"::response{ input.user: context.input.user }
17
+ };
@@ -0,0 +1,53 @@
1
+ // Adapted from dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0):
2
+ // dogwood-docs/examples/read_after_login/policy.dw
3
+ // dogwood-docs/examples/temporal_sum_formerly_transfer/policy.dw
4
+ // dogwood-docs/examples/read_heartbeat_since_login_30s/policy.dw
5
+ // dogwood-docs/examples/temporal_count_formerly_login/policy.dw
6
+ // Upstream's shape; the bytes are what chant's typed builders emit.
7
+
8
+ // Sums the numeric value `?a` over occurrences of `?body` within window `?w`.
9
+ def temporal sum_formerly(?a, ?w, ?body) {
10
+ sum ?a for (?a: Long), ($t: Timepoint). where formerly within ?w (?body && tp($t))
11
+ };
12
+
13
+ @id("read_after_login")
14
+ permit (
15
+ principal,
16
+ action == Drupe::Action::"Read",
17
+ resource
18
+ )
19
+ when temporal {
20
+ formerly within 1h Drupe::Action::"Login"::response{ input.user: context.input.user }
21
+ };
22
+
23
+ @id("transfer_sum_over_100")
24
+ permit (
25
+ principal,
26
+ action == Drupe::Action::"Alert",
27
+ resource
28
+ )
29
+ when temporal {
30
+ exists (total: Long). (sum_formerly(a, 1h, Drupe::Action::"Transfer"::request{ input.user: _, input.amount: a })) == total && total > 100
31
+ };
32
+
33
+ @id("no_tool_after_sensitive_read")
34
+ forbid (
35
+ principal,
36
+ action == Drupe::Action::"Invoke",
37
+ resource
38
+ )
39
+ when guardrails { context.input.tool != "audit" }
40
+ when temporal {
41
+ Drupe::Action::"Read"::response{ output.classification: c } since within 30m Drupe::Action::"Login"::request{}
42
+ }
43
+ unless { principal in Drupe::Group::"breakglass" };
44
+
45
+ @id("rate_limited")
46
+ permit (
47
+ principal,
48
+ action == Drupe::Action::"Transfer",
49
+ resource
50
+ )
51
+ when temporal {
52
+ (count for (t: Timepoint). where formerly within 15m (Drupe::Action::"Transfer"::request{} && tp(t))) < 5
53
+ };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Trace construction (#1661).
3
+ *
4
+ * The line grammar is #1657 §6, read from `interpreter/log_parse.rs` at the
5
+ * pinned SHA. The tests that matter most are not the rendering ones — they are
6
+ * the two traps, because both produce a *green* replay rather than a failing
7
+ * one: a field in only one bag, and an action name that is not fully
8
+ * qualified.
9
+ */
10
+ import { describe, expect, test } from "vitest";
11
+ import {
12
+ auditTrace,
13
+ decimalValue,
14
+ entityRef,
15
+ rawValue,
16
+ renderTrace,
17
+ renderTraceLine,
18
+ renderTraceValue,
19
+ traceEntity,
20
+ traceEvent,
21
+ traceFixture,
22
+ } from "./trace";
23
+
24
+ const ALICE = 'Drupe::OAuthUser::"alice"';
25
+ const GATEWAY = 'Drupe::Gateway::"gw1"';
26
+
27
+ function login(over: Partial<Parameters<typeof traceEvent>[0]> = {}) {
28
+ return traceEvent({
29
+ timestamp: 0,
30
+ action: 'Drupe::Action::"Login"',
31
+ scope: { principal: ALICE, resource: GATEWAY },
32
+ context: { input: { user: "alice" } },
33
+ record: { callerPrincipal: entityRef(ALICE), requestId: "u1" },
34
+ ...over,
35
+ });
36
+ }
37
+
38
+ // ── The line grammar ───────────────────────────────────────────────
39
+
40
+ describe("renderTraceLine", () => {
41
+ test("renders the envelopes in the one order the parser accepts", () => {
42
+ const line = renderTraceLine(
43
+ traceEvent({
44
+ timestamp: 0,
45
+ action: 'Drupe::Action::"Login"',
46
+ scope: { principal: ALICE, resource: GATEWAY },
47
+ entities: [traceEntity(ALICE, { id: "alice" }, ['Drupe::Gateway::"gw1"'])],
48
+ context: { input: { user: "alice" } },
49
+ record: { requestId: "u1" },
50
+ }),
51
+ );
52
+
53
+ expect(line).toBe(
54
+ `@0 scope(principal: ${ALICE}, resource: ${GATEWAY}) ` +
55
+ `entities(${ALICE}: { id: "alice" } in [${GATEWAY}]) ` +
56
+ `request_context(input: { user: "alice" }) ` +
57
+ `Drupe::Action::"Login"::request(input: { user: "alice" }, requestId: "u1")`,
58
+ );
59
+ });
60
+
61
+ test("every envelope is optional", () => {
62
+ const line = renderTraceLine({
63
+ timestamp: 42,
64
+ action: 'Drupe::Action::"Read"',
65
+ kind: "response",
66
+ record: { requestId: "u9" },
67
+ });
68
+ expect(line).toBe(`@42 Drupe::Action::"Read"::response(requestId: "u9")`);
69
+ });
70
+
71
+ test("a whole trace is one event per line, newline-terminated", () => {
72
+ const text = renderTrace([login(), login({ timestamp: 10, kind: "response" })]);
73
+ expect(text.split("\n")).toHaveLength(3);
74
+ expect(text.endsWith("\n")).toBe(true);
75
+ });
76
+ });
77
+
78
+ describe("values", () => {
79
+ test("Cedar surface forms, by JS type", () => {
80
+ expect(renderTraceValue("a")).toBe('"a"');
81
+ expect(renderTraceValue(7)).toBe("7");
82
+ expect(renderTraceValue(true)).toBe("true");
83
+ expect(renderTraceValue([1, "a"])).toBe('[1, "a"]');
84
+ expect(renderTraceValue({ a: 1 })).toBe("{ a: 1 }");
85
+ });
86
+
87
+ test("an entity reference renders unquoted", () => {
88
+ expect(renderTraceValue(entityRef(ALICE))).toBe(ALICE);
89
+ });
90
+
91
+ test("a decimal keeps its scale, which a JS number cannot", () => {
92
+ expect(renderTraceValue(decimalValue("1.50"))).toBe("1.50");
93
+ // The failure mode this exists to prevent: 1.50 as a number is 1.5.
94
+ expect(() => renderTraceValue(1.5)).toThrow(/decimalValue/);
95
+ });
96
+
97
+ test("raw text passes through, for shapes this module has no type for", () => {
98
+ expect(renderTraceValue(rawValue("ip(\"10.0.0.1\")"))).toBe('ip("10.0.0.1")');
99
+ });
100
+
101
+ test("a value containing // survives, because the format has no comments", () => {
102
+ expect(renderTraceValue("https://example.test/a")).toBe('"https://example.test/a"');
103
+ });
104
+
105
+ test("quotes and backslashes are escaped, not dropped", () => {
106
+ expect(renderTraceValue('a "b" \\ c')).toBe('"a \\"b\\" \\\\ c"');
107
+ });
108
+ });
109
+
110
+ // ── Trap 1: the action must be fully qualified ─────────────────────
111
+
112
+ describe("fully-qualified actions", () => {
113
+ test("a short action name is rejected at construction", () => {
114
+ // The trap: a bare name leaves every temporal predicate unmatched while
115
+ // Cedar still authorizes, so the replay is green and tested nothing.
116
+ expect(() => traceEvent({ timestamp: 0, action: "Transfer", record: {} })).toThrow(
117
+ /fully qualified/,
118
+ );
119
+ expect(() => traceEvent({ timestamp: 0, action: '"Transfer"', record: {} })).toThrow(
120
+ /fully qualified/,
121
+ );
122
+ });
123
+
124
+ test("so is a short entity reference, in scope or in value position", () => {
125
+ expect(() => entityRef("alice")).toThrow(/fully qualified/);
126
+ expect(() =>
127
+ traceEvent({
128
+ timestamp: 0,
129
+ action: 'Drupe::Action::"Read"',
130
+ scope: { principal: "alice", resource: GATEWAY },
131
+ record: {},
132
+ }),
133
+ ).toThrow(/fully qualified/);
134
+ });
135
+ });
136
+
137
+ // ── Trap 2: both bags ──────────────────────────────────────────────
138
+
139
+ describe("both-bag population", () => {
140
+ test("a context group is written to the envelope AND the logged record", () => {
141
+ const event = login();
142
+ expect(event.requestContext).toEqual({ input: { user: "alice" } });
143
+ expect(event.record).toMatchObject({ input: { user: "alice" } });
144
+ });
145
+
146
+ test("record-only fields stay out of the Cedar request", () => {
147
+ const event = login();
148
+ expect(event.requestContext).not.toHaveProperty("requestId");
149
+ expect(event.record).toHaveProperty("requestId");
150
+ });
151
+
152
+ test("the weaker forms exist, but have to be typed out", () => {
153
+ const recordOnly = login({ bags: "record-only" });
154
+ expect(recordOnly.requestContext).toBeUndefined();
155
+ expect(recordOnly.record).toHaveProperty("input");
156
+
157
+ const contextOnly = login({ bags: "context-only" });
158
+ expect(contextOnly.requestContext).toHaveProperty("input");
159
+ expect(contextOnly.record).not.toHaveProperty("input");
160
+ });
161
+ });
162
+
163
+ describe("auditTrace", () => {
164
+ test("a trace built the default way has nothing to report", () => {
165
+ expect(auditTrace([login(), login({ timestamp: 10 })])).toEqual([]);
166
+ });
167
+
168
+ test("a record-only decision event is a single-bag finding", () => {
169
+ const issues = auditTrace([login({ bags: "record-only" })]);
170
+ expect(issues.map((i) => i.kind)).toContain("no-request-context");
171
+ });
172
+
173
+ test("a context-only decision event is a single-bag finding", () => {
174
+ const issues = auditTrace([login({ bags: "context-only" })]);
175
+ expect(issues).toHaveLength(1);
176
+ expect(issues[0].kind).toBe("single-bag");
177
+ expect(issues[0].message).toMatch(/temporal predicates over input\.\* silently miss/);
178
+ });
179
+
180
+ test("a history-only event needs no request context — it never becomes one", () => {
181
+ // `response` does not decide under the default event schema, so an absent
182
+ // envelope there is not a weakening. Only the deciding kinds are held to it.
183
+ const historyOnly = traceEvent({
184
+ timestamp: 0,
185
+ action: 'Drupe::Action::"Login"',
186
+ kind: "response",
187
+ bags: "record-only",
188
+ context: { input: { user: "alice" } },
189
+ record: { requestId: "u1" },
190
+ });
191
+ expect(auditTrace([historyOnly])).toEqual([]);
192
+ // …but say `response` decides, and it is held to the same bar.
193
+ expect(auditTrace([historyOnly], { decisionKinds: ["response"] })).not.toEqual([]);
194
+ });
195
+
196
+ test("an out-of-order timestamp is reported — history accumulates in file order", () => {
197
+ const issues = auditTrace([login({ timestamp: 10 }), login({ timestamp: 0 })]);
198
+ expect(issues.map((i) => i.kind)).toEqual(["out-of-order"]);
199
+ });
200
+
201
+ test("an event that logs nothing can match no predicate", () => {
202
+ const empty = traceEvent({
203
+ timestamp: 0,
204
+ action: 'Drupe::Action::"Read"',
205
+ context: { input: { user: "alice" } },
206
+ bags: "context-only",
207
+ });
208
+ expect(auditTrace([empty]).map((i) => i.kind).sort()).toEqual(["empty-record", "single-bag"]);
209
+ });
210
+ });
211
+
212
+ describe("traceFixture", () => {
213
+ test("hands back the text when the trace is honest", () => {
214
+ const fixture = traceFixture([login(), login({ timestamp: 10 })]);
215
+ expect(fixture.issues).toEqual([]);
216
+ expect(fixture.text.split("\n").filter(Boolean)).toHaveLength(2);
217
+ });
218
+
219
+ test("refuses a fixture that would weaken its own replay", () => {
220
+ expect(() => traceFixture([login({ bags: "context-only" })])).toThrow(/weaken its own replay/);
221
+ });
222
+
223
+ test("…unless the weakening is named, which is the point of naming it", () => {
224
+ const fixture = traceFixture([login({ bags: "context-only" })], { allow: ["single-bag"] });
225
+ expect(fixture.issues.map((i) => i.kind)).toEqual(["single-bag"]);
226
+ });
227
+
228
+ test("the error names the allow-list entry that would silence it", () => {
229
+ expect(() => traceFixture([login({ bags: "context-only" })])).toThrow(/allow: \["single-bag"\]/);
230
+ });
231
+ });