pi-lens 3.8.39 → 3.8.41

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 (65) hide show
  1. package/CHANGELOG.md +84 -5
  2. package/README.md +37 -1
  3. package/clients/biome-client.ts +5 -4
  4. package/clients/cache/rule-cache.ts +1 -1
  5. package/clients/complexity-client.ts +1 -1
  6. package/clients/dependency-checker.ts +1 -1
  7. package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
  8. package/clients/dispatch/dispatcher.ts +9 -0
  9. package/clients/dispatch/fact-scheduler.ts +1 -1
  10. package/clients/dispatch/integration.ts +58 -3
  11. package/clients/dispatch/runners/index.ts +2 -0
  12. package/clients/dispatch/runners/semgrep.ts +269 -0
  13. package/clients/dispatch/runners/shellcheck.ts +2 -8
  14. package/clients/dispatch/runners/tree-sitter.ts +32 -11
  15. package/clients/dispatch/tool-profile.ts +1 -0
  16. package/clients/format-service.ts +10 -0
  17. package/clients/formatters.ts +22 -8
  18. package/clients/installer/index.ts +3 -3
  19. package/clients/knip-client.ts +360 -362
  20. package/clients/lsp/aggregation.ts +91 -0
  21. package/clients/lsp/client.ts +91 -38
  22. package/clients/lsp/index.ts +88 -72
  23. package/clients/lsp/launch.ts +107 -34
  24. package/clients/lsp/server-strategies.ts +71 -0
  25. package/clients/lsp/server.ts +76 -57
  26. package/clients/path-utils.ts +17 -0
  27. package/clients/pipeline.ts +23 -5
  28. package/clients/production-readiness.ts +2 -2
  29. package/clients/read-guard-logger.ts +41 -1
  30. package/clients/read-guard-tool-lines.ts +17 -4
  31. package/clients/read-guard.ts +95 -46
  32. package/clients/runtime-agent-end.ts +3 -0
  33. package/clients/runtime-session.ts +5 -0
  34. package/clients/runtime-tool-result.ts +48 -1
  35. package/clients/runtime-turn.ts +48 -4
  36. package/clients/sanitize.ts +1 -1
  37. package/clients/semgrep-config.ts +213 -0
  38. package/clients/tool-policy.ts +1982 -1936
  39. package/clients/tree-sitter-client.ts +1 -1
  40. package/clients/widget-state.ts +283 -0
  41. package/commands/booboo.ts +34 -2
  42. package/index.ts +231 -17
  43. package/package.json +3 -2
  44. package/rules/rule-catalog.json +25 -1
  45. package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
  46. package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
  47. package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
  48. package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
  49. package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
  50. package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
  51. package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
  52. package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
  53. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
  54. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
  55. package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
  56. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
  57. package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
  58. package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
  59. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
  60. package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
  61. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
  62. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
  63. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
  64. package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
  65. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
