{"record":{"id":"182463a4f188b0f3","repo":"iOfficeAI/OfficeCLI","slug":"plugin-contract-violation","errorCode":"plugin_contract_violation","errorMessage":"Plugin '{resolved.Manifest.Name}' emitted invalid JSON at line #{items.Count}: {ex.Message}","messagePattern":"Plugin '(.+?)' emitted invalid JSON at line #(.+?): (.+?)","errorType":"exception","errorClass":"CliException","httpStatus":null,"severity":"error","filePath":"src/officecli/CommandBuilder.Plugins.cs","lineNumber":292,"sourceCode":"            var findings = new List<LintFinding>();\n\n            void OnLine(string raw)\n            {\n                // Mirrors DumpReaderInvoker: strip per-line UTF-8 BOM so a\n                // plugin that BOMs every JSONL line passes lint as well as\n                // it passes invocation.\n                var line = raw.TrimStart('﻿').Trim();\n                if (line.Length == 0) return;\n                if (line[0] == '[')\n                    throw new CliException(\n                        $\"Plugin '{resolved.Manifest.Name}' emitted a JSON array; protocol v1 requires JSONL (one BatchItem per line).\")\n                    { Code = \"corrupt_batch\" };\n\n                BatchItem? item;\n                try { item = JsonSerializer.Deserialize(line, BatchJsonContext.Default.BatchItem); }\n                catch (JsonException ex)\n                {\n                    throw new CliException(\n                        $\"Plugin '{resolved.Manifest.Name}' emitted invalid JSON at line #{items.Count}: {ex.Message}\")\n                    { Code = \"plugin_contract_violation\" };\n                }\n                if (item is null) return;\n                items.Add(item);\n            }\n\n            var idle = resolved.Manifest.ResolveIdleTimeout(\"dump\");\n            var runResult = PluginProcess.Run(new PluginProcess.RunOptions\n            {\n                ExecutablePath = resolved.ExecutablePath,\n                Arguments = new[] { \"dump\", fixturePath },\n                IdleTimeoutSeconds = idle,\n                OnStdoutLine = OnLine,\n            });\n            if (PluginProcess.LineCallbackError is CliException ce) throw ce;\n            if (PluginProcess.LineCallbackError is not null) throw PluginProcess.LineCallbackError;\n            if (runResult.IdleTimedOut)","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/iOfficeAI/OfficeCLI/blob/1ced45e900782c5083ed550ddf328ee974e425e7/src/officecli/CommandBuilder.Plugins.cs#L274-L310","documentation":"A stdout line from the plugin failed to deserialize as a BatchItem (JsonException). The message includes the line index and the deserializer's error detail, so the offending line and reason are pinpointed.","triggerScenarios":"A plugin emits a line that is non-empty and not '[' but is not valid JSON or does not match BatchItem's expected shape — e.g. trailing comma, missing required field, a log line leaking into stdout.","commonSituations":"Stray log/debug text printed to stdout instead of stderr; a BatchItem missing a required property; trailing commas or single quotes; a partial line from a crash.","solutions":["Send all logs to stderr, never stdout (stdout is the JSONL data channel).","Validate each emitted line parses as BatchItem before publishing.","Check the reported line number and the JSON error message."],"exampleFix":"// before: plugin prints 'loading fixture...' to stdout, then items\n// after:  logs go to stderr; stdout is pure JSONL\nConsole.Error.WriteLine(\"loading fixture...\");\nConsole.WriteLine(JsonSerializer.Serialize(item));","handlingStrategy":"try-catch","validationCode":"// Plugin author: round-trip-validate every line before emitting.\nvar line = JsonSerializer.Serialize(item);\nJsonSerializer.Deserialize(line, BatchJsonContext.Default.BatchItem); // throws if bad","typeGuard":null,"tryCatchPattern":"// Lint runner: collect per-line failures into a single report.\nvar defects = new List<string>();\ntry { LintPlugin(plugin, fixture); }\ncatch (CliException e) when (e.Code == \"plugin_contract_violation\") {\n    defects.Add($\"{plugin}: {e.Message}\");\n}","preventionTips":["stdout is data-only (JSONL); all diagnostics to stderr.","Round-trip-serialize each BatchItem in the plugin's own tests.","Watch for stray prints, partial flushes, and crash fragments on stdout."],"tags":["plugins","lint","json","plugin-contract","deserialization"],"backgroundTag":null,"analyzedSha":"1ced45e900782c5083ed550ddf328ee974e425e7","analyzedAt":"2026-08-13T13:01:07.193Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}