backend-skeleton 1.4.0 → 1.6.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 (42) hide show
  1. package/README.md +113 -8
  2. package/bin/bskel.mjs +331 -53
  3. package/contracts/emit.mjs +22 -6
  4. package/contracts/openapi.mjs +125 -18
  5. package/handles/providers/java-spring/ast-bridge.mjs +85 -1
  6. package/handles/providers/java-spring/ast-helper/src/main/java/com/backendskeleton/asthelper/Main.java +407 -0
  7. package/handles/providers/java-spring/emit.mjs +126 -6
  8. package/handles/providers/java-spring/plan.mjs +244 -77
  9. package/handles/providers/java-spring/source-splice.mjs +477 -0
  10. package/handles/providers/java-spring/templates/AuthorizationPolicyStub.java.tmpl +30 -0
  11. package/handles/providers/java-spring/templates/HandleController.java.tmpl +21 -3
  12. package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +26 -0
  13. package/handles/providers/java-spring/templates/ResourceResolverPolicyStub.java.tmpl +9 -0
  14. package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +3 -3
  15. package/handles/providers/typescript-express/plan.mjs +15 -2
  16. package/lib/attest.mjs +59 -1
  17. package/lib/cli.mjs +79 -7
  18. package/lib/doctor.mjs +23 -0
  19. package/lib/exit-codes.mjs +17 -0
  20. package/lib/gate-definitions.mjs +65 -2
  21. package/lib/gate-export.mjs +199 -0
  22. package/lib/impact-export-graphify.mjs +145 -0
  23. package/lib/impact-graph.mjs +194 -0
  24. package/lib/impact-surface.mjs +158 -0
  25. package/lib/impact.mjs +286 -0
  26. package/lib/patch-kinds.mjs +24 -0
  27. package/lib/repo.mjs +46 -0
  28. package/lib/workflow.mjs +16 -0
  29. package/package.json +1 -1
  30. package/scanners/adapters/_java-spring-analyzer.mjs +55 -0
  31. package/scanners/adapters/java-spring.mjs +154 -3
  32. package/scanners/index.mjs +10 -0
  33. package/schemas/feature-contract.schema.json +12 -1
  34. package/schemas/gate-attestation.schema.json +6 -1
  35. package/schemas/gate-export.schema.json +530 -22
  36. package/schemas/handles-plan.schema.json +32 -0
  37. package/schemas/impact-baseline.schema.json +59 -0
  38. package/schemas/impact-graph.schema.json +53 -0
  39. package/schemas/impact-report.schema.json +86 -0
  40. package/schemas/impact-resolution.schema.json +33 -0
  41. package/schemas/java-source-splice.schema.json +84 -0
  42. package/schemas/patch-transaction.schema.json +87 -2
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:sbf:impact-baseline:1",
4
+ "title": "backend-skeleton impact surface baseline",
5
+ "description": "Validates specs/<feature_id>/impact-baseline.json, written only by `bskel impact accept` -- a projection of ONE feature's own public surface at the moment it was last accepted as the reference point for change detection. Git-independent by design (specs/ is commonly gitignored, see D-contract-history's own finding) -- this file itself is the durable record of 'what did this feature's surface look like last time a human/CI accepted it'. Every *_shape_hash is sha256(JSON.stringify(sortKeysDeep(subtree))), the exact canonicalization lib/attest.mjs already implements and is reused unchanged (see D-attestation-payload-completeness's K1). See D-cross-feature-impact-graph.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "feature_id", "captured_at", "surface"],
9
+ "properties": {
10
+ "schema": { "const": "sbf.impact-baseline/1" },
11
+ "feature_id": { "type": "string", "pattern": "^[0-9]{3}-[a-z0-9]+(-[a-z0-9]+)*$" },
12
+ "captured_at": { "type": "string", "format": "date-time" },
13
+ "surface": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["operations", "resources", "fields"],
17
+ "properties": {
18
+ "operations": {
19
+ "type": "object",
20
+ "additionalProperties": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": ["verb", "path", "request_shape_hash", "response_shape_hash", "error_shape_hash"],
24
+ "properties": {
25
+ "verb": { "type": "string" },
26
+ "path": { "type": "string" },
27
+ "request_shape_hash": { "type": ["string", "null"] },
28
+ "response_shape_hash": { "type": ["string", "null"] },
29
+ "error_shape_hash": { "type": ["string", "null"] }
30
+ }
31
+ }
32
+ },
33
+ "resources": {
34
+ "type": "object",
35
+ "additionalProperties": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["file_sha256", "table", "table_source"],
39
+ "properties": {
40
+ "file_sha256": { "type": ["string", "null"] },
41
+ "table": { "type": ["string", "null"] },
42
+ "table_source": { "type": ["string", "null"] }
43
+ }
44
+ }
45
+ },
46
+ "fields": {
47
+ "description": "Only fields named by a declared dependencies.json edge -- this project has no per-adapter field enumerator (see D-field-dependency's own EXIT), so a field only exists here where something else already named it. Keyed \"<ResourceType>.<field>\".",
48
+ "type": "object",
49
+ "additionalProperties": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "required": ["source_file_sha256"],
53
+ "properties": { "source_file_sha256": { "type": ["string", "null"] } }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:sbf:impact-graph:1",
4
+ "title": "backend-skeleton cross-feature impact graph",
5
+ "description": "Validates the return shape of lib/impact-graph.mjs's buildImpactGraph(root) and `bskel impact export --format json`'s output. D-cross-feature-impact-graph. Every node id is an already-persisted exact identity (a feature id, a contract operation key, a scan-report resourceType, a table name, or a declared dependency field) -- there is no inferred/guessed node. Every edge carries `basis` (which artifact, and its sha256, proves this edge exists) and `confidence` ('proven'|'heuristic') so a consumer can tell a name-identity match backed by an explicit @Table/@Column from one backed by an adapter's guessed fallback. Deliberately no `calls` relation and no third confidence value -- service-to-service/dynamic-client inference is out of scope (see DECISIONS.md's EXIT list), and a missing edge is honest while a guessed one is not.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "generated_at", "nodes", "edges"],
9
+ "properties": {
10
+ "schema": { "const": "sbf.impact-graph/1" },
11
+ "generated_at": { "type": "string", "format": "date-time" },
12
+ "nodes": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["id", "type", "label"],
18
+ "properties": {
19
+ "id": { "type": "string", "minLength": 1 },
20
+ "type": { "enum": ["feature", "operation", "resource", "table", "field"] },
21
+ "label": { "type": "string" },
22
+ "feature_id": { "type": ["string", "null"] },
23
+ "file": { "type": ["string", "null"], "description": "Repo-relative source file, when this node resolves to one (resource/field)." },
24
+ "attrs": { "type": "object", "description": "Type-specific extra facts (verb/path for operation, table/tableSource for resource, etc.) -- open, since node types genuinely carry different metadata." }
25
+ }
26
+ }
27
+ },
28
+ "edges": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "required": ["source", "target", "relation", "confidence", "basis"],
34
+ "properties": {
35
+ "source": { "type": "string" },
36
+ "target": { "type": "string" },
37
+ "relation": { "enum": ["declares_operation", "owns_resource", "maps_to_table", "derives_from", "fk_references", "name_collides_with"] },
38
+ "confidence": { "enum": ["proven", "heuristic"] },
39
+ "basis": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": ["artifact", "artifact_sha256", "locator"],
43
+ "properties": {
44
+ "artifact": { "type": "string", "description": "Repo-relative path to the persisted file this edge was read from." },
45
+ "artifact_sha256": { "type": ["string", "null"], "pattern": "^[0-9a-f]{64}$" },
46
+ "locator": { "type": "string", "description": "Where inside the artifact, e.g. a JSON pointer or field name -- human-navigable, not machine-parsed." }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:sbf:impact-report:1",
4
+ "title": "backend-skeleton impact check report",
5
+ "description": "Validates specs/<feature_id>/impact-report.json, written by `bskel impact check` -- READ-mostly: diffs the current surface against impact-baseline.json and walks the impact graph, but never advances the baseline itself (only `bskel impact accept` does that). `unknowns` follows cross-feature-report.schema.json's own 'detect and warn, never silently omit' posture verbatim. See D-cross-feature-impact-graph.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "feature_id", "generated_at", "baseline", "changes", "outbound", "inbound", "unknowns"],
9
+ "properties": {
10
+ "schema": { "const": "sbf.impact-report/1" },
11
+ "feature_id": { "type": "string" },
12
+ "generated_at": { "type": "string", "format": "date-time" },
13
+ "baseline": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["present"],
17
+ "properties": {
18
+ "present": { "type": "boolean" },
19
+ "captured_at": { "type": ["string", "null"] }
20
+ }
21
+ },
22
+ "changes": {
23
+ "type": "array",
24
+ "items": {
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "required": ["change_key", "kind", "subject"],
28
+ "properties": {
29
+ "change_key": { "type": "string", "description": "\"<kind>:<subject>:<new_hash_12>\" -- embeds the NEW hash, so a disposition recorded for one shape can never cover a later, different change to the same subject." },
30
+ "kind": {
31
+ "enum": [
32
+ "operation_added", "operation_removed", "operation_verb_changed", "operation_path_changed",
33
+ "operation_request_shape_changed", "operation_response_shape_changed", "operation_error_shape_changed",
34
+ "resource_removed", "resource_table_changed", "field_source_moved"
35
+ ]
36
+ },
37
+ "subject": { "type": "string" },
38
+ "from": {},
39
+ "to": {}
40
+ }
41
+ }
42
+ },
43
+ "outbound": {
44
+ "description": "Impact THIS feature's changes have on other, downstream features -- what `bskel impact accept` blocks on when confidence is 'proven'.",
45
+ "type": "array",
46
+ "items": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["change_key", "downstream_feature", "via", "confidence", "disposition"],
50
+ "properties": {
51
+ "change_key": { "type": "string" },
52
+ "downstream_feature": { "type": "string" },
53
+ "via": { "enum": ["derives_from", "fk_references", "name_collides_with"] },
54
+ "basis": {
55
+ "type": ["object", "null"],
56
+ "additionalProperties": false,
57
+ "properties": {
58
+ "artifact": { "type": "string" },
59
+ "artifact_sha256": { "type": ["string", "null"] },
60
+ "locator": { "type": "string" }
61
+ }
62
+ },
63
+ "confidence": { "enum": ["proven", "heuristic"] },
64
+ "disposition": { "enum": ["compatible", "migrate", "waive", null] }
65
+ }
66
+ }
67
+ },
68
+ "inbound": {
69
+ "description": "Migration obligations another feature's own impact accept recorded AGAINST this feature -- what `bskel impact ack` clears.",
70
+ "type": "array",
71
+ "items": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "required": ["upstream_feature", "change_key", "mode", "tracked_by", "acknowledged"],
75
+ "properties": {
76
+ "upstream_feature": { "type": "string" },
77
+ "change_key": { "type": "string" },
78
+ "mode": { "const": "migrate" },
79
+ "tracked_by": { "type": "string" },
80
+ "acknowledged": { "type": "boolean" }
81
+ }
82
+ }
83
+ },
84
+ "unknowns": { "type": "array", "items": { "type": "string" } }
85
+ }
86
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:sbf:impact-resolution:1",
4
+ "title": "backend-skeleton impact dispositions",
5
+ "description": "Validates specs/<feature_id>/impact-resolution.json, written by `bskel impact disposition`/`bskel impact ack`. Three modes with three genuinely different mechanical consequences (see D-cross-feature-impact-graph's D6): 'compatible' unblocks one {change_key, downstream_feature} pair and nothing else; 'migrate' requires --tracked-by and creates a two-sided handshake (an inbound obligation on the downstream feature, cleared only by that feature's own `impact ack`); 'waive' requires --expires and decays via the same mechanism D-waiver-expiry already ships for `contract waive`. Keyed by {change_key, downstream_feature} -- never a wildcard, matching cross-feature-resolution.schema.json's own stated discipline.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "feature_id", "dispositions"],
9
+ "properties": {
10
+ "schema": { "const": "sbf.impact-resolution/1" },
11
+ "feature_id": { "type": "string" },
12
+ "dispositions": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["change_key", "downstream_feature", "mode", "reason", "at"],
18
+ "properties": {
19
+ "change_key": { "type": "string" },
20
+ "downstream_feature": { "type": "string" },
21
+ "mode": { "enum": ["compatible", "migrate", "waive"] },
22
+ "reason": { "type": "string", "minLength": 1 },
23
+ "tracked_by": { "type": ["string", "null"], "description": "Required (non-empty) iff mode is 'migrate'." },
24
+ "expires_at": { "type": ["string", "null"], "format": "date-time", "description": "Required iff mode is 'waive' -- ISO instant this disposition stops covering its change_key." },
25
+ "acknowledged": { "type": "boolean", "default": false, "description": "Only meaningful for mode 'migrate' -- flipped true by the DOWNSTREAM feature's own `bskel impact ack`." },
26
+ "acknowledged_at": { "type": ["string", "null"] },
27
+ "acknowledged_reason": { "type": ["string", "null"] },
28
+ "at": { "type": "string", "format": "date-time" }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,84 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:sbf:java-source-splice:1",
4
+ "title": "backend-skeleton java-source-splice request document (--splice-file)",
5
+ "description": "D-java-source-splice: the input document for `bskel patch propose --kind java-source-splice --splice-file <path>`. A single Java file plus an ordered, closed-vocabulary list of structural edits -- exactly the four operations handles/providers/java-spring/source-splice.mjs's planJavaSourceSplice() knows how to plan. `locator` identifies a member by its language-guaranteed identity (a type's fully-qualified name plus, for a method, its erased parameter types -- the same rule javac itself uses to forbid two same-named same-erased-signature overloads), never by line/column (those are recomputed fresh on every plan, never trusted from this file).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "file", "edits"],
9
+ "properties": {
10
+ "schema": { "const": "sbf.java-source-splice/1" },
11
+ "file": { "type": "string", "minLength": 1, "description": "Repo-relative path to the single Java source file every edit in this document targets." },
12
+ "edits": {
13
+ "type": "array",
14
+ "minItems": 1,
15
+ "items": { "$ref": "#/$defs/edit" }
16
+ }
17
+ },
18
+ "$defs": {
19
+ "locator": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "required": ["type_fqn", "member_kind", "member_name"],
23
+ "properties": {
24
+ "type_fqn": { "type": "string", "minLength": 1, "description": "The single top-level type's fully-qualified name -- V1 refuses a file declaring more than one top-level type." },
25
+ "member_kind": { "enum": ["method", "field"] },
26
+ "member_name": { "type": "string", "minLength": 1 },
27
+ "erased_param_types": {
28
+ "type": "array",
29
+ "items": { "type": "string" },
30
+ "description": "Method locators only -- the exact erased (generics-stripped) fully-qualified parameter types, in order. This is what disambiguates an overload; a field locator omits it entirely."
31
+ }
32
+ }
33
+ },
34
+ "edit": {
35
+ "oneOf": [
36
+ {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["op", "locator", "replacement"],
40
+ "properties": {
41
+ "op": { "const": "replace-method-body" },
42
+ "locator": { "$ref": "#/$defs/locator" },
43
+ "replacement": { "type": "string", "minLength": 2, "description": "The whole new method body, including the enclosing braces: must start with \"{\" and end with \"}\"." }
44
+ }
45
+ },
46
+ {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["op", "locator", "statements"],
50
+ "properties": {
51
+ "op": { "const": "insert-method-body-prologue" },
52
+ "locator": { "$ref": "#/$defs/locator" },
53
+ "statements": { "type": "string", "minLength": 1, "description": "Raw statement text to insert at the top of the method body -- the planner derives the actual replacement (original body text is required to survive as a verbatim suffix)." }
54
+ }
55
+ },
56
+ {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["op", "locator", "replacement"],
60
+ "properties": {
61
+ "op": { "const": "replace-field-initializer" },
62
+ "locator": { "$ref": "#/$defs/locator" },
63
+ "replacement": { "type": "string", "minLength": 1, "description": "The new initializer expression only -- no leading \"=\", no trailing \";\"." }
64
+ }
65
+ },
66
+ {
67
+ "type": "object",
68
+ "additionalProperties": false,
69
+ "required": ["op", "imports"],
70
+ "properties": {
71
+ "op": { "const": "add-import" },
72
+ "imports": {
73
+ "type": "array",
74
+ "minItems": 1,
75
+ "maxItems": 1,
76
+ "items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_.]*$" },
77
+ "description": "Exactly one fully-qualified import -- at most one add-import edit is permitted per transaction (see the planner's own WHY). No wildcard, no static import."
78
+ }
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ }
84
+ }
@@ -10,7 +10,7 @@
10
10
  "schema": { "const": "sbf.patch-transaction/1" },
11
11
  "transaction_id": { "type": "string", "pattern": "^pt-[0-9a-f-]{36}$" },
12
12
  "feature_id": { "type": "string", "pattern": "^[0-9]{3}-[a-z0-9]+(-[a-z0-9]+)*$" },
13
- "kind": { "enum": ["config-apply", "ddl-apply"] },
13
+ "kind": { "enum": ["config-apply", "ddl-apply", "java-source-splice"] },
14
14
  "source": {
15
15
  "type": "object",
16
16
  "description": "Kind-specific opaque context needed to re-plan this transaction fresh at approve/apply time. Shape enforced per-kind by the allOf/if-then branches below."
@@ -56,7 +56,11 @@
56
56
  "properties": {
57
57
  "reason": { "type": "string" },
58
58
  "at": { "type": "string", "format": "date-time" },
59
- "forced": { "type": "boolean" }
59
+ "forced": { "type": "boolean" },
60
+ "compile_after_rollback": {
61
+ "enum": ["ok", "failed"],
62
+ "description": "D-java-source-splice: java-source-splice only. Deliberately added HERE, to the base `rollback` object, not inside a per-kind allOf/if-then branch -- `rollback` is additionalProperties:false, and a branch's `then` schema is a SEPARATE schema applied to the same instance, so it cannot widen a sibling schema's own closed property list (confirmed live: a branch-added property is still rejected by the base schema's additionalProperties:false). Optional, not required -- absent on every config-apply/ddl-apply record, and on any java-source-splice record persisted before this field existed."
63
+ }
60
64
  }
61
65
  }
62
66
  },
@@ -177,6 +181,87 @@
177
181
  }
178
182
  }