@@ -0,0 +1,269 @@
1
+ import * as path from "node:path";
2
+ import { classifyDefect } from "../diagnostic-taxonomy.js";
3
+ import { PRIORITY } from "../priorities.js";
4
+ import type {
5
+ DefectClass,
6
+ Diagnostic,
7
+ DispatchContext,
8
+ OutputSemantic,
9
+ RunnerDefinition,
10
+ RunnerResult,
11
+ } from "../types.js";
12
+ import { safeSpawnAsync } from "../../safe-spawn.js";
13
+ import { resolveSemgrepConfig } from "../../semgrep-config.js";
14
+ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
15
+
16
+ const semgrep = createAvailabilityChecker("semgrep", ".exe");
17
+ const MAX_DIAGNOSTICS = 50;
18
+
19
+ interface SemgrepJsonOutput {
20
+ results?: SemgrepResult[];
21
+ errors?: Array<{ message?: string; type?: string; level?: string }>;
22
+ }
23
+
24
+ interface SemgrepResult {
25
+ check_id?: string;
26
+ path?: string;
27
+ start?: { line?: number; col?: number };
28
+ extra?: {
29
+ message?: string;
30
+ severity?: string;
31
+ metadata?: Record<string, unknown>;
32
+ fix?: string;
33
+ fix_regex?: unknown;
34
+ };
35
+ }
36
+
37
+ function getPiLensMetadata(
38
+ metadata: Record<string, unknown>,
39
+ ): Record<string, unknown> {
40
+ const nested = metadata["pi-lens"] ?? metadata.pi_lens;
41
+ return nested && typeof nested === "object"
42
+ ? (nested as Record<string, unknown>)
43
+ : {};
44
+ }
45
+
46
+ function metadataString(
47
+ metadata: Record<string, unknown>,
48
+ piLens: Record<string, unknown>,
49
+ key: string,
50
+ ): string | undefined {
51
+ const direct = piLens[key] ?? metadata[`pi_lens_${key}`];
52
+ return typeof direct === "string" && direct.trim()
53
+ ? direct.trim()
54
+ : undefined;
55
+ }
56
+
57
+ function metadataBoolean(
58
+ metadata: Record<string, unknown>,
59
+ piLens: Record<string, unknown>,
60
+ key: string,
61
+ ): boolean {
62
+ return piLens[key] === true || metadata[`pi_lens_${key}`] === true;
63
+ }
64
+
65
+ function normalizeDefectClass(
66
+ value: string | undefined,
67
+ ): DefectClass | undefined {
68
+ if (!value) return undefined;
69
+ const normalized = value.toLowerCase().replace(/_/g, "-");
70
+ if (
71
+ normalized === "silent-error" ||
72
+ normalized === "injection" ||
73
+ normalized === "secrets" ||
74
+ normalized === "async-misuse" ||
75
+ normalized === "correctness" ||
76
+ normalized === "safety" ||
77
+ normalized === "style" ||
78
+ normalized === "unknown" ||
79
+ normalized === "unused-value"
80
+ ) {
81
+ return normalized;
82
+ }
83
+ if (
84
+ normalized.includes("traversal") ||
85
+ normalized.includes("ssrf") ||
86
+ normalized.includes("xss") ||
87
+ normalized.includes("deserial") ||
88
+ normalized.includes("crypto") ||
89
+ normalized.includes("auth")
90
+ ) {
91
+ return "safety";
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ function semgrepSemantic(
97
+ result: SemgrepResult,
98
+ defectClass: DefectClass,
99
+ ): OutputSemantic {
100
+ const metadata = result.extra?.metadata ?? {};
101
+ const piLens = getPiLensMetadata(metadata);
102
+ const explicitSemantic = metadataString(metadata, piLens, "semantic");
103
+ if (
104
+ explicitSemantic === "blocking" ||
105
+ metadataBoolean(metadata, piLens, "blocking")
106
+ ) {
107
+ return "blocking";
108
+ }
109
+ if (explicitSemantic === "warning" || explicitSemantic === "silent") {
110
+ return explicitSemantic;
111
+ }
112
+
113
+ const severity = String(result.extra?.severity ?? "").toUpperCase();
114
+ const confidence = String(
115
+ metadata.confidence ??
116
+ metadata.semgrep_confidence ??
117
+ piLens.confidence ??
118
+ "",
119
+ ).toLowerCase();
120
+ const highSignalSecurity =
121
+ defectClass === "injection" ||
122
+ defectClass === "secrets" ||
123
+ defectClass === "safety";
124
+
125
+ if (severity === "ERROR" && highSignalSecurity && confidence !== "low") {
126
+ return "blocking";
127
+ }
128
+
129
+ return "warning";
130
+ }
131
+
132
+ function mapSeverity(
133
+ semgrepSeverity: string | undefined,
134
+ semantic: OutputSemantic,
135
+ ): Diagnostic["severity"] {
136
+ if (semantic === "blocking") return "error";
137
+ const severity = String(semgrepSeverity ?? "").toUpperCase();
138
+ if (severity === "ERROR") return "error";
139
+ if (severity === "INFO") return "info";
140
+ return "warning";
141
+ }
142
+
143
+ function parseSemgrepJson(raw: string, ctx: DispatchContext): Diagnostic[] {
144
+ if (!raw.trim()) return [];
145
+ let parsed: SemgrepJsonOutput;
146
+ try {
147
+ parsed = JSON.parse(raw) as SemgrepJsonOutput;
148
+ } catch {
149
+ return [];
150
+ }
151
+
152
+ const results = Array.isArray(parsed.results) ? parsed.results : [];
153
+ const diagnostics: Diagnostic[] = [];
154
+
155
+ for (const [index, result] of results.entries()) {
156
+ if (diagnostics.length >= MAX_DIAGNOSTICS) break;
157
+ const rule = result.check_id || "semgrep";
158
+ const message = result.extra?.message || rule;
159
+ const metadata = result.extra?.metadata ?? {};
160
+ const piLens = getPiLensMetadata(metadata);
161
+ const explicitDefect = normalizeDefectClass(
162
+ metadataString(metadata, piLens, "defect_class"),
163
+ );
164
+ const defectClass =
165
+ explicitDefect ?? classifyDefect(rule, "semgrep", message);
166
+ const semantic = semgrepSemantic(result, defectClass);
167
+ const filePath = result.path || ctx.filePath;
168
+ const line = result.start?.line ?? 1;
169
+ const column = result.start?.col ?? 1;
170
+ const fixSuggestion =
171
+ metadataString(metadata, piLens, "fix") ??
172
+ (typeof result.extra?.fix === "string" ? result.extra.fix : undefined);
173
+
174
+ diagnostics.push({
175
+ id: `semgrep:${rule}:${path.basename(filePath)}:${line}:${column}:${index}`,
176
+ message: `[${rule}] ${message}`,
177
+ filePath,
178
+ line,
179
+ column,
180
+ severity: mapSeverity(result.extra?.severity, semantic),
181
+ semantic,
182
+ tool: "semgrep",
183
+ rule,
184
+ defectClass,
185
+ fixable: Boolean(fixSuggestion || result.extra?.fix_regex),
186
+ autoFixAvailable: false,
187
+ fixKind:
188
+ fixSuggestion || result.extra?.fix_regex ? "suggestion" : undefined,
189
+ fixSuggestion,
190
+ });
191
+ }
192
+
193
+ return diagnostics;
194
+ }
195
+
196
+ const semgrepRunner: RunnerDefinition = {
197
+ id: "semgrep",
198
+ appliesTo: [
199
+ "csharp",
200
+ "css",
201
+ "cxx",
202
+ "dart",
203
+ "docker",
204
+ "go",
205
+ "html",
206
+ "java",
207
+ "json",
208
+ "jsts",
209
+ "kotlin",
210
+ "lua",
211
+ "php",
212
+ "python",
213
+ "ruby",
214
+ "rust",
215
+ "shell",
216
+ "swift",
217
+ "terraform",
218
+ "yaml",
219
+ ],
220
+ priority: PRIORITY.DEEP_LANGUAGE_ANALYSIS,
221
+ enabledByDefault: false,
222
+
223
+ async when(ctx: DispatchContext): Promise<boolean> {
224
+ return resolveSemgrepConfig(ctx.cwd, {
225
+ enabled: Boolean(ctx.pi.getFlag("lens-semgrep")),
226
+ config: ctx.pi.getFlag("lens-semgrep-config"),
227
+ }).enabled;
228
+ },
229
+
230
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
231
+ const cwd = ctx.cwd || process.cwd();
232
+ const resolved = resolveSemgrepConfig(cwd, {
233
+ enabled: Boolean(ctx.pi.getFlag("lens-semgrep")),
234
+ config: ctx.pi.getFlag("lens-semgrep-config"),
235
+ });
236
+ if (!resolved.enabled) {
237
+ return { status: "skipped", diagnostics: [], semantic: "none" };
238
+ }
239
+
240
+ if (!semgrep.isAvailable(cwd)) {
241
+ return { status: "skipped", diagnostics: [], semantic: "none" };
242
+ }
243
+ const cmd = semgrep.getCommand(cwd) ?? "semgrep";
244
+ const args = ["scan", "--json", "--metrics=off", "--timeout", "5"];
245
+ if (resolved.configArg) args.push("--config", resolved.configArg);
246
+ args.push(ctx.filePath);
247
+
248
+ const result = await safeSpawnAsync(cmd, args, { cwd, timeout: 20000 });
249
+ const raw = result.stdout || "";
250
+ const diagnostics = parseSemgrepJson(raw, ctx);
251
+ if (diagnostics.length === 0) {
252
+ return {
253
+ status: result.error ? "failed" : "succeeded",
254
+ diagnostics: [],
255
+ semantic: "none",
256
+ rawOutput: (result.stderr || "").slice(0, 500),
257
+ };
258
+ }
259
+
260
+ const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
261
+ return {
262
+ status: hasBlocking ? "failed" : "succeeded",
263
+ diagnostics,
264
+ semantic: hasBlocking ? "blocking" : "warning",
265
+ };
266
+ },
267
+ };
268
+
269
+ export default semgrepRunner;
@@ -148,14 +148,8 @@ const shellcheckRunner: RunnerDefinition = {
148
148
  }
149
149
  if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" };
150
150
 
151
- // Determine shell dialect from file extension
152
- const shellDialect = ctx.filePath.endsWith(".zsh")
153
- ? "bash"
154
- : ctx.filePath.endsWith(".fish")
155
- ? "bash"
156
- : ctx.filePath.endsWith(".sh")
157
- ? "bash"
158
- : "bash"; // Default to bash for generic shell files
151
+ // Determine shell dialect from file extension (all map to bash for shellcheck)
152
+ const shellDialect = "bash";
159
153
 
160
154
  // Build args
161
155
  // --format json: JSON output
@@ -32,6 +32,11 @@ import type {
32
32
  // Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
33
33
  // WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
34
34
  let _sharedClient: TreeSitterClient | null = null;
35
+ // Once the wasm runtime aborts, the entire module-level wasm heap is corrupted — no
36
+ // recovery is possible within this process. Flag it and skip all further tree-sitter work
37
+ // rather than re-invoking the dead runtime (which prints "Aborted()" on every call and
38
+ // leaks memory on each retry).
39
+ let _wasmAborted = false;
35
40
  const blastCooldownByFile = new Map<string, number>();
36
41
  const BLAST_COOLDOWN_MS = 5_000;
37
42
 
@@ -244,7 +249,8 @@ function isLineInModifiedRanges(
244
249
  return ranges.some((r) => line >= r.start && line <= r.end);
245
250
  }
246
251
 
247
- function getSharedClient(): TreeSitterClient {
252
+ function getSharedClient(): TreeSitterClient | null {
253
+ if (_wasmAborted) return null;
248
254
  if (!_sharedClient) {
249
255
  _sharedClient = new TreeSitterClient();
250
256
  }
@@ -276,11 +282,11 @@ const treeSitterRunner: RunnerDefinition = {
276
282
  // Use singleton client — WASM must never be re-initialized after first call
277
283
  const client = getSharedClient();
278
284
  logTreeSitter({ phase: "runner_start", filePath: ctx.filePath });
279
- if (!client.isAvailable()) {
285
+ if (!client || !client.isAvailable()) {
280
286
  logTreeSitter({
281
287
  phase: "runner_skip",
282
288
  filePath: ctx.filePath,
283
- reason: "client_unavailable",
289
+ reason: _wasmAborted ? "wasm_aborted" : "client_unavailable",
284
290
  status: "skipped",
285
291
  });
286
292
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -506,14 +512,29 @@ const treeSitterRunner: RunnerDefinition = {
506
512
  }
507
513
  } catch (err) {
508
514
  // pi-lens-ignore: missing-error-propagation — per-query resilience loop, intentional
509
- console.error(`[tree-sitter] Query ${query.id} failed:`, err);
510
- logTreeSitter({
511
- phase: "query_error",
512
- filePath,
513
- languageId,
514
- queryId: query.id,
515
- error: err instanceof Error ? err.message : String(err),
516
- });
515
+ const msg = err instanceof Error ? err.message : String(err);
516
+ // Emscripten abort() corrupts the entire module-level wasm heap.
517
+ // Poison the singleton so no further queries attempt to use the dead runtime.
518
+ if (msg.includes("Aborted") || msg.includes("abort()")) {
519
+ _wasmAborted = true;
520
+ _sharedClient = null;
521
+ logTreeSitter({
522
+ phase: "query_error",
523
+ filePath,
524
+ languageId,
525
+ queryId: query.id,
526
+ error: "wasm_aborted_fatal",
527
+ });
528
+ } else {
529
+ console.error(`[tree-sitter] Query ${query.id} failed:`, err);
530
+ logTreeSitter({
531
+ phase: "query_error",
532
+ filePath,
533
+ languageId,
534
+ queryId: query.id,
535
+ error: msg,
536
+ });
537
+ }
517
538
  }
518
539
  return queryDiagnostics;
519
540
  }),
@@ -26,6 +26,7 @@ const TOOL_PROFILE_MAP: Record<string, ToolProfile> = {
26
26
  "rust-clippy": { dedupPriority: 95, lintLike: true },
27
27
  shellcheck: { dedupPriority: 95, lintLike: true },
28
28
  "type-safety": { dedupPriority: 95, lintLike: true },
29
+ semgrep: { dedupPriority: 105, lintLike: false },
29
30
  };
30
31
 
31
32
  export function getToolProfile(
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import * as path from "node:path";
15
+ import { recordFormatter } from "./widget-state.js";
15
16
  import { FileTime } from "./file-time.js";
16
17
  import {
17
18
  clearFormatterRuntimeState,
@@ -114,6 +115,15 @@ export class FormatService {
114
115
  // Record new file state after formatting
115
116
  this.fileTime.read(absolutePath);
116
117
 
118
+ for (const [index, result] of results.entries()) {
119
+ recordFormatter(
120
+ absolutePath,
121
+ formatters[index]?.name ?? "unknown",
122
+ result.changed,
123
+ result.success,
124
+ );
125
+ }
126
+
117
127
  // Build summary
118
128
  const anyChanged = results.some((r) => r.changed);
119
129
  const allSucceeded = results.every((r) => r.success);
@@ -778,7 +778,11 @@ export const cmakeFormatFormatter: FormatterInfo = {
778
778
 
779
779
  export const psscriptanalyzerFormatFormatter: FormatterInfo = {
780
780
  name: "psscriptanalyzer-format",
781
- command: ["pwsh", "-Command", "Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'"],
781
+ command: [
782
+ "pwsh",
783
+ "-Command",
784
+ "Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'",
785
+ ],
782
786
  extensions: [".ps1", ".psm1", ".psd1"],
783
787
  async resolveCommand(filePath, _cwd) {
784
788
  const pwsh = (await which("pwsh")) ?? (await which("powershell"));
@@ -794,11 +798,15 @@ export const psscriptanalyzerFormatFormatter: FormatterInfo = {
794
798
  const pwsh = (await which("pwsh")) ?? (await which("powershell"));
795
799
  if (!pwsh) return false;
796
800
  // Check PSScriptAnalyzer module is available
797
- const result = safeSpawn(pwsh, [
798
- "-NoProfile",
799
- "-Command",
800
- "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
801
- ], { timeout: 5_000 });
801
+ const result = safeSpawn(
802
+ pwsh,
803
+ [
804
+ "-NoProfile",
805
+ "-Command",
806
+ "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
807
+ ],
808
+ { timeout: 5_000 },
809
+ );
802
810
  return (result.stdout ?? "").includes("PSScriptAnalyzer");
803
811
  },
804
812
  };
@@ -925,7 +933,9 @@ export async function getFormattersForFile(
925
933
  } else if (!formatterPolicy) {
926
934
  selectionReason = "detect";
927
935
  } else {
928
- selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd))
936
+ selectionReason = candidateFormatters.some((f) =>
937
+ hasExplicitFormatterConfig(f.name, cwd),
938
+ )
929
939
  ? "explicit-config"
930
940
  : "smart-default";
931
941
  }
@@ -934,7 +944,11 @@ export async function getFormattersForFile(
934
944
  phase: "formatter_selected",
935
945
  filePath: filePath,
936
946
  durationMs: 0,
937
- metadata: { formatter: selected?.name ?? null, reason: selectionReason, cwd },
947
+ metadata: {
948
+ formatter: selected?.name ?? null,
949
+ reason: selectionReason,
950
+ cwd,
951
+ },
938
952
  });
939
953
 
940
954
  // Store the list of enabled formatter names in cache
@@ -1344,7 +1344,7 @@ async function installGitHubTool(
1344
1344
  const srcBinary = path.join(tmpDir, spec.binaryInArchive ?? binaryName);
1345
1345
  await fs.rename(srcBinary, destPath);
1346
1346
  await fs.rm(tmpDir, { recursive: true, force: true });
1347
- if (!isWindows) await fs.chmod(destPath, 0o755);
1347
+ if (!isWindows) await fs.chmod(destPath, 0o750);
1348
1348
  } else if (assetName.endsWith(".zip")) {
1349
1349
  // Write zip to temp, extract with unzip (Linux/macOS) or Expand-Archive (Windows)
1350
1350
  const tmpArchive = path.join(GITHUB_BIN_DIR, `_tmp_${assetName}`);
@@ -1395,7 +1395,7 @@ async function installGitHubTool(
1395
1395
  }
1396
1396
  await fs.rename(srcBinary, destPath);
1397
1397
  await fs.rm(tmpDir, { recursive: true, force: true });
1398
- if (!isWindows) await fs.chmod(destPath, 0o755);
1398
+ if (!isWindows) await fs.chmod(destPath, 0o750);
1399
1399
  } else {
1400
1400
  // Bare binary (e.g. shfmt_*_linux_amd64)
1401
1401
  await fs.writeFile(destPath, assetBuffer, { mode: 0o755 });
@@ -1538,7 +1538,7 @@ async function installNpmTool(
1538
1538
  // Make executable on Unix
1539
1539
  if (process.platform !== "win32") {
1540
1540
  try {
1541
- await fs.chmod(binPath, 0o755);
1541
+ await fs.chmod(binPath, 0o750);
1542
1542
  } catch {
1543
1543
  /* ignore */
1544
1544
  }