@remogram/core 0.1.0-beta.2 → 0.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Semantic diff fact inventory contract (wave 1 — registry and trust boundaries).
3
+ * Command implementations land in waves 2–6 per plan_semantic_diff_fact_inventory.
4
+ *
5
+ * @see topo/sdlc/decisions/semantic_diff_fact_layer.tg
6
+ * @see topo/sdlc/decisions/packet_trust_doctrine.tg
7
+ */
8
+
9
+ import { forgePacket, FORBIDDEN_PACKET_KEYS, SCHEMA_VERSION } from './envelope.js';
10
+
11
+ /** Authoritative v1 read/plan surface today. Fact inventory extends; does not replace. */
12
+ export const V1_READ_PLAN_COMMANDS = Object.freeze([
13
+ 'repo status',
14
+ 'refs compare',
15
+ 'refs inventory',
16
+ 'cr inventory',
17
+ 'pr view',
18
+ 'pr checks',
19
+ 'merge plan',
20
+ 'sync plan',
21
+ 'provider capabilities',
22
+ 'doctor',
23
+ ]);
24
+
25
+ /** v1 write surface (Gitea cr open only in first slice). */
26
+ export const V1_WRITE_COMMANDS = Object.freeze(['cr open']);
27
+
28
+ /**
29
+ * Planned fact-inventory packet types (not emitted until wave 2+ commands ship).
30
+ * All use schema_version 1 envelope discipline via forgePacket.
31
+ */
32
+ export const FACT_INVENTORY_PACKET_TYPES = Object.freeze({
33
+ REF_INVENTORY: 'ref_inventory',
34
+ CR_INVENTORY_SLICE: 'cr_inventory_slice',
35
+ });
36
+
37
+ /** Trusted structural envelope on every remogram packet (authoritative for agents). */
38
+ export const TRUSTED_ENVELOPE_FIELDS = Object.freeze([
39
+ 'type',
40
+ 'schema_version',
41
+ 'provider_id',
42
+ 'remote_name',
43
+ 'repo_id',
44
+ 'observed_at',
45
+ 'ok',
46
+ ]);
47
+
48
+ /**
49
+ * Normalized enum and boolean body fields agents may treat as structural facts
50
+ * (not forge prose). Provider-specific strings that are normalized to enums
51
+ * belong here; raw forge copy does not.
52
+ */
53
+ export const TRUSTED_NORMALIZED_BODY_FIELDS = Object.freeze({
54
+ mergeability: true,
55
+ check_conclusion: true,
56
+ checks_conclusion: true,
57
+ state: true,
58
+ truncated: true,
59
+ list_truncated: true,
60
+ checks_truncated: true,
61
+ slice_sort: true,
62
+ entry_count: true,
63
+ mergeability_confidence: true,
64
+ write_support: true,
65
+ diverged: true,
66
+ auth_present: true,
67
+ reused_existing: true,
68
+ idempotency_scan: true,
69
+ });
70
+
71
+ /**
72
+ * String leaves copied from forge or git resolution that remain semantically
73
+ * untrusted per decision_packet_trust_doctrine. Structurally sanitized only.
74
+ */
75
+ export const FORGE_SOURCED_STRING_LEAVES = Object.freeze({
76
+ repo_status: ['default_branch', 'auth_env', 'capabilities'],
77
+ ref_compare: ['base_ref', 'head_ref', 'base_sha', 'head_sha'],
78
+ pr_status: ['url', 'title', 'base_ref', 'head_ref', 'base_sha', 'head_sha'],
79
+ pr_checks: ['head_sha', 'statuses[].context', 'statuses[].description', 'statuses[].target_url'],
80
+ merge_plan: ['blockers[].message', 'blockers[].context'],
81
+ sync_plan: ['remote', 'local_sha', 'remote_sha', 'blockers[].message'],
82
+ ref_inventory: ['refs[].name', 'refs[].sha', 'default_ref'],
83
+ change_request_opened: ['url', 'title', 'head', 'base'],
84
+ cr_inventory_slice: [
85
+ 'entries[].url',
86
+ 'entries[].title',
87
+ 'entries[].base_ref',
88
+ 'entries[].head_ref',
89
+ 'entries[].base_sha',
90
+ 'entries[].head_sha',
91
+ 'entries[].head_reconcile.local_head_sha',
92
+ 'entries[].head_reconcile.head_sha',
93
+ 'entries[].checks[].context',
94
+ 'entries[].checks[].description',
95
+ ],
96
+ });
97
+
98
+ /** Keys that must never appear in remogram output (external planning/SDLC workflow concepts). */
99
+ export { FORBIDDEN_PACKET_KEYS };
100
+
101
+ /**
102
+ * Documented body shapes for planned fact inventory packets (wave 2+).
103
+ * Used by contract tests and provider normalization; not emitted in wave 1.
104
+ */
105
+ export const FACT_INVENTORY_BODY_SHAPES = Object.freeze({
106
+ [FACT_INVENTORY_PACKET_TYPES.REF_INVENTORY]: {
107
+ refs: 'array<{ name: string, sha: string, kind?: string, is_default?: boolean }>',
108
+ default_ref: 'string optional',
109
+ ancestry_hints: 'array<{ base_ref: string, head_ref: string, ahead_by?: number, behind_by?: number }> optional',
110
+ },
111
+ [FACT_INVENTORY_PACKET_TYPES.CR_INVENTORY_SLICE]: {
112
+ entries:
113
+ 'array<{ pr_number: number, url?: string, title?: string, state?: string, base_ref?: string, head_ref?: string, base_sha?: string, head_sha?: string, mergeability?: string, checks_conclusion?: string, checks_truncated?: boolean, blockers?: array, head_reconcile?: { stale: boolean, local_head_sha?: string, head_sha?: string } }>',
114
+ entry_count: 'number',
115
+ /** true when list cap applied (entry_count > limit), not missing entries */
116
+ truncated: 'boolean',
117
+ list_truncated: 'boolean',
118
+ /** normalized slice sort preset applied to open-list resolution */
119
+ slice_sort: 'string',
120
+ entries_skipped:
121
+ 'array<{ pr_number: number, error_code: pr_not_open | api_error | oversized_raw_output | ... }> optional',
122
+ slice_ref: 'string optional',
123
+ },
124
+ });
125
+
126
+ /**
127
+ * Build a fact-inventory packet body through the standard envelope gate.
128
+ * Throws if body contains forbidden workflow/planning-tool keys.
129
+ */
130
+ export function forgeFactInventoryPacket(type, context, body = {}, error = null) {
131
+ if (!Object.values(FACT_INVENTORY_PACKET_TYPES).includes(type)) {
132
+ throw new Error(`Unknown fact inventory packet type: ${type}`);
133
+ }
134
+ return forgePacket(type, context, body, error);
135
+ }
136
+
137
+ export { SCHEMA_VERSION };
@@ -0,0 +1,118 @@
1
+ import { sanitizeField } from './caps.js';
2
+ import { ERROR_CODES, forgeError } from './contracts/errors.js';
3
+ import { mergeBlockersFromFacts, isOpenPrState } from './merge-blockers.js';
4
+ import {
5
+ DEFAULT_CR_INVENTORY_SLICE_SORT,
6
+ normalizeCrInventorySort,
7
+ } from './open-pull-list.js';
8
+ import { staleHeadDetails } from './pr-head-reconcile.js';
9
+
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;
13
+
14
+ export function normalizeCrInventoryLimit(value) {
15
+ if (value == null || value === '') return DEFAULT_CR_INVENTORY_SAFE_LIMIT;
16
+ const n = Number(value);
17
+ if (!Number.isInteger(n) || n <= 0) {
18
+ throw Object.assign(new Error('Invalid cr inventory limit'), {
19
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, '--limit must be a positive integer'),
20
+ });
21
+ }
22
+ return n;
23
+ }
24
+
25
+ async function resolveOpenPullList(provider, ctx, entryLimit, sliceSort) {
26
+ if (typeof provider.listOpenPullsWithMeta === 'function') {
27
+ return provider.listOpenPullsWithMeta(ctx, { retain_max: entryLimit, sort: sliceSort });
28
+ }
29
+ const numbers = await provider.listOpenPulls(ctx, {});
30
+ return { numbers, list_truncated: false };
31
+ }
32
+
33
+ function skipErrorCode(err) {
34
+ if (err?.forgeError?.code) return err.forgeError.code;
35
+ return ERROR_CODES.API_ERROR;
36
+ }
37
+
38
+ export function buildHeadReconcile(ctx, view) {
39
+ const details = staleHeadDetails(
40
+ ctx.cwd,
41
+ ctx.config?.remote ?? ctx.remoteName,
42
+ view.head_ref,
43
+ view.head_sha,
44
+ );
45
+ if (!details) return { stale: false };
46
+ return {
47
+ stale: true,
48
+ local_head_sha: details.local_head_sha,
49
+ head_sha: details.head_sha,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Compose one CR inventory entry from pr view and checks facts.
55
+ */
56
+ export function buildCrInventoryEntry(ctx, view, checks) {
57
+ const entry = {
58
+ pr_number: view.pr_number,
59
+ url: view.url,
60
+ title: view.title,
61
+ state: view.state,
62
+ base_ref: view.base_ref,
63
+ head_ref: view.head_ref,
64
+ mergeability: view.mergeability,
65
+ checks_conclusion: checks.check_conclusion,
66
+ checks_truncated: checks.checks_truncated === true,
67
+ blockers: mergeBlockersFromFacts(view, checks),
68
+ head_reconcile: buildHeadReconcile(ctx, view),
69
+ };
70
+ if (view.base_sha) entry.base_sha = view.base_sha;
71
+ if (view.head_sha) entry.head_sha = view.head_sha;
72
+ return entry;
73
+ }
74
+
75
+ /**
76
+ * Aggregate open change requests into a semantic-diff-oriented inventory slice.
77
+ * @param {object} ctx forge context
78
+ * @param {object} provider must expose listOpenPulls, prView, prChecks
79
+ * @param {{ slice_ref?: string, limit?: number, sort?: string }} [opts]
80
+ */
81
+ export async function crInventory(ctx, provider, opts = {}) {
82
+ const limit = normalizeCrInventoryLimit(opts.limit);
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;
90
+ const selected = numbers.slice(0, limit);
91
+ const entries = [];
92
+ const entries_skipped = [];
93
+ for (const number of selected) {
94
+ try {
95
+ const view = await provider.prView(ctx, { number });
96
+ if (!isOpenPrState(view.state)) {
97
+ entries_skipped.push({ pr_number: number, error_code: ERROR_CODES.PR_NOT_OPEN });
98
+ continue;
99
+ }
100
+ const checks = await provider.prChecks(ctx, { number });
101
+ entries.push(buildCrInventoryEntry(ctx, view, checks));
102
+ } catch (err) {
103
+ entries_skipped.push({
104
+ pr_number: number,
105
+ error_code: skipErrorCode(err),
106
+ });
107
+ }
108
+ }
109
+ return {
110
+ entries,
111
+ ...(entries_skipped.length ? { entries_skipped } : {}),
112
+ entry_count: entryCount,
113
+ truncated: entryCount > selected.length,
114
+ list_truncated: listTruncated,
115
+ slice_sort: sliceSort ?? DEFAULT_CR_INVENTORY_SLICE_SORT,
116
+ ...(opts.slice_ref ? { slice_ref: sanitizeField(opts.slice_ref) } : {}),
117
+ };
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/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/http.js CHANGED
@@ -60,12 +60,46 @@ export function parseLinkHeader(linkHeader) {
60
60
  if (!linkHeader) return {};
61
61
  const links = {};
62
62
  for (const segment of String(linkHeader).split(',')) {
63
- const match = segment.trim().match(/^<([^>]+)>;\s*rel="([^"]+)"/);
64
- if (match) links[match[2]] = match[1];
63
+ const match = segment.trim().match(/^<([^>]+)>;\s*rel=(?:"([^"]+)"|'([^']+)')/);
64
+ if (match) links[(match[2] ?? match[3]).toLowerCase()] = match[1];
65
65
  }
66
66
  return links;
67
67
  }
68
68
 
69
+ function normalizePathname(pathname) {
70
+ if (pathname.length > 1 && pathname.endsWith('/')) {
71
+ return pathname.replace(/\/+$/, '') || '/';
72
+ }
73
+ return pathname;
74
+ }
75
+
76
+ /** Reject Link rel=next URLs that leave the configured API origin (token exfiltration guard). */
77
+ export function isTrustedPaginationUrl(trustedOrigin, url, resolveBase) {
78
+ try {
79
+ if (resolveBase == null || resolveBase === '') {
80
+ return false;
81
+ }
82
+ const baseResolved = new URL(resolveBase);
83
+ if (baseResolved.username !== '' || baseResolved.password !== '') {
84
+ return false;
85
+ }
86
+ const resolved = new URL(url, resolveBase);
87
+ if (resolved.username !== '' || resolved.password !== '') {
88
+ return false;
89
+ }
90
+ if (resolved.origin !== trustedOrigin) {
91
+ return false;
92
+ }
93
+ const basePath = normalizePathname(new URL(resolveBase).pathname);
94
+ if (normalizePathname(resolved.pathname) !== basePath) {
95
+ return false;
96
+ }
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
69
103
  export async function fetchJsonWithMeta(
70
104
  url,
71
105
  options = {},
package/index.js CHANGED
@@ -1,5 +1,25 @@
1
1
  export { SCHEMA_VERSION, PACKET_TYPES, forgePacket, forgeErrorPacket, unknownForgeContext, FORBIDDEN_PACKET_KEYS } from './contracts/envelope.js';
2
+ export {
3
+ V1_READ_PLAN_COMMANDS,
4
+ FACT_INVENTORY_PACKET_TYPES,
5
+ TRUSTED_ENVELOPE_FIELDS,
6
+ TRUSTED_NORMALIZED_BODY_FIELDS,
7
+ FORGE_SOURCED_STRING_LEAVES,
8
+ FACT_INVENTORY_BODY_SHAPES,
9
+ forgeFactInventoryPacket,
10
+ } from './contracts/semantic-diff-facts.js';
11
+ export {
12
+ OBSERVER_REMOGRAM_COMMANDS,
13
+ OBSERVER_FACT_INVENTORY_PACKETS,
14
+ observerProtoRemogramCommands,
15
+ semanticDiffFactCommands,
16
+ allObserverEligibleCommands,
17
+ } from './contracts/observer-fact-inventory.js';
2
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';
3
23
  export {
4
24
  capText,
5
25
  sanitizeField,
@@ -8,11 +28,70 @@ export {
8
28
  DEFAULT_MAX_BYTES,
9
29
  DEFAULT_FIELD_MAX_BYTES,
10
30
  FORGE_INGEST_MAX_BYTES_ENV,
31
+ DEFAULT_CHECK_STATUS_PAGE_SIZE,
32
+ MAX_CHECK_STATUS_PAGES,
33
+ DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
34
+ MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
11
35
  getEffectiveIngestMaxBytes,
12
36
  forgeIngestCapabilityFacts,
37
+ checkPaginationCapabilityFacts,
38
+ idempotencyScanCapabilityFacts,
39
+ openPullListCapabilityFacts,
13
40
  } from './caps.js';
41
+ export {
42
+ paginateCheckStatusPages,
43
+ paginateOffsetListPages,
44
+ fetchWithIngestPageBackoff,
45
+ fetchPageWithIngestBackoff,
46
+ withPerPageParam,
47
+ withLimitParam,
48
+ } from './check-pagination.js';
14
49
  export { assertGitRef, assertGitRemote } from './git-args.js';
15
- export { gitRevParse, gitCurrentBranch, gitAheadBehind, gitRepoRoot } from './git-local.js';
50
+ export { gitRevParse, gitCurrentBranch, gitAheadBehind, gitRepoRoot, gitDiffNameOnly } from './git-local.js';
51
+ export { buildRefInventoryBody, refsInventory } from './ref-inventory.js';
52
+ export { buildCrInventoryEntry, buildHeadReconcile, crInventory, DEFAULT_CR_INVENTORY_LIMIT, DEFAULT_CR_INVENTORY_SAFE_LIMIT, normalizeCrInventoryLimit } from './cr-inventory.js';
53
+ export {
54
+ CR_INVENTORY_SLICE_SORTS,
55
+ DEFAULT_CR_INVENTORY_SLICE_SORT,
56
+ normalizeCrInventorySort,
57
+ parseTotalCountHeader,
58
+ isCrInventoryFastPathEligible,
59
+ forgeOrderAuthoritative,
60
+ validateFastPathPageLength,
61
+ isNumberSortFastPathEligible,
62
+ resolvePaginatedEntryCount,
63
+ resolveListTruncatedWithTrustedTotal,
64
+ isRecentCreatedFastPathEligible,
65
+ giteaRecentCreatedTailPage,
66
+ isNumberSortFullCollectRequired,
67
+ prepareGiteaOpenPullPageItems,
68
+ orderOpenPullNumbers,
69
+ buildOpenPullListMeta,
70
+ giteaOpenPullSortQuery,
71
+ gitlabOpenPullSortQuery,
72
+ githubOpenPullSortQuery,
73
+ appendSortQuery,
74
+ } from './open-pull-list.js';
75
+ export { buildChangeRequestOpenedBody } from './cr-open.js';
76
+ export {
77
+ WRITE_COMMAND_IDS,
78
+ CONFIGURED_WRITE_COMMANDS,
79
+ writeCommandSchema,
80
+ assertWriteCommandConfigured,
81
+ isWriteCommandConfigured,
82
+ } from './write-config.js';
83
+ export { mergeBlockersFromFacts, isOpenPrState } from './merge-blockers.js';
84
+ export {
85
+ resolveMergePlanPathScope,
86
+ buildMergePlanBody,
87
+ buildMergePlanBodyFromFacts,
88
+ } from './merge-plan.js';
89
+ export {
90
+ matchPathAllowlist,
91
+ isPathAllowed,
92
+ pathsOutsideAllowlist,
93
+ allPathsAllowed,
94
+ } from './path-allowlist.js';
16
95
  export {
17
96
  localHeadShaForPr,
18
97
  staleHeadDetails,
@@ -32,7 +111,14 @@ export {
32
111
  assertForgeReady,
33
112
  forgeContext,
34
113
  } from './resolve.js';
35
- export { fetchWithTimeout, fetchJson, fetchJsonWithMeta, parseLinkHeader, fetchTextCapped } from './http.js';
114
+ export {
115
+ fetchWithTimeout,
116
+ fetchJson,
117
+ fetchJsonWithMeta,
118
+ parseLinkHeader,
119
+ isTrustedPaginationUrl,
120
+ fetchTextCapped,
121
+ } from './http.js';
36
122
  export {
37
123
  AUTH_CLASS,
38
124
  API_PROVIDER_COMMAND_AUTH,
@@ -0,0 +1,36 @@
1
+ import { allPathsAllowed } from './path-allowlist.js';
2
+
3
+ /**
4
+ * Derive merge blockers from already-fetched PR view and checks facts.
5
+ * Shared by merge plan and cr inventory aggregation.
6
+ */
7
+ export function isOpenPrState(state) {
8
+ return String(state ?? '').toLowerCase() === 'open';
9
+ }
10
+
11
+ /**
12
+ * @param {object} view
13
+ * @param {object} checks
14
+ * @param {{ allowed_paths?: string[], changed_paths?: string[] | null }} [pathScope]
15
+ */
16
+ export function mergeBlockersFromFacts(view, checks, pathScope = {}) {
17
+ const blockers = [];
18
+ if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
19
+ if (!isOpenPrState(view.state)) blockers.push('pr_not_open');
20
+ if (checks.checks_truncated === true) blockers.push('checks_incomplete');
21
+ if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
22
+ if (checks.check_conclusion === 'missing') blockers.push('checks_missing');
23
+ if (checks.check_conclusion === 'pending') blockers.push('checks_pending');
24
+
25
+ const allowedPaths = pathScope.allowed_paths;
26
+ if (Array.isArray(allowedPaths) && allowedPaths.length > 0) {
27
+ const changedPaths = pathScope.changed_paths;
28
+ if (changedPaths == null) {
29
+ blockers.push('changed_paths_unavailable');
30
+ } else if (!allPathsAllowed(allowedPaths, changedPaths)) {
31
+ blockers.push('path_scope_violation');
32
+ }
33
+ }
34
+
35
+ return blockers;
36
+ }
package/merge-plan.js ADDED
@@ -0,0 +1,34 @@
1
+ import { gitDiffNameOnly } from './git-local.js';
2
+ import { mergeBlockersFromFacts } from './merge-blockers.js';
3
+
4
+ function normalizeAllowedPaths(allowedPaths) {
5
+ if (!Array.isArray(allowedPaths)) return null;
6
+ const normalized = allowedPaths.filter((entry) => typeof entry === 'string' && entry.length > 0);
7
+ return normalized.length > 0 ? normalized : null;
8
+ }
9
+
10
+ export function resolveMergePlanPathScope(ctx, view, opts = {}) {
11
+ const allowedPaths = normalizeAllowedPaths(opts.allowed_paths);
12
+ if (!allowedPaths) {
13
+ return { allowed_paths: null, changed_paths: null };
14
+ }
15
+ if (!view.base_sha || !view.head_sha) {
16
+ return { allowed_paths: allowedPaths, changed_paths: null };
17
+ }
18
+ const changedPaths = gitDiffNameOnly(ctx.cwd, view.base_sha, view.head_sha);
19
+ return { allowed_paths: allowedPaths, changed_paths: changedPaths };
20
+ }
21
+
22
+ export function buildMergePlanBody(view, checks, pathScope = {}) {
23
+ return {
24
+ pr_number: view.pr_number,
25
+ mergeability: view.mergeability,
26
+ checks_conclusion: checks.check_conclusion,
27
+ blockers: mergeBlockersFromFacts(view, checks, pathScope),
28
+ };
29
+ }
30
+
31
+ export function buildMergePlanBodyFromFacts(ctx, view, checks, opts = {}) {
32
+ const pathScope = resolveMergePlanPathScope(ctx, view, opts);
33
+ return buildMergePlanBody(view, checks, pathScope);
34
+ }