@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.
Files changed (124) hide show
  1. package/.claude/skills/bug-fix/SKILL.md +143 -0
  2. package/.claude/skills/compose/SKILL.md +604 -0
  3. package/.compose-deps.json +89 -0
  4. package/README.md +47 -983
  5. package/bin/compose.js +473 -0
  6. package/contracts/comp-obs-contract.schema.json +362 -0
  7. package/contracts/cross-model-review-result.json +78 -0
  8. package/contracts/review-result.json +126 -0
  9. package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
  10. package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
  11. package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
  12. package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
  13. package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
  14. package/dist/assets/channel-LRG9kHqJ.js +1 -0
  15. package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
  16. package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
  17. package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
  20. package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
  21. package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
  22. package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
  25. package/dist/assets/clone-dRxgFrBv.js +1 -0
  26. package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
  27. package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
  28. package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
  29. package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
  30. package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
  31. package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
  32. package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
  33. package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
  34. package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
  36. package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
  37. package/dist/assets/index-DKBsEUJ-.css +1 -0
  38. package/dist/assets/index-DkRKLuNr.js +1144 -0
  39. package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
  41. package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
  42. package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
  43. package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
  44. package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
  45. package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
  46. package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
  47. package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
  49. package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
  52. package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
  54. package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
  55. package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
  56. package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
  57. package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
  58. package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/lib/budget-ledger.js +45 -0
  61. package/lib/bug-bisect.js +292 -0
  62. package/lib/bug-checkpoint.js +191 -0
  63. package/lib/bug-escalation.js +306 -0
  64. package/lib/bug-index-gen.js +136 -0
  65. package/lib/bug-ledger.js +126 -0
  66. package/lib/build-stream-schema.js +176 -0
  67. package/lib/build-stream-writer.js +3 -1
  68. package/lib/build.js +854 -284
  69. package/lib/connector-factory-shim.js +167 -0
  70. package/lib/constants.js +18 -0
  71. package/lib/debug-discipline.js +176 -27
  72. package/lib/deps.js +205 -0
  73. package/lib/health-score.js +4 -4
  74. package/lib/import.js +26 -13
  75. package/lib/inject-schema.js +21 -0
  76. package/lib/new.js +27 -53
  77. package/lib/result-normalizer.js +160 -144
  78. package/lib/review-lenses.js +5 -5
  79. package/lib/review-normalize.js +413 -0
  80. package/lib/review-prompt.js +163 -0
  81. package/lib/sections.js +325 -0
  82. package/lib/step-prompt.js +21 -1
  83. package/lib/step-validator.js +5 -3
  84. package/lib/stratum-mcp-client.js +172 -7
  85. package/package.json +14 -3
  86. package/pipelines/bug-fix.stratum.yaml +39 -1
  87. package/pipelines/build.stratum.yaml +28 -45
  88. package/pipelines/review-fix.stratum.yaml +1 -1
  89. package/presets/team-review.stratum.yaml +21 -14
  90. package/server/build-stream-bridge.js +28 -0
  91. package/server/cc-session-feature-resolver.js +111 -0
  92. package/server/cc-session-reader.js +327 -0
  93. package/server/cc-session-watcher.js +318 -0
  94. package/server/compose-mcp-tools.js +0 -125
  95. package/server/compose-mcp.js +2 -4
  96. package/server/contract-diff.js +192 -0
  97. package/server/decision-event-emit.js +175 -0
  98. package/server/decision-event-id.js +64 -0
  99. package/server/decision-events-snapshot.js +166 -0
  100. package/server/design-routes.js +92 -49
  101. package/server/drift-axes.js +365 -0
  102. package/server/drift-emit.js +121 -0
  103. package/server/gate-log-store.js +102 -0
  104. package/server/lifecycle-phase-history.js +44 -0
  105. package/server/open-loops-store.js +102 -0
  106. package/server/schema-validator.js +49 -0
  107. package/server/status-emit.js +27 -0
  108. package/server/status-snapshot.js +218 -0
  109. package/server/vision-routes.js +332 -4
  110. package/server/vision-server.js +104 -12
  111. package/server/vision-store.js +21 -0
  112. package/dist/assets/channel-DGElom1e.js +0 -1
  113. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
  114. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
  115. package/dist/assets/clone-DUJKJXd7.js +0 -1
  116. package/dist/assets/index-CUd6pFGF.css +0 -1
  117. package/dist/assets/index-DReRlzZI.js +0 -1144
  118. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
  119. package/server/connectors/agent-connector.js +0 -78
  120. package/server/connectors/claude-sdk-connector.js +0 -198
  121. package/server/connectors/codex-connector.js +0 -240
  122. package/server/connectors/connector-discovery.js +0 -18
  123. package/server/connectors/connector-runtime.js +0 -13
  124. package/server/connectors/opencode-connector.js +0 -200
@@ -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 LensFinding items. Each must reference the premise it came from.' },
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 LensFinding items. Each must cite the blueprint requirement [P<n>] and the implementation (or lack thereof).' },
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 LensFinding items with OWASP category. Each must trace back to a specific premise and data flow.' },
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 LensFinding items. Each must reference the specific premise and the anti-pattern it represents.' },
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 LensFinding items. Each must reference the specific API call [P<n>] and the framework docs justification.' },
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
+ }