@smartmemory/compose 0.1.1-beta → 0.1.3-beta
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/.claude/skills/bug-fix/SKILL.md +143 -0
- package/.claude/skills/compose/SKILL.md +604 -0
- package/.compose-deps.json +89 -0
- package/README.md +47 -983
- package/bin/compose.js +473 -0
- package/contracts/comp-obs-contract.schema.json +362 -0
- package/contracts/cross-model-review-result.json +78 -0
- package/contracts/review-result.json +126 -0
- package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
- package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
- package/dist/assets/channel-LRG9kHqJ.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
- package/dist/assets/clone-dRxgFrBv.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
- package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
- package/dist/assets/index-DKBsEUJ-.css +1 -0
- package/dist/assets/index-DkRKLuNr.js +1144 -0
- package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
- package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
- package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
- package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/budget-ledger.js +45 -0
- package/lib/bug-bisect.js +292 -0
- package/lib/bug-checkpoint.js +191 -0
- package/lib/bug-escalation.js +306 -0
- package/lib/bug-index-gen.js +136 -0
- package/lib/bug-ledger.js +126 -0
- package/lib/build-stream-schema.js +176 -0
- package/lib/build-stream-writer.js +3 -1
- package/lib/build.js +854 -284
- package/lib/connector-factory-shim.js +167 -0
- package/lib/constants.js +18 -0
- package/lib/debug-discipline.js +176 -27
- package/lib/deps.js +205 -0
- package/lib/health-score.js +4 -4
- package/lib/import.js +26 -13
- package/lib/inject-schema.js +21 -0
- package/lib/new.js +27 -53
- package/lib/result-normalizer.js +160 -144
- package/lib/review-lenses.js +5 -5
- package/lib/review-normalize.js +413 -0
- package/lib/review-prompt.js +163 -0
- package/lib/sections.js +325 -0
- package/lib/step-prompt.js +21 -1
- package/lib/step-validator.js +5 -3
- package/lib/stratum-mcp-client.js +172 -7
- package/package.json +14 -3
- package/pipelines/bug-fix.stratum.yaml +39 -1
- package/pipelines/build.stratum.yaml +28 -45
- package/pipelines/review-fix.stratum.yaml +1 -1
- package/presets/team-review.stratum.yaml +21 -14
- package/server/build-stream-bridge.js +28 -0
- package/server/cc-session-feature-resolver.js +111 -0
- package/server/cc-session-reader.js +327 -0
- package/server/cc-session-watcher.js +318 -0
- package/server/compose-mcp-tools.js +0 -125
- package/server/compose-mcp.js +2 -4
- package/server/contract-diff.js +192 -0
- package/server/decision-event-emit.js +175 -0
- package/server/decision-event-id.js +64 -0
- package/server/decision-events-snapshot.js +166 -0
- package/server/design-routes.js +92 -49
- package/server/drift-axes.js +365 -0
- package/server/drift-emit.js +121 -0
- package/server/gate-log-store.js +102 -0
- package/server/lifecycle-phase-history.js +44 -0
- package/server/open-loops-store.js +102 -0
- package/server/schema-validator.js +49 -0
- package/server/status-emit.js +27 -0
- package/server/status-snapshot.js +218 -0
- package/server/vision-routes.js +332 -4
- package/server/vision-server.js +104 -12
- package/server/vision-store.js +21 -0
- package/dist/assets/channel-DGElom1e.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
- package/dist/assets/clone-DUJKJXd7.js +0 -1
- package/dist/assets/index-CUd6pFGF.css +0 -1
- package/dist/assets/index-DReRlzZI.js +0 -1144
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
- package/server/connectors/agent-connector.js +0 -78
- package/server/connectors/claude-sdk-connector.js +0 -198
- package/server/connectors/codex-connector.js +0 -240
- package/server/connectors/connector-discovery.js +0 -18
- package/server/connectors/connector-runtime.js +0 -13
- package/server/connectors/opencode-connector.js +0 -200
package/lib/review-lenses.js
CHANGED
|
@@ -21,7 +21,7 @@ export const LENS_DEFINITIONS = {
|
|
|
21
21
|
sections: [
|
|
22
22
|
{ id: 'premises', label: 'Premises', description: 'List each changed function/block from the diff. Cite file:line for each.' },
|
|
23
23
|
{ id: 'trace', label: 'Quality Trace', description: 'For each premise, evaluate: naming clarity, duplication, error handling, dead code. Reference premises by ID.' },
|
|
24
|
-
{ id: 'findings', label: 'Findings', description: 'List
|
|
24
|
+
{ id: 'findings', label: 'Findings', description: 'List ReviewResult finding items. Each must reference the premise it came from.' },
|
|
25
25
|
],
|
|
26
26
|
},
|
|
27
27
|
},
|
|
@@ -36,7 +36,7 @@ export const LENS_DEFINITIONS = {
|
|
|
36
36
|
sections: [
|
|
37
37
|
{ id: 'premises', label: 'Premises', description: 'List each blueprint requirement and the file:line that implements it. List any blueprint item with NO matching implementation.' },
|
|
38
38
|
{ id: 'trace', label: 'Compliance Trace', description: 'For each premise pair (requirement <-> implementation), verify: correct path, correct signature, correct behavior. For unmatched items, confirm they are truly missing.' },
|
|
39
|
-
{ id: 'findings', label: 'Findings', description: 'List compliance gaps as
|
|
39
|
+
{ id: 'findings', label: 'Findings', description: 'List compliance gaps as ReviewResult finding items. Each must cite the blueprint requirement [P<n>] and the implementation (or lack thereof).' },
|
|
40
40
|
],
|
|
41
41
|
},
|
|
42
42
|
},
|
|
@@ -51,7 +51,7 @@ export const LENS_DEFINITIONS = {
|
|
|
51
51
|
sections: [
|
|
52
52
|
{ id: 'premises', label: 'Premises', description: 'List each security-sensitive operation in the diff: auth checks, SQL queries, user input handling, crypto, secrets. Cite file:line.' },
|
|
53
53
|
{ id: 'trace', label: 'Threat Trace', description: 'For each premise, trace the data flow from source to sink. Identify: is input validated? Is output escaped? Are secrets hardcoded? Reference premises by ID.' },
|
|
54
|
-
{ id: 'findings', label: 'Findings', description: 'List vulnerabilities as
|
|
54
|
+
{ id: 'findings', label: 'Findings', description: 'List vulnerabilities as ReviewResult finding items with OWASP category. Each must trace back to a specific premise and data flow.' },
|
|
55
55
|
],
|
|
56
56
|
},
|
|
57
57
|
},
|
|
@@ -69,7 +69,7 @@ export const LENS_DEFINITIONS = {
|
|
|
69
69
|
sections: [
|
|
70
70
|
{ id: 'premises', label: 'Premises', description: 'List each fix-iteration file change, trace evidence artifact, cross-layer reference, and type boundary check in the diff. Cite file:line for each.' },
|
|
71
71
|
{ id: 'trace', label: 'Discipline Trace', description: 'For each premise, evaluate: was this a repeated fix to the same location? Was trace evidence produced before the fix? Were all cross-layer references addressed? Are type boundaries explicit?' },
|
|
72
|
-
{ id: 'findings', label: 'Findings', description: 'List discipline violations as
|
|
72
|
+
{ id: 'findings', label: 'Findings', description: 'List discipline violations as ReviewResult finding items. Each must reference the specific premise and the anti-pattern it represents.' },
|
|
73
73
|
],
|
|
74
74
|
},
|
|
75
75
|
},
|
|
@@ -84,7 +84,7 @@ export const LENS_DEFINITIONS = {
|
|
|
84
84
|
sections: [
|
|
85
85
|
{ id: 'premises', label: 'Premises', description: 'List each framework API call or pattern used in the diff. Cite file:line. Note the framework version from package.json/requirements.txt.' },
|
|
86
86
|
{ id: 'trace', label: 'Pattern Trace', description: 'For each premise, check: is this API deprecated? Is there a preferred alternative? Does usage match framework conventions for this version?' },
|
|
87
|
-
{ id: 'findings', label: 'Findings', description: 'List anti-patterns and deprecations as
|
|
87
|
+
{ id: 'findings', label: 'Findings', description: 'List anti-patterns and deprecations as ReviewResult finding items. Each must reference the specific API call [P<n>] and the framework docs justification.' },
|
|
88
88
|
],
|
|
89
89
|
},
|
|
90
90
|
},
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-normalize.js — Parse and normalize model output to canonical ReviewResult.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: Stratum-layer schema validation runs against the post-normalize result
|
|
5
|
+
* via `ensure` expressions evaluated by the Stratum server after `stratum_step_done`.
|
|
6
|
+
* It does NOT run against raw text. The hook in result-normalizer.js must therefore
|
|
7
|
+
* run BEFORE the normalizer returns — do not move this hook later in the pipeline.
|
|
8
|
+
* See: result-normalizer.js hook position, BEFORE the `if (!hasSchema)` early return.
|
|
9
|
+
*
|
|
10
|
+
* Repair-retry model choice (blueprint Decision, Iter 3 F1): use the same model as
|
|
11
|
+
* the original call. It has conversation context, and latency cost is one short call.
|
|
12
|
+
*
|
|
13
|
+
* See: docs/features/STRAT-CLAUDE-EFFORT-PARITY/design.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// JSON parsing helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Strip markdown code fences and attempt JSON.parse.
|
|
22
|
+
* Handles ```json ... ``` and ``` ... ``` wrappers and leading/trailing prose.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} text
|
|
25
|
+
* @returns {object|null} Parsed object or null on failure
|
|
26
|
+
*/
|
|
27
|
+
export function parseReviewJson(text) {
|
|
28
|
+
if (typeof text !== 'string' || !text.trim()) return null;
|
|
29
|
+
|
|
30
|
+
let candidate = text;
|
|
31
|
+
|
|
32
|
+
// Strip markdown code fences (```json ... ``` or ``` ... ```)
|
|
33
|
+
const fenceMatch = candidate.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
34
|
+
if (fenceMatch) {
|
|
35
|
+
candidate = fenceMatch[1].trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try direct parse
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(candidate);
|
|
41
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
42
|
+
} catch { /* continue */ }
|
|
43
|
+
|
|
44
|
+
// Try extracting a JSON object from surrounding prose
|
|
45
|
+
const objMatch = candidate.match(/\{[\s\S]*\}/);
|
|
46
|
+
if (objMatch) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(objMatch[0]);
|
|
49
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
50
|
+
} catch { /* continue */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Text-mode fallback parser (low-confidence findings from prose)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract findings from unstructured text using heuristics.
|
|
62
|
+
* Used when JSON parse fails AND no repair-retry is available (or retry also failed).
|
|
63
|
+
* All extracted findings get confidence=5 (below most gates) so they don't block ship.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} text
|
|
66
|
+
* @param {string} lens
|
|
67
|
+
* @param {number} confidenceGate
|
|
68
|
+
* @returns {Array<object>} findings[]
|
|
69
|
+
*/
|
|
70
|
+
function textModeFindings(text, lens, confidenceGate) {
|
|
71
|
+
const findings = [];
|
|
72
|
+
const lines = text.split('\n');
|
|
73
|
+
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
if (!trimmed) continue;
|
|
77
|
+
|
|
78
|
+
let severity = null;
|
|
79
|
+
let content = trimmed;
|
|
80
|
+
|
|
81
|
+
// Detect severity markers
|
|
82
|
+
if (/must.fix|must_fix|critical|error:/i.test(trimmed)) {
|
|
83
|
+
severity = 'must-fix';
|
|
84
|
+
content = trimmed.replace(/^[-*]\s*/, '').replace(/\*\*(must.fix|critical)\*\*:?\s*/i, '');
|
|
85
|
+
} else if (/should.fix|should_fix|warning:|warn:/i.test(trimmed)) {
|
|
86
|
+
severity = 'should-fix';
|
|
87
|
+
content = trimmed.replace(/^[-*]\s*/, '').replace(/\*\*(should.fix|warning)\*\*:?\s*/i, '');
|
|
88
|
+
} else if (/\bnit\b|style:/i.test(trimmed)) {
|
|
89
|
+
severity = 'nit';
|
|
90
|
+
content = trimmed.replace(/^[-*]\s*/, '').replace(/\*\*nit\*\*:?\s*/i, '');
|
|
91
|
+
} else if (/^[-*]\s+/.test(trimmed)) {
|
|
92
|
+
// Bullet point without explicit severity — treat as should-fix
|
|
93
|
+
severity = 'should-fix';
|
|
94
|
+
content = trimmed.replace(/^[-*]\s+/, '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (severity && content.length > 10) {
|
|
98
|
+
findings.push({
|
|
99
|
+
lens,
|
|
100
|
+
file: null,
|
|
101
|
+
line: null,
|
|
102
|
+
severity,
|
|
103
|
+
finding: content.slice(0, 300),
|
|
104
|
+
confidence: 5, // low confidence — text mode
|
|
105
|
+
applied_gate: confidenceGate,
|
|
106
|
+
rationale: null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return findings;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Normalizer
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalize raw model output text to a canonical ReviewResult.
|
|
120
|
+
*
|
|
121
|
+
* Algorithm:
|
|
122
|
+
* 1. Try JSON parse (strip fences, extract object from prose).
|
|
123
|
+
* 2. On failure: if repairFn provided, call once with repair prompt, reparse.
|
|
124
|
+
* If no repairFn or second failure, fall through to text-mode parser.
|
|
125
|
+
* 3. Stamp applied_gate on findings missing it (per blueprint Iter 3 F3).
|
|
126
|
+
* 4. Drop findings whose confidence < applied_gate (defensive — prompt should filter).
|
|
127
|
+
* 5. Compute clean = zero blocking findings post-filter.
|
|
128
|
+
* 6. Synthesize summary if model didn't provide.
|
|
129
|
+
* 7. Attach meta.
|
|
130
|
+
* 8. Initialize lenses_run/auto_fixes/asks (populated by merge step, not per-task).
|
|
131
|
+
*
|
|
132
|
+
* @param {string} rawText - Raw model output
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {'claude'|'codex'} [opts.agentType='claude']
|
|
135
|
+
* @param {string|null} [opts.modelId]
|
|
136
|
+
* @param {number} [opts.confidenceGate=7]
|
|
137
|
+
* @param {string} [opts.lens='general']
|
|
138
|
+
* @param {Function} [opts.repairFn] - async (repairPrompt: string) => string — calls model once
|
|
139
|
+
* @returns {Promise<object>} Canonical ReviewResult
|
|
140
|
+
*/
|
|
141
|
+
export async function normalizeReviewResult(rawText, {
|
|
142
|
+
agentType = 'claude',
|
|
143
|
+
modelId = null,
|
|
144
|
+
confidenceGate = 7,
|
|
145
|
+
lens = 'general',
|
|
146
|
+
repairFn,
|
|
147
|
+
} = {}) {
|
|
148
|
+
// Step 1: Try direct JSON parse
|
|
149
|
+
let parsed = parseReviewJson(rawText);
|
|
150
|
+
|
|
151
|
+
// Step 2: Repair-retry on parse failure
|
|
152
|
+
if (!parsed && typeof repairFn === 'function') {
|
|
153
|
+
const repairPrompt = buildRepairPrompt(rawText);
|
|
154
|
+
let repairText = null;
|
|
155
|
+
try {
|
|
156
|
+
repairText = await repairFn(repairPrompt);
|
|
157
|
+
} catch { /* repair call failed — fall through to text mode */ }
|
|
158
|
+
|
|
159
|
+
if (repairText) {
|
|
160
|
+
parsed = parseReviewJson(repairText);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 2 fallback: text-mode extraction
|
|
165
|
+
if (!parsed) {
|
|
166
|
+
const fallbackFindings = textModeFindings(rawText, lens, confidenceGate);
|
|
167
|
+
parsed = {
|
|
168
|
+
summary: null,
|
|
169
|
+
findings: fallbackFindings,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Normalize findings array
|
|
174
|
+
const rawFindings = Array.isArray(parsed.findings) ? parsed.findings : [];
|
|
175
|
+
|
|
176
|
+
// Step 3: Stamp applied_gate on findings missing it
|
|
177
|
+
// Step 4: Drop findings below gate
|
|
178
|
+
const findings = rawFindings
|
|
179
|
+
.map(f => {
|
|
180
|
+
const gate = typeof f.applied_gate === 'number' ? f.applied_gate : confidenceGate;
|
|
181
|
+
return {
|
|
182
|
+
lens: f.lens ?? lens,
|
|
183
|
+
file: f.file ?? null,
|
|
184
|
+
line: f.line ?? null,
|
|
185
|
+
severity: normalizeSeverity(f.severity),
|
|
186
|
+
finding: f.finding ?? f.description ?? String(f),
|
|
187
|
+
confidence: typeof f.confidence === 'number' ? f.confidence : 5,
|
|
188
|
+
applied_gate: gate,
|
|
189
|
+
rationale: f.rationale ?? null,
|
|
190
|
+
};
|
|
191
|
+
})
|
|
192
|
+
.filter(f => f.confidence >= f.applied_gate);
|
|
193
|
+
|
|
194
|
+
// Step 5: Compute clean
|
|
195
|
+
const blocking = findings.filter(f =>
|
|
196
|
+
f.severity === 'must-fix' || f.severity === 'should-fix'
|
|
197
|
+
);
|
|
198
|
+
const clean = blocking.length === 0;
|
|
199
|
+
|
|
200
|
+
// Step 6: Synthesize summary if not provided
|
|
201
|
+
const mustFixCount = findings.filter(f => f.severity === 'must-fix').length;
|
|
202
|
+
const shouldFixCount = findings.filter(f => f.severity === 'should-fix').length;
|
|
203
|
+
const nitCount = findings.filter(f => f.severity === 'nit').length;
|
|
204
|
+
const summary = (typeof parsed.summary === 'string' && parsed.summary.trim())
|
|
205
|
+
? parsed.summary.trim()
|
|
206
|
+
: `${findings.length} findings (${mustFixCount} must-fix, ${shouldFixCount} should-fix, ${nitCount} nit).`;
|
|
207
|
+
|
|
208
|
+
// Steps 7-8: Attach meta and initialize optional fields
|
|
209
|
+
return {
|
|
210
|
+
clean,
|
|
211
|
+
summary,
|
|
212
|
+
findings,
|
|
213
|
+
meta: {
|
|
214
|
+
agent_type: agentType,
|
|
215
|
+
model_id: modelId ?? null,
|
|
216
|
+
},
|
|
217
|
+
lenses_run: [],
|
|
218
|
+
auto_fixes: [],
|
|
219
|
+
asks: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Cross-model synthesis normalizer
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Normalize raw synthesis output from runCrossModelReview to a canonical
|
|
229
|
+
* CrossModelReviewResult — a ReviewResult extended with consensus, claude_only,
|
|
230
|
+
* and codex_only arrays of canonical finding items.
|
|
231
|
+
*
|
|
232
|
+
* Algorithm:
|
|
233
|
+
* 1. Parse the synthesis JSON (strip fences, extract from prose).
|
|
234
|
+
* 2. On failure: if repairFn provided, call once with a cross-model repair prompt.
|
|
235
|
+
* If no repairFn or retry failed, fall back to placing all text-mode findings in codex_only.
|
|
236
|
+
* 3. Normalize each finding in the three arrays: stamp applied_gate if missing,
|
|
237
|
+
* enforce canonical severity vocab.
|
|
238
|
+
* 4. Merge all findings into top-level `findings` array (for ReviewResult compatibility).
|
|
239
|
+
* 5. Compute clean = zero blocking findings across all three arrays.
|
|
240
|
+
* 6. Attach meta with agent_type='claude' (synthesis is always run by Claude).
|
|
241
|
+
*
|
|
242
|
+
* @param {string} rawText - Raw synthesis model output
|
|
243
|
+
* @param {object} opts
|
|
244
|
+
* @param {string|null} [opts.modelId]
|
|
245
|
+
* @param {number} [opts.confidenceGate=7]
|
|
246
|
+
* @param {Array} [opts.claudeFindingsFallback=[]] - Claude findings to use when parse fails
|
|
247
|
+
* @param {Array} [opts.codexFindingsFallback=[]] - Codex-as-fallback findings to use when parse fails
|
|
248
|
+
* @param {Function} [opts.repairFn] - async (repairPrompt: string) => string
|
|
249
|
+
* @returns {Promise<object>} Canonical CrossModelReviewResult
|
|
250
|
+
*/
|
|
251
|
+
export async function normalizeCrossModelResult(rawText, {
|
|
252
|
+
modelId = null,
|
|
253
|
+
confidenceGate = 7,
|
|
254
|
+
claudeFindingsFallback = [],
|
|
255
|
+
codexFindingsFallback = [],
|
|
256
|
+
repairFn,
|
|
257
|
+
} = {}) {
|
|
258
|
+
// Step 1: Try direct JSON parse
|
|
259
|
+
let parsed = parseReviewJson(rawText);
|
|
260
|
+
|
|
261
|
+
// Step 2: Repair-retry on parse failure
|
|
262
|
+
if (!parsed && typeof repairFn === 'function') {
|
|
263
|
+
const repairPrompt = buildCrossModelRepairPrompt(rawText);
|
|
264
|
+
let repairText = null;
|
|
265
|
+
try {
|
|
266
|
+
repairText = await repairFn(repairPrompt);
|
|
267
|
+
} catch { /* repair call failed — fall through to fallback */ }
|
|
268
|
+
|
|
269
|
+
if (repairText) {
|
|
270
|
+
parsed = parseReviewJson(repairText);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Step 2 fallback: treat the synthesis as all-or-nothing.
|
|
275
|
+
// A partial response (e.g., consensus parsed but claude_only/codex_only missing) cannot be
|
|
276
|
+
// trusted to have correctly partitioned findings — mixing parsed `consensus` with full Claude
|
|
277
|
+
// fallback would duplicate items that the model already moved into consensus.
|
|
278
|
+
// If any of the three required arrays is missing, fall back wholesale.
|
|
279
|
+
let consensusRaw, claudeOnlyRaw, codexOnlyRaw;
|
|
280
|
+
const hasAllArrays = parsed && typeof parsed === 'object'
|
|
281
|
+
&& Array.isArray(parsed.consensus)
|
|
282
|
+
&& Array.isArray(parsed.claude_only)
|
|
283
|
+
&& Array.isArray(parsed.codex_only);
|
|
284
|
+
if (!hasAllArrays) {
|
|
285
|
+
consensusRaw = [];
|
|
286
|
+
claudeOnlyRaw = claudeFindingsFallback;
|
|
287
|
+
codexOnlyRaw = codexFindingsFallback;
|
|
288
|
+
} else {
|
|
289
|
+
consensusRaw = parsed.consensus;
|
|
290
|
+
claudeOnlyRaw = parsed.claude_only;
|
|
291
|
+
codexOnlyRaw = parsed.codex_only;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Step 3: Normalize each finding in all three arrays
|
|
295
|
+
const normalizeFinding = (f) => {
|
|
296
|
+
const gate = typeof f.applied_gate === 'number' ? f.applied_gate : confidenceGate;
|
|
297
|
+
return {
|
|
298
|
+
lens: f.lens ?? 'general',
|
|
299
|
+
file: f.file ?? null,
|
|
300
|
+
line: f.line ?? null,
|
|
301
|
+
severity: normalizeSeverity(f.severity),
|
|
302
|
+
finding: f.finding ?? f.description ?? String(f),
|
|
303
|
+
confidence: typeof f.confidence === 'number' ? f.confidence : 5,
|
|
304
|
+
applied_gate: gate,
|
|
305
|
+
rationale: f.rationale ?? null,
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const consensus = consensusRaw.map(normalizeFinding).filter(f => f.confidence >= f.applied_gate);
|
|
310
|
+
const claude_only = claudeOnlyRaw.map(normalizeFinding).filter(f => f.confidence >= f.applied_gate);
|
|
311
|
+
const codex_only = codexOnlyRaw.map(normalizeFinding).filter(f => f.confidence >= f.applied_gate);
|
|
312
|
+
|
|
313
|
+
// Step 4: Merge all findings into top-level findings array
|
|
314
|
+
const findings = [...consensus, ...claude_only, ...codex_only];
|
|
315
|
+
|
|
316
|
+
// Step 5: Compute clean across all three arrays
|
|
317
|
+
const blocking = findings.filter(f => f.severity === 'must-fix' || f.severity === 'should-fix');
|
|
318
|
+
const clean = blocking.length === 0;
|
|
319
|
+
|
|
320
|
+
// Synthesize summary
|
|
321
|
+
const mustFixCount = findings.filter(f => f.severity === 'must-fix').length;
|
|
322
|
+
const shouldFixCount = findings.filter(f => f.severity === 'should-fix').length;
|
|
323
|
+
const nitCount = findings.filter(f => f.severity === 'nit').length;
|
|
324
|
+
const summary = (typeof parsed?.summary === 'string' && parsed.summary.trim())
|
|
325
|
+
? parsed.summary.trim()
|
|
326
|
+
: `Cross-model synthesis: ${consensus.length} consensus, ${claude_only.length} Claude-only, ${codex_only.length} Codex-only. ${findings.length} total (${mustFixCount} must-fix, ${shouldFixCount} should-fix, ${nitCount} nit).`;
|
|
327
|
+
|
|
328
|
+
// Step 6: Attach meta
|
|
329
|
+
return {
|
|
330
|
+
clean,
|
|
331
|
+
summary,
|
|
332
|
+
findings,
|
|
333
|
+
meta: {
|
|
334
|
+
agent_type: 'claude',
|
|
335
|
+
model_id: modelId ?? null,
|
|
336
|
+
},
|
|
337
|
+
lenses_run: [],
|
|
338
|
+
auto_fixes: [],
|
|
339
|
+
asks: [],
|
|
340
|
+
consensus,
|
|
341
|
+
claude_only,
|
|
342
|
+
codex_only,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Helpers
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Build a repair prompt asking the model to fix malformed JSON.
|
|
352
|
+
*
|
|
353
|
+
* @param {string} badText - The malformed model output
|
|
354
|
+
* @returns {string}
|
|
355
|
+
*/
|
|
356
|
+
function buildRepairPrompt(badText) {
|
|
357
|
+
return (
|
|
358
|
+
'The following text was supposed to be a JSON object matching the ReviewResult schema ' +
|
|
359
|
+
'but failed to parse. Please fix it and return ONLY valid JSON — no prose.\n\n' +
|
|
360
|
+
'Expected schema:\n' +
|
|
361
|
+
'{\n' +
|
|
362
|
+
' "summary": string,\n' +
|
|
363
|
+
' "findings": [\n' +
|
|
364
|
+
' { "lens": string, "file": string|null, "line": integer|null,\n' +
|
|
365
|
+
' "severity": "must-fix"|"should-fix"|"nit",\n' +
|
|
366
|
+
' "finding": string, "confidence": 1-10, "applied_gate": 1-10 }\n' +
|
|
367
|
+
' ]\n' +
|
|
368
|
+
'}\n\n' +
|
|
369
|
+
'Malformed input:\n' +
|
|
370
|
+
badText.slice(0, 2000)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Build a repair prompt for malformed cross-model synthesis JSON.
|
|
376
|
+
*
|
|
377
|
+
* @param {string} badText - The malformed synthesis output
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
380
|
+
function buildCrossModelRepairPrompt(badText) {
|
|
381
|
+
return (
|
|
382
|
+
'The following text was supposed to be a JSON object matching the CrossModelReviewResult schema ' +
|
|
383
|
+
'but failed to parse. Please fix it and return ONLY valid JSON — no prose.\n\n' +
|
|
384
|
+
'Expected schema:\n' +
|
|
385
|
+
'{\n' +
|
|
386
|
+
' "summary": string,\n' +
|
|
387
|
+
' "consensus": [\n' +
|
|
388
|
+
' { "lens": string, "file": string|null, "line": integer|null,\n' +
|
|
389
|
+
' "severity": "must-fix"|"should-fix"|"nit",\n' +
|
|
390
|
+
' "finding": string, "confidence": 1-10, "applied_gate": 1-10 }\n' +
|
|
391
|
+
' ],\n' +
|
|
392
|
+
' "claude_only": [ <same shape> ],\n' +
|
|
393
|
+
' "codex_only": [ <same shape> ]\n' +
|
|
394
|
+
'}\n\n' +
|
|
395
|
+
'Malformed input:\n' +
|
|
396
|
+
badText.slice(0, 2000)
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Normalize severity strings to canonical values.
|
|
402
|
+
* Accepts: must-fix, must_fix, MUST-FIX, should-fix, should_fix, nit, etc.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} raw
|
|
405
|
+
* @returns {'must-fix'|'should-fix'|'nit'}
|
|
406
|
+
*/
|
|
407
|
+
function normalizeSeverity(raw) {
|
|
408
|
+
if (!raw) return 'nit';
|
|
409
|
+
const s = String(raw).toLowerCase().replace(/_/g, '-');
|
|
410
|
+
if (s === 'must-fix' || s === 'critical' || s === 'error') return 'must-fix';
|
|
411
|
+
if (s === 'should-fix' || s === 'warning' || s === 'warn') return 'should-fix';
|
|
412
|
+
return 'nit';
|
|
413
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-prompt.js — Shared review prompt scaffold for STRAT-CLAUDE-EFFORT-PARITY.
|
|
3
|
+
*
|
|
4
|
+
* Builds a unified system prompt injected at review time. Both Claude and Codex
|
|
5
|
+
* review paths use the same severity vocabulary, confidence scale, and output format.
|
|
6
|
+
* Per-model nudges are appended at the end.
|
|
7
|
+
*
|
|
8
|
+
* Cert (reasoning template) injection is NOT done here — call sites in build.js
|
|
9
|
+
* compose buildReviewPrompt(...) then call injectCertInstructions(scaffold, template)
|
|
10
|
+
* separately, matching the existing pattern at build.js:2625.
|
|
11
|
+
*
|
|
12
|
+
* See: docs/features/STRAT-CLAUDE-EFFORT-PARITY/design.md (Decision 3)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Severity vocabulary block — identical text across all calls
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const SEVERITY_VOCAB_BLOCK = `
|
|
20
|
+
## Severity Vocabulary
|
|
21
|
+
|
|
22
|
+
Use EXACTLY these severity values — no others:
|
|
23
|
+
- **must-fix**: Blocks ship. Correctness bugs, security vulnerabilities, broken contracts, data loss risks.
|
|
24
|
+
- **should-fix**: Address in next iteration. Clarity gaps, missing edge-case tests, fragile patterns.
|
|
25
|
+
- **nit**: Logged only, does not block. Style, naming, minor consistency.
|
|
26
|
+
`.trim();
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Confidence scale block
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const CONFIDENCE_SCALE_BLOCK = `
|
|
33
|
+
## Confidence Scale
|
|
34
|
+
|
|
35
|
+
Score your confidence in each finding from 1 to 10:
|
|
36
|
+
- 10: Certain — the issue is definitively present with direct evidence.
|
|
37
|
+
- 7-9: High — strong evidence, highly probable issue.
|
|
38
|
+
- 4-6: Medium — plausible but requires verification.
|
|
39
|
+
- 1-3: Low — speculative, insufficient evidence.
|
|
40
|
+
`.trim();
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Output format block
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
const OUTPUT_FORMAT_BLOCK = `
|
|
47
|
+
## Output Format
|
|
48
|
+
|
|
49
|
+
Return a JSON object matching this schema exactly:
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
"summary": "<1-3 sentence narrative>",
|
|
53
|
+
"findings": [
|
|
54
|
+
{
|
|
55
|
+
"lens": "<lens name or 'general'>",
|
|
56
|
+
"file": "<relative file path or null>",
|
|
57
|
+
"line": <integer or null>,
|
|
58
|
+
"severity": "must-fix" | "should-fix" | "nit",
|
|
59
|
+
"finding": "<concise, actionable description>",
|
|
60
|
+
"confidence": <integer 1-10>,
|
|
61
|
+
"applied_gate": <integer — the confidence gate used>,
|
|
62
|
+
"rationale": "<optional structured reasoning or null>"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
If no findings meet the confidence gate, return: { "summary": "No findings above gate.", "findings": [] }
|
|
68
|
+
`.trim();
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Main builder
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build a unified review prompt string.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} opts
|
|
78
|
+
* @param {'claude'|'codex'} opts.agentType - Which model type will receive this prompt
|
|
79
|
+
* @param {string} [opts.lens='general'] - Lens name or 'general' for single-pass
|
|
80
|
+
* @param {string} [opts.lensFocus] - Lens-specific focus instructions
|
|
81
|
+
* @param {string} [opts.exclusions] - What NOT to flag (false-positive exclusions)
|
|
82
|
+
* @param {number} [opts.confidenceGate=7] - Minimum confidence to emit a finding
|
|
83
|
+
* @param {string} [opts.taskDescription] - What was implemented
|
|
84
|
+
* @param {string} [opts.blueprint] - Blueprint/spec content
|
|
85
|
+
* @param {object} [opts.reasoningTemplate] - Ignored here; call injectCertInstructions() at the call site after buildReviewPrompt returns.
|
|
86
|
+
* @returns {string} Complete prompt string
|
|
87
|
+
*/
|
|
88
|
+
export function buildReviewPrompt({
|
|
89
|
+
agentType = 'claude',
|
|
90
|
+
lens = 'general',
|
|
91
|
+
lensFocus,
|
|
92
|
+
exclusions,
|
|
93
|
+
confidenceGate = 7,
|
|
94
|
+
taskDescription,
|
|
95
|
+
blueprint,
|
|
96
|
+
reasoningTemplate,
|
|
97
|
+
} = {}) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
|
|
100
|
+
// 1. Role line
|
|
101
|
+
if (lens !== 'general') {
|
|
102
|
+
parts.push(`You are a ${lens} reviewer performing a focused code review.`);
|
|
103
|
+
} else {
|
|
104
|
+
parts.push('You are a senior code reviewer performing a comprehensive code review.');
|
|
105
|
+
}
|
|
106
|
+
parts.push('');
|
|
107
|
+
|
|
108
|
+
// 2. Severity vocabulary (identical across calls)
|
|
109
|
+
parts.push(SEVERITY_VOCAB_BLOCK);
|
|
110
|
+
parts.push('');
|
|
111
|
+
|
|
112
|
+
// 3. Confidence scale
|
|
113
|
+
parts.push(CONFIDENCE_SCALE_BLOCK);
|
|
114
|
+
parts.push('');
|
|
115
|
+
|
|
116
|
+
// 4. Output format
|
|
117
|
+
parts.push(OUTPUT_FORMAT_BLOCK);
|
|
118
|
+
parts.push('');
|
|
119
|
+
|
|
120
|
+
// 5. Confidence gate instruction
|
|
121
|
+
parts.push(
|
|
122
|
+
`## Confidence Gate\n\n` +
|
|
123
|
+
`Only emit findings with confidence >= ${confidenceGate}. ` +
|
|
124
|
+
`Stamp \`applied_gate = ${confidenceGate}\` on every finding you emit. ` +
|
|
125
|
+
`Silently discard findings below this threshold.`
|
|
126
|
+
);
|
|
127
|
+
parts.push('');
|
|
128
|
+
|
|
129
|
+
// 6. Per-lens focus (when not general)
|
|
130
|
+
if (lens !== 'general' && lensFocus) {
|
|
131
|
+
parts.push(`## Lens Focus: ${lens}\n\n${lensFocus}`);
|
|
132
|
+
parts.push('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 7. Exclusions
|
|
136
|
+
if (exclusions) {
|
|
137
|
+
parts.push(`## Exclusions\n\nDo NOT flag: ${exclusions}`);
|
|
138
|
+
parts.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 8. Task / blueprint context
|
|
142
|
+
if (taskDescription) {
|
|
143
|
+
parts.push(`## Task\n\n${taskDescription}`);
|
|
144
|
+
parts.push('');
|
|
145
|
+
}
|
|
146
|
+
if (blueprint) {
|
|
147
|
+
parts.push(`## Blueprint\n\n${blueprint}`);
|
|
148
|
+
parts.push('');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 9. Per-model nudge
|
|
152
|
+
if (agentType === 'codex') {
|
|
153
|
+
parts.push(
|
|
154
|
+
'## Output Instruction\n\n' +
|
|
155
|
+
'Output exactly one JSON code-fence containing the result object described above. ' +
|
|
156
|
+
'No prose before or after the code-fence.'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
// For Claude: cert (reasoning template) injection is handled at the call site in build.js
|
|
160
|
+
// via injectCertInstructions(scaffold, reasoningTemplate) after buildReviewPrompt returns.
|
|
161
|
+
|
|
162
|
+
return parts.join('\n');
|
|
163
|
+
}
|