@remogram/core 0.1.0-beta.3 → 0.1.0-beta.5
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/auth-classes.js +25 -4
- package/branch-protection.js +155 -0
- package/caps.js +58 -0
- package/check-pagination.js +94 -17
- package/config-schema.js +2 -0
- package/contracts/envelope.js +18 -2
- package/contracts/errors.js +10 -2
- package/contracts/forge-error-fields.js +97 -0
- package/contracts/observer-fact-inventory.js +7 -5
- package/contracts/semantic-diff-facts.js +30 -2
- package/cr-comments.js +93 -0
- package/cr-files.js +62 -0
- package/cr-inventory.js +19 -12
- package/cr-open.js +33 -0
- package/forge-changes.js +181 -0
- package/git-local.js +12 -0
- package/index.js +104 -2
- package/merge-blockers.js +19 -1
- package/merge-plan.js +37 -0
- package/open-pull-list.js +256 -0
- package/package.json +1 -1
- package/path-allowlist.js +63 -0
- package/status-set.js +87 -0
- package/stub-provider.js +7 -0
- package/whoami.js +114 -0
- package/write-config.js +43 -0
|
@@ -20,8 +20,16 @@ export const V1_READ_PLAN_COMMANDS = Object.freeze([
|
|
|
20
20
|
'sync plan',
|
|
21
21
|
'provider capabilities',
|
|
22
22
|
'doctor',
|
|
23
|
+
'whoami',
|
|
24
|
+
'branch protection',
|
|
25
|
+
'cr files',
|
|
26
|
+
'cr comments',
|
|
27
|
+
'forge changes',
|
|
23
28
|
]);
|
|
24
29
|
|
|
30
|
+
/** v1 write surface (Gitea cr open and status set in first slices). */
|
|
31
|
+
export const V1_WRITE_COMMANDS = Object.freeze(['cr open', 'status set']);
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* Planned fact-inventory packet types (not emitted until wave 2+ commands ship).
|
|
27
35
|
* All use schema_version 1 envelope discipline via forgePacket.
|
|
@@ -55,11 +63,18 @@ export const TRUSTED_NORMALIZED_BODY_FIELDS = Object.freeze({
|
|
|
55
63
|
truncated: true,
|
|
56
64
|
list_truncated: true,
|
|
57
65
|
checks_truncated: true,
|
|
66
|
+
paths_truncated: true,
|
|
67
|
+
comments_truncated: true,
|
|
68
|
+
events_truncated: true,
|
|
69
|
+
slice_sort: true,
|
|
58
70
|
entry_count: true,
|
|
59
71
|
mergeability_confidence: true,
|
|
60
72
|
write_support: true,
|
|
61
73
|
diverged: true,
|
|
62
74
|
auth_present: true,
|
|
75
|
+
can_write: true,
|
|
76
|
+
reused_existing: true,
|
|
77
|
+
idempotency_scan: true,
|
|
63
78
|
});
|
|
64
79
|
|
|
65
80
|
/**
|
|
@@ -74,6 +89,17 @@ export const FORGE_SOURCED_STRING_LEAVES = Object.freeze({
|
|
|
74
89
|
merge_plan: ['blockers[].message', 'blockers[].context'],
|
|
75
90
|
sync_plan: ['remote', 'local_sha', 'remote_sha', 'blockers[].message'],
|
|
76
91
|
ref_inventory: ['refs[].name', 'refs[].sha', 'default_ref'],
|
|
92
|
+
change_request_opened: ['url', 'title', 'head', 'base'],
|
|
93
|
+
commit_status_set: ['sha', 'context', 'description', 'target_url'],
|
|
94
|
+
provider_identity: ['login'],
|
|
95
|
+
branch_protection: [
|
|
96
|
+
'branch_ref',
|
|
97
|
+
'required_status_contexts[]',
|
|
98
|
+
'protected_branch_rules[].name',
|
|
99
|
+
],
|
|
100
|
+
cr_files: ['changed_paths[]'],
|
|
101
|
+
cr_comments: ['comments[].id', 'comments[].author', 'comments[].path', 'comments[].body'],
|
|
102
|
+
forge_changes: ['events[].title', 'events[].url', 'events[].head_sha'],
|
|
77
103
|
cr_inventory_slice: [
|
|
78
104
|
'entries[].url',
|
|
79
105
|
'entries[].title',
|
|
@@ -88,7 +114,7 @@ export const FORGE_SOURCED_STRING_LEAVES = Object.freeze({
|
|
|
88
114
|
],
|
|
89
115
|
});
|
|
90
116
|
|
|
91
|
-
/** Keys that must never appear in remogram output (
|
|
117
|
+
/** Keys that must never appear in remogram output (external planning/SDLC workflow concepts). */
|
|
92
118
|
export { FORBIDDEN_PACKET_KEYS };
|
|
93
119
|
|
|
94
120
|
/**
|
|
@@ -108,6 +134,8 @@ export const FACT_INVENTORY_BODY_SHAPES = Object.freeze({
|
|
|
108
134
|
/** true when list cap applied (entry_count > limit), not missing entries */
|
|
109
135
|
truncated: 'boolean',
|
|
110
136
|
list_truncated: 'boolean',
|
|
137
|
+
/** normalized slice sort preset applied to open-list resolution */
|
|
138
|
+
slice_sort: 'string',
|
|
111
139
|
entries_skipped:
|
|
112
140
|
'array<{ pr_number: number, error_code: pr_not_open | api_error | oversized_raw_output | ... }> optional',
|
|
113
141
|
slice_ref: 'string optional',
|
|
@@ -116,7 +144,7 @@ export const FACT_INVENTORY_BODY_SHAPES = Object.freeze({
|
|
|
116
144
|
|
|
117
145
|
/**
|
|
118
146
|
* Build a fact-inventory packet body through the standard envelope gate.
|
|
119
|
-
* Throws if body contains forbidden
|
|
147
|
+
* Throws if body contains forbidden workflow/planning-tool keys.
|
|
120
148
|
*/
|
|
121
149
|
export function forgeFactInventoryPacket(type, context, body = {}, error = null) {
|
|
122
150
|
if (!Object.values(FACT_INVENTORY_PACKET_TYPES).includes(type)) {
|
package/cr-comments.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { sanitizeField } from './caps.js';
|
|
2
|
+
|
|
3
|
+
export const MAX_CR_COMMENTS = 256;
|
|
4
|
+
|
|
5
|
+
export function normalizeCrComment(raw) {
|
|
6
|
+
if (raw == null || typeof raw !== 'object') return null;
|
|
7
|
+
const id = raw.id ?? raw.comment_id;
|
|
8
|
+
if (id == null) return null;
|
|
9
|
+
|
|
10
|
+
const author =
|
|
11
|
+
sanitizeField(
|
|
12
|
+
raw.author ??
|
|
13
|
+
raw.user?.login ??
|
|
14
|
+
raw.user?.username ??
|
|
15
|
+
raw.user?.name ??
|
|
16
|
+
'',
|
|
17
|
+
) || null;
|
|
18
|
+
const path =
|
|
19
|
+
raw.path != null && String(raw.path).trim() !== '' ? sanitizeField(raw.path) : null;
|
|
20
|
+
const lineRaw = raw.line ?? raw.original_line ?? raw.new_line;
|
|
21
|
+
const line =
|
|
22
|
+
lineRaw != null && Number.isFinite(Number(lineRaw)) ? Math.floor(Number(lineRaw)) : null;
|
|
23
|
+
const body = sanitizeField(raw.body ?? raw.note ?? '') ?? '';
|
|
24
|
+
const resolved = Boolean(raw.resolved ?? raw.is_resolved ?? false);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
id: sanitizeField(String(id)),
|
|
28
|
+
author,
|
|
29
|
+
path,
|
|
30
|
+
line,
|
|
31
|
+
body,
|
|
32
|
+
resolved,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildCrCommentsBody({ pr_number, comments, comments_truncated, comment_count }) {
|
|
37
|
+
const list = Array.isArray(comments) ? comments : [];
|
|
38
|
+
const count = Number.isFinite(Number(comment_count))
|
|
39
|
+
? Math.floor(Number(comment_count))
|
|
40
|
+
: list.length;
|
|
41
|
+
return {
|
|
42
|
+
pr_number: Math.floor(Number(pr_number)),
|
|
43
|
+
comments: list,
|
|
44
|
+
comments_truncated: Boolean(comments_truncated),
|
|
45
|
+
comment_count: count,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildCrCommentsFromNormalizedList(prNumber, all) {
|
|
50
|
+
const comment_count = all.length;
|
|
51
|
+
const capped = all.length > MAX_CR_COMMENTS;
|
|
52
|
+
const comments = all.slice(0, MAX_CR_COMMENTS);
|
|
53
|
+
return buildCrCommentsBody({
|
|
54
|
+
pr_number: prNumber,
|
|
55
|
+
comments,
|
|
56
|
+
comments_truncated: capped,
|
|
57
|
+
comment_count,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildCrCommentsFromGiteaComments(prNumber, commentsArray) {
|
|
62
|
+
const all = [];
|
|
63
|
+
if (Array.isArray(commentsArray)) {
|
|
64
|
+
for (const item of commentsArray) {
|
|
65
|
+
const normalized = normalizeCrComment(item);
|
|
66
|
+
if (normalized) all.push(normalized);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return buildCrCommentsFromNormalizedList(prNumber, all);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildCrCommentsFromGitLabDiscussions(prNumber, discussionsArray) {
|
|
73
|
+
const all = [];
|
|
74
|
+
if (Array.isArray(discussionsArray)) {
|
|
75
|
+
for (const discussion of discussionsArray) {
|
|
76
|
+
const notes = Array.isArray(discussion?.notes) ? discussion.notes : [];
|
|
77
|
+
for (const note of notes) {
|
|
78
|
+
if (note?.system === true) continue;
|
|
79
|
+
const position = note.position && typeof note.position === 'object' ? note.position : null;
|
|
80
|
+
const normalized = normalizeCrComment({
|
|
81
|
+
id: note.id,
|
|
82
|
+
author: note.author?.username ?? note.author?.name ?? '',
|
|
83
|
+
path: position?.new_path ?? position?.old_path ?? null,
|
|
84
|
+
line: position?.new_line ?? position?.old_line ?? null,
|
|
85
|
+
body: note.body ?? '',
|
|
86
|
+
resolved: note.resolved ?? false,
|
|
87
|
+
});
|
|
88
|
+
if (normalized) all.push(normalized);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return buildCrCommentsFromNormalizedList(prNumber, all);
|
|
93
|
+
}
|
package/cr-files.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { sanitizeField } from './caps.js';
|
|
2
|
+
|
|
3
|
+
export const MAX_CR_FILES_PATHS = 256;
|
|
4
|
+
|
|
5
|
+
function sanitizePathList(filesArray) {
|
|
6
|
+
if (!Array.isArray(filesArray)) return [];
|
|
7
|
+
const paths = [];
|
|
8
|
+
for (const file of filesArray) {
|
|
9
|
+
if (file == null || typeof file !== 'object') continue;
|
|
10
|
+
const sanitized = sanitizeField(file.filename);
|
|
11
|
+
if (sanitized) paths.push(sanitized);
|
|
12
|
+
}
|
|
13
|
+
return paths;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildCrFilesBody({ pr_number, changed_paths, paths_truncated, path_count }) {
|
|
17
|
+
const paths = Array.isArray(changed_paths) ? changed_paths : [];
|
|
18
|
+
const count = Number.isFinite(Number(path_count)) ? Math.floor(Number(path_count)) : paths.length;
|
|
19
|
+
return {
|
|
20
|
+
pr_number: Math.floor(Number(pr_number)),
|
|
21
|
+
changed_paths: paths,
|
|
22
|
+
paths_truncated: Boolean(paths_truncated),
|
|
23
|
+
path_count: count,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildCrFilesFromGiteaFiles(prNumber, filesArray) {
|
|
28
|
+
const allPaths = sanitizePathList(filesArray);
|
|
29
|
+
const path_count = allPaths.length;
|
|
30
|
+
const capped = allPaths.length > MAX_CR_FILES_PATHS;
|
|
31
|
+
const changed_paths = allPaths.slice(0, MAX_CR_FILES_PATHS);
|
|
32
|
+
return buildCrFilesBody({
|
|
33
|
+
pr_number: prNumber,
|
|
34
|
+
changed_paths,
|
|
35
|
+
paths_truncated: capped,
|
|
36
|
+
path_count,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildCrFilesFromGitLabChanges(prNumber, changesArray) {
|
|
41
|
+
const paths = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
if (Array.isArray(changesArray)) {
|
|
44
|
+
for (const change of changesArray) {
|
|
45
|
+
if (change == null || typeof change !== 'object') continue;
|
|
46
|
+
const path = sanitizeField(change.new_path ?? change.old_path ?? '');
|
|
47
|
+
if (path && !seen.has(path)) {
|
|
48
|
+
seen.add(path);
|
|
49
|
+
paths.push(path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const path_count = paths.length;
|
|
54
|
+
const capped = paths.length > MAX_CR_FILES_PATHS;
|
|
55
|
+
const changed_paths = paths.slice(0, MAX_CR_FILES_PATHS);
|
|
56
|
+
return buildCrFilesBody({
|
|
57
|
+
pr_number: prNumber,
|
|
58
|
+
changed_paths,
|
|
59
|
+
paths_truncated: capped,
|
|
60
|
+
path_count,
|
|
61
|
+
});
|
|
62
|
+
}
|
package/cr-inventory.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { sanitizeField } from './caps.js';
|
|
2
2
|
import { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
3
3
|
import { mergeBlockersFromFacts, isOpenPrState } from './merge-blockers.js';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_CR_INVENTORY_SLICE_SORT,
|
|
6
|
+
normalizeCrInventorySort,
|
|
7
|
+
} from './open-pull-list.js';
|
|
4
8
|
import { staleHeadDetails } from './pr-head-reconcile.js';
|
|
5
9
|
|
|
6
10
|
export const DEFAULT_CR_INVENTORY_LIMIT = 50;
|
|
11
|
+
/** Default bound when `--limit` is omitted (keeps forge ingest under cap on large repos). */
|
|
12
|
+
export const DEFAULT_CR_INVENTORY_SAFE_LIMIT = 3;
|
|
7
13
|
|
|
8
14
|
export function normalizeCrInventoryLimit(value) {
|
|
9
|
-
if (value == null || value === '') return
|
|
15
|
+
if (value == null || value === '') return DEFAULT_CR_INVENTORY_SAFE_LIMIT;
|
|
10
16
|
const n = Number(value);
|
|
11
17
|
if (!Number.isInteger(n) || n <= 0) {
|
|
12
18
|
throw Object.assign(new Error('Invalid cr inventory limit'), {
|
|
@@ -16,12 +22,11 @@ export function normalizeCrInventoryLimit(value) {
|
|
|
16
22
|
return n;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
async function resolveOpenPullList(provider, ctx,
|
|
20
|
-
const listOpts = { limit };
|
|
25
|
+
async function resolveOpenPullList(provider, ctx, entryLimit, sliceSort) {
|
|
21
26
|
if (typeof provider.listOpenPullsWithMeta === 'function') {
|
|
22
|
-
return provider.listOpenPullsWithMeta(ctx,
|
|
27
|
+
return provider.listOpenPullsWithMeta(ctx, { retain_max: entryLimit, sort: sliceSort });
|
|
23
28
|
}
|
|
24
|
-
const numbers = await provider.listOpenPulls(ctx,
|
|
29
|
+
const numbers = await provider.listOpenPulls(ctx, {});
|
|
25
30
|
return { numbers, list_truncated: false };
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -71,16 +76,17 @@ export function buildCrInventoryEntry(ctx, view, checks) {
|
|
|
71
76
|
* Aggregate open change requests into a semantic-diff-oriented inventory slice.
|
|
72
77
|
* @param {object} ctx forge context
|
|
73
78
|
* @param {object} provider must expose listOpenPulls, prView, prChecks
|
|
74
|
-
* @param {{ slice_ref?: string, limit?: number }} [opts]
|
|
79
|
+
* @param {{ slice_ref?: string, limit?: number, sort?: string }} [opts]
|
|
75
80
|
*/
|
|
76
81
|
export async function crInventory(ctx, provider, opts = {}) {
|
|
77
82
|
const limit = normalizeCrInventoryLimit(opts.limit);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const sliceSort = normalizeCrInventorySort(opts.sort);
|
|
84
|
+
const {
|
|
85
|
+
numbers,
|
|
86
|
+
list_truncated: listTruncated,
|
|
87
|
+
entry_count: providerEntryCount,
|
|
88
|
+
} = await resolveOpenPullList(provider, ctx, limit, sliceSort);
|
|
89
|
+
const entryCount = providerEntryCount ?? numbers.length;
|
|
84
90
|
const selected = numbers.slice(0, limit);
|
|
85
91
|
const entries = [];
|
|
86
92
|
const entries_skipped = [];
|
|
@@ -106,6 +112,7 @@ export async function crInventory(ctx, provider, opts = {}) {
|
|
|
106
112
|
entry_count: entryCount,
|
|
107
113
|
truncated: entryCount > selected.length,
|
|
108
114
|
list_truncated: listTruncated,
|
|
115
|
+
slice_sort: sliceSort ?? DEFAULT_CR_INVENTORY_SLICE_SORT,
|
|
109
116
|
...(opts.slice_ref ? { slice_ref: sanitizeField(opts.slice_ref) } : {}),
|
|
110
117
|
};
|
|
111
118
|
}
|
package/cr-open.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { sanitizeField, sanitizeUrl } from './caps.js';
|
|
2
|
+
import { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
3
|
+
|
|
4
|
+
/** Normalize Gitea pull create response into change_request_opened body fields. */
|
|
5
|
+
export function buildChangeRequestOpenedBody(
|
|
6
|
+
pull,
|
|
7
|
+
{ head, base, title },
|
|
8
|
+
{ reusedExisting = false } = {},
|
|
9
|
+
) {
|
|
10
|
+
const prNumber = Number(pull?.number);
|
|
11
|
+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
|
12
|
+
throw Object.assign(new Error('Provider returned invalid pull number'), {
|
|
13
|
+
forgeError: forgeError(
|
|
14
|
+
ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
|
|
15
|
+
'Provider returned invalid pull number',
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const resolvedTitle = reusedExisting
|
|
20
|
+
? sanitizeField(pull?.title ?? title)
|
|
21
|
+
: sanitizeField(title ?? pull?.title);
|
|
22
|
+
const body = {
|
|
23
|
+
pr_number: prNumber,
|
|
24
|
+
url: sanitizeUrl(pull.html_url ?? pull.url),
|
|
25
|
+
head: sanitizeField(head),
|
|
26
|
+
base: sanitizeField(base),
|
|
27
|
+
title: resolvedTitle,
|
|
28
|
+
};
|
|
29
|
+
if (reusedExisting) {
|
|
30
|
+
body.reused_existing = true;
|
|
31
|
+
}
|
|
32
|
+
return body;
|
|
33
|
+
}
|
package/forge-changes.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { sanitizeField, sanitizeUrl } from './caps.js';
|
|
2
|
+
import { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
3
|
+
|
|
4
|
+
export const MAX_FORGE_CHANGES_EVENTS = 256;
|
|
5
|
+
|
|
6
|
+
export const FORGE_CHANGE_EVENT_KINDS = Object.freeze({
|
|
7
|
+
PR_OPENED: 'pr_opened',
|
|
8
|
+
PR_CLOSED: 'pr_closed',
|
|
9
|
+
PR_MERGED: 'pr_merged',
|
|
10
|
+
HEAD_SHA_MOVED: 'head_sha_moved',
|
|
11
|
+
CHECKS_CONCLUSION_OBSERVED: 'checks_conclusion_observed',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export function parseSinceObservedAt(raw) {
|
|
15
|
+
if (raw == null || String(raw).trim() === '') {
|
|
16
|
+
throw Object.assign(new Error('--since required'), {
|
|
17
|
+
forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--since required for forge changes'),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const ms = Date.parse(String(raw));
|
|
21
|
+
if (!Number.isFinite(ms)) {
|
|
22
|
+
throw Object.assign(new Error('Invalid --since'), {
|
|
23
|
+
forgeError: forgeError(
|
|
24
|
+
ERROR_CODES.INVALID_ARGS,
|
|
25
|
+
'--since must be a parseable ISO-8601 timestamp',
|
|
26
|
+
),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return new Date(ms).toISOString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function timestampMs(value) {
|
|
33
|
+
const ms = Date.parse(value ?? '');
|
|
34
|
+
return Number.isFinite(ms) ? ms : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isAtOrAfter(value, sinceIso) {
|
|
38
|
+
const ms = timestampMs(value);
|
|
39
|
+
const sinceMs = timestampMs(sinceIso);
|
|
40
|
+
if (ms == null || sinceMs == null) return false;
|
|
41
|
+
return ms >= sinceMs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isBefore(value, sinceIso) {
|
|
45
|
+
const ms = timestampMs(value);
|
|
46
|
+
const sinceMs = timestampMs(sinceIso);
|
|
47
|
+
if (ms == null || sinceMs == null) return false;
|
|
48
|
+
return ms < sinceMs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePrState(state) {
|
|
52
|
+
const normalized = String(state ?? '').toLowerCase();
|
|
53
|
+
if (normalized === 'open') return 'open';
|
|
54
|
+
if (normalized === 'closed') return 'closed';
|
|
55
|
+
return 'unknown';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeIsoTimestamp(value) {
|
|
59
|
+
const ms = timestampMs(value);
|
|
60
|
+
return ms == null ? null : new Date(ms).toISOString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function baseEventFields(pull) {
|
|
64
|
+
return {
|
|
65
|
+
pr_number: Math.floor(Number(pull.number)),
|
|
66
|
+
title: sanitizeField(pull.title ?? '') || null,
|
|
67
|
+
url: sanitizeUrl(pull.html_url ?? pull.url) || null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildChecksConclusionObservedEvent(prNumber, checksBody) {
|
|
72
|
+
return {
|
|
73
|
+
kind: FORGE_CHANGE_EVENT_KINDS.CHECKS_CONCLUSION_OBSERVED,
|
|
74
|
+
pr_number: Math.floor(Number(prNumber)),
|
|
75
|
+
head_sha: sanitizeField(checksBody?.head_sha ?? '') || null,
|
|
76
|
+
check_conclusion: sanitizeField(checksBody?.check_conclusion ?? '') || 'unknown',
|
|
77
|
+
checks_truncated: Boolean(checksBody?.checks_truncated ?? false),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildForgeChangesBody({
|
|
82
|
+
since,
|
|
83
|
+
events,
|
|
84
|
+
events_truncated,
|
|
85
|
+
event_count,
|
|
86
|
+
since_kind = 'observed_at',
|
|
87
|
+
}) {
|
|
88
|
+
const list = Array.isArray(events) ? events : [];
|
|
89
|
+
const count = Number.isFinite(Number(event_count))
|
|
90
|
+
? Math.floor(Number(event_count))
|
|
91
|
+
: list.length;
|
|
92
|
+
return {
|
|
93
|
+
since,
|
|
94
|
+
since_kind,
|
|
95
|
+
events: list,
|
|
96
|
+
events_truncated: Boolean(events_truncated),
|
|
97
|
+
event_count: count,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function capForgeChangeEvents(allEvents, sinceIso, { listTruncated = false } = {}) {
|
|
102
|
+
const event_count = allEvents.length;
|
|
103
|
+
const capped = allEvents.length > MAX_FORGE_CHANGES_EVENTS;
|
|
104
|
+
return buildForgeChangesBody({
|
|
105
|
+
since: sinceIso,
|
|
106
|
+
events: allEvents.slice(0, MAX_FORGE_CHANGES_EVENTS),
|
|
107
|
+
events_truncated: capped || listTruncated,
|
|
108
|
+
event_count,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function appendForgeChangeEvents(body, additionalEvents, { listTruncated = false } = {}) {
|
|
113
|
+
const merged = [...(body.events ?? []), ...(Array.isArray(additionalEvents) ? additionalEvents : [])];
|
|
114
|
+
return capForgeChangeEvents(merged, body.since, {
|
|
115
|
+
listTruncated: listTruncated || body.events_truncated,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildForgeChangesFromGiteaPulls(sinceIso, pullsArray, opts = {}) {
|
|
120
|
+
const since = parseSinceObservedAt(sinceIso);
|
|
121
|
+
const events = [];
|
|
122
|
+
if (Array.isArray(pullsArray)) {
|
|
123
|
+
for (const pull of pullsArray) {
|
|
124
|
+
if (pull == null || pull.number == null) continue;
|
|
125
|
+
const state = normalizePrState(pull.state);
|
|
126
|
+
const base = baseEventFields(pull);
|
|
127
|
+
const createdAt = pull.created_at;
|
|
128
|
+
const updatedAt = pull.updated_at;
|
|
129
|
+
const closedAt = pull.closed_at;
|
|
130
|
+
const mergedAt = pull.merged_at;
|
|
131
|
+
const mergedInWindow = isAtOrAfter(mergedAt, since);
|
|
132
|
+
|
|
133
|
+
if (isAtOrAfter(createdAt, since)) {
|
|
134
|
+
events.push({
|
|
135
|
+
kind: FORGE_CHANGE_EVENT_KINDS.PR_OPENED,
|
|
136
|
+
...base,
|
|
137
|
+
state,
|
|
138
|
+
opened_at: normalizeIsoTimestamp(createdAt),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (mergedInWindow) {
|
|
143
|
+
events.push({
|
|
144
|
+
kind: FORGE_CHANGE_EVENT_KINDS.PR_MERGED,
|
|
145
|
+
...base,
|
|
146
|
+
state: 'closed',
|
|
147
|
+
merged_at: normalizeIsoTimestamp(mergedAt),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const closedNotMerged =
|
|
152
|
+
state === 'closed' && (mergedAt == null || String(mergedAt).trim() === '');
|
|
153
|
+
const closedInWindow =
|
|
154
|
+
isAtOrAfter(closedAt, since) ||
|
|
155
|
+
(closedAt == null && closedNotMerged && isAtOrAfter(updatedAt, since));
|
|
156
|
+
if (!mergedInWindow && closedNotMerged && closedInWindow) {
|
|
157
|
+
events.push({
|
|
158
|
+
kind: FORGE_CHANGE_EVENT_KINDS.PR_CLOSED,
|
|
159
|
+
...base,
|
|
160
|
+
state: 'closed',
|
|
161
|
+
closed_at: normalizeIsoTimestamp(closedAt ?? updatedAt),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
state === 'open' &&
|
|
167
|
+
isAtOrAfter(updatedAt, since) &&
|
|
168
|
+
isBefore(createdAt, since)
|
|
169
|
+
) {
|
|
170
|
+
events.push({
|
|
171
|
+
kind: FORGE_CHANGE_EVENT_KINDS.HEAD_SHA_MOVED,
|
|
172
|
+
...base,
|
|
173
|
+
state: 'open',
|
|
174
|
+
head_sha: sanitizeField(pull.head?.sha ?? '') || null,
|
|
175
|
+
updated_at: normalizeIsoTimestamp(updatedAt),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return capForgeChangeEvents(events, since, { listTruncated: Boolean(opts.listTruncated) });
|
|
181
|
+
}
|
package/git-local.js
CHANGED
|
@@ -43,3 +43,15 @@ export function gitAheadBehind(cwd, base, head) {
|
|
|
43
43
|
return { ahead_by: null, behind_by: null };
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
export function gitDiffNameOnly(cwd, base, head) {
|
|
48
|
+
assertGitRef(base, 'base');
|
|
49
|
+
assertGitRef(head, 'head');
|
|
50
|
+
try {
|
|
51
|
+
const out = gitExec(cwd, ['diff', '--name-only', base, head]);
|
|
52
|
+
if (!out) return [];
|
|
53
|
+
return out.split('\n').filter(Boolean);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/index.js
CHANGED
|
@@ -16,6 +16,10 @@ export {
|
|
|
16
16
|
allObserverEligibleCommands,
|
|
17
17
|
} from './contracts/observer-fact-inventory.js';
|
|
18
18
|
export { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
19
|
+
export {
|
|
20
|
+
FORGE_ERROR_FIELD_ALLOWLIST,
|
|
21
|
+
normalizeForgeErrorFields,
|
|
22
|
+
} from './contracts/forge-error-fields.js';
|
|
19
23
|
export {
|
|
20
24
|
capText,
|
|
21
25
|
sanitizeField,
|
|
@@ -26,9 +30,14 @@ export {
|
|
|
26
30
|
FORGE_INGEST_MAX_BYTES_ENV,
|
|
27
31
|
DEFAULT_CHECK_STATUS_PAGE_SIZE,
|
|
28
32
|
MAX_CHECK_STATUS_PAGES,
|
|
33
|
+
DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
|
|
34
|
+
MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
|
|
29
35
|
getEffectiveIngestMaxBytes,
|
|
30
36
|
forgeIngestCapabilityFacts,
|
|
31
37
|
checkPaginationCapabilityFacts,
|
|
38
|
+
idempotencyScanCapabilityFacts,
|
|
39
|
+
statusSetIdempotencyScanCapabilityFacts,
|
|
40
|
+
openPullListCapabilityFacts,
|
|
32
41
|
} from './caps.js';
|
|
33
42
|
export {
|
|
34
43
|
paginateCheckStatusPages,
|
|
@@ -39,10 +48,103 @@ export {
|
|
|
39
48
|
withLimitParam,
|
|
40
49
|
} from './check-pagination.js';
|
|
41
50
|
export { assertGitRef, assertGitRemote } from './git-args.js';
|
|
42
|
-
export { gitRevParse, gitCurrentBranch, gitAheadBehind, gitRepoRoot } from './git-local.js';
|
|
51
|
+
export { gitRevParse, gitCurrentBranch, gitAheadBehind, gitRepoRoot, gitDiffNameOnly } from './git-local.js';
|
|
43
52
|
export { buildRefInventoryBody, refsInventory } from './ref-inventory.js';
|
|
44
|
-
export { buildCrInventoryEntry, buildHeadReconcile, crInventory, DEFAULT_CR_INVENTORY_LIMIT, normalizeCrInventoryLimit } from './cr-inventory.js';
|
|
53
|
+
export { buildCrInventoryEntry, buildHeadReconcile, crInventory, DEFAULT_CR_INVENTORY_LIMIT, DEFAULT_CR_INVENTORY_SAFE_LIMIT, normalizeCrInventoryLimit } from './cr-inventory.js';
|
|
54
|
+
export {
|
|
55
|
+
CR_INVENTORY_SLICE_SORTS,
|
|
56
|
+
DEFAULT_CR_INVENTORY_SLICE_SORT,
|
|
57
|
+
normalizeCrInventorySort,
|
|
58
|
+
parseTotalCountHeader,
|
|
59
|
+
isCrInventoryFastPathEligible,
|
|
60
|
+
forgeOrderAuthoritative,
|
|
61
|
+
validateFastPathPageLength,
|
|
62
|
+
isNumberSortFastPathEligible,
|
|
63
|
+
resolvePaginatedEntryCount,
|
|
64
|
+
resolveListTruncatedWithTrustedTotal,
|
|
65
|
+
isRecentCreatedFastPathEligible,
|
|
66
|
+
giteaRecentCreatedTailPage,
|
|
67
|
+
isNumberSortFullCollectRequired,
|
|
68
|
+
prepareGiteaOpenPullPageItems,
|
|
69
|
+
orderOpenPullNumbers,
|
|
70
|
+
buildOpenPullListMeta,
|
|
71
|
+
giteaOpenPullSortQuery,
|
|
72
|
+
gitlabOpenPullSortQuery,
|
|
73
|
+
githubOpenPullSortQuery,
|
|
74
|
+
appendSortQuery,
|
|
75
|
+
} from './open-pull-list.js';
|
|
76
|
+
export { buildChangeRequestOpenedBody } from './cr-open.js';
|
|
77
|
+
export {
|
|
78
|
+
STATUS_SET_STATES,
|
|
79
|
+
assertCommitSha,
|
|
80
|
+
normalizeStatusSetState,
|
|
81
|
+
parseStatusSetArgs,
|
|
82
|
+
buildCommitStatusSetBody,
|
|
83
|
+
} from './status-set.js';
|
|
84
|
+
export {
|
|
85
|
+
buildProviderIdentityBody,
|
|
86
|
+
buildProviderIdentityFromGiteaUser,
|
|
87
|
+
parseGitHubOAuthScopes,
|
|
88
|
+
githubCanWriteFromScopes,
|
|
89
|
+
buildProviderIdentityFromGitHubUser,
|
|
90
|
+
normalizeGitLabCanWrite,
|
|
91
|
+
parseGitLabPatSelfSignals,
|
|
92
|
+
buildProviderIdentityFromGitLabUser,
|
|
93
|
+
normalizeGiteaCanWrite,
|
|
94
|
+
unimplementedTokenScopeSignal,
|
|
95
|
+
unimplementedTokenExpirySignal,
|
|
96
|
+
} from './whoami.js';
|
|
97
|
+
export {
|
|
98
|
+
buildBranchProtectionBody,
|
|
99
|
+
buildBranchProtectionFromGiteaProtection,
|
|
100
|
+
buildBranchProtectionFromGitHubProtection,
|
|
101
|
+
buildBranchProtectionFromGitLabProtection,
|
|
102
|
+
unimplementedApprovalsRequiredSignal,
|
|
103
|
+
MAX_BRANCH_PROTECTION_STATUS_CONTEXTS,
|
|
104
|
+
MAX_BRANCH_PROTECTION_RULES,
|
|
105
|
+
} from './branch-protection.js';
|
|
106
|
+
export {
|
|
107
|
+
buildCrFilesBody,
|
|
108
|
+
buildCrFilesFromGiteaFiles,
|
|
109
|
+
buildCrFilesFromGitLabChanges,
|
|
110
|
+
MAX_CR_FILES_PATHS,
|
|
111
|
+
} from './cr-files.js';
|
|
112
|
+
export {
|
|
113
|
+
buildCrCommentsBody,
|
|
114
|
+
buildCrCommentsFromGiteaComments,
|
|
115
|
+
buildCrCommentsFromGitLabDiscussions,
|
|
116
|
+
normalizeCrComment,
|
|
117
|
+
MAX_CR_COMMENTS,
|
|
118
|
+
} from './cr-comments.js';
|
|
119
|
+
export {
|
|
120
|
+
parseSinceObservedAt,
|
|
121
|
+
buildForgeChangesBody,
|
|
122
|
+
buildForgeChangesFromGiteaPulls,
|
|
123
|
+
buildChecksConclusionObservedEvent,
|
|
124
|
+
appendForgeChangeEvents,
|
|
125
|
+
MAX_FORGE_CHANGES_EVENTS,
|
|
126
|
+
FORGE_CHANGE_EVENT_KINDS,
|
|
127
|
+
} from './forge-changes.js';
|
|
128
|
+
export {
|
|
129
|
+
WRITE_COMMAND_IDS,
|
|
130
|
+
CONFIGURED_WRITE_COMMANDS,
|
|
131
|
+
writeCommandSchema,
|
|
132
|
+
assertWriteCommandConfigured,
|
|
133
|
+
writeNotConfiguredMessage,
|
|
134
|
+
isWriteCommandConfigured,
|
|
135
|
+
} from './write-config.js';
|
|
45
136
|
export { mergeBlockersFromFacts, isOpenPrState } from './merge-blockers.js';
|
|
137
|
+
export {
|
|
138
|
+
resolveMergePlanPathScope,
|
|
139
|
+
buildMergePlanBody,
|
|
140
|
+
buildMergePlanBodyFromFacts,
|
|
141
|
+
} from './merge-plan.js';
|
|
142
|
+
export {
|
|
143
|
+
matchPathAllowlist,
|
|
144
|
+
isPathAllowed,
|
|
145
|
+
pathsOutsideAllowlist,
|
|
146
|
+
allPathsAllowed,
|
|
147
|
+
} from './path-allowlist.js';
|
|
46
148
|
export {
|
|
47
149
|
localHeadShaForPr,
|
|
48
150
|
staleHeadDetails,
|