179
183
  }
184
+ },
185
+ {
186
+ "if": { "properties": { "kind": { "const": "java-source-splice" } } },
187
+ "then": {
188
+ "properties": {
189
+ "source": {
190
+ "additionalProperties": false,
191
+ "required": ["request_file"],
192
+ "properties": {
193
+ "request_file": { "type": "string", "description": "The --splice-file path this transaction was originally proposed from -- audit trail only, never re-read at approve/apply time (paramsFromTxn() reconstructs planner params entirely from `target`)." }
194
+ }
195
+ },
196
+ "target": {
197
+ "additionalProperties": false,
198
+ "required": ["file", "source_root", "line_terminator", "edits"],
199
+ "properties": {
200
+ "file": { "type": "string" },
201
+ "source_root": { "type": "string" },
202
+ "line_terminator": { "enum": ["\n", "\r\n"] },
203
+ "edits": {
204
+ "type": "array",
205
+ "minItems": 1,
206
+ "items": {
207
+ "type": "object",
208
+ "additionalProperties": false,
209
+ "required": ["op", "region_hash", "signature_hash", "located"],
210
+ "properties": {
211
+ "op": { "enum": ["replace-method-body", "insert-method-body-prologue", "replace-field-initializer", "add-import"] },
212
+ "locator": {
213
+ "type": "object",
214
+ "additionalProperties": false,
215
+ "required": ["type_fqn", "member_kind", "member_name"],
216
+ "properties": {
217
+ "type_fqn": { "type": "string" },
218
+ "member_kind": { "enum": ["method", "field"] },
219
+ "member_name": { "type": "string" },
220
+ "erased_param_types": { "type": "array", "items": { "type": "string" } }
221
+ }
222
+ },
223
+ "replacement": { "type": "string" },
224
+ "statements": { "type": "string" },
225
+ "imports": { "type": "array", "items": { "type": "string" } },
226
+ "region_hash": { "type": "string" },
227
+ "signature_hash": { "type": ["string", "null"] },
228
+ "located": {
229
+ "type": "object",
230
+ "additionalProperties": false,
231
+ "required": ["start", "end", "begin_line", "end_line"],
232
+ "description": "A cache of where this edit's region was found at the time of the last plan -- NEVER authoritative (re-derived fresh from `locator` on every plan; a mismatch with the current file just means something else moved, not an error).",
233
+ "properties": {
234
+ "start": { "type": "integer" },
235
+ "end": { "type": "integer" },
236
+ "begin_line": { "type": ["integer", "null"] },
237
+ "end_line": { "type": ["integer", "null"] }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ },
245
+ "postcondition": {
246
+ "additionalProperties": false,
247
+ "required": ["kind", "build_tool", "build_command"],
248
+ "properties": {
249
+ "kind": { "const": "java-compiles" },
250
+ "build_tool": { "type": "string" },
251
+ "build_command": { "type": "string" }
252
+ }
253
+ },
254
+ "apply": {
255
+ "additionalProperties": false,
256
+ "required": ["at", "postimage_file_hash", "build_tool"],
257
+ "properties": {
258
+ "at": { "type": "string", "format": "date-time" },
259
+ "postimage_file_hash": { "type": "string" },
260
+ "build_tool": { "type": "string" }
261
+ }
262
+ }
263
+ }
264
+ }
180
265
  }
181
266
  ]
182
267
  }