pumuki 6.3.39 → 6.3.40
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/README.md +21 -12
- package/VERSION +1 -1
- package/core/gate/evaluateRules.test.ts +40 -0
- package/core/gate/evaluateRules.ts +7 -1
- package/core/rules/Consequence.ts +1 -0
- package/docs/CONFIGURATION.md +50 -0
- package/docs/INSTALLATION.md +38 -11
- package/docs/MCP_SERVERS.md +1 -1
- package/docs/README.md +1 -0
- package/docs/RELEASE_NOTES.md +44 -0
- package/docs/USAGE.md +191 -9
- package/docs/registro-maestro-de-seguimiento.md +2 -2
- package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1592 -1
- package/docs/validation/README.md +2 -1
- package/docs/validation/ast-intelligence-roadmap.md +96 -0
- package/integrations/config/skillsCustomRules.ts +14 -0
- package/integrations/config/skillsDetectorRegistry.ts +11 -1
- package/integrations/config/skillsLock.ts +30 -0
- package/integrations/config/skillsMarkdownRules.ts +14 -3
- package/integrations/config/skillsRuleSet.ts +25 -3
- package/integrations/evidence/readEvidence.test.ts +3 -2
- package/integrations/evidence/readEvidence.ts +14 -4
- package/integrations/evidence/repoState.ts +10 -2
- package/integrations/evidence/schema.test.ts +3 -2
- package/integrations/evidence/schema.ts +3 -0
- package/integrations/evidence/writeEvidence.test.ts +3 -2
- package/integrations/gate/evaluateAiGate.ts +511 -2
- package/integrations/git/GitService.ts +5 -1
- package/integrations/git/astIntelligenceDualValidation.ts +275 -0
- package/integrations/git/gitAtomicity.ts +42 -9
- package/integrations/git/resolveGitRefs.ts +37 -0
- package/integrations/git/runPlatformGate.ts +228 -1
- package/integrations/git/runPlatformGateEvaluation.ts +4 -0
- package/integrations/git/stageRunners.ts +116 -2
- package/integrations/lifecycle/cli.ts +759 -22
- package/integrations/lifecycle/doctor.ts +62 -0
- package/integrations/lifecycle/index.ts +1 -0
- package/integrations/lifecycle/packageInfo.ts +25 -3
- package/integrations/lifecycle/policyReconcile.ts +304 -0
- package/integrations/lifecycle/preWriteAutomation.ts +42 -2
- package/integrations/lifecycle/watch.ts +365 -0
- package/integrations/mcp/aiGateCheck.ts +59 -2
- package/integrations/mcp/autoExecuteAiStart.ts +25 -1
- package/integrations/mcp/preFlightCheck.ts +13 -0
- package/integrations/sdd/evidenceScaffold.ts +223 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/stateSync.ts +400 -0
- package/integrations/sdd/syncDocs.ts +97 -2
- package/package.json +4 -1
- package/scripts/backlog-action-reasons-lib.ts +38 -0
- package/scripts/backlog-id-issue-map-lib.ts +69 -0
- package/scripts/backlog-json-contract-lib.ts +3 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
- package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
- package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
- package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
- package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
- package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
- package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
- package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
- package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
- package/scripts/watch-consumer-backlog-lib.ts +465 -0
- package/scripts/watch-consumer-backlog.ts +326 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { Fact } from '../../core/facts/Fact';
|
|
2
|
+
import type { Finding } from '../../core/gate/Finding';
|
|
3
|
+
import type { RuleSet } from '../../core/rules/RuleSet';
|
|
4
|
+
|
|
5
|
+
export type AstIntelligenceDualValidationMode = 'off' | 'shadow' | 'strict';
|
|
6
|
+
|
|
7
|
+
export type AstIntelligenceDualValidationSummary = {
|
|
8
|
+
mapped_rules: number;
|
|
9
|
+
compared_rules: number;
|
|
10
|
+
legacy_triggered: number;
|
|
11
|
+
ast_triggered: number;
|
|
12
|
+
divergences: number;
|
|
13
|
+
false_positives: number;
|
|
14
|
+
false_negatives: number;
|
|
15
|
+
latency_ms: number;
|
|
16
|
+
languages: ReadonlyArray<string>;
|
|
17
|
+
sample_divergence_rule_ids: ReadonlyArray<string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type AstIntelligenceDualValidationResult = {
|
|
21
|
+
mode: AstIntelligenceDualValidationMode;
|
|
22
|
+
summary: AstIntelligenceDualValidationSummary;
|
|
23
|
+
finding?: Finding;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ComparableMappedRule = {
|
|
27
|
+
ruleId: string;
|
|
28
|
+
astNodeRuleIds: ReadonlyArray<string>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const AST_INTELLIGENCE_DUAL_MODE_ENV = 'PUMUKI_AST_INTELLIGENCE_DUAL_MODE';
|
|
32
|
+
const SKILLS_IR_PREFIX = 'skills-ir:';
|
|
33
|
+
const AST_NODES_TOKEN_PATTERN = /(?:^|;)ast_nodes=\[([^\]]*)\]/;
|
|
34
|
+
|
|
35
|
+
const EMPTY_SUMMARY: AstIntelligenceDualValidationSummary = {
|
|
36
|
+
mapped_rules: 0,
|
|
37
|
+
compared_rules: 0,
|
|
38
|
+
legacy_triggered: 0,
|
|
39
|
+
ast_triggered: 0,
|
|
40
|
+
divergences: 0,
|
|
41
|
+
false_positives: 0,
|
|
42
|
+
false_negatives: 0,
|
|
43
|
+
latency_ms: 0,
|
|
44
|
+
languages: [],
|
|
45
|
+
sample_divergence_rule_ids: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isString = (value: unknown): value is string =>
|
|
49
|
+
typeof value === 'string' && value.trim().length > 0;
|
|
50
|
+
|
|
51
|
+
const toNormalizedPath = (path: string): string => path.replace(/\\/g, '/').trim();
|
|
52
|
+
|
|
53
|
+
const toLanguageTokenFromPath = (path: string): string | null => {
|
|
54
|
+
const normalized = toNormalizedPath(path).toLowerCase();
|
|
55
|
+
if (
|
|
56
|
+
normalized.endsWith('.ts')
|
|
57
|
+
|| normalized.endsWith('.tsx')
|
|
58
|
+
|| normalized.endsWith('.js')
|
|
59
|
+
|| normalized.endsWith('.jsx')
|
|
60
|
+
|| normalized.endsWith('.mts')
|
|
61
|
+
|| normalized.endsWith('.cts')
|
|
62
|
+
|| normalized.endsWith('.mjs')
|
|
63
|
+
|| normalized.endsWith('.cjs')
|
|
64
|
+
) {
|
|
65
|
+
return 'typescript';
|
|
66
|
+
}
|
|
67
|
+
if (normalized.endsWith('.swift')) {
|
|
68
|
+
return 'swift';
|
|
69
|
+
}
|
|
70
|
+
if (normalized.endsWith('.kt') || normalized.endsWith('.kts')) {
|
|
71
|
+
return 'kotlin';
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const detectLanguagesFromFacts = (facts: ReadonlyArray<Fact>): ReadonlyArray<string> => {
|
|
77
|
+
const languages = new Set<string>();
|
|
78
|
+
for (const fact of facts) {
|
|
79
|
+
if (fact.kind !== 'FileChange' && fact.kind !== 'FileContent' && fact.kind !== 'Heuristic') {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const rawPath = fact.kind === 'Heuristic' ? fact.filePath : fact.path;
|
|
83
|
+
if (!isString(rawPath)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const language = toLanguageTokenFromPath(rawPath);
|
|
87
|
+
if (language) {
|
|
88
|
+
languages.add(language);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [...languages].sort();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const parseAstNodesFromSource = (source: string | undefined): ReadonlyArray<string> => {
|
|
95
|
+
if (!isString(source) || !source.startsWith(SKILLS_IR_PREFIX)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const match = source.match(AST_NODES_TOKEN_PATTERN);
|
|
99
|
+
if (!match || !isString(match[1])) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const astNodeRuleIds = match[1]
|
|
103
|
+
.split(',')
|
|
104
|
+
.map((token) => token.trim())
|
|
105
|
+
.filter((token) => token.length > 0 && token !== 'none');
|
|
106
|
+
return [...new Set(astNodeRuleIds)].sort();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const collectComparableMappedRules = (
|
|
110
|
+
skillsRules: RuleSet
|
|
111
|
+
): ReadonlyArray<ComparableMappedRule> => {
|
|
112
|
+
return skillsRules
|
|
113
|
+
.filter(
|
|
114
|
+
(rule) =>
|
|
115
|
+
rule.id.startsWith('skills.')
|
|
116
|
+
&& rule.then.kind === 'Finding'
|
|
117
|
+
&& parseAstNodesFromSource(rule.then.source).length > 0
|
|
118
|
+
)
|
|
119
|
+
.map((rule) => ({
|
|
120
|
+
ruleId: rule.id,
|
|
121
|
+
astNodeRuleIds: parseAstNodesFromSource(rule.then.source),
|
|
122
|
+
}))
|
|
123
|
+
.sort((left, right) => left.ruleId.localeCompare(right.ruleId));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const collectHeuristicRuleIds = (facts: ReadonlyArray<Fact>): ReadonlySet<string> => {
|
|
127
|
+
const heuristicRuleIds = new Set<string>();
|
|
128
|
+
for (const fact of facts) {
|
|
129
|
+
if (fact.kind === 'Heuristic' && isString(fact.ruleId)) {
|
|
130
|
+
heuristicRuleIds.add(fact.ruleId.trim());
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return heuristicRuleIds;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const collectLegacyTriggeredRuleIds = (
|
|
137
|
+
legacyFindings: ReadonlyArray<Finding>
|
|
138
|
+
): ReadonlySet<string> => {
|
|
139
|
+
const legacyRuleIds = new Set<string>();
|
|
140
|
+
for (const finding of legacyFindings) {
|
|
141
|
+
if (isString(finding.ruleId)) {
|
|
142
|
+
legacyRuleIds.add(finding.ruleId.trim());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return legacyRuleIds;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const resolveAstIntelligenceDualValidationMode = (
|
|
149
|
+
explicitMode?: string
|
|
150
|
+
): AstIntelligenceDualValidationMode => {
|
|
151
|
+
const raw = (explicitMode ?? process.env[AST_INTELLIGENCE_DUAL_MODE_ENV] ?? 'off')
|
|
152
|
+
.trim()
|
|
153
|
+
.toLowerCase();
|
|
154
|
+
if (raw === 'strict' || raw === 'enforce' || raw === 'block') {
|
|
155
|
+
return 'strict';
|
|
156
|
+
}
|
|
157
|
+
if (raw === 'shadow' || raw === 'dual' || raw === '1' || raw === 'true' || raw === 'on') {
|
|
158
|
+
return 'shadow';
|
|
159
|
+
}
|
|
160
|
+
return 'off';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const toDualValidationFinding = (params: {
|
|
164
|
+
mode: Exclude<AstIntelligenceDualValidationMode, 'off'>;
|
|
165
|
+
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
166
|
+
summary: AstIntelligenceDualValidationSummary;
|
|
167
|
+
}): Finding | undefined => {
|
|
168
|
+
if (params.summary.divergences === 0) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const sampleRuleIds = params.summary.sample_divergence_rule_ids.join(', ');
|
|
172
|
+
if (params.mode === 'strict') {
|
|
173
|
+
return {
|
|
174
|
+
ruleId: 'governance.ast-intelligence.dual-validation.mismatch',
|
|
175
|
+
severity: 'ERROR',
|
|
176
|
+
code: 'AST_INTELLIGENCE_DUAL_VALIDATION_MISMATCH_HIGH',
|
|
177
|
+
message:
|
|
178
|
+
`AST Intelligence dual validation mismatch at ${params.stage}: ` +
|
|
179
|
+
`divergences=${params.summary.divergences} ` +
|
|
180
|
+
`false_positives=${params.summary.false_positives} ` +
|
|
181
|
+
`false_negatives=${params.summary.false_negatives} ` +
|
|
182
|
+
`sample_rule_ids=[${sampleRuleIds}].`,
|
|
183
|
+
filePath: '.ai_evidence.json',
|
|
184
|
+
matchedBy: 'AstIntelligenceDualValidationGuard',
|
|
185
|
+
source: 'ast-intelligence-dual-mode',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
ruleId: 'governance.ast-intelligence.dual-validation.shadow',
|
|
190
|
+
severity: 'INFO',
|
|
191
|
+
code: 'AST_INTELLIGENCE_DUAL_VALIDATION_SHADOW',
|
|
192
|
+
message:
|
|
193
|
+
`AST Intelligence dual validation shadow at ${params.stage}: ` +
|
|
194
|
+
`divergences=${params.summary.divergences} ` +
|
|
195
|
+
`false_positives=${params.summary.false_positives} ` +
|
|
196
|
+
`false_negatives=${params.summary.false_negatives} ` +
|
|
197
|
+
`sample_rule_ids=[${sampleRuleIds}].`,
|
|
198
|
+
filePath: '.ai_evidence.json',
|
|
199
|
+
matchedBy: 'AstIntelligenceDualValidationGuard',
|
|
200
|
+
source: 'ast-intelligence-dual-mode',
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const evaluateAstIntelligenceDualValidation = (params: {
|
|
205
|
+
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
206
|
+
skillsRules: RuleSet;
|
|
207
|
+
facts: ReadonlyArray<Fact>;
|
|
208
|
+
legacyFindings: ReadonlyArray<Finding>;
|
|
209
|
+
mode?: string;
|
|
210
|
+
}): AstIntelligenceDualValidationResult => {
|
|
211
|
+
const mode = resolveAstIntelligenceDualValidationMode(params.mode);
|
|
212
|
+
if (mode === 'off') {
|
|
213
|
+
return {
|
|
214
|
+
mode,
|
|
215
|
+
summary: EMPTY_SUMMARY,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const startedAt = Date.now();
|
|
219
|
+
const comparableMappedRules = collectComparableMappedRules(params.skillsRules);
|
|
220
|
+
const heuristicRuleIds = collectHeuristicRuleIds(params.facts);
|
|
221
|
+
const legacyTriggeredRuleIds = collectLegacyTriggeredRuleIds(params.legacyFindings);
|
|
222
|
+
const languages = detectLanguagesFromFacts(params.facts);
|
|
223
|
+
|
|
224
|
+
let legacyTriggered = 0;
|
|
225
|
+
let astTriggered = 0;
|
|
226
|
+
let falsePositives = 0;
|
|
227
|
+
let falseNegatives = 0;
|
|
228
|
+
const divergenceRuleIds: string[] = [];
|
|
229
|
+
|
|
230
|
+
for (const mappedRule of comparableMappedRules) {
|
|
231
|
+
const isLegacyTriggered = legacyTriggeredRuleIds.has(mappedRule.ruleId);
|
|
232
|
+
const isAstTriggered = mappedRule.astNodeRuleIds.some((astNodeRuleId) =>
|
|
233
|
+
heuristicRuleIds.has(astNodeRuleId)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (isLegacyTriggered) {
|
|
237
|
+
legacyTriggered += 1;
|
|
238
|
+
}
|
|
239
|
+
if (isAstTriggered) {
|
|
240
|
+
astTriggered += 1;
|
|
241
|
+
}
|
|
242
|
+
if (isLegacyTriggered === isAstTriggered) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (isAstTriggered) {
|
|
246
|
+
falsePositives += 1;
|
|
247
|
+
} else {
|
|
248
|
+
falseNegatives += 1;
|
|
249
|
+
}
|
|
250
|
+
divergenceRuleIds.push(mappedRule.ruleId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const summary: AstIntelligenceDualValidationSummary = {
|
|
254
|
+
mapped_rules: comparableMappedRules.length,
|
|
255
|
+
compared_rules: comparableMappedRules.length,
|
|
256
|
+
legacy_triggered: legacyTriggered,
|
|
257
|
+
ast_triggered: astTriggered,
|
|
258
|
+
divergences: divergenceRuleIds.length,
|
|
259
|
+
false_positives: falsePositives,
|
|
260
|
+
false_negatives: falseNegatives,
|
|
261
|
+
latency_ms: Math.max(0, Date.now() - startedAt),
|
|
262
|
+
languages,
|
|
263
|
+
sample_divergence_rule_ids: divergenceRuleIds.slice(0, 5),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
mode,
|
|
268
|
+
summary,
|
|
269
|
+
finding: toDualValidationFinding({
|
|
270
|
+
mode,
|
|
271
|
+
stage: params.stage,
|
|
272
|
+
summary,
|
|
273
|
+
}),
|
|
274
|
+
};
|
|
275
|
+
};
|
|
@@ -138,6 +138,25 @@ const parseLines = (value: string): ReadonlyArray<string> =>
|
|
|
138
138
|
.map((line) => line.trim())
|
|
139
139
|
.filter((line) => line.length > 0);
|
|
140
140
|
|
|
141
|
+
const toErrorMessage = (error: unknown): string => {
|
|
142
|
+
if (error instanceof Error) {
|
|
143
|
+
return error.message;
|
|
144
|
+
}
|
|
145
|
+
return String(error);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const isUnresolvableRevisionError = (error: unknown): boolean => {
|
|
149
|
+
const message = toErrorMessage(error).toLowerCase();
|
|
150
|
+
return (
|
|
151
|
+
message.includes('unknown revision') ||
|
|
152
|
+
message.includes('bad revision') ||
|
|
153
|
+
message.includes('ambiguous argument') ||
|
|
154
|
+
message.includes('argumento ambiguo') ||
|
|
155
|
+
message.includes('invalid object name') ||
|
|
156
|
+
message.includes('not a valid object name')
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
141
160
|
const resolveScopeKey = (filePath: string): string => {
|
|
142
161
|
const normalized = filePath.replace(/\\/g, '/').trim();
|
|
143
162
|
const segments = normalized.split('/').filter((segment) => segment.length > 0);
|
|
@@ -168,12 +187,19 @@ const collectChangedPaths = (params: {
|
|
|
168
187
|
if (!params.fromRef || !params.toRef) {
|
|
169
188
|
return [];
|
|
170
189
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
try {
|
|
191
|
+
return parseLines(
|
|
192
|
+
params.git.runGit(
|
|
193
|
+
['diff', '--name-only', '--diff-filter=ACMR', `${params.fromRef}..${params.toRef}`],
|
|
194
|
+
params.repoRoot
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (isUnresolvableRevisionError(error)) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
177
203
|
};
|
|
178
204
|
|
|
179
205
|
const collectCommitSubjects = (params: {
|
|
@@ -185,9 +211,16 @@ const collectCommitSubjects = (params: {
|
|
|
185
211
|
if (!params.fromRef || !params.toRef) {
|
|
186
212
|
return [];
|
|
187
213
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
try {
|
|
215
|
+
return parseLines(
|
|
216
|
+
params.git.runGit(['log', '--format=%s', `${params.fromRef}..${params.toRef}`], params.repoRoot)
|
|
217
|
+
);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (isUnresolvableRevisionError(error)) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
191
224
|
};
|
|
192
225
|
|
|
193
226
|
export const evaluateGitAtomicity = (params: {
|
|
@@ -34,6 +34,43 @@ export const resolveUpstreamRef = (): string | null => {
|
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
export const resolveCurrentBranchRef = (): string | null => {
|
|
38
|
+
try {
|
|
39
|
+
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
40
|
+
if (branch.length === 0 || branch === 'HEAD') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return branch;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const resolveUpstreamTrackingRef = (): string | null => {
|
|
50
|
+
try {
|
|
51
|
+
return runGit(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const resolveAheadBehindFromRef = (
|
|
58
|
+
fromRef: string
|
|
59
|
+
): { ahead: number; behind: number } | null => {
|
|
60
|
+
try {
|
|
61
|
+
const raw = runGit(['rev-list', '--left-right', '--count', `${fromRef}...HEAD`]);
|
|
62
|
+
const [behindRaw, aheadRaw] = raw.split(/\s+/);
|
|
63
|
+
const ahead = Number.parseInt(aheadRaw ?? '', 10);
|
|
64
|
+
const behind = Number.parseInt(behindRaw ?? '', 10);
|
|
65
|
+
if (!Number.isFinite(ahead) || !Number.isFinite(behind)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return { ahead, behind };
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
37
74
|
export const resolveCiBaseRef = (): string => {
|
|
38
75
|
const fromEnv = process.env.GITHUB_BASE_REF?.trim();
|
|
39
76
|
if (fromEnv) {
|