@clear-capabilities/agentic-security-scanner 0.185.0 → 0.186.1

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 (167) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/agentic-security.js +24 -1
  3. package/dist/1128.index.js +25 -21
  4. package/dist/2584.index.js +1 -1
  5. package/dist/4171.index.js +2 -2
  6. package/dist/5752.index.js +466 -0
  7. package/dist/7405.index.js +1 -1
  8. package/dist/8158.index.js +1713 -0
  9. package/dist/8798.index.js +1 -1
  10. package/dist/9382.index.js +2 -2
  11. package/dist/9579.index.js +1 -1
  12. package/dist/agentic-security.mjs +29 -29
  13. package/dist/agentic-security.mjs.sha256 +1 -1
  14. package/dist/{0b301d45b231f144a1b4.json → ca62ee93079be4c24eee.json} +7 -7
  15. package/package.json +7 -7
  16. package/src/dataflow/CLAUDE.md +6 -3
  17. package/src/dataflow/access-paths.js +12 -2
  18. package/src/dataflow/demand-query.js +279 -40
  19. package/src/dataflow/dep-fingerprint.js +1 -1
  20. package/src/dataflow/exploit-prover.js +6 -2
  21. package/src/dataflow/ir-cache.js +268 -0
  22. package/src/dataflow/privacy-taint.js +7 -0
  23. package/src/engine.js +62 -5
  24. package/src/ir/CLAUDE.md +4 -0
  25. package/src/ir/callgraph.js +36 -8
  26. package/src/ir/index.js +18 -4
  27. package/src/ir/parser-js.js +10 -0
  28. package/src/language/CLAUDE.md +13 -0
  29. package/src/language/nix-option-types.js +170 -0
  30. package/src/language/nix-script-taint.js +46 -3
  31. package/src/language/nix-secrets.js +20 -2
  32. package/src/lineage/CLAUDE.md +2 -1
  33. package/src/lineage/smt-orchestrator.js +47 -11
  34. package/src/lineage/smt-z3-backend.js +294 -0
  35. package/src/posture/CLAUDE.md +5 -1
  36. package/src/posture/business-logic.js +16 -7
  37. package/src/posture/dag-commands.js +442 -0
  38. package/src/posture/finding-defaults.js +19 -0
  39. package/src/posture/git-history.js +91 -1
  40. package/src/sast/CLAUDE.md +2 -0
  41. package/src/sast/_call-args.js +70 -0
  42. package/src/sast/init-scope.js +87 -0
  43. package/src/sast/listen-bind-address.js +49 -0
  44. package/src/sast/redirect-target.js +106 -0
  45. package/src/sast/reflected-input.js +76 -0
  46. package/src/sast/send-file-root.js +27 -0
  47. package/src/sast/sensitive-data-logging.js +38 -1
  48. package/dist/119.index.js +0 -197
  49. package/dist/128.index.js +0 -129
  50. package/dist/1293.index.js +0 -5689
  51. package/dist/1336.index.js +0 -16331
  52. package/dist/1351.index.js +0 -133
  53. package/dist/1387.index.js +0 -271
  54. package/dist/1437.index.js +0 -23
  55. package/dist/1445.index.js +0 -110
  56. package/dist/1505.index.js +0 -151
  57. package/dist/1557.index.js +0 -293
  58. package/dist/1621.index.js +0 -311
  59. package/dist/1751.index.js +0 -160
  60. package/dist/1765.index.js +0 -727
  61. package/dist/1861.index.js +0 -690
  62. package/dist/1906.index.js +0 -433
  63. package/dist/208.index.js +0 -1631
  64. package/dist/2223.index.js +0 -159
  65. package/dist/2238.index.js +0 -295
  66. package/dist/2530.index.js +0 -152
  67. package/dist/2565.index.js +0 -116
  68. package/dist/2574.index.js +0 -127
  69. package/dist/2620.index.js +0 -345
  70. package/dist/2773.index.js +0 -98
  71. package/dist/2775.index.js +0 -32
  72. package/dist/2966.index.js +0 -171
  73. package/dist/2972.index.js +0 -87
  74. package/dist/2991.index.js +0 -131
  75. package/dist/3063.index.js +0 -833
  76. package/dist/3075.index.js +0 -227
  77. package/dist/3121.index.js +0 -297
  78. package/dist/3314.index.js +0 -263
  79. package/dist/3480.index.js +0 -1822
  80. package/dist/3481.index.js +0 -24
  81. package/dist/3506.index.js +0 -268
  82. package/dist/3533.index.js +0 -591
  83. package/dist/3653.index.js +0 -450
  84. package/dist/3740.index.js +0 -722
  85. package/dist/3822.index.js +0 -131
  86. package/dist/3868.index.js +0 -292
  87. package/dist/3918.index.js +0 -192
  88. package/dist/4090.index.js +0 -288
  89. package/dist/4113.index.js +0 -1286
  90. package/dist/4121.index.js +0 -1190
  91. package/dist/4142.index.js +0 -690
  92. package/dist/4168.index.js +0 -509
  93. package/dist/4281.index.js +0 -993
  94. package/dist/4336.index.js +0 -436
  95. package/dist/4387.index.js +0 -532
  96. package/dist/4413.index.js +0 -302
  97. package/dist/4476.index.js +0 -4510
  98. package/dist/4491.index.js +0 -133
  99. package/dist/4538.index.js +0 -185
  100. package/dist/4684.index.js +0 -319
  101. package/dist/4740.index.js +0 -165
  102. package/dist/4780.index.js +0 -209
  103. package/dist/4802.index.js +0 -225
  104. package/dist/4812.index.js +0 -379
  105. package/dist/4835.index.js +0 -126
  106. package/dist/494.index.js +0 -226
  107. package/dist/4991.index.js +0 -549
  108. package/dist/5199.index.js +0 -305
  109. package/dist/5216.index.js +0 -109
  110. package/dist/5439.index.js +0 -301
  111. package/dist/5443.index.js +0 -97
  112. package/dist/5496.index.js +0 -377
  113. package/dist/5738.index.js +0 -240
  114. package/dist/5801.index.js +0 -588
  115. package/dist/5817.index.js +0 -714
  116. package/dist/5858.index.js +0 -223
  117. package/dist/5933.index.js +0 -143
  118. package/dist/601.index.js +0 -23
  119. package/dist/6173.index.js +0 -273
  120. package/dist/6291.index.js +0 -1871
  121. package/dist/6312.index.js +0 -666
  122. package/dist/634.index.js +0 -16182
  123. package/dist/644.index.js +0 -87
  124. package/dist/6508.index.js +0 -283
  125. package/dist/6560.index.js +0 -645
  126. package/dist/6576.index.js +0 -422
  127. package/dist/6579.index.js +0 -271
  128. package/dist/6655.index.js +0 -3221
  129. package/dist/6745.index.js +0 -305
  130. package/dist/6818.index.js +0 -21642
  131. package/dist/6932.index.js +0 -1190
  132. package/dist/6972.index.js +0 -167
  133. package/dist/6995.index.js +0 -163
  134. package/dist/7249.index.js +0 -191
  135. package/dist/7372.index.js +0 -1197
  136. package/dist/7546.index.js +0 -541
  137. package/dist/76.index.js +0 -448
  138. package/dist/7879.index.js +0 -163
  139. package/dist/8025.index.js +0 -130
  140. package/dist/8054.index.js +0 -248
  141. package/dist/808.index.js +0 -1330
  142. package/dist/8097.index.js +0 -201
  143. package/dist/8126.index.js +0 -191
  144. package/dist/8178.index.js +0 -78
  145. package/dist/8382.index.js +0 -93
  146. package/dist/8408.index.js +0 -261
  147. package/dist/8540.index.js +0 -174
  148. package/dist/8635.index.js +0 -802
  149. package/dist/8688.index.js +0 -410
  150. package/dist/8699.index.js +0 -779
  151. package/dist/8891.index.js +0 -457
  152. package/dist/8916.index.js +0 -885
  153. package/dist/9146.index.js +0 -170
  154. package/dist/9168.index.js +0 -841
  155. package/dist/9292.index.js +0 -369
  156. package/dist/9422.index.js +0 -866
  157. package/dist/9440.index.js +0 -453
  158. package/dist/9479.index.js +0 -152
  159. package/dist/9481.index.js +0 -220
  160. package/dist/9539.index.js +0 -100
  161. package/dist/9547.index.js +0 -103
  162. package/dist/9692.index.js +0 -90
  163. package/dist/9800.index.js +0 -462
  164. package/dist/9899.index.js +0 -138
  165. package/dist/9981.index.js +0 -206
  166. package/dist/999.index.js +0 -117
  167. package/dist/9999.index.js +0 -1698
