@lsctech/polaris 0.4.9 → 0.5.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.
- package/dist/autoresearch/gates.js +47 -0
- package/dist/autoresearch/proposal.js +183 -1
- package/dist/autoresearch/score.js +276 -0
- package/dist/cli/init.js +17 -0
- package/dist/cluster-state/store.js +70 -1
- package/dist/config/defaults.js +25 -0
- package/dist/config/validator.js +390 -0
- package/dist/finalize/artifact-policy.js +3 -1
- package/dist/finalize/index.js +73 -17
- package/dist/finalize/linear.js +45 -0
- package/dist/finalize/run-report.js +68 -12
- package/dist/loop/adapters/agent-subtask.js +19 -0
- package/dist/loop/adapters/cli-subtask-bridge.js +0 -1
- package/dist/loop/adapters/terminal-cli.js +194 -14
- package/dist/loop/checkpoint.js +40 -0
- package/dist/loop/dispatch-boundary.js +29 -0
- package/dist/loop/dispatch.js +193 -22
- package/dist/loop/orphan-recovery.js +56 -0
- package/dist/loop/parent.js +197 -22
- package/dist/loop/router/engine.js +229 -0
- package/dist/loop/router/index.js +5 -0
- package/dist/loop/router/types.js +2 -0
- package/dist/loop/worker-packet.js +16 -0
- package/dist/qc/artifacts.js +117 -0
- package/dist/qc/attribution.js +266 -0
- package/dist/qc/autofix.js +77 -0
- package/dist/qc/index.js +30 -0
- package/dist/qc/orchestration.js +150 -0
- package/dist/qc/policy.js +70 -0
- package/dist/qc/provider.js +28 -0
- package/dist/qc/providers/coderabbit.js +214 -0
- package/dist/qc/providers/index.js +5 -0
- package/dist/qc/registry.js +16 -0
- package/dist/qc/routing.js +55 -0
- package/dist/qc/runner.js +137 -0
- package/dist/qc/schemas.js +110 -0
- package/dist/qc/security-category.js +11 -0
- package/dist/qc/severity.js +78 -0
- package/dist/qc/triggers.js +92 -0
- package/dist/qc/types.js +9 -0
- package/dist/runtime/scheduling/child-selector.js +81 -0
- package/package.json +1 -1
|
@@ -195,6 +195,10 @@ function compileStartupPacket(input) {
|
|
|
195
195
|
prompt_mode: 'full',
|
|
196
196
|
prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
|
|
197
197
|
role_context: roleContextForWorkerRole('startup'),
|
|
198
|
+
routing_context: {
|
|
199
|
+
task_type: "startup",
|
|
200
|
+
required_capabilities: ["orchestration"],
|
|
201
|
+
},
|
|
198
202
|
result_file_contract: {
|
|
199
203
|
result_file: input.resultFile,
|
|
200
204
|
result_required_fields: Object.fromEntries([
|
|
@@ -287,6 +291,10 @@ function compileImplPacket(input) {
|
|
|
287
291
|
prompt_mode: promptMode,
|
|
288
292
|
prompt_metrics: promptResult.metrics,
|
|
289
293
|
role_context: roleContextForWorkerRole('impl'),
|
|
294
|
+
routing_context: {
|
|
295
|
+
task_type: "impl",
|
|
296
|
+
required_capabilities: ["implementation"],
|
|
297
|
+
},
|
|
290
298
|
prohibited_write_paths: exports.WORKER_PROHIBITED_WRITE_PATHS,
|
|
291
299
|
result_file_contract: {
|
|
292
300
|
result_file: input.resultFile,
|
|
@@ -341,6 +349,10 @@ function compileFinalizePacket(input) {
|
|
|
341
349
|
prompt_mode: 'full',
|
|
342
350
|
prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
|
|
343
351
|
role_context: roleContextForWorkerRole('finalize'),
|
|
352
|
+
routing_context: {
|
|
353
|
+
task_type: "finalize",
|
|
354
|
+
required_capabilities: ["finalization"],
|
|
355
|
+
},
|
|
344
356
|
result_file_contract: {
|
|
345
357
|
result_file: input.resultFile,
|
|
346
358
|
result_required_fields: Object.fromEntries([
|
|
@@ -390,6 +402,10 @@ function compilePreflightPacket(input) {
|
|
|
390
402
|
prompt_mode: 'full',
|
|
391
403
|
prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
|
|
392
404
|
role_context: roleContextForWorkerRole('preflight'),
|
|
405
|
+
routing_context: {
|
|
406
|
+
task_type: "startup",
|
|
407
|
+
required_capabilities: ["orchestration"],
|
|
408
|
+
},
|
|
393
409
|
result_file_contract: {
|
|
394
410
|
result_file: input.resultFile,
|
|
395
411
|
result_required_fields: Object.fromEntries([
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getQcArtifactDir = getQcArtifactDir;
|
|
37
|
+
exports.writeQcArtifact = writeQcArtifact;
|
|
38
|
+
exports.readQcArtifact = readQcArtifact;
|
|
39
|
+
exports.listQcArtifactIds = listQcArtifactIds;
|
|
40
|
+
const node_fs_1 = require("node:fs");
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const schemas_js_1 = require("./schemas.js");
|
|
43
|
+
/**
|
|
44
|
+
* Returns the cluster-scoped QC evidence directory.
|
|
45
|
+
* Artifacts live under `.polaris/clusters/<cluster-id>/qc/` so that finalize's
|
|
46
|
+
* artifact policy can promote them as durable evidence.
|
|
47
|
+
*/
|
|
48
|
+
function getQcArtifactDir(clusterId, repoRoot) {
|
|
49
|
+
return path.join(repoRoot || process.cwd(), ".polaris", "clusters", clusterId, "qc");
|
|
50
|
+
}
|
|
51
|
+
function getArtifactPath(clusterId, qcRunId, repoRoot) {
|
|
52
|
+
return path.join(getQcArtifactDir(clusterId, repoRoot), `${qcRunId}.json`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Write a normalized QC result under the active cluster's evidence surface.
|
|
56
|
+
* The file is written atomically via a temp file + rename.
|
|
57
|
+
* Returns the absolute artifact path.
|
|
58
|
+
*/
|
|
59
|
+
function writeQcArtifact(clusterId, result, repoRoot) {
|
|
60
|
+
const artifactDir = getQcArtifactDir(clusterId, repoRoot);
|
|
61
|
+
(0, node_fs_1.mkdirSync)(artifactDir, { recursive: true });
|
|
62
|
+
const artifactPath = getArtifactPath(clusterId, result.qcRunId, repoRoot);
|
|
63
|
+
const tempPath = `${artifactPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
64
|
+
try {
|
|
65
|
+
(0, node_fs_1.writeFileSync)(tempPath, JSON.stringify(result, null, 2), "utf-8");
|
|
66
|
+
(0, node_fs_1.renameSync)(tempPath, artifactPath);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
try {
|
|
70
|
+
(0, node_fs_1.unlinkSync)(tempPath);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Ignore cleanup failure.
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
return artifactPath;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Read a QC artifact by its run id.
|
|
81
|
+
* Returns null if the file does not exist or fails validation.
|
|
82
|
+
*/
|
|
83
|
+
function readQcArtifact(clusterId, qcRunId, repoRoot) {
|
|
84
|
+
const artifactPath = getArtifactPath(clusterId, qcRunId, repoRoot);
|
|
85
|
+
try {
|
|
86
|
+
const data = (0, node_fs_1.readFileSync)(artifactPath, "utf-8");
|
|
87
|
+
const parsed = JSON.parse(data);
|
|
88
|
+
const validation = (0, schemas_js_1.validateQcResult)(parsed);
|
|
89
|
+
if (!validation.success) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return validation.result;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error.code === "ENOENT") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* List persisted QC run ids for a cluster.
|
|
103
|
+
*/
|
|
104
|
+
function listQcArtifactIds(clusterId, repoRoot) {
|
|
105
|
+
const artifactDir = getQcArtifactDir(clusterId, repoRoot);
|
|
106
|
+
try {
|
|
107
|
+
return (0, node_fs_1.readdirSync)(artifactDir, "utf-8")
|
|
108
|
+
.filter((name) => name.endsWith(".json"))
|
|
109
|
+
.map((name) => name.slice(0, -5));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === "ENOENT") {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC attribution resolver.
|
|
4
|
+
*
|
|
5
|
+
* Correlates normalized QC findings to the child/worker most likely responsible
|
|
6
|
+
* for the changed code. It uses durable evidence only: result packet pointers,
|
|
7
|
+
* child commits, dispatch records, optional PR review metadata, and (as a
|
|
8
|
+
* fallback) git history.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.resolveAttribution = resolveAttribution;
|
|
45
|
+
exports.buildChangedFileOwnership = buildChangedFileOwnership;
|
|
46
|
+
exports.resolveAttributionWithOwnership = resolveAttributionWithOwnership;
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const node_child_process_1 = require("node:child_process");
|
|
49
|
+
function normalizeFilePath(filePath, repoRoot) {
|
|
50
|
+
if (!filePath)
|
|
51
|
+
return null;
|
|
52
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.join(repoRoot ?? process.cwd(), filePath);
|
|
53
|
+
return path.relative(repoRoot ?? process.cwd(), absolute).replace(/\\/g, "/");
|
|
54
|
+
}
|
|
55
|
+
function collectResultChangedFiles(result) {
|
|
56
|
+
if (result.changed_files && result.changed_files.length > 0) {
|
|
57
|
+
return result.changed_files;
|
|
58
|
+
}
|
|
59
|
+
const fromData = result.result_data?.changed_files;
|
|
60
|
+
if (Array.isArray(fromData) && fromData.every((f) => typeof f === "string")) {
|
|
61
|
+
return fromData;
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
function collectChangedFilesByChild(context) {
|
|
66
|
+
if (context.changedFilesByChild) {
|
|
67
|
+
return context.changedFilesByChild;
|
|
68
|
+
}
|
|
69
|
+
const ownership = {};
|
|
70
|
+
if (context.completedResults) {
|
|
71
|
+
for (const [childId, result] of Object.entries(context.completedResults)) {
|
|
72
|
+
const files = collectResultChangedFiles(result);
|
|
73
|
+
if (files.length > 0) {
|
|
74
|
+
ownership[childId] = files.map((f) => normalizeFilePath(f, context.repoRoot)).filter((f) => f !== null);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (Object.keys(ownership).length > 0) {
|
|
79
|
+
return ownership;
|
|
80
|
+
}
|
|
81
|
+
if (context.clusterState?.commits) {
|
|
82
|
+
const base = context.baseBranch ?? "main";
|
|
83
|
+
for (const [childId, commitSha] of Object.entries(context.clusterState.commits)) {
|
|
84
|
+
const files = diffFilesForCommit(context.repoRoot, base, commitSha);
|
|
85
|
+
if (files.length > 0) {
|
|
86
|
+
ownership[childId] = files.map((f) => normalizeFilePath(f, context.repoRoot)).filter((f) => f !== null);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return ownership;
|
|
91
|
+
}
|
|
92
|
+
function diffFilesForCommit(repoRoot, baseBranch, commitSha) {
|
|
93
|
+
if (!repoRoot)
|
|
94
|
+
return [];
|
|
95
|
+
try {
|
|
96
|
+
const output = (0, node_child_process_1.execFileSync)("git", ["diff", "--name-only", `${baseBranch}..${commitSha}`], { cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
97
|
+
return output
|
|
98
|
+
.trim()
|
|
99
|
+
.split("\n")
|
|
100
|
+
.filter((line) => line.length > 0);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function ownersForFile(filePath, ownership) {
|
|
107
|
+
const owners = [];
|
|
108
|
+
for (const [childId, files] of Object.entries(ownership)) {
|
|
109
|
+
if (files.includes(filePath)) {
|
|
110
|
+
owners.push(childId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return owners;
|
|
114
|
+
}
|
|
115
|
+
function resolveCommitSha(finding) {
|
|
116
|
+
return finding.commitSha ?? finding.attribution?.commitSha;
|
|
117
|
+
}
|
|
118
|
+
function isKnownChildCommit(commitSha, context) {
|
|
119
|
+
if (!commitSha)
|
|
120
|
+
return false;
|
|
121
|
+
const commits = Object.values(context.clusterState?.commits ?? {});
|
|
122
|
+
if (commits.includes(commitSha))
|
|
123
|
+
return true;
|
|
124
|
+
if (context.completedResults) {
|
|
125
|
+
return Object.values(context.completedResults).some((r) => r.commit === commitSha);
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
function isPreExistingFile(filePath, context) {
|
|
130
|
+
if (!context.repoRoot)
|
|
131
|
+
return false;
|
|
132
|
+
try {
|
|
133
|
+
(0, node_child_process_1.execFileSync)("git", ["cat-file", "-e", `HEAD:${filePath}`], {
|
|
134
|
+
cwd: context.repoRoot,
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
136
|
+
});
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the most likely attribution for a finding.
|
|
145
|
+
*
|
|
146
|
+
* The resolver never fabricates confidence. When evidence is weak it falls
|
|
147
|
+
* back to explicitly named reason codes so downstream routing can treat the
|
|
148
|
+
* finding conservatively.
|
|
149
|
+
*/
|
|
150
|
+
function resolveAttribution(finding, context) {
|
|
151
|
+
const threshold = context.providerConfidenceThreshold ?? 0.5;
|
|
152
|
+
const filePath = normalizeFilePath(finding.filePath, context.repoRoot);
|
|
153
|
+
const ownership = filePath ? collectChangedFilesByChild(context) : {};
|
|
154
|
+
const owners = filePath ? ownersForFile(filePath, ownership) : [];
|
|
155
|
+
const commitSha = resolveCommitSha(finding);
|
|
156
|
+
if (finding.confidence !== undefined && finding.confidence < threshold) {
|
|
157
|
+
const childId = owners.length === 1 ? owners[0] : undefined;
|
|
158
|
+
return {
|
|
159
|
+
confidence: "low",
|
|
160
|
+
reason: "provider-uncertain",
|
|
161
|
+
...(childId ? { childId } : {}),
|
|
162
|
+
filePath: filePath ?? undefined,
|
|
163
|
+
commitSha,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (!filePath) {
|
|
167
|
+
return {
|
|
168
|
+
confidence: "unattributed",
|
|
169
|
+
reason: "provider-uncertain",
|
|
170
|
+
commitSha,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const commitKnown = isKnownChildCommit(commitSha, context);
|
|
174
|
+
const commitMatchesOwner = commitSha && owners.some((childId) => {
|
|
175
|
+
const childCommit = context.clusterState?.commits?.[childId] ?? context.completedResults?.[childId]?.commit;
|
|
176
|
+
return childCommit === commitSha;
|
|
177
|
+
});
|
|
178
|
+
if (owners.length === 1) {
|
|
179
|
+
const childId = owners[0];
|
|
180
|
+
if (commitSha && !commitMatchesOwner) {
|
|
181
|
+
return {
|
|
182
|
+
confidence: "low",
|
|
183
|
+
reason: "provider-uncertain",
|
|
184
|
+
childId,
|
|
185
|
+
filePath,
|
|
186
|
+
commitSha,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (commitMatchesOwner) {
|
|
190
|
+
return {
|
|
191
|
+
confidence: "high",
|
|
192
|
+
reason: "commit-line-match",
|
|
193
|
+
childId,
|
|
194
|
+
filePath,
|
|
195
|
+
commitSha,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
confidence: "high",
|
|
200
|
+
reason: "changed-file-owner",
|
|
201
|
+
childId,
|
|
202
|
+
filePath,
|
|
203
|
+
commitSha,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (owners.length > 1) {
|
|
207
|
+
return {
|
|
208
|
+
confidence: "low",
|
|
209
|
+
reason: "shared-file",
|
|
210
|
+
childId: owners[0],
|
|
211
|
+
filePath,
|
|
212
|
+
commitSha,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// File is not owned by any child. Use PR review metadata as a weak signal.
|
|
216
|
+
const reviewerFiles = Object.values(context.prReviewMetadata?.changedFilesByReviewer ?? {}).flat();
|
|
217
|
+
if (reviewerFiles.includes(filePath)) {
|
|
218
|
+
return {
|
|
219
|
+
confidence: "low",
|
|
220
|
+
reason: "child-scope-match",
|
|
221
|
+
filePath,
|
|
222
|
+
commitSha,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (commitSha) {
|
|
226
|
+
if (commitKnown) {
|
|
227
|
+
return {
|
|
228
|
+
confidence: "medium",
|
|
229
|
+
reason: "commit-line-match",
|
|
230
|
+
filePath,
|
|
231
|
+
commitSha,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
confidence: "low",
|
|
236
|
+
reason: "provider-uncertain",
|
|
237
|
+
filePath,
|
|
238
|
+
commitSha,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (isPreExistingFile(filePath, context)) {
|
|
242
|
+
return {
|
|
243
|
+
confidence: "low",
|
|
244
|
+
reason: "pre-existing",
|
|
245
|
+
filePath,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
confidence: "unattributed",
|
|
250
|
+
reason: "unattributed",
|
|
251
|
+
filePath,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Expose the ownership map built by the resolver. Useful for tests and for
|
|
256
|
+
* callers that want to cache the map across many findings.
|
|
257
|
+
*/
|
|
258
|
+
function buildChangedFileOwnership(context) {
|
|
259
|
+
return collectChangedFilesByChild(context);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Re-resolve attribution using a pre-built ownership map.
|
|
263
|
+
*/
|
|
264
|
+
function resolveAttributionWithOwnership(finding, context, ownership) {
|
|
265
|
+
return resolveAttribution(finding, { ...context, changedFilesByChild: ownership });
|
|
266
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC auto-fix gating.
|
|
4
|
+
*
|
|
5
|
+
* Determines whether a normalized finding is allowed to attempt an unattended
|
|
6
|
+
* auto-fix. Gating is intentionally conservative: auto-fix is allowed only for
|
|
7
|
+
* explicitly eligible providers, low/medium severities, non-security categories,
|
|
8
|
+
* clean branches, and fix modes that are known to be safe.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.QC_SAFE_FIX_MODES = void 0;
|
|
12
|
+
exports.isAutofixEligible = isAutofixEligible;
|
|
13
|
+
const security_category_js_1 = require("./security-category.js");
|
|
14
|
+
const severity_js_1 = require("./severity.js");
|
|
15
|
+
/** Fix modes considered safe for unattended application. */
|
|
16
|
+
exports.QC_SAFE_FIX_MODES = [
|
|
17
|
+
"refactor",
|
|
18
|
+
"style",
|
|
19
|
+
"format",
|
|
20
|
+
"typo",
|
|
21
|
+
"lint-fix",
|
|
22
|
+
"apply-suggestion",
|
|
23
|
+
"safe",
|
|
24
|
+
];
|
|
25
|
+
function providerConfig(config, provider) {
|
|
26
|
+
if (!provider)
|
|
27
|
+
return undefined;
|
|
28
|
+
return config.providers?.[provider];
|
|
29
|
+
}
|
|
30
|
+
function isSafeFixMode(suggestedAction) {
|
|
31
|
+
if (!suggestedAction?.trim())
|
|
32
|
+
return false;
|
|
33
|
+
const normalized = suggestedAction.trim().toLowerCase();
|
|
34
|
+
if (exports.QC_SAFE_FIX_MODES.includes(normalized))
|
|
35
|
+
return true;
|
|
36
|
+
// A provider may describe a safe fix with a sentence that starts with a safe mode keyword.
|
|
37
|
+
return exports.QC_SAFE_FIX_MODES.some((mode) => normalized.startsWith(mode));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check whether a finding may attempt an unattended auto-fix.
|
|
41
|
+
*
|
|
42
|
+
* Returns an object so callers can log why a fix was blocked.
|
|
43
|
+
*/
|
|
44
|
+
function isAutofixEligible(finding, config, context = {}) {
|
|
45
|
+
const globalPolicy = config.autoFix ?? "disabled";
|
|
46
|
+
if (globalPolicy === "disabled") {
|
|
47
|
+
return { eligible: false, reason: "auto-fix disabled globally" };
|
|
48
|
+
}
|
|
49
|
+
const routePolicy = context.routeName ? config.routes?.[context.routeName]?.autoFix : undefined;
|
|
50
|
+
if (routePolicy === "disabled") {
|
|
51
|
+
return { eligible: false, reason: "auto-fix disabled for route" };
|
|
52
|
+
}
|
|
53
|
+
const pCfg = providerConfig(config, context.provider);
|
|
54
|
+
if (!pCfg?.autoFixEligible) {
|
|
55
|
+
return { eligible: false, reason: "provider not auto-fix eligible" };
|
|
56
|
+
}
|
|
57
|
+
const maxAutofixSeverity = "medium";
|
|
58
|
+
if ((0, severity_js_1.compareSeverity)(finding.severity, maxAutofixSeverity) > 0) {
|
|
59
|
+
return { eligible: false, reason: `severity ${finding.severity} exceeds auto-fix threshold` };
|
|
60
|
+
}
|
|
61
|
+
if ((0, security_category_js_1.isSecurityCategory)(finding.category)) {
|
|
62
|
+
return { eligible: false, reason: "security-sensitive finding" };
|
|
63
|
+
}
|
|
64
|
+
if (context.branchDirty) {
|
|
65
|
+
return { eligible: false, reason: "branch has uncommitted changes" };
|
|
66
|
+
}
|
|
67
|
+
if (!finding.fixAvailable) {
|
|
68
|
+
return { eligible: false, reason: "no fix available" };
|
|
69
|
+
}
|
|
70
|
+
if (!isSafeFixMode(finding.suggestedAction)) {
|
|
71
|
+
return { eligible: false, reason: `fix mode "${finding.suggestedAction}" is not in safe list` };
|
|
72
|
+
}
|
|
73
|
+
if (globalPolicy === "dry-run") {
|
|
74
|
+
return { eligible: true, reason: "dry-run mode: fix may be generated but not applied" };
|
|
75
|
+
}
|
|
76
|
+
return { eligible: true, reason: "auto-fix eligible" };
|
|
77
|
+
}
|
package/dist/qc/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types.js"), exports);
|
|
18
|
+
__exportStar(require("./provider.js"), exports);
|
|
19
|
+
__exportStar(require("./providers/index.js"), exports);
|
|
20
|
+
__exportStar(require("./severity.js"), exports);
|
|
21
|
+
__exportStar(require("./schemas.js"), exports);
|
|
22
|
+
__exportStar(require("./artifacts.js"), exports);
|
|
23
|
+
__exportStar(require("./triggers.js"), exports);
|
|
24
|
+
__exportStar(require("./policy.js"), exports);
|
|
25
|
+
__exportStar(require("./runner.js"), exports);
|
|
26
|
+
__exportStar(require("./orchestration.js"), exports);
|
|
27
|
+
__exportStar(require("./registry.js"), exports);
|
|
28
|
+
__exportStar(require("./attribution.js"), exports);
|
|
29
|
+
__exportStar(require("./autofix.js"), exports);
|
|
30
|
+
__exportStar(require("./routing.js"), exports);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC trigger orchestration.
|
|
4
|
+
*
|
|
5
|
+
* Wires QC providers into the Polaris lifecycle at:
|
|
6
|
+
* - completed-cluster (after all children done, before final delivery)
|
|
7
|
+
* - pr (after a PR is created, for providers that require a PR URL)
|
|
8
|
+
* - child (selected at dispatch time for high-risk scopes)
|
|
9
|
+
*
|
|
10
|
+
* The orchestrator is passive/dry-run by default; only configured providers
|
|
11
|
+
* are invoked, and only blocking policy stops the loop/finalize flow.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.runQcAtTrigger = runQcAtTrigger;
|
|
15
|
+
const store_js_1 = require("../cluster-state/store.js");
|
|
16
|
+
const policy_js_1 = require("./policy.js");
|
|
17
|
+
const runner_js_1 = require("./runner.js");
|
|
18
|
+
const triggers_js_1 = require("./triggers.js");
|
|
19
|
+
const attribution_js_1 = require("./attribution.js");
|
|
20
|
+
const autofix_js_1 = require("./autofix.js");
|
|
21
|
+
const routing_js_1 = require("./routing.js");
|
|
22
|
+
function buildAttributionContext(options) {
|
|
23
|
+
const { repoRoot, clusterId, branch, state } = options;
|
|
24
|
+
const dispatchRecords = {};
|
|
25
|
+
if (state?.open_children_meta) {
|
|
26
|
+
for (const [childId, meta] of Object.entries(state.open_children_meta)) {
|
|
27
|
+
if (meta?.dispatch_record) {
|
|
28
|
+
dispatchRecords[childId] = meta.dispatch_record;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
repoRoot,
|
|
34
|
+
baseBranch: branch ?? state?.branch ?? "main",
|
|
35
|
+
completedResults: state?.completed_children_results,
|
|
36
|
+
dispatchRecords,
|
|
37
|
+
clusterState: repoRoot ? ((0, store_js_1.readClusterStateSync)(clusterId, repoRoot) ?? undefined) : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function applyAttributionAndRouting(result, config, context, ownership, routeName) {
|
|
41
|
+
const updatedFindings = result.findings.map((finding) => {
|
|
42
|
+
const attribution = (0, attribution_js_1.resolveAttributionWithOwnership)(finding, context, ownership);
|
|
43
|
+
const autofix = (0, autofix_js_1.isAutofixEligible)(finding, config, {
|
|
44
|
+
provider: result.provider,
|
|
45
|
+
routeName,
|
|
46
|
+
});
|
|
47
|
+
const routing = (0, routing_js_1.decideRepairRouting)({ ...finding, attribution, autofixEligible: autofix.eligible }, config, autofix.eligible, { routeName });
|
|
48
|
+
return {
|
|
49
|
+
...finding,
|
|
50
|
+
attribution,
|
|
51
|
+
autofixEligible: autofix.eligible,
|
|
52
|
+
routingDecision: routing,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const updatedResult = {
|
|
56
|
+
...result,
|
|
57
|
+
findings: updatedFindings,
|
|
58
|
+
policyDecision: (0, policy_js_1.computeQcPolicyDecision)({ ...result, findings: updatedFindings }, config),
|
|
59
|
+
};
|
|
60
|
+
return updatedResult;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Run QC for the given lifecycle trigger when configured.
|
|
64
|
+
*
|
|
65
|
+
* Returns a no-op "pass" result when QC is disabled, no providers are configured
|
|
66
|
+
* for the trigger, or all providers run successfully without findings that
|
|
67
|
+
* match a blocking/follow-up policy.
|
|
68
|
+
*/
|
|
69
|
+
async function runQcAtTrigger(options) {
|
|
70
|
+
const { config, registry, trigger, prUrl, repoRoot, runId, clusterId, branch, telemetryFile, timeoutMs, state, routeName, } = options;
|
|
71
|
+
if (!config.enabled) {
|
|
72
|
+
return {
|
|
73
|
+
trigger,
|
|
74
|
+
results: [],
|
|
75
|
+
action: "pass",
|
|
76
|
+
summary: "QC disabled by configuration",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const providers = (0, triggers_js_1.activeProvidersForTrigger)(config, trigger);
|
|
80
|
+
if (providers.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
trigger,
|
|
83
|
+
results: [],
|
|
84
|
+
action: "pass",
|
|
85
|
+
summary: `No QC providers configured for trigger "${trigger}"`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const attributionContext = buildAttributionContext(options);
|
|
89
|
+
const ownership = (0, attribution_js_1.buildChangedFileOwnership)(attributionContext);
|
|
90
|
+
const results = [];
|
|
91
|
+
const errors = [];
|
|
92
|
+
for (const [name, providerConfig] of providers) {
|
|
93
|
+
const provider = registry.get(name);
|
|
94
|
+
if (!provider) {
|
|
95
|
+
errors.push(`Unknown QC provider "${name}"`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const scope = {
|
|
99
|
+
clusterId,
|
|
100
|
+
runId,
|
|
101
|
+
...(trigger === "pr" ? { prUrl } : { branch }),
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const rawResult = await (0, runner_js_1.executeQcProvider)(provider, scope, {
|
|
105
|
+
repoRoot,
|
|
106
|
+
runId,
|
|
107
|
+
clusterId,
|
|
108
|
+
branch,
|
|
109
|
+
telemetryFile,
|
|
110
|
+
timeoutMs,
|
|
111
|
+
});
|
|
112
|
+
const result = applyAttributionAndRouting(rawResult, config, attributionContext, ownership, routeName);
|
|
113
|
+
try {
|
|
114
|
+
await (0, store_js_1.recordQcRun)(clusterId, result, repoRoot);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
errors.push(`Failed to record QC run ${result.qcRunId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
|
+
}
|
|
119
|
+
results.push(result);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
errors.push(`QC provider "${name}" execution error: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let action = "pass";
|
|
126
|
+
for (const result of results) {
|
|
127
|
+
const a = (0, policy_js_1.decideQcAction)(result, config);
|
|
128
|
+
if (a === "block") {
|
|
129
|
+
action = "block";
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (a === "follow-up" && action === "pass") {
|
|
133
|
+
action = "follow-up";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const successfulRuns = results.filter((result) => result.status !== "failed");
|
|
137
|
+
if (action === "pass" && successfulRuns.length === 0 && (results.length > 0 || errors.length > 0)) {
|
|
138
|
+
action = "block";
|
|
139
|
+
}
|
|
140
|
+
const summaryParts = results.map((r) => `${r.provider}: ${r.status} (${r.findings.length} findings)`);
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
summaryParts.push(`errors: ${errors.join(", ")}`);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
trigger,
|
|
146
|
+
results,
|
|
147
|
+
action,
|
|
148
|
+
summary: summaryParts.join("; ") || "No QC providers produced results",
|
|
149
|
+
};
|
|
150
|
+
}
|