@loucompanion/forge-bridge 0.1.3-dev.04a84914da09 → 0.1.3-dev.b6be5e7f9668
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.
|
@@ -113,6 +113,44 @@ const resolvedFindingStatuses = new Set(resolvedFindingStatusValues);
|
|
|
113
113
|
const qaFindingSeverities = new Set(["critical", "major", "minor"]);
|
|
114
114
|
const qaFindingScopes = new Set(["in_scope", "out_of_scope", "unclear"]);
|
|
115
115
|
const qaCandidateBugActions = new Set(["create_bug", "create_follow_up", "accept_risk", "attach_to_current_item"]);
|
|
116
|
+
const qaScopeSurfaces = new Set([
|
|
117
|
+
"domain_contract",
|
|
118
|
+
"backend_persistence",
|
|
119
|
+
"api_route",
|
|
120
|
+
"ui_browser",
|
|
121
|
+
"infra_config",
|
|
122
|
+
"data_migration",
|
|
123
|
+
"test_only",
|
|
124
|
+
"documentation"
|
|
125
|
+
]);
|
|
126
|
+
const qaScopeEvidence = new Set([
|
|
127
|
+
"release_build",
|
|
128
|
+
"unit_tests",
|
|
129
|
+
"console_harness",
|
|
130
|
+
"source_scope_review",
|
|
131
|
+
"api_http",
|
|
132
|
+
"browser_manual",
|
|
133
|
+
"migration_check",
|
|
134
|
+
"config_check"
|
|
135
|
+
]);
|
|
136
|
+
const qaScopeSources = new Set(["user", "draft", "decompose", "architect", "heuristic", "system"]);
|
|
137
|
+
const qaScopeAuthorities = new Set(["user", "decompose", "architect", "system"]);
|
|
138
|
+
const qaScopeConfidence = new Set(["explicit", "inferred"]);
|
|
139
|
+
const qaScopeFields = new Set([
|
|
140
|
+
"schema_version",
|
|
141
|
+
"surface",
|
|
142
|
+
"required_evidence",
|
|
143
|
+
"not_applicable",
|
|
144
|
+
"browser_required",
|
|
145
|
+
"api_required",
|
|
146
|
+
"source",
|
|
147
|
+
"authority",
|
|
148
|
+
"confidence",
|
|
149
|
+
"confirmed_by_user",
|
|
150
|
+
"locked",
|
|
151
|
+
"derived_from",
|
|
152
|
+
"rationale"
|
|
153
|
+
]);
|
|
116
154
|
const phaseFiles = {
|
|
117
155
|
implementation: {
|
|
118
156
|
jsonFileName: "implementation.completeness.json",
|
|
@@ -391,6 +429,7 @@ export function completionPromptForPhase(phase) {
|
|
|
391
429
|
Do not write QA artifacts by hand. When QA is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the required structured QA payload.
|
|
392
430
|
That tool writes the durable QA bundle under \`.forge/work-items/{workItemId}/artifacts/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`${artifactName}\`, and \`qa.report.md\`.
|
|
393
431
|
Use \`status: "pass"\` only when the validated changes satisfy acceptance criteria with concrete evidence. Use \`status: "needs_rework"\` with actionable in-scope findings, or \`status: "blocked"\` with a concrete blocker_reason.
|
|
432
|
+
Include \`policy_warnings\` for non-blocking Forge policy or scope mismatches that should remain visible after QA.
|
|
394
433
|
Forge will mirror only the tool-written QA artifacts; reporting QA completion in prose is not enough.`;
|
|
395
434
|
}
|
|
396
435
|
if (phase === "architect") {
|
|
@@ -398,8 +437,8 @@ Forge will mirror only the tool-written QA artifacts; reporting QA completion in
|
|
|
398
437
|
Do not write Architect artifacts by hand. When the plan is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the full structured Architect planning payload.
|
|
399
438
|
That tool writes \`.forge/work-items/{workItemId}/artifacts/implementation.plan.json\`, \`qa.plan.json\`, \`${artifactName}\`, and \`architect.report.md\`.
|
|
400
439
|
The payload must put \`phases\`, \`tasks\`, \`dependencies\`, and \`phase_gates\` as top-level fields inside \`implementation_plan\`, for example:
|
|
401
|
-
\`{ "status": "pass", "summary": "...", "implementation_plan": { "schema_version": 1, "plan_id": "plan-...", "phases": [{ "id": "phase-1", "tasks": ["task-1"], "gate": "gate-1" }], "tasks": [{ "id": "task-1", "tests": [{ "command": "npm test" }] }], "dependencies": [], "phase_gates": [{ "id": "gate-1", "phase": "phase-1" }] }, "qa_plan": { "schema_version": 1, "final_qa": [{ "id": "qa-1", "coverage": "..." }] }, "report_markdown": "# Architect Report" }\`.
|
|
402
|
-
\`implementation_plan.tasks\` are Develop child tasks only: each task must require a focused repo/product implementation change. Put parent-story QA, sign-off, child-count/materialization inventory, parent plan artifact checks, and final integrated acceptance proof in \`qa_plan.final_qa\`, not as child Develop tasks.
|
|
440
|
+
\`{ "status": "pass", "summary": "...", "implementation_plan": { "schema_version": 1, "plan_id": "plan-...", "phases": [{ "id": "phase-1", "tasks": ["task-1"], "gate": "gate-1" }], "tasks": [{ "id": "task-1", "verification_scope": { "schema_version": 1, "surface": ["domain_contract"], "required_evidence": ["unit_tests"], "not_applicable": ["browser_manual", "api_http"], "browser_required": false, "api_required": false, "rationale": "Backend-only child task." }, "tests": [{ "command": "npm test" }] }], "dependencies": [], "phase_gates": [{ "id": "gate-1", "phase": "phase-1" }] }, "qa_plan": { "schema_version": 1, "final_qa": [{ "id": "qa-1", "coverage": "..." }] }, "report_markdown": "# Architect Report" }\`.
|
|
441
|
+
\`implementation_plan.tasks\` are Develop child tasks only: each task should include \`verification_scope\` describing task-level QA evidence, and must require a focused repo/product implementation change. Put parent-story QA, sign-off, child-count/materialization inventory, parent plan artifact checks, and final integrated acceptance proof in \`qa_plan.final_qa\`, not as child Develop tasks.
|
|
403
442
|
Dependency entries use canonical \`before\` and \`after\` task ids, for example \`{ "before": "task-1", "after": "task-2" }\`. Forge accepts \`from\`/\`to\` only as aliases and normalizes them before writing artifacts.
|
|
404
443
|
Use \`status: "pass"\` for a valid pending-approval plan, \`needs_revision\` when story/design input must change, or \`blocked\` with a concrete blocker.
|
|
405
444
|
Forge will mirror only the tool-written Architect artifacts; reporting the plan in prose is not enough.`;
|
|
@@ -432,6 +471,7 @@ Complete the QA cycle in order inside this same provider session:
|
|
|
432
471
|
7. Follow the returned verdict instruction, then call \`forge.complete_qa_verdict(...)\`.
|
|
433
472
|
Use \`status: "skipped"\` with a reason for gates that are not applicable. If a gate is blocked, keep moving through later gates as skipped due to the blocker and finish with a blocked verdict.
|
|
434
473
|
The final verdict tool writes the canonical QA bundle under \`.forge/work-items/{workItemId}/artifacts/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`qa.verdict.json\`, and \`qa.report.md\`.
|
|
474
|
+
Include \`policy_warnings\` in the final verdict for non-blocking Forge policy or scope mismatches that should remain visible after QA.
|
|
435
475
|
The legacy \`forge.complete_qa\` one-shot tool is not available in normal QA-cycle mode. Reporting QA completion in prose is not enough.`;
|
|
436
476
|
}
|
|
437
477
|
export function completionPromptForDraftCycle(currentGate = "draft_intent") {
|
|
@@ -461,7 +501,7 @@ Required order:
|
|
|
461
501
|
5. Follow the returned dependency-graph instruction, validate keys/dependencies/order, then call \`forge.complete_decompose_dependency_graph(...)\`.
|
|
462
502
|
6. Follow the returned critic instruction, critique scope, DAG safety, manual reconciliation risk, and missing acceptance detail, then call \`forge.complete_decompose_critic(...)\`.
|
|
463
503
|
7. Follow the returned proposal instruction, then call \`forge.complete_decompose_proposal(...)\` with final JSON in \`proposal_json\`.
|
|
464
|
-
The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates.`;
|
|
504
|
+
The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates. Work Order-to-Feature candidates may include \`estimated_qa_scope\`; Feature-to-Story candidates should include \`qa_scope\` with schema_version 1.`;
|
|
465
505
|
}
|
|
466
506
|
export function completionPromptForDebugCycle(currentGate = "debug_repro") {
|
|
467
507
|
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
@@ -514,7 +554,7 @@ export class CompletionToolServerSession {
|
|
|
514
554
|
: completionPromptForPhase(this.currentPhase);
|
|
515
555
|
}
|
|
516
556
|
toolDefinitions() {
|
|
517
|
-
return completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name)));
|
|
557
|
+
return completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name), decomposeGateFromToolName(name)));
|
|
518
558
|
}
|
|
519
559
|
async callTool(toolName, input) {
|
|
520
560
|
const requestedPhase = phaseFromToolName(toolName);
|
|
@@ -765,9 +805,7 @@ async function writeDecomposeGateCompletionArtifact(config, gate, toolName, inpu
|
|
|
765
805
|
const status = normalizeDraftStatus(optionalString(payload, "status") ?? "pass");
|
|
766
806
|
if (gate === "decompose_proposal") {
|
|
767
807
|
const proposalJson = asRecord(payload["proposal_json"] ?? payload["decomposition_json"], "proposal_json");
|
|
768
|
-
|
|
769
|
-
throw new Error("proposal_json.candidates: expected array");
|
|
770
|
-
}
|
|
808
|
+
validateDecomposeProposalJson(proposalJson);
|
|
771
809
|
}
|
|
772
810
|
const redactedPayload = redactSecrets({ ...payload, status, phase: "decompose", gate, tool_name: toolName }, config.redactionSecrets ?? []);
|
|
773
811
|
const root = artifactRootFor(config.worktreePath, config.workItemId);
|
|
@@ -828,9 +866,23 @@ function normalizeDraftStatus(value) {
|
|
|
828
866
|
return normalized === "blocked" || normalized === "needs_revision" || normalized === "needs_rework" ? normalized : "pass";
|
|
829
867
|
}
|
|
830
868
|
function validateDraftProposalJson(draftJson) {
|
|
869
|
+
if (draftJson.suggested_qa_scope !== undefined && draftJson.suggested_qa_scope !== null) {
|
|
870
|
+
if (!draftJson.suggested_qa_scope || typeof draftJson.suggested_qa_scope !== "object" || Array.isArray(draftJson.suggested_qa_scope)) {
|
|
871
|
+
throw new Error("draft_json.suggested_qa_scope: expected object when present.");
|
|
872
|
+
}
|
|
873
|
+
validateQaScopeRecord(draftJson.suggested_qa_scope, "draft_json.suggested_qa_scope");
|
|
874
|
+
}
|
|
831
875
|
if (isDecompositionDraftJson(draftJson)) {
|
|
832
876
|
return;
|
|
833
877
|
}
|
|
878
|
+
if (isWorkItemDraftJson(draftJson)) {
|
|
879
|
+
for (const key of ["title", "description", "acceptanceCriteria"]) {
|
|
880
|
+
if (typeof draftJson[key] !== "string" || draftJson[key].trim().length === 0) {
|
|
881
|
+
throw new Error(`draft_json.${key}: expected non-empty string`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
834
886
|
for (const key of ["title", "summary", "objective", "planningPacket", "decompositionContext"]) {
|
|
835
887
|
if (typeof draftJson[key] !== "string" || draftJson[key].trim().length === 0) {
|
|
836
888
|
throw new Error(`draft_json.${key}: expected non-empty string`);
|
|
@@ -843,6 +895,37 @@ function isDecompositionDraftJson(draftJson) {
|
|
|
843
895
|
&& summary.trim().length > 0
|
|
844
896
|
&& Array.isArray(draftJson["candidates"]);
|
|
845
897
|
}
|
|
898
|
+
function isWorkItemDraftJson(draftJson) {
|
|
899
|
+
return typeof draftJson.title === "string"
|
|
900
|
+
&& typeof draftJson.description === "string"
|
|
901
|
+
&& typeof draftJson.acceptanceCriteria === "string";
|
|
902
|
+
}
|
|
903
|
+
function validateDecomposeProposalJson(proposalJson) {
|
|
904
|
+
const candidates = proposalJson.candidates;
|
|
905
|
+
if (!Array.isArray(candidates)) {
|
|
906
|
+
throw new Error("proposal_json.candidates: expected array");
|
|
907
|
+
}
|
|
908
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
909
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
910
|
+
throw new Error(`proposal_json.candidates[${index}]: expected object.`);
|
|
911
|
+
}
|
|
912
|
+
const record = candidate;
|
|
913
|
+
validateOptionalCandidateQaScope(record, "estimated_qa_scope", `proposal_json.candidates[${index}].estimated_qa_scope`);
|
|
914
|
+
validateOptionalCandidateQaScope(record, "estimatedQaScope", `proposal_json.candidates[${index}].estimatedQaScope`);
|
|
915
|
+
validateOptionalCandidateQaScope(record, "qa_scope", `proposal_json.candidates[${index}].qa_scope`);
|
|
916
|
+
validateOptionalCandidateQaScope(record, "qaScope", `proposal_json.candidates[${index}].qaScope`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function validateOptionalCandidateQaScope(candidate, key, pathLabel) {
|
|
920
|
+
const value = candidate[key];
|
|
921
|
+
if (value === undefined || value === null) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
925
|
+
throw new Error(`${pathLabel}: expected object when present.`);
|
|
926
|
+
}
|
|
927
|
+
validateQaScopeRecord(value, pathLabel);
|
|
928
|
+
}
|
|
846
929
|
export async function writeCompletionArtifact(config, toolName, input) {
|
|
847
930
|
const phase = phaseFromToolName(toolName);
|
|
848
931
|
if (phase !== config.phase) {
|
|
@@ -1018,6 +1101,7 @@ async function writeQaCompletionArtifact(config, payload, status, producedBy) {
|
|
|
1018
1101
|
const testedAcceptanceCriteria = qaStringArray(payload, "tested_acceptance_criteria");
|
|
1019
1102
|
const explicitFindingIds = qaStringArray(payload, "finding_ids");
|
|
1020
1103
|
const explicitCandidateBugIds = qaStringArray(payload, "candidate_bug_ids");
|
|
1104
|
+
const policyWarnings = qaPolicyWarnings(payload);
|
|
1021
1105
|
const blockerReason = optionalString(payload, "blocker_reason");
|
|
1022
1106
|
const checklist = qaStringArray(payload, "checklist");
|
|
1023
1107
|
const planJson = jsonText({
|
|
@@ -1065,6 +1149,7 @@ async function writeQaCompletionArtifact(config, payload, status, producedBy) {
|
|
|
1065
1149
|
tested_acceptance_criteria: testedAcceptanceCriteria,
|
|
1066
1150
|
finding_ids: findingIds,
|
|
1067
1151
|
candidate_bug_ids: candidateBugIds,
|
|
1152
|
+
policy_warnings: policyWarnings,
|
|
1068
1153
|
blocker_reason: status === "blocked" ? normalizedString(blockerReason) : null,
|
|
1069
1154
|
evidence_path: relativeEvidencePath,
|
|
1070
1155
|
findings_path: relativeFindingsPath,
|
|
@@ -1526,11 +1611,11 @@ async function handleJsonRpcRequest(session, method, params) {
|
|
|
1526
1611
|
}
|
|
1527
1612
|
throw new Error(`Unsupported MCP method: ${method}`);
|
|
1528
1613
|
}
|
|
1529
|
-
function toolDefinition(name, phase, qaGate = null) {
|
|
1614
|
+
function toolDefinition(name, phase, qaGate = null, decomposeGate = null) {
|
|
1530
1615
|
return {
|
|
1531
1616
|
name,
|
|
1532
|
-
description: toolDescription(phase, qaGate),
|
|
1533
|
-
inputSchema: toolInputSchema(phase, qaGate)
|
|
1617
|
+
description: toolDescription(phase, qaGate, decomposeGate),
|
|
1618
|
+
inputSchema: toolInputSchema(phase, qaGate, decomposeGate)
|
|
1534
1619
|
};
|
|
1535
1620
|
}
|
|
1536
1621
|
function nextPhaseAfter(phase) {
|
|
@@ -1657,12 +1742,17 @@ function qaGateInstruction(gate) {
|
|
|
1657
1742
|
return "Prepare the QA checklist, then call forge.complete_qa_plan.";
|
|
1658
1743
|
}
|
|
1659
1744
|
}
|
|
1660
|
-
function toolDescription(phase, qaGate = null) {
|
|
1745
|
+
function toolDescription(phase, qaGate = null, decomposeGate = null) {
|
|
1661
1746
|
if (qaGate) {
|
|
1662
1747
|
return qaGate === "qa_verdict"
|
|
1663
1748
|
? "Complete the final Forge QA verdict by writing the canonical QA bundle with pass, needs_rework, or blocked evidence."
|
|
1664
1749
|
: `Complete the ${qaGate.replace(/_/g, " ")} QA gate by writing ${qaGateFileNames[qaGate]} before continuing to the next QA gate.`;
|
|
1665
1750
|
}
|
|
1751
|
+
if (decomposeGate) {
|
|
1752
|
+
return decomposeGate === "decompose_proposal"
|
|
1753
|
+
? "Complete the Forge Decompose proposal by writing decompose.proposal.json with candidate child items and optional structured QA scopes."
|
|
1754
|
+
: `Complete the ${decomposeGate.replace(/_/g, " ")} Decompose gate by writing ${decomposeGateFileNames[decomposeGate]} before continuing.`;
|
|
1755
|
+
}
|
|
1666
1756
|
if (isDebugCompletionPhase(phase)) {
|
|
1667
1757
|
return `Complete the ${phase.replace(/_/g, " ")} Debug gate by writing ${phaseFiles[phase].jsonFileName} with structured bug-fix evidence before continuing.`;
|
|
1668
1758
|
}
|
|
@@ -1680,7 +1770,7 @@ function toolDescription(phase, qaGate = null) {
|
|
|
1680
1770
|
}
|
|
1681
1771
|
return "Complete Forge validation by writing validation.verdict.json with command, build/test/lint/design, drift, acceptance-criteria, and ready-for-dev-test evidence.";
|
|
1682
1772
|
}
|
|
1683
|
-
function toolInputSchema(phase, qaGate = null) {
|
|
1773
|
+
function toolInputSchema(phase, qaGate = null, decomposeGate = null) {
|
|
1684
1774
|
const baseProperties = {
|
|
1685
1775
|
status: {
|
|
1686
1776
|
type: "string",
|
|
@@ -1702,6 +1792,9 @@ function toolInputSchema(phase, qaGate = null) {
|
|
|
1702
1792
|
if (qaGate && qaGate !== "qa_verdict") {
|
|
1703
1793
|
return qaGateToolInputSchema(qaGate);
|
|
1704
1794
|
}
|
|
1795
|
+
if (decomposeGate) {
|
|
1796
|
+
return decomposeGateToolInputSchema(decomposeGate, baseProperties);
|
|
1797
|
+
}
|
|
1705
1798
|
if (isDebugCompletionPhase(phase)) {
|
|
1706
1799
|
return debugToolInputSchema(phase, baseProperties);
|
|
1707
1800
|
}
|
|
@@ -1891,6 +1984,16 @@ function toolInputSchema(phase, qaGate = null) {
|
|
|
1891
1984
|
suggested_action: { type: "string", enum: ["create_bug", "create_follow_up", "accept_risk", "attach_to_current_item"] }
|
|
1892
1985
|
})
|
|
1893
1986
|
},
|
|
1987
|
+
policy_warnings: {
|
|
1988
|
+
type: "array",
|
|
1989
|
+
description: "Non-blocking Forge policy or scope warnings surfaced with the QA verdict.",
|
|
1990
|
+
items: evidenceObjectSchema(["code", "message"], {
|
|
1991
|
+
code: { type: "string" },
|
|
1992
|
+
message: { type: "string" },
|
|
1993
|
+
severity: { type: "string" },
|
|
1994
|
+
evidence: { type: "object" }
|
|
1995
|
+
})
|
|
1996
|
+
},
|
|
1894
1997
|
blocker_reason: {
|
|
1895
1998
|
type: "string",
|
|
1896
1999
|
enum: [...qaBlockerReasons],
|
|
@@ -1920,8 +2023,20 @@ function toolInputSchema(phase, qaGate = null) {
|
|
|
1920
2023
|
},
|
|
1921
2024
|
tasks: {
|
|
1922
2025
|
type: "array",
|
|
1923
|
-
description: "Required Develop child task list. Each task must include id
|
|
1924
|
-
items: {
|
|
2026
|
+
description: "Required Develop child task list. Each task must include id, a non-empty tests array, and should include verification_scope for task-level QA evidence. Do not put parent-story QA/sign-off, child-count/materialization inventory, parent plan artifact checks, or final acceptance proof here; those belong in qa_plan.final_qa.",
|
|
2027
|
+
items: {
|
|
2028
|
+
type: "object",
|
|
2029
|
+
additionalProperties: true,
|
|
2030
|
+
properties: {
|
|
2031
|
+
id: { type: "string" },
|
|
2032
|
+
title: { type: "string" },
|
|
2033
|
+
verification_scope: qaScopeSchema("Task-level QA/verification scope for the materialized child task."),
|
|
2034
|
+
tests: {
|
|
2035
|
+
type: "array",
|
|
2036
|
+
items: { type: "object", additionalProperties: true }
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
1925
2040
|
},
|
|
1926
2041
|
dependencies: {
|
|
1927
2042
|
type: "array",
|
|
@@ -1999,6 +2114,65 @@ function toolInputSchema(phase, qaGate = null) {
|
|
|
1999
2114
|
ready_for_dev_test: { type: "boolean", description: "Required for pass and must be true." }
|
|
2000
2115
|
}, ["status", "summary"], "For status pass, commands must include at least one passing deterministic command, acceptance criteria entries must use status: satisfied plus evidence, and ready_for_dev_test must be true.");
|
|
2001
2116
|
}
|
|
2117
|
+
function decomposeGateToolInputSchema(gate, baseProperties) {
|
|
2118
|
+
if (gate !== "decompose_proposal") {
|
|
2119
|
+
return objectToolSchema({
|
|
2120
|
+
...baseProperties,
|
|
2121
|
+
findings: {
|
|
2122
|
+
type: "array",
|
|
2123
|
+
description: "Optional issues, constraints, or risks discovered in this Decompose gate.",
|
|
2124
|
+
items: { type: "object", additionalProperties: true }
|
|
2125
|
+
}
|
|
2126
|
+
}, ["status", "summary"], `Record the ${gate.replace(/_/g, " ")} Decompose gate before continuing.`);
|
|
2127
|
+
}
|
|
2128
|
+
const candidateSchema = {
|
|
2129
|
+
type: "object",
|
|
2130
|
+
additionalProperties: true,
|
|
2131
|
+
properties: {
|
|
2132
|
+
origin: { type: "string" },
|
|
2133
|
+
key: { type: "string" },
|
|
2134
|
+
title: { type: "string" },
|
|
2135
|
+
summary: { type: "string" },
|
|
2136
|
+
description: { type: "string" },
|
|
2137
|
+
acceptanceCriteria: { type: "string" },
|
|
2138
|
+
planningPacket: { type: "string" },
|
|
2139
|
+
sortOrder: { type: "number" },
|
|
2140
|
+
points: { type: "number" },
|
|
2141
|
+
dependsOn: { type: "array", items: { type: "string" } },
|
|
2142
|
+
estimatedQaScope: qaScopeSchema("Work Order-to-Feature planning QA estimate. This is not dispatch-authoritative QA scope."),
|
|
2143
|
+
estimated_qa_scope: qaScopeSchema("Work Order-to-Feature planning QA estimate. Snake_case alias for estimatedQaScope."),
|
|
2144
|
+
qaScope: qaScopeSchema("Feature-to-Story QA scope that becomes Story-level QA scope after proposal approval."),
|
|
2145
|
+
qa_scope: qaScopeSchema("Feature-to-Story QA scope. Snake_case alias for qaScope.")
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
const proposalSchema = {
|
|
2149
|
+
type: "object",
|
|
2150
|
+
additionalProperties: true,
|
|
2151
|
+
description: "Final Decompose proposal JSON. Must include candidates, and may include blockedReason.",
|
|
2152
|
+
properties: {
|
|
2153
|
+
summary: { type: "string" },
|
|
2154
|
+
blockedReason: { type: "string" },
|
|
2155
|
+
blocked_reason: { type: "string" },
|
|
2156
|
+
candidates: {
|
|
2157
|
+
type: "array",
|
|
2158
|
+
items: candidateSchema
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
return objectToolSchema({
|
|
2163
|
+
...baseProperties,
|
|
2164
|
+
status: {
|
|
2165
|
+
type: "string",
|
|
2166
|
+
enum: ["pass", "needs_rework", "blocked"],
|
|
2167
|
+
description: "Use pass only when the Decompose proposal is ready for user sign-off."
|
|
2168
|
+
},
|
|
2169
|
+
proposal_json: proposalSchema,
|
|
2170
|
+
decomposition_json: {
|
|
2171
|
+
...proposalSchema,
|
|
2172
|
+
description: "Alias for proposal_json when a client cannot send proposal_json."
|
|
2173
|
+
}
|
|
2174
|
+
}, ["status", "summary"], "For the final Decompose proposal, include proposal_json with a candidates array. Work Order-to-Feature candidates may include estimatedQaScope/estimated_qa_scope; Feature-to-Story candidates may include qaScope/qa_scope.");
|
|
2175
|
+
}
|
|
2002
2176
|
function debugToolInputSchema(phase, baseProperties) {
|
|
2003
2177
|
if (phase === "debug_repro") {
|
|
2004
2178
|
return objectToolSchema({
|
|
@@ -2154,6 +2328,36 @@ function evidenceObjectSchema(required, properties) {
|
|
|
2154
2328
|
required: [...required]
|
|
2155
2329
|
};
|
|
2156
2330
|
}
|
|
2331
|
+
function qaScopeSchema(description) {
|
|
2332
|
+
return {
|
|
2333
|
+
type: "object",
|
|
2334
|
+
additionalProperties: false,
|
|
2335
|
+
description,
|
|
2336
|
+
properties: {
|
|
2337
|
+
schema_version: { type: "number", enum: [1] },
|
|
2338
|
+
surface: { type: "array", items: { type: "string", enum: [...qaScopeSurfaces] } },
|
|
2339
|
+
required_evidence: { type: "array", items: { type: "string", enum: [...qaScopeEvidence] } },
|
|
2340
|
+
not_applicable: { type: "array", items: { type: "string", enum: [...qaScopeEvidence] } },
|
|
2341
|
+
browser_required: { type: "boolean" },
|
|
2342
|
+
api_required: { type: "boolean" },
|
|
2343
|
+
source: { type: "string", enum: [...qaScopeSources] },
|
|
2344
|
+
authority: { type: "string", enum: [...qaScopeAuthorities] },
|
|
2345
|
+
confidence: { type: "string", enum: [...qaScopeConfidence] },
|
|
2346
|
+
confirmed_by_user: { type: "boolean" },
|
|
2347
|
+
locked: { type: "boolean" },
|
|
2348
|
+
derived_from: {
|
|
2349
|
+
type: "object",
|
|
2350
|
+
additionalProperties: false,
|
|
2351
|
+
properties: {
|
|
2352
|
+
work_item_id: { type: "string" },
|
|
2353
|
+
mode: { type: "string" }
|
|
2354
|
+
}
|
|
2355
|
+
},
|
|
2356
|
+
rationale: { type: "string" }
|
|
2357
|
+
},
|
|
2358
|
+
required: ["schema_version", "required_evidence", "not_applicable"]
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2157
2361
|
function commandEvidenceSchema() {
|
|
2158
2362
|
return evidenceObjectSchema(["command", "exit_code", "status", "summary"], {
|
|
2159
2363
|
command: { type: "string" },
|
|
@@ -2433,6 +2637,7 @@ function validateArchitectPayloadShape(payload, status) {
|
|
|
2433
2637
|
}
|
|
2434
2638
|
taskIds.add(taskId);
|
|
2435
2639
|
validateArchitectTaskDevelopScope(taskRecord, taskId);
|
|
2640
|
+
validateArchitectVerificationScope(taskRecord, taskId);
|
|
2436
2641
|
requireArchitectArray(taskRecord, "tests", payload, { path: `implementation_plan.tasks[${optionalString(taskRecord, "id") ?? "?"}].tests` });
|
|
2437
2642
|
if (arrayLength(taskRecord, "tests") === 0) {
|
|
2438
2643
|
throw new Error(`implementation_plan.tasks[${optionalString(taskRecord, "id") ?? "?"}].tests: expected non-empty array; correction: include task-specific verification.`);
|
|
@@ -2454,6 +2659,123 @@ function validateArchitectTaskDevelopScope(task, taskId) {
|
|
|
2454
2659
|
throw new Error(`implementation_plan.tasks[${taskId}]: parent-story materialization or sign-off verification belongs in qa_plan.final_qa, not implementation_plan.tasks; correction: keep child tasks focused on implementation work and put child-count/parent-plan artifact checks in final QA coverage.`);
|
|
2455
2660
|
}
|
|
2456
2661
|
}
|
|
2662
|
+
function validateArchitectVerificationScope(task, taskId) {
|
|
2663
|
+
const value = task.verification_scope;
|
|
2664
|
+
if (value === undefined || value === null) {
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2668
|
+
throw new Error(`implementation_plan.tasks[${taskId}].verification_scope: expected object when present.`);
|
|
2669
|
+
}
|
|
2670
|
+
validateQaScopeRecord(value, `implementation_plan.tasks[${taskId}].verification_scope`);
|
|
2671
|
+
}
|
|
2672
|
+
function validateQaScopeRecord(scope, pathLabel) {
|
|
2673
|
+
for (const key of Object.keys(scope)) {
|
|
2674
|
+
if (!qaScopeFields.has(key)) {
|
|
2675
|
+
throw new Error(`${pathLabel}.${key}: unsupported QA scope field.`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (scope.schema_version !== 1) {
|
|
2679
|
+
throw new Error(`${pathLabel}.schema_version must be numeric 1.`);
|
|
2680
|
+
}
|
|
2681
|
+
const surface = qaScopeStringArray(scope, "surface", pathLabel, qaScopeSurfaces);
|
|
2682
|
+
const requiredEvidence = qaScopeStringArray(scope, "required_evidence", pathLabel, qaScopeEvidence);
|
|
2683
|
+
const notApplicable = qaScopeStringArray(scope, "not_applicable", pathLabel, qaScopeEvidence);
|
|
2684
|
+
const requiredSet = new Set(requiredEvidence);
|
|
2685
|
+
for (const evidence of notApplicable) {
|
|
2686
|
+
if (requiredSet.has(evidence)) {
|
|
2687
|
+
throw new Error(`${pathLabel}: '${evidence}' cannot be both required and not applicable.`);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
const browserRequired = requiredSet.has("browser_manual");
|
|
2691
|
+
const apiRequired = requiredSet.has("api_http");
|
|
2692
|
+
validateOptionalBoolean(scope, "browser_required", pathLabel, browserRequired);
|
|
2693
|
+
validateOptionalBoolean(scope, "api_required", pathLabel, apiRequired);
|
|
2694
|
+
validateOptionalBoolean(scope, "confirmed_by_user", pathLabel);
|
|
2695
|
+
validateOptionalBoolean(scope, "locked", pathLabel);
|
|
2696
|
+
if (surface.includes("ui_browser") && !browserRequired) {
|
|
2697
|
+
throw new Error(`${pathLabel}.surface ui_browser requires browser_manual evidence.`);
|
|
2698
|
+
}
|
|
2699
|
+
if (surface.includes("api_route") && !apiRequired) {
|
|
2700
|
+
throw new Error(`${pathLabel}.surface api_route requires api_http evidence.`);
|
|
2701
|
+
}
|
|
2702
|
+
validateOptionalKnownString(scope, "source", pathLabel, qaScopeSources);
|
|
2703
|
+
validateOptionalKnownString(scope, "authority", pathLabel, qaScopeAuthorities);
|
|
2704
|
+
validateOptionalKnownString(scope, "confidence", pathLabel, qaScopeConfidence);
|
|
2705
|
+
if (scope.rationale !== undefined && scope.rationale !== null && typeof scope.rationale !== "string") {
|
|
2706
|
+
throw new Error(`${pathLabel}.rationale must be a string.`);
|
|
2707
|
+
}
|
|
2708
|
+
validateOptionalQaScopeDerivedFrom(scope, pathLabel);
|
|
2709
|
+
}
|
|
2710
|
+
function qaScopeStringArray(scope, key, pathLabel, allowed) {
|
|
2711
|
+
const value = scope[key];
|
|
2712
|
+
if (value === undefined || value === null) {
|
|
2713
|
+
return [];
|
|
2714
|
+
}
|
|
2715
|
+
if (!Array.isArray(value)) {
|
|
2716
|
+
throw new Error(`${pathLabel}.${key} must be an array.`);
|
|
2717
|
+
}
|
|
2718
|
+
const normalized = [];
|
|
2719
|
+
for (const [index, entry] of value.entries()) {
|
|
2720
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
2721
|
+
throw new Error(`${pathLabel}.${key}[${index}] must be a non-empty string.`);
|
|
2722
|
+
}
|
|
2723
|
+
const candidate = entry.trim().toLowerCase();
|
|
2724
|
+
if (!allowed.has(candidate)) {
|
|
2725
|
+
throw new Error(`${pathLabel}.${key}[${index}] value '${candidate}' is unsupported.`);
|
|
2726
|
+
}
|
|
2727
|
+
if (!normalized.includes(candidate)) {
|
|
2728
|
+
normalized.push(candidate);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
return normalized;
|
|
2732
|
+
}
|
|
2733
|
+
function validateOptionalBoolean(scope, key, pathLabel, expected) {
|
|
2734
|
+
const value = scope[key];
|
|
2735
|
+
if (value === undefined || value === null) {
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (typeof value !== "boolean") {
|
|
2739
|
+
throw new Error(`${pathLabel}.${key} must be boolean.`);
|
|
2740
|
+
}
|
|
2741
|
+
if (expected !== undefined && value !== expected) {
|
|
2742
|
+
throw new Error(`${pathLabel}.${key} must match required_evidence.`);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
function validateOptionalKnownString(scope, key, pathLabel, allowed) {
|
|
2746
|
+
const value = scope[key];
|
|
2747
|
+
if (value === undefined || value === null) {
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2751
|
+
throw new Error(`${pathLabel}.${key} must be a non-empty string.`);
|
|
2752
|
+
}
|
|
2753
|
+
const normalized = value.trim().toLowerCase();
|
|
2754
|
+
if (!allowed.has(normalized)) {
|
|
2755
|
+
throw new Error(`${pathLabel}.${key} value '${normalized}' is unsupported.`);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
function validateOptionalQaScopeDerivedFrom(scope, pathLabel) {
|
|
2759
|
+
const value = scope.derived_from;
|
|
2760
|
+
if (value === undefined || value === null) {
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2764
|
+
throw new Error(`${pathLabel}.derived_from must be an object.`);
|
|
2765
|
+
}
|
|
2766
|
+
const derivedFrom = value;
|
|
2767
|
+
for (const key of Object.keys(derivedFrom)) {
|
|
2768
|
+
if (key !== "work_item_id" && key !== "mode") {
|
|
2769
|
+
throw new Error(`${pathLabel}.derived_from.${key}: unsupported QA scope provenance field.`);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
if (derivedFrom.work_item_id !== undefined && derivedFrom.work_item_id !== null && typeof derivedFrom.work_item_id !== "string") {
|
|
2773
|
+
throw new Error(`${pathLabel}.derived_from.work_item_id must be a string.`);
|
|
2774
|
+
}
|
|
2775
|
+
if (derivedFrom.mode !== undefined && derivedFrom.mode !== null && typeof derivedFrom.mode !== "string") {
|
|
2776
|
+
throw new Error(`${pathLabel}.derived_from.mode must be a string.`);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2457
2779
|
function firstOptionalString(record, keys) {
|
|
2458
2780
|
for (const key of keys) {
|
|
2459
2781
|
const value = optionalString(record, key);
|
|
@@ -2688,6 +3010,7 @@ function validateQaPayloadShape(payload, status) {
|
|
|
2688
3010
|
const candidateBugs = qaObjectArray(payload, "candidate_bugs");
|
|
2689
3011
|
const explicitFindingIds = qaStringArray(payload, "finding_ids");
|
|
2690
3012
|
const explicitCandidateBugIds = qaStringArray(payload, "candidate_bug_ids");
|
|
3013
|
+
qaPolicyWarnings(payload);
|
|
2691
3014
|
if (payload.schema_version !== undefined && payload.schema_version !== 1) {
|
|
2692
3015
|
throw new Error("QA schema_version must be numeric 1 when provided.");
|
|
2693
3016
|
}
|
|
@@ -3197,6 +3520,27 @@ function qaObjectArray(payload, key) {
|
|
|
3197
3520
|
return entry;
|
|
3198
3521
|
});
|
|
3199
3522
|
}
|
|
3523
|
+
function qaPolicyWarnings(payload) {
|
|
3524
|
+
return qaArray(payload, "policy_warnings").map((entry, index) => {
|
|
3525
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
3526
|
+
throw new Error(`policy_warnings[${index}] must be an object.`);
|
|
3527
|
+
}
|
|
3528
|
+
const warning = entry;
|
|
3529
|
+
const prefix = `policy_warnings[${index}].`;
|
|
3530
|
+
requireNonEmptyStringRecord(warning, "code", prefix);
|
|
3531
|
+
requireNonEmptyStringRecord(warning, "message", prefix);
|
|
3532
|
+
const normalized = {
|
|
3533
|
+
...warning,
|
|
3534
|
+
code: optionalString(warning, "code"),
|
|
3535
|
+
message: optionalString(warning, "message")
|
|
3536
|
+
};
|
|
3537
|
+
if (warning.severity !== undefined && warning.severity !== null) {
|
|
3538
|
+
requireNonEmptyStringRecord(warning, "severity", prefix);
|
|
3539
|
+
normalized.severity = optionalString(warning, "severity");
|
|
3540
|
+
}
|
|
3541
|
+
return normalized;
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3200
3544
|
function qaStringArray(payload, key) {
|
|
3201
3545
|
const value = payload[key];
|
|
3202
3546
|
if (value === undefined || value === null) {
|
|
@@ -2469,18 +2469,27 @@ function phaseWorkInstruction(phase) {
|
|
|
2469
2469
|
}
|
|
2470
2470
|
function qaBrowserInstruction(dispatch) {
|
|
2471
2471
|
const qaBrowser = dispatch.qaBrowser;
|
|
2472
|
+
const qaScope = dispatch.qaScope;
|
|
2472
2473
|
if (!qaBrowser) {
|
|
2473
2474
|
return "Browser/app launch context: not provided by Forge.";
|
|
2474
2475
|
}
|
|
2476
|
+
const scopeAuthority = qaScope
|
|
2477
|
+
? qaScope.source === "heuristic"
|
|
2478
|
+
? "Forge's transient heuristic fallback"
|
|
2479
|
+
: "persisted QA scope"
|
|
2480
|
+
: "Forge heuristics";
|
|
2481
|
+
const scopeEvidence = qaScope
|
|
2482
|
+
? `\n- QA scope required evidence: ${qaScope.required_evidence.length > 0 ? qaScope.required_evidence.join(", ") : "none"}\n- QA scope not applicable: ${qaScope.not_applicable.length > 0 ? qaScope.not_applicable.join(", ") : "none"}${qaScope.rationale ? `\n- QA scope rationale: ${qaScope.rationale}` : ""}`
|
|
2483
|
+
: "";
|
|
2475
2484
|
const launchTarget = qaBrowser.launchTarget;
|
|
2476
2485
|
if (qaBrowser.required && !hasLaunchTarget(launchTarget ?? null)) {
|
|
2477
|
-
return `Browser/app launch context:\n- UI/browser QA is required for this item.\n- No app launch target was provided.\n- Return blocked with blocker_reason: missing_app_launch_target instead of pretending browser testing occurred
|
|
2486
|
+
return `Browser/app launch context:\n- UI/browser QA is required for this item by ${scopeAuthority}.\n- No app launch target was provided.\n- Return blocked with blocker_reason: missing_app_launch_target instead of pretending browser testing occurred.${scopeEvidence}\n- Artifact sidecars path when available: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
|
|
2478
2487
|
}
|
|
2479
2488
|
if (!launchTarget) {
|
|
2480
|
-
return `Browser/app launch context:\n- UI/browser QA is not required by
|
|
2489
|
+
return `Browser/app launch context:\n- UI/browser QA is not required by ${scopeAuthority} for this item.\n- If you determine browser testing is needed, return blocked with blocker_reason: missing_app_launch_target unless you can safely identify a launch target from the repo.${scopeEvidence}\n- Browser sidecars path: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
|
|
2481
2490
|
}
|
|
2482
2491
|
const routes = launchTarget.routes.length > 0 ? launchTarget.routes.join(", ") : "not specified";
|
|
2483
|
-
return `Browser/app launch context:\n- Browser QA required: ${qaBrowser.required ?
|
|
2492
|
+
return `Browser/app launch context:\n- Browser QA required: ${qaBrowser.required ? `yes, by ${scopeAuthority}` : `no by ${scopeAuthority}, unless the diff/AC requires it`}\n- URL: ${launchTarget.url || "not specified"}\n- Ready URL: ${launchTarget.ready_url || "not specified"}\n- Launch command: ${launchTarget.command || "not specified"}\n- Working directory: ${launchTarget.working_directory || "repo root"}\n- Routes to cover: ${routes}\n- Notes: ${launchTarget.notes || "none"}${scopeEvidence}\n- Browser sidecars path: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
|
|
2484
2493
|
}
|
|
2485
2494
|
function storyQaInstruction(dispatch) {
|
|
2486
2495
|
const storyQa = dispatch.storyQa;
|
|
@@ -13,6 +13,7 @@ export interface RunOnceDispatchPacket {
|
|
|
13
13
|
readonly artifacts: readonly DispatchResolvedArtifact[];
|
|
14
14
|
readonly warmPathOverrides?: readonly string[];
|
|
15
15
|
readonly qaBrowser?: DispatchQaBrowserContext | null;
|
|
16
|
+
readonly qaScope?: DispatchQaScope | null;
|
|
16
17
|
readonly qaRework?: DispatchQaReworkContext | null;
|
|
17
18
|
readonly architectRevision?: DispatchArchitectRevisionContext | null;
|
|
18
19
|
readonly storyDevelop?: DispatchStoryDevelopContext | null;
|
|
@@ -32,6 +33,21 @@ export interface DispatchQaBrowserContext {
|
|
|
32
33
|
readonly artifactsPath: string;
|
|
33
34
|
readonly missingTargetReason?: string | null;
|
|
34
35
|
}
|
|
36
|
+
export interface DispatchQaScope {
|
|
37
|
+
readonly schema_version: number;
|
|
38
|
+
readonly surface: readonly string[];
|
|
39
|
+
readonly required_evidence: readonly string[];
|
|
40
|
+
readonly not_applicable: readonly string[];
|
|
41
|
+
readonly browser_required: boolean;
|
|
42
|
+
readonly api_required: boolean;
|
|
43
|
+
readonly source?: string | null;
|
|
44
|
+
readonly authority?: string | null;
|
|
45
|
+
readonly confidence?: string | null;
|
|
46
|
+
readonly confirmed_by_user?: boolean | null;
|
|
47
|
+
readonly locked?: boolean | null;
|
|
48
|
+
readonly derived_from?: Readonly<Record<string, unknown>> | null;
|
|
49
|
+
readonly rationale?: string | null;
|
|
50
|
+
}
|
|
35
51
|
export interface DispatchQaAppLaunchTarget {
|
|
36
52
|
readonly url?: string | null;
|
|
37
53
|
readonly command?: string | null;
|
|
@@ -20,6 +20,7 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
20
20
|
artifacts: parseArtifacts(packet.artifacts),
|
|
21
21
|
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
|
|
22
22
|
qaBrowser: parseQaBrowser(packet.qaBrowser),
|
|
23
|
+
qaScope: parseQaScope(packet.qaScope ?? packet.qa_scope),
|
|
23
24
|
qaRework: parseQaRework(packet.qaRework),
|
|
24
25
|
architectRevision: parseArchitectRevision(packet.architectRevision),
|
|
25
26
|
storyDevelop: parseStoryDevelop(packet.storyDevelop),
|
|
@@ -197,6 +198,27 @@ function parseQaBrowser(value) {
|
|
|
197
198
|
missingTargetReason: optionalString(qaBrowser, "missingTargetReason") ?? optionalString(qaBrowser, "missing_target_reason")
|
|
198
199
|
};
|
|
199
200
|
}
|
|
201
|
+
function parseQaScope(value) {
|
|
202
|
+
if (value === undefined || value === null) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const qaScope = asObject(value, "qaScope");
|
|
206
|
+
return {
|
|
207
|
+
schema_version: requiredNumber(qaScope, "schema_version"),
|
|
208
|
+
surface: parseOptionalStringArray(qaScope.surface, "qaScope.surface"),
|
|
209
|
+
required_evidence: parseOptionalStringArray(qaScope.required_evidence, "qaScope.required_evidence"),
|
|
210
|
+
not_applicable: parseOptionalStringArray(qaScope.not_applicable, "qaScope.not_applicable"),
|
|
211
|
+
browser_required: optionalBoolean(qaScope, "browser_required") ?? optionalBoolean(qaScope, "browserRequired") ?? false,
|
|
212
|
+
api_required: optionalBoolean(qaScope, "api_required") ?? optionalBoolean(qaScope, "apiRequired") ?? false,
|
|
213
|
+
source: optionalString(qaScope, "source"),
|
|
214
|
+
authority: optionalString(qaScope, "authority"),
|
|
215
|
+
confidence: optionalString(qaScope, "confidence"),
|
|
216
|
+
confirmed_by_user: optionalBoolean(qaScope, "confirmed_by_user") ?? optionalBoolean(qaScope, "confirmedByUser"),
|
|
217
|
+
locked: optionalBoolean(qaScope, "locked"),
|
|
218
|
+
derived_from: parseOptionalObject(qaScope.derived_from ?? qaScope.derivedFrom, "qaScope.derived_from"),
|
|
219
|
+
rationale: optionalString(qaScope, "rationale")
|
|
220
|
+
};
|
|
221
|
+
}
|
|
200
222
|
function parseQaAppLaunchTarget(value) {
|
|
201
223
|
if (value === undefined || value === null) {
|
|
202
224
|
return null;
|
|
@@ -376,6 +398,18 @@ function parseStringArray(value, label) {
|
|
|
376
398
|
return entry.trim();
|
|
377
399
|
});
|
|
378
400
|
}
|
|
401
|
+
function parseOptionalStringArray(value, label) {
|
|
402
|
+
if (value === undefined || value === null) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
return parseStringArray(value, label);
|
|
406
|
+
}
|
|
407
|
+
function parseOptionalObject(value, label) {
|
|
408
|
+
if (value === undefined || value === null) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return asObject(value, label);
|
|
412
|
+
}
|
|
379
413
|
function parseArtifacts(value) {
|
|
380
414
|
if (value === undefined || value === null) {
|
|
381
415
|
return [];
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.3-dev.b6be5e7f9668";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.3-dev.b6be5e7f9668";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loucompanion/forge-bridge",
|
|
3
|
-
"version": "0.1.3-dev.
|
|
3
|
+
"version": "0.1.3-dev.b6be5e7f9668",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|