@@ -1,24 +0,0 @@
1
- export const id = 3481;
2
- export const ids = [3481];
3
- export const modules = {
4
-
5
- /***/ 13481:
6
- /***/ ((__webpack_module__, __webpack_exports__, __webpack_require__) => {
7
-
8
- __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
9
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
- /* harmony export */ parseManifests: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Ye),
11
- /* harmony export */ queryOSV: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Vk)
12
- /* harmony export */ });
13
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(28369);
14
- var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_engine_js__WEBPACK_IMPORTED_MODULE_0__]);
15
- _engine_js__WEBPACK_IMPORTED_MODULE_0__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
16
- // SCA submodule view of the engine — dependency vulnerability + reachability.
17
-
18
-
19
- __webpack_async_result__();
20
- } catch(e) { __webpack_async_result__(e); } });
21
-
22
- /***/ })
23
-
24
- };
@@ -1,268 +0,0 @@
1
- export const id = 3506;
2
- export const ids = [3506];
3
- export const modules = {
4
-
5
- /***/ 3506:
6
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
-
8
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
- /* harmony export */ applyScenario: () => (/* binding */ applyScenario)
10
- /* harmony export */ });
11
- /* harmony import */ var _protection_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(59114);
12
- /* harmony import */ var _dataflow_privacy_sink_policy_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(54937);
13
- // scenario-engine.js — M5 deliverable #3a (FR-502): the clone-and-
14
- // override engine. Deep-clones the base graph, applies each declared
15
- // Scenario operation, then re-runs the SAME two pure aggregators
16
- // graph-builder.js's own real pipeline uses (aggregateVerdicts,
17
- // isSinkPermitted) over only the flows an operation actually touched —
18
- // never re-running the taint/path pipeline, per this sub-project's own
19
- // scoping doc.
20
- //
21
- // The exact recomputation this module mirrors, confirmed by direct
22
- // read of graph-builder.js's own real edge/flow minting pass:
23
- // flow.protectionSummary = aggregateVerdicts([
24
- // edge.protection.transit.verdict,
25
- // edge.protection.atRest.verdict,
26
- // edge.protection.handling.verdict,
27
- // ])
28
- // flow.policyVerdict = isSinkPermitted(dataElement.dataClasses, sinkNode.subtype,
29
- // opts.privacySinkPolicy, { environment: opts.environment, destination: sinkNode.destination?.literalValue ?? null })
30
- // ? 'permitted' : 'prohibited' (or 'not_evaluated' if no policy/classes/sinkKind)
31
-
32
-
33
-
34
-
35
- function _deepClone(v) { return JSON.parse(JSON.stringify(v)); }
36
-
37
- function _byId(arr) { return new Map((arr ?? []).map((x) => [x.id, x])); }
38
-
39
- // Aggregates protectionSummary across EVERY edge belonging to `flow`, not
40
- // just one of them — a flow's `edgeIds` can in principle name more than
41
- // one edge (graph-builder.js's own real flows are always single-edge
42
- // today, but the schema and this module both accept a multi-edge array
43
- // with no restriction), and aggregateVerdicts's own documented purpose is
44
- // exactly this cross-branch/cross-edge reduction (PRD §8.4). Picking a
45
- // single arbitrary edge here would silently go stale the moment a
46
- // scenario is built against a genuinely multi-hop flow.
47
- function _recomputeProtectionSummaryForFlow(flow, graph) {
48
- const edges = graph.edges.filter((e) => flow.edgeIds.includes(e.id));
49
- const verdicts = edges.flatMap((e) => [
50
- e.protection.transit.verdict,
51
- e.protection.atRest.verdict,
52
- e.protection.handling.verdict,
53
- ]);
54
- return (0,_protection_js__WEBPACK_IMPORTED_MODULE_0__/* .aggregateVerdicts */ .SX)(verdicts);
55
- }
56
-
57
- function _recomputePolicyVerdict(flow, graph, opts) {
58
- if (opts.privacySinkPolicy == null) return null; // signal: leave flow.policyVerdict untouched
59
- const de = graph.dataElements.find((d) => flow.dataElementIds.includes(d.id));
60
- const sinkNode = graph.nodes.find((n) => n.id === flow.sink);
61
- const classes = de?.dataClasses ?? [];
62
- const sinkKind = sinkNode?.subtype ?? null;
63
- if (!classes.length || !sinkKind) return 'not_evaluated';
64
- const ctx = {
65
- environment: opts.environment ?? process.env.AGENTIC_SECURITY_ENVIRONMENT ?? null,
66
- destination: sinkNode?.destination?.literalValue ?? null,
67
- };
68
- return (0,_dataflow_privacy_sink_policy_js__WEBPACK_IMPORTED_MODULE_1__/* .isSinkPermitted */ .i$)(classes, sinkKind, opts.privacySinkPolicy, ctx) ? 'permitted' : 'prohibited';
69
- }
70
-
71
- // Every flow using this edge (by edgeIds membership) gets protectionSummary
72
- // recomputed; policyVerdict only for flows whose sink node's destination
73
- // or the flow's own dataClasses could plausibly have changed — but since
74
- // applyScenario always calls this after ANY node/edge touch to be safe
75
- // (recomputation is cheap and idempotent), scope is simply "every flow
76
- // touching this edge or this node".
77
- function _touchedFlows(graph, { edgeId, nodeId }) {
78
- return graph.flows.filter((f) =>
79
- (edgeId && f.edgeIds.includes(edgeId)) ||
80
- (nodeId && (f.source === nodeId || f.sink === nodeId)));
81
- }
82
-
83
- function _recomputeTouchedFlows(graph, touch, opts) {
84
- for (const flow of _touchedFlows(graph, touch)) {
85
- flow.protectionSummary = _recomputeProtectionSummaryForFlow(flow, graph);
86
- const newPolicyVerdict = _recomputePolicyVerdict(flow, graph, opts);
87
- if (newPolicyVerdict !== null) flow.policyVerdict = newPolicyVerdict;
88
- }
89
- }
90
-
91
- function _applyRequireTransitProtection(graph, op) {
92
- const edge = _byId(graph.edges).get(op.targetEdgeId);
93
- if (!edge) return { ok: false, reason: `targetEdgeId "${op.targetEdgeId}" not found in graph.edges` };
94
- edge.protection.transit = { verdict: 'protected', evidenceGrade: 'assumed' };
95
- _recomputeTouchedFlows(graph, { edgeId: edge.id }, op._opts);
96
- return { ok: true };
97
- }
98
-
99
- function _applyHandling(graph, op) {
100
- const edge = _byId(graph.edges).get(op.targetEdgeId);
101
- if (!edge) return { ok: false, reason: `targetEdgeId "${op.targetEdgeId}" not found in graph.edges` };
102
- // Mirrors graph-builder.js's own gate (~line 757): 'encrypted' handling
103
- // only earns the atRest 'protected' verdict when the edge's sink node
104
- // is store-kind (database/file/object-storage/cache/client-storage/
105
- // backup/export). For any other sink kind (e.g. 'external-api',
106
- // 'queue') "at rest" isn't even a meaningful concept — falls through to
107
- // the same 'handling' dimension the non-encrypted case already sets.
108
- const sinkNode = graph.nodes.find((n) => n.id === edge.to);
109
- if (op.handling === 'encrypted' && sinkNode?.kind === 'store') {
110
- edge.protection.atRest = { verdict: 'protected', evidenceGrade: 'assumed' };
111
- } else {
112
- edge.protection.handling = { verdict: 'protected', evidenceGrade: 'assumed' };
113
- }
114
- _recomputeTouchedFlows(graph, { edgeId: edge.id }, op._opts);
115
- return { ok: true };
116
- }
117
-
118
- function _applyRemoveEntity(graph, op) {
119
- const node = _byId(graph.nodes).get(op.targetNodeId);
120
- if (!node) return { ok: false, reason: `targetNodeId "${op.targetNodeId}" not found in graph.nodes` };
121
- const removedEdgeIds = new Set(graph.edges.filter((e) => e.from === node.id || e.to === node.id).map((e) => e.id));
122
- graph.edges = graph.edges.filter((e) => !removedEdgeIds.has(e.id));
123
- graph.flows = graph.flows.filter((f) => f.source !== node.id && f.sink !== node.id
124
- && !f.edgeIds.some((id) => removedEdgeIds.has(id)));
125
- graph.nodes = graph.nodes.filter((n) => n.id !== node.id);
126
- // Cascade-prune graph.recipientProfiles[] too — a RecipientProfile whose
127
- // contributingGraphIds referenced ONLY this now-removed node must not
128
- // survive dangling (a downstream `dataflow export --format recipients`
129
- // consumer filters by exactly contributingGraphIds against surviving
130
- // node ids); a profile with OTHER surviving contributing ids just loses
131
- // this one id, never the whole record. Guarded on the array existing —
132
- // the base graph shape used throughout this sub-project's own tests
133
- // doesn't always include it. A profile with NO contributingGraphIds at
134
- // all (null/undefined — real graph-builder.js output always mints a
135
- // non-empty array, so this is defensive against a hand-built/external
136
- // graph only) is left untouched rather than normalized to `[]` and
137
- // dropped, since a missing array is not evidence the profile referenced
138
- // this node.
139
- // NOTE (final-review re-review N2, disclosed not fixed): this cascade
140
- // is real graph mutation, but scenario-diff.js's WATCHED_SCENARIO_FIELDS
141
- // has no `recipientProfile` kind, so diffScenarioGraph's own delta
142
- // report stays silent about a recipient record that disappeared or lost
143
- // a contributing id here — a future increment's fuller fix, not scope
144
- // creep for this pass.
145
- if (graph.recipientProfiles?.length) {
146
- graph.recipientProfiles = graph.recipientProfiles
147
- .map((rp) => (rp.contributingGraphIds == null ? rp
148
- : { ...rp, contributingGraphIds: rp.contributingGraphIds.filter((id) => id !== node.id) }))
149
- .filter((rp) => rp.contributingGraphIds == null || rp.contributingGraphIds.length > 0);
150
- }
151
- return { ok: true };
152
- }
153
-
154
- // Scoped, at the applier level, to exactly `node.destination` — this
155
- // sub-project's own scoping doc maps "replace a provider or move region"
156
- // onto node.destination, the graph-level equivalent of a recipient's
157
- // technical endpoint, and nothing else. scenario.js's own
158
- // SCENARIO_OPERATION_REQUIRED_FIELDS only requires `field` be a non-empty
159
- // string, never restricts its value — a real applied write to any other
160
- // top-level node field (e.g. 'subtype') would land on the node but be
161
- // completely invisible to scenario-diff.js's WATCHED_SCENARIO_FIELDS.node
162
- // (which only watches 'destination'/'storeDetail'), since that list can
163
- // only stay exhaustive if this applier never writes outside it. Rejecting
164
- // any other field here — never widening the watch list — is what keeps
165
- // that list exhaustive by construction. Mirrors the existing "skip a
166
- // target id it can't find" pattern: reported in skippedOperations, never
167
- // thrown.
168
- function _applyReplaceRecipientFact(graph, op) {
169
- const node = _byId(graph.nodes).get(op.targetNodeId);
170
- if (!node) return { ok: false, reason: `targetNodeId "${op.targetNodeId}" not found in graph.nodes` };
171
- if (op.field !== 'destination') {
172
- return { ok: false, reason: `replace_recipient_fact only supports field "destination" (got "${op.field}") — this operation is scoped to a node's recipient/destination fact only` };
173
- }
174
- node[op.field] = _deepClone(op.value);
175
- // The destination just changed — any edge.protection.transit verdict INTO
176
- // this node was derived from the OLD destination (transit-protection.js's
177
- // resolveTransitProtectionForSite is keyed on the sink's own destination)
178
- // and is now stale. Never let a stale verdict survive with a real (non-
179
- // 'assumed') evidence grade — reset to the honest "we don't know anymore"
180
- // state rather than re-derive (re-deriving needs transitEvidenceByFile,
181
- // IR-level data unavailable post-build) or silently keep the old verdict.
182
- // NOTE (final-review re-review N1, disclosed not fixed): this demotion is
183
- // per-EDGE and is always honest at that level (and always visible in
184
- // diffScenarioGraph's own delta, since edge.protection.transit is a
185
- // WATCHED_SCENARIO_FIELDS.edge entry) — but flow.protectionSummary
186
- // aggregates transit/atRest/handling via aggregateVerdicts, whose
187
- // precedence ranks not_assessed LAST, so a scenario that ALSO sets this
188
- // edge's own handling dimension to 'protected' (via apply_handling) in
189
- // the SAME operations list can mask the demotion at the flow-summary
190
- // level, even though the masking verdict itself is honestly graded
191
- // 'assumed'. graph-builder.js's own cross-dimension aggregation is
192
- // documented safe only because transit/atRest are mutually exclusive by
193
- // construction and handling is "never written" by any real analyzer —
194
- // this module (scenario-engine.js) is the one exception to that last
195
- // clause, which is exactly what makes the masking reachable here.
196
- for (const edge of graph.edges) {
197
- if (edge.to === node.id) {
198
- edge.protection.transit = { verdict: 'not_assessed', evidenceGrade: 'none' };
199
- }
200
- }
201
- _recomputeTouchedFlows(graph, { nodeId: node.id }, op._opts);
202
- return { ok: true };
203
- }
204
-
205
- function _applyChangeStorageFact(graph, op) {
206
- const node = _byId(graph.nodes).get(op.targetNodeId);
207
- if (!node) return { ok: false, reason: `targetNodeId "${op.targetNodeId}" not found in graph.nodes` };
208
- node.storeDetail = { ...(node.storeDetail ?? {}), [op.field]: _deepClone(op.value) };
209
- return { ok: true };
210
- }
211
-
212
- function _applyChangeGovernanceFact(graph, op) {
213
- const flow = _byId(graph.flows).get(op.targetFlowId);
214
- if (!flow) return { ok: false, reason: `targetFlowId "${op.targetFlowId}" not found in graph.flows` };
215
- flow.governanceRefs = { ...(flow.governanceRefs ?? {}), [op.field]: _deepClone(op.value) };
216
- return { ok: true };
217
- }
218
-
219
- const _APPLIERS = {
220
- require_transit_protection: _applyRequireTransitProtection,
221
- apply_handling: _applyHandling,
222
- remove_entity: _applyRemoveEntity,
223
- replace_recipient_fact: _applyReplaceRecipientFact,
224
- change_storage_fact: _applyChangeStorageFact,
225
- change_governance_fact: _applyChangeGovernanceFact,
226
- };
227
-
228
- /**
229
- * Apply `scenario.operations` to a deep clone of `baseGraph`. Never
230
- * mutates `baseGraph`. An operation whose target id does not exist in
231
- * the graph is skipped (reported in `skippedOperations`), never thrown —
232
- * a Scenario written against an older snapshot must degrade honestly.
233
- * `opts.privacySinkPolicy`/`opts.environment` mirror graph-builder.js's
234
- * own opts; omitting privacySinkPolicy means policyVerdict
235
- * recomputation is skipped entirely (base value kept on every touched
236
- * flow) rather than guessed.
237
- *
238
- * Scenario-shape validation (`validateScenario`, `scenario.js`) is the
239
- * CALLER's responsibility, not this module's — this function does no
240
- * upfront shape checking beyond each operation's own per-target `_byId`
241
- * lookup. An unrecognized `op.kind` is skipped via the `_APPLIERS` map
242
- * lookup below rather than validated against `SCENARIO_OPERATION_KINDS`
243
- * ahead of time. This mirrors `recipient-profile.js`'s own precedent,
244
- * where `validateRecipientProfile` is never called internally by any
245
- * producer either — the CLI layer (Task 4) calls `validateScenario`
246
- * before `applyScenario`, by design.
247
- */
248
- function applyScenario(baseGraph, scenario, opts = {}) {
249
- const graph = _deepClone(baseGraph);
250
- const appliedOperations = [];
251
- const skippedOperations = [];
252
- for (const op of scenario.operations ?? []) {
253
- const applier = _APPLIERS[op.kind];
254
- if (!applier) {
255
- skippedOperations.push({ operation: op, reason: `unrecognized operation kind "${op.kind}"` });
256
- continue;
257
- }
258
- const result = applier(graph, { ...op, _opts: opts });
259
- if (result.ok) appliedOperations.push({ operation: op });
260
- else skippedOperations.push({ operation: op, reason: result.reason });
261
- }
262
- return { graph, appliedOperations, skippedOperations };
263
- }
264
-
265
-
266
- /***/ })
267
-
268
- };