@bridge_gpt/mcp-server 0.2.68 → 0.2.70
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 +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* epic-contract-context — the MCP side of epic-aware review resolution (BAPI-1428).
|
|
3
|
+
*
|
|
4
|
+
* Three jobs live here, kept out of `index.ts` so they can be unit-tested:
|
|
5
|
+
*
|
|
6
|
+
* 1. **The switch.** `EPIC_CONTRACT_CONTEXT_ENABLED` is the same default-ON
|
|
7
|
+
* variable the Python service reads
|
|
8
|
+
* (`api/library/tickets/epic_contract_context_config_lib.py`). It is
|
|
9
|
+
* independent of delivery readiness and of tool-surface gating.
|
|
10
|
+
* 2. **Transport.** Building the context request, and turning a complete
|
|
11
|
+
* transport failure into a disclosed, degraded result instead of an error,
|
|
12
|
+
* so an unreachable context service never stops a review.
|
|
13
|
+
* 3. **The structured resolution result.** Parsing and validating the fenced
|
|
14
|
+
* `bapi-epic-contract-resolution` block that `evaluate-and-recommend.md`
|
|
15
|
+
* writes into `<KEY>-review-and-resolution.md`, and deciding whether terminal
|
|
16
|
+
* decision capture may proceed. Capture is refused only while a finding is
|
|
17
|
+
* recorded as an unresolved mandatory incompatibility.
|
|
18
|
+
*
|
|
19
|
+
* The resolution duty is not duplicated here: it is the canonical
|
|
20
|
+
* `api/library/tickets/epic_contract_resolution_duty.md`, bundled verbatim into
|
|
21
|
+
* `SHARED_RESOURCES` (`shared-resources.generated.ts`) by
|
|
22
|
+
* `scripts/bundle-pipelines.js`.
|
|
23
|
+
*/
|
|
24
|
+
import { parseDefaultOnEnvFlag } from "./env-flags.js";
|
|
25
|
+
import { isValidTicketKey } from "./ticket-key-utils.js";
|
|
26
|
+
/** The single switch, shared with the Python service. */
|
|
27
|
+
export const EPIC_CONTRACT_CONTEXT_ENV_VAR = "EPIC_CONTRACT_CONTEXT_ENABLED";
|
|
28
|
+
/** The fenced-block info string the structured result is written under. */
|
|
29
|
+
export const EPIC_RESOLUTION_FENCE = "bapi-epic-contract-resolution";
|
|
30
|
+
/** Version of the structured result this module reads and validates. */
|
|
31
|
+
export const EPIC_RESOLUTION_SCHEMA_VERSION = 1;
|
|
32
|
+
/** Bundled resource name of the canonical resolution duty. */
|
|
33
|
+
export const EPIC_CONTRACT_DUTY_RESOURCE = "epic-contract-resolution-duty.md";
|
|
34
|
+
/**
|
|
35
|
+
* Whether epic-contract resolution is on for new reviews in this process.
|
|
36
|
+
*
|
|
37
|
+
* `env` defaults to `process.env`; tests inject an explicit object.
|
|
38
|
+
*/
|
|
39
|
+
export function epicContractContextEnabled(env = process.env) {
|
|
40
|
+
return parseDefaultOnEnvFlag(env[EPIC_CONTRACT_CONTEXT_ENV_VAR]);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the context route's request body. Optional selectors are omitted when
|
|
44
|
+
* absent rather than sent as null or empty values.
|
|
45
|
+
*/
|
|
46
|
+
export function buildEpicContractContextBody(args, repoName) {
|
|
47
|
+
const body = {
|
|
48
|
+
repo_name: repoName,
|
|
49
|
+
ticket_key: args.ticket_key.trim().toUpperCase(),
|
|
50
|
+
};
|
|
51
|
+
if (args.policy_token)
|
|
52
|
+
body.policy_token = args.policy_token;
|
|
53
|
+
if (args.sibling_keys && args.sibling_keys.length > 0)
|
|
54
|
+
body.sibling_keys = args.sibling_keys;
|
|
55
|
+
if (args.children_cursor)
|
|
56
|
+
body.children_cursor = args.children_cursor;
|
|
57
|
+
if (args.page_limit !== undefined)
|
|
58
|
+
body.page_limit = args.page_limit;
|
|
59
|
+
if (args.include_parent !== undefined)
|
|
60
|
+
body.include_parent = args.include_parent;
|
|
61
|
+
if (args.excerpts && args.excerpts.length > 0)
|
|
62
|
+
body.excerpts = args.excerpts;
|
|
63
|
+
if (args.revalidate && args.revalidate.length > 0)
|
|
64
|
+
body.revalidate = args.revalidate;
|
|
65
|
+
return body;
|
|
66
|
+
}
|
|
67
|
+
/** The result returned when the switch is off and no starting-policy token was supplied. */
|
|
68
|
+
export function disabledContextResult(ticketKey) {
|
|
69
|
+
return {
|
|
70
|
+
status: "disabled",
|
|
71
|
+
subject_key: ticketKey.trim().toUpperCase(),
|
|
72
|
+
policy_enabled: false,
|
|
73
|
+
coverage: { complete: false, gaps: [] },
|
|
74
|
+
note: "Epic-contract resolution is switched off for new reviews. Continue the ordinary review; " +
|
|
75
|
+
"recorded decisions are unaffected.",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The degraded result for a complete transport failure.
|
|
80
|
+
*
|
|
81
|
+
* The review continues with the evidence it already has. Fallback telemetry is
|
|
82
|
+
* reported as `unavailable`: this process has no authenticated, non-admin
|
|
83
|
+
* diagnostic channel it could use instead, and it never claims a report was
|
|
84
|
+
* delivered when it was not.
|
|
85
|
+
*/
|
|
86
|
+
export function transportFailureResult(ticketKey, duty) {
|
|
87
|
+
return {
|
|
88
|
+
status: "degraded",
|
|
89
|
+
subject_key: ticketKey.trim().toUpperCase(),
|
|
90
|
+
parent: null,
|
|
91
|
+
inventory: null,
|
|
92
|
+
siblings: [],
|
|
93
|
+
revalidations: [],
|
|
94
|
+
coverage: {
|
|
95
|
+
fetched: [],
|
|
96
|
+
partial: [],
|
|
97
|
+
missing_or_failed: [],
|
|
98
|
+
unhydrated: [],
|
|
99
|
+
complete: false,
|
|
100
|
+
gaps: [
|
|
101
|
+
{
|
|
102
|
+
reason: "read_failed",
|
|
103
|
+
ticket_key: null,
|
|
104
|
+
detail: "The epic-contract context service could not be reached.",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
telemetry: { status: "unavailable", reported_gap_count: 0 },
|
|
109
|
+
duty: duty ?? null,
|
|
110
|
+
note: "No epic contract was retrieved. Disclose this gap, resolve with the ticket and code " +
|
|
111
|
+
"evidence you have, and do not claim any parent or sibling contract was checked.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const FENCE_PATTERN = new RegExp("^```" + EPIC_RESOLUTION_FENCE + "[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n```[ \\t]*$", "gm");
|
|
115
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
116
|
+
const STATUSES = new Set([
|
|
117
|
+
"settled",
|
|
118
|
+
"mandatory_incompatibility",
|
|
119
|
+
"not_applicable",
|
|
120
|
+
"disabled",
|
|
121
|
+
]);
|
|
122
|
+
const RESOLUTIONS = new Set([
|
|
123
|
+
"accepted",
|
|
124
|
+
"adapted",
|
|
125
|
+
"rejected",
|
|
126
|
+
"alternative",
|
|
127
|
+
"unresolved",
|
|
128
|
+
]);
|
|
129
|
+
function isStringArray(value) {
|
|
130
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
131
|
+
}
|
|
132
|
+
function validateFinding(raw, index, errors) {
|
|
133
|
+
const where = `findings[${index}]`;
|
|
134
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
135
|
+
errors.push(`${where} must be an object`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const f = raw;
|
|
139
|
+
if (typeof f.id !== "string" || !f.id.trim())
|
|
140
|
+
errors.push(`${where}.id must be a non-empty string`);
|
|
141
|
+
if (!isStringArray(f.source_ids))
|
|
142
|
+
errors.push(`${where}.source_ids must be a string array`);
|
|
143
|
+
if (typeof f.resolution !== "string" || !RESOLUTIONS.has(f.resolution)) {
|
|
144
|
+
errors.push(`${where}.resolution is not a recognized value`);
|
|
145
|
+
}
|
|
146
|
+
if (f.status !== "settled" && f.status !== "mandatory_incompatibility") {
|
|
147
|
+
errors.push(`${where}.status must be settled or mandatory_incompatibility`);
|
|
148
|
+
}
|
|
149
|
+
const cited = Array.isArray(f.cited_sources) ? f.cited_sources : null;
|
|
150
|
+
if (!cited) {
|
|
151
|
+
errors.push(`${where}.cited_sources must be an array`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
cited.forEach((c, i) => {
|
|
155
|
+
const s = c;
|
|
156
|
+
if (!s ||
|
|
157
|
+
typeof s.ticket_key !== "string" ||
|
|
158
|
+
!isValidTicketKey(s.ticket_key) ||
|
|
159
|
+
typeof s.content_sha256 !== "string" ||
|
|
160
|
+
!SHA256_PATTERN.test(s.content_sha256)) {
|
|
161
|
+
errors.push(`${where}.cited_sources[${i}] needs a ticket_key and a 64-hex content_sha256`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (f.status === "mandatory_incompatibility") {
|
|
166
|
+
if (f.resolution !== "unresolved") {
|
|
167
|
+
errors.push(`${where} is a mandatory incompatibility, so its resolution must be unresolved`);
|
|
168
|
+
}
|
|
169
|
+
if (!isStringArray(f.conflicting_obligations) || f.conflicting_obligations.length < 2) {
|
|
170
|
+
errors.push(`${where}.conflicting_obligations must cite at least two obligations`);
|
|
171
|
+
}
|
|
172
|
+
if (!isStringArray(f.attempted_alternatives) || f.attempted_alternatives.length < 1) {
|
|
173
|
+
errors.push(`${where}.attempted_alternatives must list what was tried`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof f.recommended_resolution !== "string" || !f.recommended_resolution.trim()) {
|
|
176
|
+
errors.push(`${where}.recommended_resolution must name the recommended decision`);
|
|
177
|
+
}
|
|
178
|
+
if (cited && cited.length < 1) {
|
|
179
|
+
errors.push(`${where}.cited_sources must cite the conflicting contracts`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (f.resolution === "unresolved") {
|
|
183
|
+
errors.push(`${where} is settled, so its resolution cannot be unresolved`);
|
|
184
|
+
}
|
|
185
|
+
return errors.length > 0 ? null : f;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Validate a decoded structured result. Returns the typed result or the list of
|
|
189
|
+
* problems found; never throws.
|
|
190
|
+
*/
|
|
191
|
+
export function validateEpicResolution(raw) {
|
|
192
|
+
const errors = [];
|
|
193
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
194
|
+
return { found: true, ok: false, errors: ["the block must be a JSON object"] };
|
|
195
|
+
}
|
|
196
|
+
const r = raw;
|
|
197
|
+
if (r.version !== EPIC_RESOLUTION_SCHEMA_VERSION) {
|
|
198
|
+
errors.push(`version must be ${EPIC_RESOLUTION_SCHEMA_VERSION}`);
|
|
199
|
+
}
|
|
200
|
+
if (typeof r.subject_key !== "string" || !isValidTicketKey(r.subject_key)) {
|
|
201
|
+
errors.push("subject_key must be a ticket key");
|
|
202
|
+
}
|
|
203
|
+
if (r.generation !== null && !(Number.isInteger(r.generation) && r.generation > 0)) {
|
|
204
|
+
errors.push("generation must be a positive integer or null");
|
|
205
|
+
}
|
|
206
|
+
if (r.source_bundle_sha256 !== null &&
|
|
207
|
+
!(typeof r.source_bundle_sha256 === "string" && SHA256_PATTERN.test(r.source_bundle_sha256))) {
|
|
208
|
+
errors.push("source_bundle_sha256 must be 64 lowercase hex or null");
|
|
209
|
+
}
|
|
210
|
+
const policy = r.policy;
|
|
211
|
+
if (!policy || typeof policy.enabled !== "boolean" || !Number.isInteger(policy.policy_version)) {
|
|
212
|
+
errors.push("policy must carry enabled and policy_version");
|
|
213
|
+
}
|
|
214
|
+
if (typeof r.status !== "string" || !STATUSES.has(r.status)) {
|
|
215
|
+
errors.push("status is not a recognized value");
|
|
216
|
+
}
|
|
217
|
+
const coverage = r.coverage;
|
|
218
|
+
if (!coverage || typeof coverage.complete !== "boolean" || !isStringArray(coverage.gaps)) {
|
|
219
|
+
errors.push("coverage must carry complete and a gaps string array");
|
|
220
|
+
}
|
|
221
|
+
const findings = [];
|
|
222
|
+
if (!Array.isArray(r.findings)) {
|
|
223
|
+
errors.push("findings must be an array");
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
r.findings.forEach((raw, i) => {
|
|
227
|
+
const findingErrors = [];
|
|
228
|
+
const finding = validateFinding(raw, i, findingErrors);
|
|
229
|
+
errors.push(...findingErrors);
|
|
230
|
+
if (finding)
|
|
231
|
+
findings.push(finding);
|
|
232
|
+
});
|
|
233
|
+
const ids = new Set();
|
|
234
|
+
for (const f of findings) {
|
|
235
|
+
if (ids.has(f.id))
|
|
236
|
+
errors.push(`finding id ${f.id} is duplicated`);
|
|
237
|
+
ids.add(f.id);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (errors.length === 0) {
|
|
241
|
+
const anyConflict = findings.some((f) => f.status === "mandatory_incompatibility");
|
|
242
|
+
if (anyConflict && r.status !== "mandatory_incompatibility") {
|
|
243
|
+
errors.push("a finding is a mandatory incompatibility, so status must be mandatory_incompatibility");
|
|
244
|
+
}
|
|
245
|
+
if (!anyConflict && r.status === "mandatory_incompatibility") {
|
|
246
|
+
errors.push("status is mandatory_incompatibility but no finding records one");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (errors.length > 0)
|
|
250
|
+
return { found: true, ok: false, errors };
|
|
251
|
+
return { found: true, ok: true, result: r };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Find and validate the structured result in a review-and-resolution document.
|
|
255
|
+
*
|
|
256
|
+
* `{found: false}` when the document has no block — every standalone review,
|
|
257
|
+
* and every review from before this feature. More than one block is refused:
|
|
258
|
+
* a capture must never have to choose between two records.
|
|
259
|
+
*/
|
|
260
|
+
export function parseEpicResolutionBlock(markdown) {
|
|
261
|
+
const matches = [...markdown.matchAll(FENCE_PATTERN)];
|
|
262
|
+
if (matches.length === 0)
|
|
263
|
+
return { found: false };
|
|
264
|
+
if (matches.length > 1) {
|
|
265
|
+
return { found: true, ok: false, errors: ["the document has more than one resolution block"] };
|
|
266
|
+
}
|
|
267
|
+
let decoded;
|
|
268
|
+
try {
|
|
269
|
+
decoded = JSON.parse(matches[0][1]);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return { found: true, ok: false, errors: ["the resolution block is not valid JSON"] };
|
|
273
|
+
}
|
|
274
|
+
return validateEpicResolution(decoded);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Decide whether terminal decision capture may proceed for this document.
|
|
278
|
+
*
|
|
279
|
+
* Capture proceeds when the document has no block (unrelated callers are
|
|
280
|
+
* unaffected), or when its block is valid, belongs to this ticket and
|
|
281
|
+
* generation, and records no unresolved mandatory incompatibility.
|
|
282
|
+
*/
|
|
283
|
+
export function evaluateEpicCaptureGate(markdown, ticketNumber, generation) {
|
|
284
|
+
if (markdown === null)
|
|
285
|
+
return { allowed: true };
|
|
286
|
+
const parsed = parseEpicResolutionBlock(markdown);
|
|
287
|
+
if (!parsed.found)
|
|
288
|
+
return { allowed: true };
|
|
289
|
+
if (!parsed.ok) {
|
|
290
|
+
return {
|
|
291
|
+
allowed: false,
|
|
292
|
+
reason: "epic_resolution_malformed",
|
|
293
|
+
message: "The epic-contract resolution block in the review-and-resolution document is not valid. " +
|
|
294
|
+
"Fix the block, then capture again.",
|
|
295
|
+
errors: parsed.errors,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const result = parsed.result;
|
|
299
|
+
if (result.subject_key.toUpperCase() !== ticketNumber.trim().toUpperCase()) {
|
|
300
|
+
return {
|
|
301
|
+
allowed: false,
|
|
302
|
+
reason: "epic_resolution_malformed",
|
|
303
|
+
message: "The epic-contract resolution block names a different ticket.",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (result.generation !== null && result.generation !== generation) {
|
|
307
|
+
return {
|
|
308
|
+
allowed: false,
|
|
309
|
+
reason: "epic_resolution_generation_mismatch",
|
|
310
|
+
message: "The epic-contract resolution was recorded against a different review generation. " +
|
|
311
|
+
"Wait for the review again and re-adjudicate against the current documents.",
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const blocked = result.findings.filter((f) => f.status === "mandatory_incompatibility");
|
|
315
|
+
if (blocked.length > 0) {
|
|
316
|
+
return {
|
|
317
|
+
allowed: false,
|
|
318
|
+
reason: "epic_contract_incompatibility_unresolved",
|
|
319
|
+
message: "A finding is recorded as an unresolved mandatory incompatibility between epic contracts. " +
|
|
320
|
+
"Present the conflicting obligations and the recommended resolution; capture after an " +
|
|
321
|
+
"explicit resolving decision is recorded in the block.",
|
|
322
|
+
finding_ids: blocked.map((f) => f.id),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { allowed: true };
|
|
326
|
+
}
|