@lsctech/polaris 0.5.0 → 0.5.3
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 +113 -0
- package/dist/autoresearch/score.js +179 -0
- package/dist/cluster-state/store.js +35 -1
- package/dist/config/defaults.js +17 -0
- package/dist/config/validator.js +244 -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/cli-subtask-bridge.js +0 -1
- package/dist/loop/adapters/terminal-cli.js +0 -3
- package/dist/loop/dispatch-boundary.js +29 -0
- package/dist/loop/parent.js +50 -2
- 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/package.json +1 -1
package/dist/config/validator.js
CHANGED
|
@@ -79,6 +79,33 @@ const SUPPORTED_LIFECYCLE_STATES = [
|
|
|
79
79
|
"cancelled",
|
|
80
80
|
"no_status_change",
|
|
81
81
|
];
|
|
82
|
+
const SUPPORTED_QC_TRIGGERS = ["pr", "completed-cluster", "child"];
|
|
83
|
+
const SUPPORTED_QC_SEVERITIES = ["critical", "high", "medium", "low", "info"];
|
|
84
|
+
const SUPPORTED_QC_PROVIDER_MODES = ["local", "pr", "metrics-import"];
|
|
85
|
+
const SUPPORTED_QC_AUTO_FIX_POLICIES = ["disabled", "dry-run", "apply"];
|
|
86
|
+
const SUPPORTED_QC_REPAIR_ROUTING_POLICIES = ["block", "route", "follow-up", "log"];
|
|
87
|
+
const SUPPORTED_QC_PROVIDER_CAPABILITIES = [
|
|
88
|
+
"diff-review",
|
|
89
|
+
"pr-review",
|
|
90
|
+
"result-parsing",
|
|
91
|
+
"auto-fix",
|
|
92
|
+
"metrics-import",
|
|
93
|
+
];
|
|
94
|
+
const SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
95
|
+
function severityIndex(severity) {
|
|
96
|
+
return SEVERITY_ORDER.indexOf(severity);
|
|
97
|
+
}
|
|
98
|
+
function hasEligibleQcAutoFixProvider(providers) {
|
|
99
|
+
if (!isPlainObject(providers))
|
|
100
|
+
return false;
|
|
101
|
+
return Object.values(providers).some((providerConfig) => {
|
|
102
|
+
if (!isPlainObject(providerConfig))
|
|
103
|
+
return false;
|
|
104
|
+
const capabilities = providerConfig.capabilities;
|
|
105
|
+
const eligible = providerConfig.autoFixEligible;
|
|
106
|
+
return eligible === true && Array.isArray(capabilities) && capabilities.includes("auto-fix");
|
|
107
|
+
});
|
|
108
|
+
}
|
|
82
109
|
function validateConfig(config) {
|
|
83
110
|
const result = { valid: true, errors: [], warnings: [] };
|
|
84
111
|
if (!isPlainObject(config)) {
|
|
@@ -797,6 +824,222 @@ function validateConfig(config) {
|
|
|
797
824
|
}
|
|
798
825
|
}
|
|
799
826
|
}
|
|
827
|
+
// qc
|
|
828
|
+
if ("qc" in config && config.qc !== undefined) {
|
|
829
|
+
if (!isPlainObject(config.qc)) {
|
|
830
|
+
result.valid = false;
|
|
831
|
+
result.errors.push("qc must be an object");
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
if ("enabled" in config.qc && config.qc.enabled !== undefined) {
|
|
835
|
+
if (!isBoolean(config.qc.enabled)) {
|
|
836
|
+
result.valid = false;
|
|
837
|
+
result.errors.push("qc.enabled must be a boolean");
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if ("defaultTrigger" in config.qc && config.qc.defaultTrigger !== undefined) {
|
|
841
|
+
if (!isString(config.qc.defaultTrigger) ||
|
|
842
|
+
!SUPPORTED_QC_TRIGGERS.includes(config.qc.defaultTrigger)) {
|
|
843
|
+
result.valid = false;
|
|
844
|
+
result.errors.push('qc.defaultTrigger must be one of "pr", "completed-cluster", "child"');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const providers = isPlainObject(config.qc.providers) ? config.qc.providers : null;
|
|
848
|
+
if ("providers" in config.qc && config.qc.providers !== undefined) {
|
|
849
|
+
if (!isPlainObject(config.qc.providers)) {
|
|
850
|
+
result.valid = false;
|
|
851
|
+
result.errors.push("qc.providers must be a plain object");
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
for (const [providerName, providerConfig] of Object.entries(config.qc.providers)) {
|
|
855
|
+
if (!isPlainObject(providerConfig)) {
|
|
856
|
+
result.valid = false;
|
|
857
|
+
result.errors.push(`qc.providers.${providerName} must be a plain object`);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if ("name" in providerConfig && providerConfig.name !== undefined && !isString(providerConfig.name)) {
|
|
861
|
+
result.valid = false;
|
|
862
|
+
result.errors.push(`qc.providers.${providerName}.name must be a string`);
|
|
863
|
+
}
|
|
864
|
+
if ("mode" in providerConfig && providerConfig.mode !== undefined) {
|
|
865
|
+
if (!isString(providerConfig.mode) ||
|
|
866
|
+
!SUPPORTED_QC_PROVIDER_MODES.includes(providerConfig.mode)) {
|
|
867
|
+
result.valid = false;
|
|
868
|
+
result.errors.push(`qc.providers.${providerName}.mode must be one of local, pr, metrics-import`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
result.valid = false;
|
|
873
|
+
result.errors.push(`qc.providers.${providerName}.mode is required`);
|
|
874
|
+
}
|
|
875
|
+
if ("capabilities" in providerConfig && providerConfig.capabilities !== undefined) {
|
|
876
|
+
if (!Array.isArray(providerConfig.capabilities) ||
|
|
877
|
+
!providerConfig.capabilities.every((capability) => isString(capability) &&
|
|
878
|
+
SUPPORTED_QC_PROVIDER_CAPABILITIES.includes(capability))) {
|
|
879
|
+
result.valid = false;
|
|
880
|
+
result.errors.push(`qc.providers.${providerName}.capabilities must contain only: diff-review, pr-review, result-parsing, auto-fix, metrics-import`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if ("trigger" in providerConfig && providerConfig.trigger !== undefined) {
|
|
884
|
+
if (!isString(providerConfig.trigger) ||
|
|
885
|
+
!SUPPORTED_QC_TRIGGERS.includes(providerConfig.trigger)) {
|
|
886
|
+
result.valid = false;
|
|
887
|
+
result.errors.push(`qc.providers.${providerName}.trigger must be one of pr, completed-cluster, child`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if ("autoFixEligible" in providerConfig && providerConfig.autoFixEligible !== undefined) {
|
|
891
|
+
if (!isBoolean(providerConfig.autoFixEligible)) {
|
|
892
|
+
result.valid = false;
|
|
893
|
+
result.errors.push(`qc.providers.${providerName}.autoFixEligible must be a boolean`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if ("severityMapping" in providerConfig && providerConfig.severityMapping !== undefined) {
|
|
897
|
+
if (!isPlainObject(providerConfig.severityMapping)) {
|
|
898
|
+
result.valid = false;
|
|
899
|
+
result.errors.push(`qc.providers.${providerName}.severityMapping must be a plain object`);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
for (const [label, severity] of Object.entries(providerConfig.severityMapping)) {
|
|
903
|
+
if (!isString(severity) ||
|
|
904
|
+
!SUPPORTED_QC_SEVERITIES.includes(severity)) {
|
|
905
|
+
result.valid = false;
|
|
906
|
+
result.errors.push(`qc.providers.${providerName}.severityMapping.${label} must be one of critical, high, medium, low, info`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if ("severityThresholds" in config.qc && config.qc.severityThresholds !== undefined) {
|
|
915
|
+
if (!isPlainObject(config.qc.severityThresholds)) {
|
|
916
|
+
result.valid = false;
|
|
917
|
+
result.errors.push("qc.severityThresholds must be a plain object");
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
const thresholds = config.qc.severityThresholds;
|
|
921
|
+
for (const key of ["block", "repair", "followUp"]) {
|
|
922
|
+
if (key in thresholds && thresholds[key] !== undefined) {
|
|
923
|
+
if (!isString(thresholds[key]) ||
|
|
924
|
+
!SUPPORTED_QC_SEVERITIES.includes(thresholds[key])) {
|
|
925
|
+
result.valid = false;
|
|
926
|
+
result.errors.push(`qc.severityThresholds.${key} must be one of critical, high, medium, low, info`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (isString(thresholds.block) &&
|
|
931
|
+
SUPPORTED_QC_SEVERITIES.includes(thresholds.block) &&
|
|
932
|
+
isString(thresholds.repair) &&
|
|
933
|
+
SUPPORTED_QC_SEVERITIES.includes(thresholds.repair) &&
|
|
934
|
+
severityIndex(thresholds.block) < severityIndex(thresholds.repair)) {
|
|
935
|
+
result.valid = false;
|
|
936
|
+
result.errors.push("qc.severityThresholds.repair must be at or below qc.severityThresholds.block severity");
|
|
937
|
+
}
|
|
938
|
+
if (isString(thresholds.repair) &&
|
|
939
|
+
SUPPORTED_QC_SEVERITIES.includes(thresholds.repair) &&
|
|
940
|
+
isString(thresholds.followUp) &&
|
|
941
|
+
SUPPORTED_QC_SEVERITIES.includes(thresholds.followUp) &&
|
|
942
|
+
severityIndex(thresholds.repair) < severityIndex(thresholds.followUp)) {
|
|
943
|
+
result.valid = false;
|
|
944
|
+
result.errors.push("qc.severityThresholds.followUp must be at or below qc.severityThresholds.repair severity");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if ("autoFix" in config.qc && config.qc.autoFix !== undefined) {
|
|
949
|
+
if (!isString(config.qc.autoFix) ||
|
|
950
|
+
!SUPPORTED_QC_AUTO_FIX_POLICIES.includes(config.qc.autoFix)) {
|
|
951
|
+
result.valid = false;
|
|
952
|
+
result.errors.push('qc.autoFix must be one of "disabled", "dry-run", "apply"');
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if ("repairRouting" in config.qc && config.qc.repairRouting !== undefined) {
|
|
956
|
+
if (!isString(config.qc.repairRouting) ||
|
|
957
|
+
!SUPPORTED_QC_REPAIR_ROUTING_POLICIES.includes(config.qc.repairRouting)) {
|
|
958
|
+
result.valid = false;
|
|
959
|
+
result.errors.push('qc.repairRouting must be one of "block", "route", "follow-up", "log"');
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if ("artifactRetention" in config.qc && config.qc.artifactRetention !== undefined) {
|
|
963
|
+
if (!isPlainObject(config.qc.artifactRetention)) {
|
|
964
|
+
result.valid = false;
|
|
965
|
+
result.errors.push("qc.artifactRetention must be a plain object");
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
const retention = config.qc.artifactRetention;
|
|
969
|
+
if ("retainRawOutput" in retention && retention.retainRawOutput !== undefined) {
|
|
970
|
+
if (!isBoolean(retention.retainRawOutput)) {
|
|
971
|
+
result.valid = false;
|
|
972
|
+
result.errors.push("qc.artifactRetention.retainRawOutput must be a boolean");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if ("maxRuns" in retention && retention.maxRuns !== undefined) {
|
|
976
|
+
if (!isPositiveInteger(retention.maxRuns)) {
|
|
977
|
+
result.valid = false;
|
|
978
|
+
result.errors.push("qc.artifactRetention.maxRuns must be a positive integer");
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if ("routes" in config.qc && config.qc.routes !== undefined) {
|
|
984
|
+
if (!isPlainObject(config.qc.routes)) {
|
|
985
|
+
result.valid = false;
|
|
986
|
+
result.errors.push("qc.routes must be a plain object");
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
for (const [routeName, routePolicy] of Object.entries(config.qc.routes)) {
|
|
990
|
+
if (!isPlainObject(routePolicy)) {
|
|
991
|
+
result.valid = false;
|
|
992
|
+
result.errors.push(`qc.routes.${routeName} must be a plain object`);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if ("childLevel" in routePolicy && routePolicy.childLevel !== undefined) {
|
|
996
|
+
if (!isBoolean(routePolicy.childLevel)) {
|
|
997
|
+
result.valid = false;
|
|
998
|
+
result.errors.push(`qc.routes.${routeName}.childLevel must be a boolean`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if ("blockThreshold" in routePolicy && routePolicy.blockThreshold !== undefined) {
|
|
1002
|
+
if (!isString(routePolicy.blockThreshold) ||
|
|
1003
|
+
!SUPPORTED_QC_SEVERITIES.includes(routePolicy.blockThreshold)) {
|
|
1004
|
+
result.valid = false;
|
|
1005
|
+
result.errors.push(`qc.routes.${routeName}.blockThreshold must be one of critical, high, medium, low, info`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if ("autoFix" in routePolicy && routePolicy.autoFix !== undefined) {
|
|
1009
|
+
if (!isString(routePolicy.autoFix) ||
|
|
1010
|
+
!SUPPORTED_QC_AUTO_FIX_POLICIES.includes(routePolicy.autoFix)) {
|
|
1011
|
+
result.valid = false;
|
|
1012
|
+
result.errors.push(`qc.routes.${routeName}.autoFix must be one of disabled, dry-run, apply`);
|
|
1013
|
+
}
|
|
1014
|
+
if (routePolicy.autoFix === "apply" && !hasEligibleQcAutoFixProvider(providers)) {
|
|
1015
|
+
result.valid = false;
|
|
1016
|
+
result.errors.push(`qc.routes.${routeName}.autoFix "apply" requires at least one provider with capability "auto-fix" and autoFixEligible true`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// Unsafe auto-fix policy combinations
|
|
1023
|
+
const autoFix = isString(config.qc.autoFix)
|
|
1024
|
+
? config.qc.autoFix
|
|
1025
|
+
: "disabled";
|
|
1026
|
+
const blockSeverity = isPlainObject(config.qc.severityThresholds)
|
|
1027
|
+
? config.qc.severityThresholds.block
|
|
1028
|
+
: undefined;
|
|
1029
|
+
if (autoFix === "apply") {
|
|
1030
|
+
if (!hasEligibleQcAutoFixProvider(providers)) {
|
|
1031
|
+
result.valid = false;
|
|
1032
|
+
result.errors.push('qc.autoFix "apply" requires at least one provider with capability "auto-fix" and autoFixEligible true');
|
|
1033
|
+
}
|
|
1034
|
+
if (isString(blockSeverity) &&
|
|
1035
|
+
SUPPORTED_QC_SEVERITIES.includes(blockSeverity) &&
|
|
1036
|
+
severityIndex(blockSeverity) <= severityIndex("medium")) {
|
|
1037
|
+
result.valid = false;
|
|
1038
|
+
result.errors.push('qc.autoFix "apply" is unsafe when qc.severityThresholds.block is medium or lower');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
800
1043
|
// unknown top-level fields -> warnings
|
|
801
1044
|
const knownKeys = new Set([
|
|
802
1045
|
"version",
|
|
@@ -812,6 +1055,7 @@ function validateConfig(config) {
|
|
|
812
1055
|
"providers",
|
|
813
1056
|
"budget",
|
|
814
1057
|
"compact",
|
|
1058
|
+
"qc",
|
|
815
1059
|
]);
|
|
816
1060
|
for (const key of Object.keys(config)) {
|
|
817
1061
|
if (!knownKeys.has(key)) {
|
|
@@ -38,7 +38,8 @@ function isPromotedClusterArtifact(relativePath, activeClusterId) {
|
|
|
38
38
|
return (suffix === "clusters.json"
|
|
39
39
|
|| suffix === "cluster-state.json"
|
|
40
40
|
|| suffix.startsWith("packets/")
|
|
41
|
-
|| suffix.startsWith("results/")
|
|
41
|
+
|| suffix.startsWith("results/")
|
|
42
|
+
|| suffix.startsWith("qc/"));
|
|
42
43
|
}
|
|
43
44
|
function classifyArtifactPath(filePath, activeClusterId) {
|
|
44
45
|
const relativePath = normalizeArtifactPath(filePath);
|
|
@@ -132,6 +133,7 @@ function getArtifactPromotionPolicy(activeClusterId) {
|
|
|
132
133
|
`${activeClusterPrefix}cluster-state.json`,
|
|
133
134
|
`${activeClusterPrefix}packets/**`,
|
|
134
135
|
`${activeClusterPrefix}results/**`,
|
|
136
|
+
`${activeClusterPrefix}qc/**`,
|
|
135
137
|
PROMOTED_RUN_LEDGER,
|
|
136
138
|
`${PROMOTED_COGNITION_ARCHIVE_PREFIX}**`,
|
|
137
139
|
`${PROMOTED_MAP_PREFIX}**`,
|
package/dist/finalize/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const local_graph_js_1 = require("../tracker/local-graph.js");
|
|
|
29
29
|
const index_js_1 = require("../tracker/sync/index.js");
|
|
30
30
|
const finalize_evidence_js_1 = require("../loop/finalize-evidence.js");
|
|
31
31
|
const delivery_integrity_js_1 = require("./delivery-integrity.js");
|
|
32
|
+
const index_js_2 = require("../qc/index.js");
|
|
32
33
|
function getBranch(repoRoot) {
|
|
33
34
|
try {
|
|
34
35
|
return (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
@@ -178,6 +179,36 @@ function checkLibrarianGate(repoRoot, clusterId) {
|
|
|
178
179
|
return `Librarian result gate error: ${err instanceof Error ? err.message : String(err)}`;
|
|
179
180
|
}
|
|
180
181
|
}
|
|
182
|
+
function resolveQcTelemetryFile(state, repoRoot) {
|
|
183
|
+
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
184
|
+
return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
|
|
185
|
+
}
|
|
186
|
+
async function runQcGate(options) {
|
|
187
|
+
const { config, state, repoRoot, branch, trigger, prUrl, stepLabel } = options;
|
|
188
|
+
const registry = (0, index_js_2.createDefaultQcRegistry)();
|
|
189
|
+
const result = await (0, index_js_2.runQcAtTrigger)({
|
|
190
|
+
config,
|
|
191
|
+
registry,
|
|
192
|
+
trigger,
|
|
193
|
+
prUrl,
|
|
194
|
+
repoRoot,
|
|
195
|
+
runId: state.run_id,
|
|
196
|
+
clusterId: state.cluster_id,
|
|
197
|
+
branch,
|
|
198
|
+
telemetryFile: resolveQcTelemetryFile(state, repoRoot),
|
|
199
|
+
state,
|
|
200
|
+
});
|
|
201
|
+
if (result.action === "pass") {
|
|
202
|
+
console.log(`${stepLabel} QC ${trigger} passed: ${result.summary}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (result.action === "follow-up") {
|
|
206
|
+
console.warn(`${stepLabel} QC ${trigger} produced follow-up work: ${result.summary}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
process.stderr.write(`${stepLabel} QC ${trigger} blocked finalize: ${result.summary}\n`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
181
212
|
async function runFinalize(options) {
|
|
182
213
|
const { repoRoot, stateFile, dryRun, skipDelivery, skipLibrarian } = options;
|
|
183
214
|
const config = (0, loader_js_1.loadConfig)(repoRoot);
|
|
@@ -356,6 +387,16 @@ async function runFinalize(options) {
|
|
|
356
387
|
}
|
|
357
388
|
}
|
|
358
389
|
}
|
|
390
|
+
// Step 5.8: Completed-cluster QC trigger (when configured)
|
|
391
|
+
// Runs after all authoritative gates and before final commit/delivery.
|
|
392
|
+
await runQcGate({
|
|
393
|
+
config: config.qc,
|
|
394
|
+
state,
|
|
395
|
+
repoRoot,
|
|
396
|
+
branch,
|
|
397
|
+
trigger: "completed-cluster",
|
|
398
|
+
stepLabel: "[5.8/14]",
|
|
399
|
+
});
|
|
359
400
|
// Step 6: Tracker Reconciliation
|
|
360
401
|
// LinearAdapter is sync-in only; only McpBridgeAdapter supports full reconciliation.
|
|
361
402
|
const trackerType = config.tracker?.adapter;
|
|
@@ -392,28 +433,32 @@ async function runFinalize(options) {
|
|
|
392
433
|
else {
|
|
393
434
|
console.warn(`[6/14] Unknown tracker adapter '${trackerType}' — skipping reconciliation.`);
|
|
394
435
|
}
|
|
395
|
-
// Step 7:
|
|
396
|
-
|
|
436
|
+
// Step 7: Closeout Librarian gate (pre-commit preflight)
|
|
437
|
+
// Must run before the finalize commit so that a missing or invalid librarian
|
|
438
|
+
// result aborts before any mutating git I/O.
|
|
439
|
+
if (!skipDelivery) {
|
|
440
|
+
if (!skipLibrarian) {
|
|
441
|
+
console.log("[7/14] Checking Closeout Librarian gate...");
|
|
442
|
+
const librarianBlocker = checkLibrarianGate(repoRoot, state.cluster_id);
|
|
443
|
+
if (librarianBlocker) {
|
|
444
|
+
process.stderr.write(`finalize aborted: Closeout Librarian gate failed.\n${librarianBlocker}\n`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
console.log("[7/14] Closeout Librarian gate passed.");
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
console.log("[7/14] Closeout Librarian gate skipped (--skip-librarian).");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Step 8: Single final commit: source changes + durable Polaris artifacts
|
|
454
|
+
console.log("[8/14] Committing durable Polaris state + map..."); // Step count updated
|
|
397
455
|
const resolvedStateFile = (0, node_path_1.resolve)(stateFile);
|
|
398
456
|
(0, _06_commit_js_1.stepCommit)(repoRoot, state, resolvedStateFile, reportPath);
|
|
399
457
|
if (skipDelivery) {
|
|
400
|
-
console.log("[
|
|
401
|
-
console.log("polaris finalize steps 1–
|
|
458
|
+
console.log("[9–14/14] Delivery skipped (--skip-delivery).");
|
|
459
|
+
console.log("polaris finalize steps 1–8 complete.");
|
|
402
460
|
return;
|
|
403
461
|
}
|
|
404
|
-
// Step 8: Closeout Librarian gate
|
|
405
|
-
if (!skipLibrarian) {
|
|
406
|
-
console.log("[8/14] Checking Closeout Librarian gate...");
|
|
407
|
-
const librarianBlocker = checkLibrarianGate(repoRoot, state.cluster_id);
|
|
408
|
-
if (librarianBlocker) {
|
|
409
|
-
process.stderr.write(`finalize aborted: Closeout Librarian gate failed.\n${librarianBlocker}\n`);
|
|
410
|
-
process.exit(1);
|
|
411
|
-
}
|
|
412
|
-
console.log("[8/14] Closeout Librarian gate passed.");
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
console.log("[8/14] Closeout Librarian gate skipped (--skip-librarian).");
|
|
416
|
-
}
|
|
417
462
|
// Step 9: git push
|
|
418
463
|
console.log("[9/14] Pushing branch...");
|
|
419
464
|
(0, _07_push_js_1.stepPush)(repoRoot, branch);
|
|
@@ -421,6 +466,17 @@ async function runFinalize(options) {
|
|
|
421
466
|
const prDraft = config.finalize?.prDraft ?? true;
|
|
422
467
|
console.log("[10/14] Creating draft PR...");
|
|
423
468
|
const prUrl = (0, _08_create_pr_js_1.stepCreatePr)(repoRoot, branch, state, prDraft);
|
|
469
|
+
// Step 10.5: PR-required QC trigger (when configured)
|
|
470
|
+
// Providers that require a PR URL run here after the PR is created.
|
|
471
|
+
await runQcGate({
|
|
472
|
+
config: config.qc,
|
|
473
|
+
state,
|
|
474
|
+
repoRoot,
|
|
475
|
+
branch,
|
|
476
|
+
trigger: "pr",
|
|
477
|
+
prUrl,
|
|
478
|
+
stepLabel: "[10.5/14]",
|
|
479
|
+
});
|
|
424
480
|
// Step 11: Write PR URL to current-state.json
|
|
425
481
|
console.log("[11/14] Writing PR URL to state...");
|
|
426
482
|
state = (0, _09_update_state_js_1.stepUpdateState)(resolvedStateFile, state, prUrl);
|
package/dist/finalize/linear.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.assertNotDoneState = assertNotDoneState;
|
|
|
4
4
|
exports.findReviewState = findReviewState;
|
|
5
5
|
exports.postLinearComment = postLinearComment;
|
|
6
6
|
exports.updateLinearIssueAfterFinalize = updateLinearIssueAfterFinalize;
|
|
7
|
+
exports.buildFollowUpIssuePayload = buildFollowUpIssuePayload;
|
|
8
|
+
exports.createLinearFollowUpIssue = createLinearFollowUpIssue;
|
|
7
9
|
const node_https_1 = require("node:https");
|
|
8
10
|
const lifecycle_policy_js_1 = require("../tracker/lifecycle-policy.js");
|
|
9
11
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -211,3 +213,46 @@ async function updateLinearIssueAfterFinalize(options) {
|
|
|
211
213
|
throw new Error(`Linear commentCreate failed: ${JSON.stringify(commentData.commentCreate)}`);
|
|
212
214
|
}
|
|
213
215
|
}
|
|
216
|
+
async function findIssueTeamId(issueId, apiKey) {
|
|
217
|
+
const issueData = await linearGraphQL(`query GetIssueTeam($id: String!) { issue(id: $id) { team { id } } }`, { id: issueId }, apiKey);
|
|
218
|
+
return issueData.issue?.team?.id ?? null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Build the GraphQL variables for a Linear follow-up issue.
|
|
222
|
+
* Exported separately so tests can inspect the payload shape.
|
|
223
|
+
*/
|
|
224
|
+
function buildFollowUpIssuePayload(options) {
|
|
225
|
+
const input = {
|
|
226
|
+
title: options.title,
|
|
227
|
+
description: options.description,
|
|
228
|
+
parentId: options.parentIssueId,
|
|
229
|
+
teamId: options.teamId,
|
|
230
|
+
};
|
|
231
|
+
if (options.stateId)
|
|
232
|
+
input.stateId = options.stateId;
|
|
233
|
+
if (options.labelIds && options.labelIds.length > 0)
|
|
234
|
+
input.labelIds = options.labelIds;
|
|
235
|
+
return {
|
|
236
|
+
query: `mutation CreateFollowUpIssue($input: IssueCreateInput!) {
|
|
237
|
+
issueCreate(input: $input) { success issue { id identifier url } }
|
|
238
|
+
}`,
|
|
239
|
+
variables: { input },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Create a Linear follow-up issue linked to a parent issue.
|
|
244
|
+
*
|
|
245
|
+
* This is the repair path for QC findings that are routed to follow-up work.
|
|
246
|
+
*/
|
|
247
|
+
async function createLinearFollowUpIssue(options) {
|
|
248
|
+
const teamId = options.teamId ?? await findIssueTeamId(options.parentIssueId, options.apiKey);
|
|
249
|
+
if (!teamId) {
|
|
250
|
+
throw new Error(`Unable to resolve Linear team for parent issue ${options.parentIssueId}`);
|
|
251
|
+
}
|
|
252
|
+
const payload = buildFollowUpIssuePayload({ ...options, teamId });
|
|
253
|
+
const data = await linearGraphQL(payload.query, payload.variables, options.apiKey);
|
|
254
|
+
if (data.issueCreate?.success !== true || !data.issueCreate?.issue) {
|
|
255
|
+
throw new Error(`Linear issueCreate failed: ${JSON.stringify(data.issueCreate)}`);
|
|
256
|
+
}
|
|
257
|
+
return data.issueCreate.issue;
|
|
258
|
+
}
|
|
@@ -1,8 +1,61 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateRunReport = generateRunReport;
|
|
4
|
+
function renderQcSection(qcSummary) {
|
|
5
|
+
if (!qcSummary)
|
|
6
|
+
return "";
|
|
7
|
+
const { total_findings, blocking_findings, autofixed_findings, repaired_findings, waived_findings, unvalidated_findings, open_by_severity, blocks_delivery, qc_run_count, weighted_open_score, qc_penalty, provider_breakdown, routing_breakdown } = qcSummary;
|
|
8
|
+
const openTotal = open_by_severity.critical + open_by_severity.high +
|
|
9
|
+
open_by_severity.medium + open_by_severity.low + open_by_severity.info;
|
|
10
|
+
const deliveryStatus = blocks_delivery
|
|
11
|
+
? "**BLOCKED** — unresolved critical/high findings must be addressed before delivery"
|
|
12
|
+
: "Not blocking delivery";
|
|
13
|
+
const providers = Object.entries(provider_breakdown)
|
|
14
|
+
.map(([provider, summary]) => `| ${provider} | ${summary.total} | ${summary.blocking} | ${summary.unvalidated} |`)
|
|
15
|
+
.join("\n");
|
|
16
|
+
const routing = `| original-worker | ${routing_breakdown.original_worker} |
|
|
17
|
+
| repair-worker | ${routing_breakdown.repair_worker} |
|
|
18
|
+
| follow-up | ${routing_breakdown.follow_up} |
|
|
19
|
+
| operator-review | ${routing_breakdown.operator_review} |
|
|
20
|
+
| unset | ${routing_breakdown.unset} |`;
|
|
21
|
+
const solImpactValue = qc_penalty > 0
|
|
22
|
+
? `-${(qc_penalty * 100).toFixed(1)}% (weighted open score ${weighted_open_score.toFixed(2)})`
|
|
23
|
+
: `none (weighted open score ${weighted_open_score.toFixed(2)})`;
|
|
24
|
+
return `
|
|
25
|
+
## QC summary
|
|
26
|
+
|
|
27
|
+
| Metric | Value |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **QC runs** | ${qc_run_count} |
|
|
30
|
+
| **Delivery status** | ${deliveryStatus} |
|
|
31
|
+
| **Total findings** | ${total_findings} (${unvalidated_findings} unvalidated/provider-noise excluded from scoring) |
|
|
32
|
+
| **SOL score impact** | ${solImpactValue} |
|
|
33
|
+
|
|
34
|
+
| Status | Count |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Blocking (critical/high, open) | ${blocking_findings} |
|
|
37
|
+
| Open (all severities) | ${openTotal} |
|
|
38
|
+
| Autofixed | ${autofixed_findings} |
|
|
39
|
+
| Repaired | ${repaired_findings} |
|
|
40
|
+
| Waived | ${waived_findings} |
|
|
41
|
+
|
|
42
|
+
| **Open by severity** | critical=${open_by_severity.critical} high=${open_by_severity.high} medium=${open_by_severity.medium} low=${open_by_severity.low} info=${open_by_severity.info} |
|
|
43
|
+
|
|
44
|
+
### Providers
|
|
45
|
+
|
|
46
|
+
| Provider | Total | Blocking | Unvalidated |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
${providers || "| _none_ | — | — | — |"}
|
|
49
|
+
|
|
50
|
+
### Repair routing
|
|
51
|
+
|
|
52
|
+
| Decision | Count |
|
|
53
|
+
|---|---|
|
|
54
|
+
${routing}
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
4
57
|
function generateRunReport(data) {
|
|
5
|
-
const { state, branch, validationPassed, prUrl, artifacts, notes } = data;
|
|
58
|
+
const { state, branch, validationPassed, prUrl, artifacts, notes, qcSummary } = data;
|
|
6
59
|
const total = state.completed_children.length + state.open_children.length;
|
|
7
60
|
const completedCount = state.completed_children.length;
|
|
8
61
|
const childRows = state.completed_children
|
|
@@ -18,19 +71,22 @@ function generateRunReport(data) {
|
|
|
18
71
|
const blockerNote = state.blocker
|
|
19
72
|
? `\n**Blocker:** ${state.blocker.reason} (child: ${state.blocker.child_id})\n`
|
|
20
73
|
: "";
|
|
74
|
+
const qcSection = renderQcSection(qcSummary);
|
|
21
75
|
return `# Run Report: ${state.run_id}
|
|
22
76
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
**
|
|
26
|
-
**
|
|
27
|
-
**
|
|
28
|
-
${
|
|
29
|
-
|
|
77
|
+
| Field | Value |
|
|
78
|
+
|---|---|
|
|
79
|
+
| **Status** | ${state.status} |
|
|
80
|
+
| **Branch** | ${branch} |
|
|
81
|
+
| **PR** | ${prUrl ?? "TBD — set at delivery step 9"} |
|
|
82
|
+
| **Children completed** | ${completedCount} of ${total} |
|
|
83
|
+
| **Validation** | ${validationPassed ? "passed" : "failed"} |
|
|
84
|
+
${blockerNote}
|
|
85
|
+
## Children
|
|
30
86
|
|
|
31
|
-
| ID | Title | Commit | Status |
|
|
32
|
-
|---|---|---|---|
|
|
33
|
-
${childRows || "_No children recorded_"}
|
|
87
|
+
| ID | Title | Commit | Status |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
${childRows || "_No children recorded_"}
|
|
34
90
|
|
|
35
91
|
## Artifacts produced
|
|
36
92
|
|
|
@@ -39,7 +95,7 @@ ${artifactList}
|
|
|
39
95
|
## Validation summary
|
|
40
96
|
|
|
41
97
|
${validationPassed ? "Map validate passed. Schema validate passed." : "One or more validations failed — see step output above."}
|
|
42
|
-
|
|
98
|
+
${qcSection}
|
|
43
99
|
## Notes
|
|
44
100
|
|
|
45
101
|
${notesList}
|
|
@@ -65,7 +65,6 @@ function writeSealedResultFromSummary(packet, parsedSummary) {
|
|
|
65
65
|
const validation = parsedSummary["validation"] ?? parsedSummary["validation_summary"];
|
|
66
66
|
const sealedResult = {
|
|
67
67
|
run_id: packet.run_id,
|
|
68
|
-
cluster_id: packet.cluster_id,
|
|
69
68
|
child_id: packet.active_child,
|
|
70
69
|
status: normalizeSealedStatus(parsedSummary["status"]),
|
|
71
70
|
};
|
|
@@ -501,9 +501,7 @@ class TerminalCliAdapter {
|
|
|
501
501
|
// that return minimal status objects instead of proper CompactReturn structs.
|
|
502
502
|
const effectiveStatus = (exitCode === 0 && isValidCompactReturn) ? "success" : "failure";
|
|
503
503
|
const sealedResult = {
|
|
504
|
-
...normalized,
|
|
505
504
|
run_id: packet.run_id,
|
|
506
|
-
cluster_id: packet.cluster_id,
|
|
507
505
|
child_id: String(normalized["child_id"] ?? packet.active_child),
|
|
508
506
|
status: effectiveStatus,
|
|
509
507
|
commit: typeof normalized["commit"] === "string"
|
|
@@ -519,7 +517,6 @@ class TerminalCliAdapter {
|
|
|
519
517
|
? normalized["error_message"]
|
|
520
518
|
: stdout || summary)
|
|
521
519
|
: undefined,
|
|
522
|
-
...(compactReturnErrors.length > 0 ? { compact_return_errors: compactReturnErrors } : {}),
|
|
523
520
|
};
|
|
524
521
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(resultFile), { recursive: true });
|
|
525
522
|
node_fs_1.default.writeFileSync(resultFile, JSON.stringify(sealedResult, null, 2), "utf-8");
|
|
@@ -40,8 +40,10 @@ exports.validateTransition = validateTransition;
|
|
|
40
40
|
exports.initialDispatchBoundary = initialDispatchBoundary;
|
|
41
41
|
exports.advanceDispatchEpoch = advanceDispatchEpoch;
|
|
42
42
|
exports.advanceContinueEpoch = advanceContinueEpoch;
|
|
43
|
+
exports.assertChildQcSelectionAllowed = assertChildQcSelectionAllowed;
|
|
43
44
|
const node_fs_1 = require("node:fs");
|
|
44
45
|
const node_path_1 = require("node:path");
|
|
46
|
+
const triggers_js_1 = require("../qc/triggers.js");
|
|
45
47
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
46
48
|
// Error constants
|
|
47
49
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -333,3 +335,30 @@ function advanceContinueEpoch(current) {
|
|
|
333
335
|
last_dispatched_child: current.last_dispatched_child,
|
|
334
336
|
};
|
|
335
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Validate that a child-level QC trigger is only selected when permitted.
|
|
340
|
+
*
|
|
341
|
+
* Child-level QC is opt-in: high-risk scope, route policy, or explicit operator
|
|
342
|
+
* request. If a packet carries child-level QC without meeting one of those
|
|
343
|
+
* conditions, the dispatch boundary is violated.
|
|
344
|
+
*/
|
|
345
|
+
function assertChildQcSelectionAllowed(state, childId, config, allowedScope, labels, telemetryFile) {
|
|
346
|
+
const meta = state.open_children_meta?.[childId];
|
|
347
|
+
if (meta?.qc_trigger !== "child")
|
|
348
|
+
return;
|
|
349
|
+
const routeName = childId; // route-specific overrides are keyed by child/route identifier
|
|
350
|
+
if ((0, triggers_js_1.isChildQcSelected)(config, childId, allowedScope, labels, routeName)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const reason = `Child-level QC selected for ${childId} but no opt-in condition is met (high-risk scope, route policy, or "qc-child" label)`;
|
|
354
|
+
appendDispatchViolationEvent(telemetryFile, {
|
|
355
|
+
event: "illegal-state-transition",
|
|
356
|
+
run_id: state.run_id,
|
|
357
|
+
child_id: childId,
|
|
358
|
+
from_state: getMachineState(state),
|
|
359
|
+
to_state: "dispatched",
|
|
360
|
+
reason,
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
});
|
|
363
|
+
throw new Error(reason);
|
|
364
|
+
}
|