spec-and-loop 3.3.2 → 3.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * supervisor-state.js — Budget bookkeeping, blocker hashing, hint
5
+ * normalization, and small return-shape helpers for the supervisor loop.
6
+ *
7
+ * These helpers are intentionally side-effect-light: each takes plain
8
+ * options/state objects and returns plain objects. The only functions that
9
+ * touch durable state are `_readSupervisorState` and `_writeSupervisorState`,
10
+ * which proxy to `state.read` / `state.update` (the same store the runner
11
+ * uses) so all supervisor budget-tracking shares one ralph-loop.state.json
12
+ * record. Moved verbatim from supervisor.js so existing tests keep passing.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const crypto = require('crypto');
18
+
19
+ const state = require('./state');
20
+
21
+ function _computeBlockerHash(blockerNote) {
22
+ return crypto
23
+ .createHash('sha256')
24
+ .update(String(blockerNote || '').trim())
25
+ .digest('hex')
26
+ .slice(0, 16);
27
+ }
28
+
29
+ function _readSupervisorState(ralphDir) {
30
+ const current = state.read(ralphDir) || {};
31
+ return current.supervisor && typeof current.supervisor === 'object'
32
+ ? current.supervisor
33
+ : {};
34
+ }
35
+
36
+ function _writeSupervisorState(ralphDir, supervisorUpdate) {
37
+ const current = state.read(ralphDir) || {};
38
+ state.update(ralphDir, {
39
+ supervisor: Object.assign({}, current.supervisor || {}, supervisorUpdate),
40
+ });
41
+ }
42
+
43
+ function _nonNegativeInteger(value) {
44
+ return Number.isInteger(value) && value >= 0 ? value : 0;
45
+ }
46
+
47
+ function _decideBoundedBudget(options = {}) {
48
+ const totalAttempts = _nonNegativeInteger(options.totalAttempts);
49
+ const maxTotalAttempts = _nonNegativeInteger(options.maxTotalAttempts);
50
+ const attempts = options.attempts && typeof options.attempts === 'object' ? options.attempts : {};
51
+ const budgetKey = String(options.budgetKey || '');
52
+
53
+ if (!budgetKey) {
54
+ return { allowed: true, reason: 'unbounded' };
55
+ }
56
+
57
+ if (maxTotalAttempts > 0 && totalAttempts >= maxTotalAttempts) {
58
+ return { allowed: false, reason: 'global_budget_exhausted' };
59
+ }
60
+
61
+ if (Object.prototype.hasOwnProperty.call(attempts, budgetKey)) {
62
+ return { allowed: false, reason: 'task_class_budget_exhausted' };
63
+ }
64
+
65
+ return { allowed: true, reason: 'authorized' };
66
+ }
67
+
68
+ function _consumeBoundedBudget(options = {}) {
69
+ const priorState = options.state && typeof options.state === 'object' ? options.state : {};
70
+ const budgetKey = String(options.budgetKey || '');
71
+ if (!budgetKey) {
72
+ return priorState;
73
+ }
74
+
75
+ const attempts = Object.assign({}, priorState.attempts || {});
76
+ attempts[budgetKey] = Object.assign({}, options.entry || {});
77
+
78
+ return Object.assign({}, priorState, {
79
+ totalAttempts: _nonNegativeInteger(priorState.totalAttempts) + 1,
80
+ attempts,
81
+ });
82
+ }
83
+
84
+ function _normalizeInvestigationHints(rawHints, options = {}) {
85
+ const workspaceRoot = _resolveHintWorkspaceRoot(options);
86
+ const hints = [];
87
+ const hintsDropped = [];
88
+
89
+ for (const rawHint of Array.isArray(rawHints) ? rawHints : []) {
90
+ if (!rawHint || typeof rawHint !== 'object') {
91
+ continue;
92
+ }
93
+
94
+ const originalPath = String(rawHint.path || '').trim();
95
+ const rationale = _truncateHintRationale(rawHint.rationale);
96
+ const normalizedPath = _normalizeHintPath(originalPath, workspaceRoot);
97
+
98
+ if (!normalizedPath) {
99
+ hintsDropped.push({ path: originalPath, rationale, reason: 'out_of_tree' });
100
+ continue;
101
+ }
102
+
103
+ if (hints.length >= 5) {
104
+ hintsDropped.push({ path: normalizedPath, rationale, reason: 'cap_exceeded' });
105
+ continue;
106
+ }
107
+
108
+ hints.push({ path: normalizedPath, rationale });
109
+ }
110
+
111
+ return { hints, hintsDropped };
112
+ }
113
+
114
+ function _resolveHintWorkspaceRoot(options = {}) {
115
+ if (options.workspaceRoot) {
116
+ return path.resolve(options.workspaceRoot);
117
+ }
118
+ if (options.openspecRoot) {
119
+ return path.resolve(options.openspecRoot, '..');
120
+ }
121
+ if (options.changeDir) {
122
+ return path.resolve(options.changeDir, '..', '..');
123
+ }
124
+ if (options.ralphDir) {
125
+ return path.resolve(options.ralphDir, '..');
126
+ }
127
+ return process.cwd();
128
+ }
129
+
130
+ function _normalizeHintPath(rawPath, workspaceRoot) {
131
+ if (!rawPath) {
132
+ return '';
133
+ }
134
+
135
+ const resolved = path.resolve(workspaceRoot, rawPath);
136
+ const relative = path.relative(workspaceRoot, resolved);
137
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || !fs.existsSync(resolved)) {
138
+ return '';
139
+ }
140
+
141
+ try {
142
+ if (!fs.statSync(resolved).isFile()) {
143
+ return '';
144
+ }
145
+ } catch {
146
+ return '';
147
+ }
148
+
149
+ return relative.split(path.sep).join('/');
150
+ }
151
+
152
+ function _truncateHintRationale(rationale) {
153
+ const text = String(rationale || '').trim();
154
+ if (text.length <= 200) {
155
+ return text;
156
+ }
157
+ return `${text.slice(0, 200)}…`;
158
+ }
159
+
160
+ function _formatPreviousSupervisorAttempts(attempts) {
161
+ return Array.isArray(attempts) && attempts.length > 0
162
+ ? attempts.join('\n')
163
+ : '';
164
+ }
165
+
166
+ function _joinSummaryParts(summary, downstreamFailures) {
167
+ const parts = [];
168
+ if (summary) {
169
+ parts.push(String(summary));
170
+ }
171
+ if (Array.isArray(downstreamFailures) && downstreamFailures.length > 0) {
172
+ parts.push(`Downstream patch failures: ${downstreamFailures.join(', ')}`);
173
+ }
174
+ return parts.join(' ');
175
+ }
176
+
177
+ function _firstNonEmptyText() {
178
+ for (const part of arguments) {
179
+ const text = String(part || '').trim();
180
+ if (text) {
181
+ return text;
182
+ }
183
+ }
184
+ return '';
185
+ }
186
+
187
+ function _buildSupervisorReturn(options = {}) {
188
+ return {
189
+ outcome: options.outcome,
190
+ patchedTasks: Array.isArray(options.patchedTasks) ? options.patchedTasks : [],
191
+ hints: Array.isArray(options.hints) ? options.hints : [],
192
+ hintsDropped: Array.isArray(options.hintsDropped) ? options.hintsDropped : [],
193
+ attempts: Array.isArray(options.attempts) ? options.attempts : [],
194
+ attemptsExhausted: options.attemptsExhausted === true,
195
+ readLogs: options.readLogs === undefined ? null : options.readLogs,
196
+ readLogsBytes: options.readLogsBytes === undefined ? null : options.readLogsBytes,
197
+ softWarnings: Array.isArray(options.softWarnings) ? options.softWarnings : [],
198
+ summary: String(options.summary || ''),
199
+ blockerHash: String(options.blockerHash || ''),
200
+ };
201
+ }
202
+
203
+ module.exports = {
204
+ _computeBlockerHash,
205
+ _readSupervisorState,
206
+ _writeSupervisorState,
207
+ _nonNegativeInteger,
208
+ _decideBoundedBudget,
209
+ _consumeBoundedBudget,
210
+ _normalizeInvestigationHints,
211
+ _resolveHintWorkspaceRoot,
212
+ _normalizeHintPath,
213
+ _truncateHintRationale,
214
+ _formatPreviousSupervisorAttempts,
215
+ _joinSummaryParts,
216
+ _firstNonEmptyText,
217
+ _buildSupervisorReturn,
218
+ };