@thinkingsage/kanon 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 (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. package/templates/knowledge/mcp-servers.yaml.njk +2 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Rosetta Stone — Request Guard
3
+ *
4
+ * Validates, normalizes, and freezes translation requests before dispatch.
5
+ * Rejects impure values (functions, symbols, class instances, streams, accessors)
6
+ * and reserved environmental keys from caller context.
7
+ *
8
+ * CONSTRAINTS:
9
+ * - NO filesystem, process, clock, random, Git, or network imports
10
+ * - Pure functions only
11
+ *
12
+ * Requirements: 1.3, 1.4, 3.6, 3.7, 8.1, 12.6, 12.7, 13.1, 13.2
13
+ */
14
+
15
+ import type { TranslationDiagnostic, TranslationRequest } from "../schemas";
16
+ import { FormatIdentifierSchema, TranslationRequestSchema } from "../schemas";
17
+ import { deepFreeze } from "./contracts";
18
+ import { createDiagnostic } from "./diagnostics";
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+ // Reserved Environmental Keys
22
+ // ═══════════════════════════════════════════════════════════════════════════════
23
+
24
+ /**
25
+ * Keys that MUST be rejected in callerContext to enforce the pure boundary.
26
+ * These represent environmental concerns that translators must never access.
27
+ */
28
+ const RESERVED_CONTEXT_KEYS: ReadonlySet<string> = new Set([
29
+ "filesystem",
30
+ "git",
31
+ "network",
32
+ "process",
33
+ "env",
34
+ "clock",
35
+ "random",
36
+ "prompt",
37
+ "writer",
38
+ ]);
39
+
40
+ // ═══════════════════════════════════════════════════════════════════════════════
41
+ // Guard Result Types
42
+ // ═══════════════════════════════════════════════════════════════════════════════
43
+
44
+ /**
45
+ * Metadata about a default value applied during guard normalization.
46
+ */
47
+ export interface AppliedGuardDefault {
48
+ readonly field: string;
49
+ readonly value: unknown;
50
+ readonly reason: string;
51
+ }
52
+
53
+ /**
54
+ * Discriminated result of request guard validation.
55
+ */
56
+ export type GuardResult =
57
+ | {
58
+ ok: true;
59
+ request: Readonly<TranslationRequest>;
60
+ appliedDefaults: readonly AppliedGuardDefault[];
61
+ }
62
+ | {
63
+ ok: false;
64
+ diagnostics: TranslationDiagnostic[];
65
+ };
66
+
67
+ // ═══════════════════════════════════════════════════════════════════════════════
68
+ // Main Guard Function
69
+ // ═══════════════════════════════════════════════════════════════════════════════
70
+
71
+ /**
72
+ * Validate, normalize, and freeze a raw translation request before dispatch.
73
+ *
74
+ * Returns all diagnostics found (not just the first), then a frozen request
75
+ * if validation passes.
76
+ */
77
+ export function guardRequest(raw: unknown): GuardResult {
78
+ const diagnostics: TranslationDiagnostic[] = [];
79
+ const appliedDefaults: AppliedGuardDefault[] = [];
80
+
81
+ // --- Step 1: Zod parse ---
82
+ const parseResult = TranslationRequestSchema.safeParse(raw);
83
+ if (!parseResult.success) {
84
+ for (const issue of parseResult.error.issues) {
85
+ diagnostics.push(
86
+ createDiagnostic("RS_INVALID_REQUEST", {
87
+ message: `Validation failed at "${issue.path.join(".")}": ${issue.message}`,
88
+ }),
89
+ );
90
+ }
91
+ return { ok: false, diagnostics };
92
+ }
93
+
94
+ const request = parseResult.data;
95
+
96
+ // --- Step 2: Reserved keys in callerContext ---
97
+ const callerContext = request.callerContext;
98
+ for (const key of Object.keys(callerContext)) {
99
+ if (RESERVED_CONTEXT_KEYS.has(key)) {
100
+ diagnostics.push(
101
+ createDiagnostic("RS_INVALID_REQUEST", {
102
+ message: `callerContext contains reserved environmental key "${key}".`,
103
+ }),
104
+ );
105
+ }
106
+ }
107
+
108
+ // --- Step 3: Impure value scan on callerContext ---
109
+ const impureIssues = scanForImpureValues(callerContext, "callerContext");
110
+ for (const issue of impureIssues) {
111
+ diagnostics.push(
112
+ createDiagnostic("RS_INVALID_REQUEST", {
113
+ message: issue,
114
+ }),
115
+ );
116
+ }
117
+
118
+ // --- Step 4: Path normalization & validation (inbound/transcode) ---
119
+ if (request.mode === "inbound" || request.mode === "transcode") {
120
+ const paths = request.sourceDocuments.map((doc) => doc.path);
121
+ const pathIssues = validatePaths(paths);
122
+ for (const issue of pathIssues) {
123
+ diagnostics.push(
124
+ createDiagnostic("RS_INVALID_REQUEST", {
125
+ message: issue,
126
+ }),
127
+ );
128
+ }
129
+ }
130
+
131
+ // --- Step 5: Binary/text constraints ---
132
+ if (request.mode === "inbound" || request.mode === "transcode") {
133
+ const hasBinaryContent = request.sourceDocuments.some(
134
+ (doc) => doc.content instanceof Uint8Array,
135
+ );
136
+ if (hasBinaryContent && request.source.formatId) {
137
+ // If the source format is explicit and documents contain binary,
138
+ // we note it but don't reject — the translator will handle format
139
+ // constraints. This is a structural validation; format-specific
140
+ // text-only constraints are checked after detection.
141
+ }
142
+ }
143
+
144
+ // --- Step 6: Format identifier validation ---
145
+ if (request.mode === "inbound" || request.mode === "transcode") {
146
+ if (request.source.formatId !== undefined) {
147
+ const fmtResult = FormatIdentifierSchema.safeParse(
148
+ request.source.formatId,
149
+ );
150
+ if (!fmtResult.success) {
151
+ diagnostics.push(
152
+ createDiagnostic("RS_INVALID_REQUEST", {
153
+ message: `Invalid source format identifier "${request.source.formatId}".`,
154
+ }),
155
+ );
156
+ }
157
+ }
158
+ }
159
+ if (request.mode === "outbound" || request.mode === "transcode") {
160
+ const targetFmtResult = FormatIdentifierSchema.safeParse(
161
+ request.target.formatId,
162
+ );
163
+ if (!targetFmtResult.success) {
164
+ diagnostics.push(
165
+ createDiagnostic("RS_INVALID_REQUEST", {
166
+ message: `Invalid target format identifier "${request.target.formatId}".`,
167
+ }),
168
+ );
169
+ }
170
+ }
171
+
172
+ // --- Return diagnostics if any ---
173
+ if (diagnostics.length > 0) {
174
+ return { ok: false, diagnostics };
175
+ }
176
+
177
+ // --- Step 7: Deep-freeze and return ---
178
+ const frozen = deepFreeze(structuredClone(request));
179
+
180
+ return {
181
+ ok: true,
182
+ request: frozen,
183
+ appliedDefaults,
184
+ };
185
+ }
186
+
187
+ // ═══════════════════════════════════════════════════════════════════════════════
188
+ // Impure Value Scanner
189
+ // ═══════════════════════════════════════════════════════════════════════════════
190
+
191
+ /**
192
+ * Recursively scan a value for impure elements:
193
+ * - Functions
194
+ * - Symbols
195
+ * - Class instances (prototype other than Object/Array/null)
196
+ * - Getters/setters (accessors)
197
+ * - Streams (objects with `pipe` or `read` methods)
198
+ * - Promises
199
+ *
200
+ * Returns an array of human-readable issue descriptions.
201
+ */
202
+ function scanForImpureValues(
203
+ value: unknown,
204
+ path: string,
205
+ visited: WeakSet<object> = new WeakSet(),
206
+ ): string[] {
207
+ const issues: string[] = [];
208
+
209
+ if (value === null || value === undefined) {
210
+ return issues;
211
+ }
212
+
213
+ if (typeof value === "function") {
214
+ issues.push(`${path} contains a function.`);
215
+ return issues;
216
+ }
217
+
218
+ if (typeof value === "symbol") {
219
+ issues.push(`${path} contains a symbol.`);
220
+ return issues;
221
+ }
222
+
223
+ if (typeof value !== "object") {
224
+ // Primitives (string, number, boolean) are always safe
225
+ return issues;
226
+ }
227
+
228
+ // Avoid infinite recursion on circular references
229
+ if (visited.has(value as object)) {
230
+ return issues;
231
+ }
232
+ visited.add(value as object);
233
+
234
+ // Check for Promises
235
+ if (value instanceof Promise) {
236
+ issues.push(`${path} contains a Promise.`);
237
+ return issues;
238
+ }
239
+
240
+ // Check prototype: only plain objects, arrays, and null-prototype objects allowed
241
+ const proto = Object.getPrototypeOf(value);
242
+ if (
243
+ proto !== Object.prototype &&
244
+ proto !== Array.prototype &&
245
+ proto !== null &&
246
+ !(value instanceof Uint8Array)
247
+ ) {
248
+ issues.push(
249
+ `${path} contains a class instance (${proto?.constructor?.name ?? "unknown"}).`,
250
+ );
251
+ return issues;
252
+ }
253
+
254
+ // Check for stream-like objects (have pipe or read methods)
255
+ if (
256
+ typeof (value as Record<string, unknown>).pipe === "function" ||
257
+ typeof (value as Record<string, unknown>).read === "function"
258
+ ) {
259
+ issues.push(`${path} contains a stream-like object.`);
260
+ return issues;
261
+ }
262
+
263
+ // Check for accessor properties (getters/setters)
264
+ const descriptors = Object.getOwnPropertyDescriptors(value);
265
+ for (const [key, desc] of Object.entries(descriptors)) {
266
+ if (desc.get || desc.set) {
267
+ issues.push(`${path}.${key} uses a getter/setter accessor.`);
268
+ }
269
+ }
270
+
271
+ // Recurse into properties
272
+ if (Array.isArray(value)) {
273
+ for (let i = 0; i < value.length; i++) {
274
+ issues.push(...scanForImpureValues(value[i], `${path}[${i}]`, visited));
275
+ }
276
+ } else {
277
+ for (const key of Object.keys(value as Record<string, unknown>)) {
278
+ const desc = descriptors[key];
279
+ // Only recurse into data properties (accessors already flagged)
280
+ if (desc && "value" in desc) {
281
+ issues.push(
282
+ ...scanForImpureValues(desc.value, `${path}.${key}`, visited),
283
+ );
284
+ }
285
+ }
286
+ }
287
+
288
+ return issues;
289
+ }
290
+
291
+ // ═══════════════════════════════════════════════════════════════════════════════
292
+ // Path Validation
293
+ // ═══════════════════════════════════════════════════════════════════════════════
294
+
295
+ /**
296
+ * Validate an array of source document paths:
297
+ * - All paths must be NFC-normalized
298
+ * - No traversal (..)
299
+ * - No duplicates after NFC normalization
300
+ *
301
+ * Returns an array of human-readable issue descriptions.
302
+ */
303
+ function validatePaths(paths: string[]): string[] {
304
+ const issues: string[] = [];
305
+ const normalized = new Set<string>();
306
+
307
+ for (const rawPath of paths) {
308
+ // NFC normalization check
309
+ const nfcPath = rawPath.normalize("NFC");
310
+ if (rawPath !== nfcPath) {
311
+ issues.push(`Source document path "${rawPath}" is not NFC-normalized.`);
312
+ }
313
+
314
+ // Traversal check
315
+ const segments = nfcPath.split("/");
316
+ for (const seg of segments) {
317
+ if (seg === "..") {
318
+ issues.push(
319
+ `Source document path "${rawPath}" contains traversal ("..").`,
320
+ );
321
+ break;
322
+ }
323
+ }
324
+
325
+ // Duplicate check (after normalization)
326
+ if (normalized.has(nfcPath)) {
327
+ issues.push(
328
+ `Duplicate source document path after normalization: "${nfcPath}".`,
329
+ );
330
+ }
331
+ normalized.add(nfcPath);
332
+ }
333
+
334
+ return issues;
335
+ }