@lsctech/polaris 0.4.0 → 0.4.2

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.
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Dev gate: autoresearch commands are only allowed inside a Polaris development context.
4
+ *
5
+ * "Polaris dev context" = the working directory or one of its ancestors contains a
6
+ * `package.json` whose `name` field equals `"@lsctech/polaris"`.
7
+ *
8
+ * This is intentionally narrow: it must match the monorepo where Polaris is developed,
9
+ * not arbitrary consumer repos that happen to depend on it.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.isPolarisDevContext = isPolarisDevContext;
13
+ exports.assertPolarisDevContext = assertPolarisDevContext;
14
+ const node_fs_1 = require("node:fs");
15
+ const node_path_1 = require("node:path");
16
+ function findPackageJson(startDir) {
17
+ let dir = (0, node_path_1.resolve)(startDir);
18
+ const root = (0, node_path_1.dirname)(dir); // stop at filesystem root
19
+ for (let i = 0; i < 20; i++) {
20
+ const candidate = (0, node_path_1.join)(dir, "package.json");
21
+ if ((0, node_fs_1.existsSync)(candidate))
22
+ return candidate;
23
+ const parent = (0, node_path_1.dirname)(dir);
24
+ if (parent === dir)
25
+ break; // reached root
26
+ dir = parent;
27
+ }
28
+ return null;
29
+ }
30
+ function isPolarisDevContext(startDir = process.cwd()) {
31
+ const pkgPath = findPackageJson(startDir);
32
+ if (!pkgPath)
33
+ return false;
34
+ try {
35
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf-8"));
36
+ return pkg["name"] === "@lsctech/polaris";
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /**
43
+ * Throws if not in a Polaris dev context.
44
+ * Call this at the start of any dev-gated command.
45
+ */
46
+ function assertPolarisDevContext(startDir) {
47
+ if (!isPolarisDevContext(startDir)) {
48
+ throw new Error("polaris autoresearch score is a dev-only command and can only run inside the Polaris development repository.\n" +
49
+ "It is not available in consumer repos.");
50
+ }
51
+ }
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ /**
3
+ * Binary gate evaluators for the autoresearch scoring pipeline.
4
+ *
5
+ * Gates are binary: false = PASSED (no problem detected), true = FAILED.
6
+ * When data is unavailable the gate is SKIPPED (not counted in score denominator).
7
+ *
8
+ * Gate contract:
9
+ * evaluate(artifacts): GateResult
10
+ *
11
+ * Gate names match the v1 spec from POL-422:
12
+ * user-intervened, foreman-resent-packet, foreman-fixed-worker-output,
13
+ * worker-output-required-fixing, validation-failed,
14
+ * worker-went-out-of-scope, foreman-token-burn-over-budget, state-repair-required
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ALL_GATES = void 0;
18
+ exports.gateUserIntervened = gateUserIntervened;
19
+ exports.gateForemanResentPacket = gateForemanResentPacket;
20
+ exports.gateForemanFixedWorkerOutput = gateForemanFixedWorkerOutput;
21
+ exports.gateWorkerOutputRequiredFixing = gateWorkerOutputRequiredFixing;
22
+ exports.gateValidationFailed = gateValidationFailed;
23
+ exports.gateWorkerWentOutOfScope = gateWorkerWentOutOfScope;
24
+ exports.gateForemanTokenBurnOverBudget = gateForemanTokenBurnOverBudget;
25
+ exports.gateStateRepairRequired = gateStateRepairRequired;
26
+ exports.readJsonLines = readJsonLines;
27
+ const node_fs_1 = require("node:fs");
28
+ const node_path_1 = require("node:path");
29
+ // ──────────────────────────────────────────────
30
+ // Token-burn budget constant (heuristic: 150k combined tokens = over budget)
31
+ // ──────────────────────────────────────────────
32
+ const TOKEN_BURN_THRESHOLD = 150_000;
33
+ // ──────────────────────────────────────────────
34
+ // Helpers
35
+ // ──────────────────────────────────────────────
36
+ function readJson(filePath) {
37
+ try {
38
+ return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ function readJsonLines(filePath) {
45
+ if (!(0, node_fs_1.existsSync)(filePath))
46
+ return [];
47
+ const lines = (0, node_fs_1.readFileSync)(filePath, "utf-8")
48
+ .split("\n")
49
+ .filter((l) => l.trim().length > 0);
50
+ const out = [];
51
+ for (const line of lines) {
52
+ try {
53
+ out.push(JSON.parse(line));
54
+ }
55
+ catch {
56
+ // skip malformed lines
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ function asRecord(v) {
62
+ if (v && typeof v === "object" && !Array.isArray(v)) {
63
+ return v;
64
+ }
65
+ return undefined;
66
+ }
67
+ // ──────────────────────────────────────────────
68
+ // Gate implementations
69
+ // ──────────────────────────────────────────────
70
+ /**
71
+ * user-intervened: Were any commits pushed to the branch after polaris-finalize
72
+ * but before PR merge?
73
+ *
74
+ * Evidence: WorkerResultContract.user_intervened (when populated by POL-421).
75
+ * Falls back to checking the last result file in results/ for the flag.
76
+ * Skips if no result files exist.
77
+ */
78
+ function gateUserIntervened(artifacts) {
79
+ const contract = artifacts.workerResultContracts.find((c) => c.user_intervened !== null && c.user_intervened !== undefined);
80
+ if (!contract)
81
+ return { gate: "user-intervened", outcome: "skipped", detail: "no contract with user_intervened populated" };
82
+ return {
83
+ gate: "user-intervened",
84
+ outcome: contract.user_intervened ? "failed" : "passed",
85
+ detail: contract.user_intervened ? "user_intervened=true on result contract" : undefined,
86
+ };
87
+ }
88
+ /**
89
+ * foreman-resent-packet: Was the packet dispatched more than once (dispatch_epoch > 1)?
90
+ *
91
+ * Evidence: dispatch_boundary.dispatch_epoch in current-state.json.
92
+ */
93
+ function gateForemanResentPacket(artifacts) {
94
+ const state = artifacts.currentState;
95
+ if (!state)
96
+ return { gate: "foreman-resent-packet", outcome: "skipped", detail: "current-state.json not found" };
97
+ const dispatchEpoch = asRecord(asRecord(state)?.["dispatch_boundary"])?.["dispatch_epoch"];
98
+ if (typeof dispatchEpoch !== "number") {
99
+ return { gate: "foreman-resent-packet", outcome: "skipped", detail: "dispatch_epoch not present in state" };
100
+ }
101
+ const failed = dispatchEpoch > 1;
102
+ return {
103
+ gate: "foreman-resent-packet",
104
+ outcome: failed ? "failed" : "passed",
105
+ detail: failed ? `dispatch_epoch=${dispatchEpoch}` : undefined,
106
+ };
107
+ }
108
+ /**
109
+ * foreman-fixed-worker-output: Did foreman make commits between child-complete
110
+ * and the next dispatch?
111
+ *
112
+ * Evidence: WorkerResultContract.foreman_intervened.
113
+ * Skips if no contracts present.
114
+ */
115
+ function gateForemanFixedWorkerOutput(artifacts) {
116
+ const contract = artifacts.workerResultContracts.find((c) => c.foreman_intervened !== null && c.foreman_intervened !== undefined);
117
+ if (!contract) {
118
+ return {
119
+ gate: "foreman-fixed-worker-output",
120
+ outcome: "skipped",
121
+ detail: "no contract with foreman_intervened populated",
122
+ };
123
+ }
124
+ return {
125
+ gate: "foreman-fixed-worker-output",
126
+ outcome: contract.foreman_intervened ? "failed" : "passed",
127
+ detail: contract.foreman_intervened ? "foreman_intervened=true on result contract" : undefined,
128
+ };
129
+ }
130
+ /**
131
+ * worker-output-required-fixing: Were commits pushed after polaris-finalize?
132
+ *
133
+ * Evidence: WorkerResultContract.user_intervened or foreman_intervened OR
134
+ * checking ledger for a "finalized" event and comparing last_commit timing.
135
+ * Skips when insufficient data is available.
136
+ */
137
+ function gateWorkerOutputRequiredFixing(artifacts) {
138
+ // If either intervention flag is set, worker output required fixing
139
+ const anyIntervention = artifacts.workerResultContracts.some((c) => (c.user_intervened !== null && c.user_intervened === true) ||
140
+ (c.foreman_intervened !== null && c.foreman_intervened === true));
141
+ if (anyIntervention) {
142
+ return {
143
+ gate: "worker-output-required-fixing",
144
+ outcome: "failed",
145
+ detail: "user or foreman intervention detected after finalize",
146
+ };
147
+ }
148
+ // Check ledger for finalized event + commit after finalize
149
+ const finalizedEvent = artifacts.ledgerEvents.find((e) => {
150
+ const rec = asRecord(e);
151
+ return rec?.["event"] === "finalized";
152
+ });
153
+ if (!finalizedEvent) {
154
+ return { gate: "worker-output-required-fixing", outcome: "skipped", detail: "no finalized event in ledger" };
155
+ }
156
+ // We can't easily inspect git log here (no exec in scoring engine — that is by design).
157
+ // With current artifact data, mark passed when finalized and no intervention flags.
158
+ return { gate: "worker-output-required-fixing", outcome: "passed" };
159
+ }
160
+ /**
161
+ * validation-failed: Did any child's result packet report a validation failure?
162
+ *
163
+ * Evidence: result JSONs in clusters/<cluster-id>/results/.
164
+ */
165
+ function gateValidationFailed(artifacts) {
166
+ if (artifacts.resultPackets.length === 0) {
167
+ return { gate: "validation-failed", outcome: "skipped", detail: "no result packets found" };
168
+ }
169
+ const failed = artifacts.resultPackets.some((packet) => {
170
+ const rec = asRecord(packet);
171
+ if (!rec)
172
+ return false;
173
+ // SuccessResultPacket has validation: { passed: string[] } — non-empty = passed
174
+ const validation = rec["validation"];
175
+ if (!validation)
176
+ return true; // no validation record → treat as failed
177
+ const valRec = asRecord(validation);
178
+ if (valRec) {
179
+ // { passed: string[] } form
180
+ if (Array.isArray(valRec["passed"]) && valRec["passed"].length > 0)
181
+ return false;
182
+ }
183
+ // string form: "passed" / "failed" / "skipped"
184
+ if (typeof validation === "string") {
185
+ const v = validation.toLowerCase();
186
+ return v === "failed" || v === "failure";
187
+ }
188
+ return false;
189
+ });
190
+ const allFailed = artifacts.resultPackets.every((packet) => {
191
+ const rec = asRecord(packet);
192
+ return rec?.["status"] === "failure";
193
+ });
194
+ if (allFailed && artifacts.resultPackets.length > 0) {
195
+ return { gate: "validation-failed", outcome: "failed", detail: "all result packets have status=failure" };
196
+ }
197
+ return {
198
+ gate: "validation-failed",
199
+ outcome: failed ? "failed" : "passed",
200
+ detail: failed ? "one or more result packets report validation failure" : undefined,
201
+ };
202
+ }
203
+ /**
204
+ * worker-went-out-of-scope: Did any worker result indicate out-of-scope work
205
+ * or emit worker-blocked events with approval_type=out-of-scope?
206
+ *
207
+ * Evidence: telemetry.jsonl worker-blocked events + result status.
208
+ */
209
+ function gateWorkerWentOutOfScope(artifacts) {
210
+ // Check telemetry for worker-blocked with out-of-scope
211
+ const outOfScopeBlock = artifacts.telemetryEvents.some((e) => {
212
+ const rec = asRecord(e);
213
+ return rec?.["event"] === "worker-blocked" && rec?.["approval_type"] === "out-of-scope";
214
+ });
215
+ if (outOfScopeBlock) {
216
+ return {
217
+ gate: "worker-went-out-of-scope",
218
+ outcome: "failed",
219
+ detail: "worker-blocked with approval_type=out-of-scope in telemetry",
220
+ };
221
+ }
222
+ // Check result packets for "blocked" status
223
+ const blockedResult = artifacts.resultPackets.some((packet) => {
224
+ const rec = asRecord(packet);
225
+ return rec?.["status"] === "blocked";
226
+ });
227
+ if (blockedResult) {
228
+ return { gate: "worker-went-out-of-scope", outcome: "failed", detail: "result packet status=blocked" };
229
+ }
230
+ if (artifacts.telemetryEvents.length === 0 && artifacts.resultPackets.length === 0) {
231
+ return { gate: "worker-went-out-of-scope", outcome: "skipped", detail: "no telemetry or result packets" };
232
+ }
233
+ return { gate: "worker-went-out-of-scope", outcome: "passed" };
234
+ }
235
+ /**
236
+ * foreman-token-burn-over-budget: Did the foreman's bootstrap context exceed the budget?
237
+ *
238
+ * Evidence: bootstrap-context-size events in telemetry.jsonl.
239
+ * Skipped when no such events are present.
240
+ */
241
+ function gateForemanTokenBurnOverBudget(artifacts) {
242
+ const sizeEvents = artifacts.telemetryEvents.filter((e) => {
243
+ const rec = asRecord(e);
244
+ return rec?.["event"] === "bootstrap-context-size";
245
+ });
246
+ if (sizeEvents.length === 0) {
247
+ return {
248
+ gate: "foreman-token-burn-over-budget",
249
+ outcome: "skipped",
250
+ detail: "no bootstrap-context-size events in telemetry",
251
+ };
252
+ }
253
+ // Use the maximum combined_estimated_tokens across all events
254
+ const maxTokens = Math.max(...sizeEvents.map((e) => {
255
+ const rec = asRecord(e);
256
+ const v = rec?.["combined_estimated_tokens"];
257
+ return typeof v === "number" ? v : 0;
258
+ }));
259
+ const failed = maxTokens > TOKEN_BURN_THRESHOLD;
260
+ return {
261
+ gate: "foreman-token-burn-over-budget",
262
+ outcome: failed ? "failed" : "passed",
263
+ detail: failed ? `max combined_estimated_tokens=${maxTokens} exceeds threshold=${TOKEN_BURN_THRESHOLD}` : undefined,
264
+ };
265
+ }
266
+ /**
267
+ * state-repair-required: Were medic result artifacts created for this cluster?
268
+ *
269
+ * Evidence: clusters/<cluster-id>/ directory — presence of medic result files.
270
+ */
271
+ function gateStateRepairRequired(artifacts) {
272
+ if (!artifacts.clusterDir || !(0, node_fs_1.existsSync)(artifacts.clusterDir)) {
273
+ return { gate: "state-repair-required", outcome: "skipped", detail: "cluster directory not found" };
274
+ }
275
+ // Medic results have filenames like CHART-xxx.json or medic-result-*.json
276
+ const files = safeReaddir(artifacts.clusterDir);
277
+ const hasMedicResult = files.some((f) => f.startsWith("CHART-") ||
278
+ f.startsWith("medic-result-") ||
279
+ f.includes("medic") && f.endsWith(".json"));
280
+ // Also check results/ subdirectory for any medic artifacts
281
+ const resultsDir = (0, node_path_1.join)(artifacts.clusterDir, "results");
282
+ const resultFiles = (0, node_fs_1.existsSync)(resultsDir) ? safeReaddir(resultsDir) : [];
283
+ const hasMedicInResults = resultFiles.some((f) => f.startsWith("CHART-") || f.startsWith("medic-result-"));
284
+ if (hasMedicResult || hasMedicInResults) {
285
+ return {
286
+ gate: "state-repair-required",
287
+ outcome: "failed",
288
+ detail: "medic result artifacts detected in cluster directory",
289
+ };
290
+ }
291
+ return { gate: "state-repair-required", outcome: "passed" };
292
+ }
293
+ function safeReaddir(dir) {
294
+ try {
295
+ return (0, node_fs_1.readdirSync)(dir);
296
+ }
297
+ catch {
298
+ return [];
299
+ }
300
+ }
301
+ exports.ALL_GATES = [
302
+ gateUserIntervened,
303
+ gateForemanResentPacket,
304
+ gateForemanFixedWorkerOutput,
305
+ gateWorkerOutputRequiredFixing,
306
+ gateValidationFailed,
307
+ gateWorkerWentOutOfScope,
308
+ gateForemanTokenBurnOverBudget,
309
+ gateStateRepairRequired,
310
+ ];
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routeProposals = exports.validateDiagnosisReport = exports.loadDiagnosisReport = exports.buildProposals = exports.FIX_ZONE_MAP = exports.ALL_GATES = exports.buildDiagnosisHints = exports.computeScore = exports.loadRunArtifacts = exports.scoreRun = exports.assertPolarisDevContext = exports.isPolarisDevContext = void 0;
4
+ var dev_gate_js_1 = require("./dev-gate.js");
5
+ Object.defineProperty(exports, "isPolarisDevContext", { enumerable: true, get: function () { return dev_gate_js_1.isPolarisDevContext; } });
6
+ Object.defineProperty(exports, "assertPolarisDevContext", { enumerable: true, get: function () { return dev_gate_js_1.assertPolarisDevContext; } });
7
+ var score_js_1 = require("./score.js");
8
+ Object.defineProperty(exports, "scoreRun", { enumerable: true, get: function () { return score_js_1.scoreRun; } });
9
+ Object.defineProperty(exports, "loadRunArtifacts", { enumerable: true, get: function () { return score_js_1.loadRunArtifacts; } });
10
+ Object.defineProperty(exports, "computeScore", { enumerable: true, get: function () { return score_js_1.computeScore; } });
11
+ Object.defineProperty(exports, "buildDiagnosisHints", { enumerable: true, get: function () { return score_js_1.buildDiagnosisHints; } });
12
+ var gates_js_1 = require("./gates.js");
13
+ Object.defineProperty(exports, "ALL_GATES", { enumerable: true, get: function () { return gates_js_1.ALL_GATES; } });
14
+ var proposal_js_1 = require("./proposal.js");
15
+ Object.defineProperty(exports, "FIX_ZONE_MAP", { enumerable: true, get: function () { return proposal_js_1.FIX_ZONE_MAP; } });
16
+ Object.defineProperty(exports, "buildProposals", { enumerable: true, get: function () { return proposal_js_1.buildProposals; } });
17
+ Object.defineProperty(exports, "loadDiagnosisReport", { enumerable: true, get: function () { return proposal_js_1.loadDiagnosisReport; } });
18
+ Object.defineProperty(exports, "validateDiagnosisReport", { enumerable: true, get: function () { return proposal_js_1.validateDiagnosisReport; } });
19
+ var routing_js_1 = require("./routing.js");
20
+ Object.defineProperty(exports, "routeProposals", { enumerable: true, get: function () { return routing_js_1.routeProposals; } });
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Autoresearch proposal schema and fix zone mapping.
4
+ *
5
+ * AutresearchProposal is the canonical output of the propose command.
6
+ * It maps failed binary gates to Polaris artifact fix zones and records
7
+ * the metadata needed for Linear issue creation.
8
+ *
9
+ * Non-goal: proposals are never auto-applied. They are filed for human review.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FIX_ZONE_MAP = void 0;
13
+ exports.buildProposals = buildProposals;
14
+ exports.loadDiagnosisReport = loadDiagnosisReport;
15
+ exports.validateDiagnosisReport = validateDiagnosisReport;
16
+ const node_fs_1 = require("node:fs");
17
+ // ──────────────────────────────────────────────
18
+ // Fix zone mapping table: gate → artifact type + hint
19
+ // ──────────────────────────────────────────────
20
+ exports.FIX_ZONE_MAP = {
21
+ "user-intervened": {
22
+ artifact_type: "worker-template",
23
+ hint: "User pushed commits after polaris-finalize. Tighten worker packet scope and instructions to reduce ambiguity.",
24
+ },
25
+ "foreman-resent-packet": {
26
+ artifact_type: "foreman-template",
27
+ hint: "Foreman dispatched the same child more than once. Review dispatch-boundary logic in foreman templates.",
28
+ },
29
+ "foreman-fixed-worker-output": {
30
+ artifact_type: "worker-template",
31
+ hint: "Foreman made corrective commits after child-complete. Strengthen allowed_scope and acceptance criteria in the worker template.",
32
+ },
33
+ "worker-output-required-fixing": {
34
+ artifact_type: "worker-template",
35
+ hint: "Post-finalize commits indicate insufficient worker output. Add stricter validation commands to the worker template.",
36
+ },
37
+ "validation-failed": {
38
+ artifact_type: "scoring-rule",
39
+ hint: "One or more children failed validation. Review validation_commands in the worker packet and the scoring rule thresholds.",
40
+ },
41
+ "worker-went-out-of-scope": {
42
+ artifact_type: "worker-template",
43
+ hint: "Worker emitted out-of-scope blocks or returned blocked status. Narrow allowed_scope in the worker template.",
44
+ },
45
+ "foreman-token-burn-over-budget": {
46
+ artifact_type: "foreman-template",
47
+ hint: "Foreman bootstrap context exceeds token budget. Reduce packet size in foreman template or raise budget threshold.",
48
+ },
49
+ "state-repair-required": {
50
+ artifact_type: "medic-template",
51
+ hint: "Medic artifacts detected — state repair was required. Review medic template heuristics and state-repair triggers.",
52
+ },
53
+ };
54
+ // ──────────────────────────────────────────────
55
+ // Build proposals from a DiagnosisReport
56
+ // ──────────────────────────────────────────────
57
+ /**
58
+ * Maps a DiagnosisReport's failed gates to AutresearchProposal objects.
59
+ * Gates without a fix zone entry are skipped.
60
+ */
61
+ function buildProposals(report) {
62
+ const proposals = [];
63
+ for (const gateId of report.failed_gates) {
64
+ const entry = exports.FIX_ZONE_MAP[gateId];
65
+ if (!entry)
66
+ continue; // no mapping → skip
67
+ proposals.push({
68
+ gate_id: gateId,
69
+ artifact_type: entry.artifact_type,
70
+ hint: entry.hint,
71
+ run_id: report.run_id,
72
+ evidence_run_ids: [report.run_id],
73
+ confidence: report.score,
74
+ fix_zone: `${entry.artifact_type}/${gateId}`,
75
+ });
76
+ }
77
+ return proposals;
78
+ }
79
+ // ──────────────────────────────────────────────
80
+ // Diagnosis file loader + schema validator
81
+ // ──────────────────────────────────────────────
82
+ /**
83
+ * Loads and validates a diagnosis file produced by `polaris autoresearch score`.
84
+ * Throws if the file is missing, unparseable, or does not match DiagnosisReport shape.
85
+ */
86
+ function loadDiagnosisReport(filePath) {
87
+ if (!(0, node_fs_1.existsSync)(filePath)) {
88
+ throw new Error(`Diagnosis file not found: ${filePath}`);
89
+ }
90
+ let raw;
91
+ try {
92
+ raw = JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
93
+ }
94
+ catch (err) {
95
+ throw new Error(`Failed to parse diagnosis file: ${err instanceof Error ? err.message : String(err)}`);
96
+ }
97
+ return validateDiagnosisReport(raw);
98
+ }
99
+ /**
100
+ * Validates a parsed value against the DiagnosisReport schema.
101
+ * Throws a descriptive error if required fields are missing or have wrong types.
102
+ */
103
+ function validateDiagnosisReport(raw) {
104
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
105
+ throw new Error("Diagnosis file must be a JSON object.");
106
+ }
107
+ const r = raw;
108
+ if (typeof r["run_id"] !== "string")
109
+ throw new Error("Diagnosis report missing required string field: run_id");
110
+ if (typeof r["evaluated_at"] !== "string")
111
+ throw new Error("Diagnosis report missing required string field: evaluated_at");
112
+ if (!Array.isArray(r["gate_results"]))
113
+ throw new Error("Diagnosis report missing required array field: gate_results");
114
+ if (!Array.isArray(r["failed_gates"]))
115
+ throw new Error("Diagnosis report missing required array field: failed_gates");
116
+ if (typeof r["score"] !== "number")
117
+ throw new Error("Diagnosis report missing required number field: score");
118
+ if (!Array.isArray(r["diagnosis_hints"]))
119
+ throw new Error("Diagnosis report missing required array field: diagnosis_hints");
120
+ return raw;
121
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ /**
3
+ * Autoresearch proposal routing.
4
+ *
5
+ * Routes AutresearchProposal objects to the Polaris development tracker (Linear)
6
+ * as new issues. Proposals are NEVER auto-applied — they are filed for human review.
7
+ *
8
+ * Each filed issue includes:
9
+ * - gate IDs, fix zone, evidence run IDs, artifact type, confidence
10
+ * - autoresearch-proposal label (when label ID is resolvable)
11
+ *
12
+ * This module is dev-gated: it must only run inside the Polaris dev context.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.routeProposals = routeProposals;
16
+ const node_https_1 = require("node:https");
17
+ async function linearGraphql(apiKey, query, variables) {
18
+ const payload = JSON.stringify({ query, variables });
19
+ const raw = await new Promise((resolve, reject) => {
20
+ const req = (0, node_https_1.request)({
21
+ hostname: "api.linear.app",
22
+ path: "/graphql",
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ Authorization: apiKey,
27
+ },
28
+ }, (res) => {
29
+ const chunks = [];
30
+ res.on("data", (chunk) => chunks.push(chunk));
31
+ res.on("end", () => {
32
+ const body = Buffer.concat(chunks).toString("utf-8");
33
+ if ((res.statusCode ?? 0) >= 400) {
34
+ reject(new Error(`Linear API ${res.statusCode}: ${body}`));
35
+ return;
36
+ }
37
+ resolve(body);
38
+ });
39
+ });
40
+ req.on("error", reject);
41
+ req.write(payload);
42
+ req.end();
43
+ });
44
+ const parsed = JSON.parse(raw);
45
+ if (parsed.errors && parsed.errors.length > 0) {
46
+ throw new Error(`Linear GraphQL errors: ${JSON.stringify(parsed.errors)}`);
47
+ }
48
+ if (!parsed.data)
49
+ throw new Error("Linear API returned no data.");
50
+ return parsed.data;
51
+ }
52
+ async function resolveTeamId(apiKey, teamKey) {
53
+ // teamKey may be a UUID or a display name
54
+ const data = await linearGraphql(apiKey, "query AutresearchTeams { teams(first: 250) { nodes { id name } } }", {});
55
+ const match = data.teams.nodes.find((t) => t.id === teamKey || t.name === teamKey || t.name.toLowerCase() === teamKey.toLowerCase());
56
+ if (!match)
57
+ throw new Error(`Linear team not found: '${teamKey}'. Available: ${data.teams.nodes.map((t) => t.name).join(", ")}`);
58
+ return match.id;
59
+ }
60
+ async function resolveAutoresearchLabelId(apiKey, teamId) {
61
+ try {
62
+ const data = await linearGraphql(apiKey, `query AutresearchLabel($teamId: String!) {
63
+ issueLabels(filter: { team: { id: { eq: $teamId } }, name: { containsIgnoreCase: "autoresearch-proposal" } }, first: 10) {
64
+ nodes { id name }
65
+ }
66
+ }`, { teamId });
67
+ return data.issueLabels.nodes[0]?.id;
68
+ }
69
+ catch {
70
+ // Label lookup is best-effort — don't fail the whole route if it fails
71
+ return undefined;
72
+ }
73
+ }
74
+ // ──────────────────────────────────────────────
75
+ // Issue body builder
76
+ // ──────────────────────────────────────────────
77
+ function buildIssueBody(proposal) {
78
+ return [
79
+ `## Autoresearch Proposal`,
80
+ ``,
81
+ `> This issue was filed automatically by \`polaris autoresearch propose\`. **Do not auto-apply.** Human review is required.`,
82
+ ``,
83
+ `| Field | Value |`,
84
+ `|---|---|`,
85
+ `| Gate ID | \`${proposal.gate_id}\` |`,
86
+ `| Artifact type | \`${proposal.artifact_type}\` |`,
87
+ `| Fix zone | \`${proposal.fix_zone}\` |`,
88
+ `| Confidence | ${(proposal.confidence * 100).toFixed(1)}% |`,
89
+ `| Run ID | \`${proposal.run_id}\` |`,
90
+ `| Evidence run IDs | ${proposal.evidence_run_ids.map((id) => `\`${id}\``).join(", ")} |`,
91
+ ``,
92
+ `## Hint`,
93
+ ``,
94
+ proposal.hint,
95
+ ``,
96
+ `## Review checklist`,
97
+ ``,
98
+ `- [ ] Confirm the gate failure is reproducible`,
99
+ `- [ ] Identify the specific artifact to change`,
100
+ `- [ ] Draft the proposed change`,
101
+ `- [ ] Test against a real run`,
102
+ `- [ ] Approve and merge`,
103
+ ].join("\n");
104
+ }
105
+ /**
106
+ * Routes proposals to Linear as new issues for human review.
107
+ * Never auto-applies proposals.
108
+ */
109
+ async function routeProposals(proposals, options) {
110
+ const { apiKey, teamKey, dryRun = false } = options;
111
+ const runId = proposals[0]?.run_id ?? "unknown";
112
+ const teamId = await resolveTeamId(apiKey, teamKey);
113
+ const labelId = dryRun ? undefined : await resolveAutoresearchLabelId(apiKey, teamId);
114
+ const filed = [];
115
+ for (const proposal of proposals) {
116
+ const title = `[autoresearch-proposal] ${proposal.artifact_type}: ${proposal.gate_id}`;
117
+ const body = buildIssueBody(proposal);
118
+ if (dryRun) {
119
+ filed.push({
120
+ proposal_gate_id: proposal.gate_id,
121
+ created: false,
122
+ issue_id: "(dry-run)",
123
+ issue_identifier: "(dry-run)",
124
+ issue_url: undefined,
125
+ });
126
+ continue;
127
+ }
128
+ try {
129
+ const data = await linearGraphql(apiKey, `mutation AutresearchPropose($teamId: String!, $title: String!, $body: String!, $labelIds: [String!]) {
130
+ issueCreate(input: {
131
+ teamId: $teamId
132
+ title: $title
133
+ description: $body
134
+ labelIds: $labelIds
135
+ }) {
136
+ success
137
+ issue { id identifier url }
138
+ }
139
+ }`, {
140
+ teamId,
141
+ title,
142
+ body,
143
+ labelIds: labelId ? [labelId] : [],
144
+ });
145
+ if (data.issueCreate.success && data.issueCreate.issue) {
146
+ filed.push({
147
+ proposal_gate_id: proposal.gate_id,
148
+ created: true,
149
+ issue_id: data.issueCreate.issue.id,
150
+ issue_identifier: data.issueCreate.issue.identifier,
151
+ issue_url: data.issueCreate.issue.url,
152
+ });
153
+ }
154
+ else {
155
+ filed.push({
156
+ proposal_gate_id: proposal.gate_id,
157
+ created: false,
158
+ error: "issueCreate returned success=false",
159
+ });
160
+ }
161
+ }
162
+ catch (err) {
163
+ filed.push({
164
+ proposal_gate_id: proposal.gate_id,
165
+ created: false,
166
+ error: err instanceof Error ? err.message : String(err),
167
+ });
168
+ }
169
+ }
170
+ const totalCreated = filed.filter((r) => r.created).length;
171
+ const totalErrors = filed.filter((r) => !r.created && r.error).length;
172
+ return {
173
+ run_id: runId,
174
+ team_id: teamId,
175
+ filed,
176
+ total_proposals: proposals.length,
177
+ total_created: totalCreated,
178
+ total_errors: totalErrors,
179
+ };
180
+ }