@zalom/plastic 2.0.0-alpha.22 → 2.0.0-alpha.23
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/agents/plastic-enforcer.md +6 -3
- package/agents/plastic-executor.md +4 -0
- package/agents/plastic-node-research.md +28 -0
- package/agents/plastic-node-verify.md +27 -0
- package/agents/plastic-node-work.md +32 -0
- package/bin/lib/context_budget.rb +1 -1
- package/hooks/hooks.json +12 -0
- package/hooks/statusline +28 -0
- package/hooks/stop +5 -0
- package/package.json +1 -1
- package/scripts/doctor.rb +80 -6
- package/scripts/end-intent +3 -3
- package/scripts/graph-measure +249 -0
- package/scripts/hook-capture +1 -0
- package/scripts/hook-savepoint +24 -2
- package/scripts/hook-session-start +40 -0
- package/scripts/hook-stop +57 -0
- package/scripts/lib/active_delivery.rb +61 -0
- package/scripts/lib/agent_models.rb +10 -1
- package/scripts/lib/codex_adapter.rb +197 -0
- package/scripts/lib/doctor_core.rb +8 -3
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/graph_edges.rb +4 -4
- package/scripts/lib/graph_file.rb +4 -4
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +408 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +411 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +2 -2
- package/scripts/lib/handoff.rb +36 -5
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +13 -1
- package/scripts/lib/hook_replay.rb +23 -5
- package/scripts/lib/index_projection.rb +1 -1
- package/scripts/lib/installer_core.rb +109 -3
- package/scripts/lib/intent_screen.rb +1 -1
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/meter_watch.rb +15 -9
- package/scripts/lib/node_file.rb +3 -3
- package/scripts/lib/node_ledger.rb +8 -1
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/outcome_report.rb +1 -1
- package/scripts/lib/report_screen.rb +10 -6
- package/scripts/lib/roadmap_graph.rb +1 -1
- package/scripts/lib/roadmap_queue.rb +1 -1
- package/scripts/lib/roadmap_render.rb +1 -1
- package/scripts/lib/runner_absorb.rb +31 -5
- package/scripts/lib/runner_dispatch.rb +26 -11
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +3 -3
- package/scripts/lib/session_git.rb +2 -2
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/verify_intent.rb +2 -2
- package/scripts/lib/work_graph_validator.rb +6 -6
- package/scripts/new-intent +1 -1
- package/scripts/node-run +224 -0
- package/scripts/read-config +6 -0
- package/scripts/runner +203 -19
- package/scripts/verify-intent +1 -1
- package/skills/auto/SKILL.md +1 -1
- package/skills/conventions/references/knowledge-graph.md +9 -0
- package/skills/doctor/SKILL.md +3 -3
- package/skills/intent-creating/evals/evals.json +1 -1
- package/skills/intent-executing/SKILL.md +6 -0
- package/skills/tutorial/references/track-2-auto.md +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require_relative "node_ledger"
|
|
8
|
+
require_relative "node_file"
|
|
9
|
+
require_relative "node_packet"
|
|
10
|
+
require_relative "packet_wrapper"
|
|
11
|
+
|
|
12
|
+
# GraphMeasureBudget (intent 343, G10, n4): whether the `budget:` a node's
|
|
13
|
+
# envelope declares (327 C19, D47) is a ceiling that ever actually held.
|
|
14
|
+
# Intent 340 found at 22:14Z on 2026-09-10 that every runner-built packet
|
|
15
|
+
# was capped at 8000 tokens whatever the node declared (v1 major M7); this
|
|
16
|
+
# module is the measurement of whether that still holds, from the ledger
|
|
17
|
+
# and the real packet files alone.
|
|
18
|
+
#
|
|
19
|
+
# Read-only (spec D2): it opens `nodes/*.md`, `savepoint.md` and files under
|
|
20
|
+
# `packets/`, and writes nothing. A measure with no source prints
|
|
21
|
+
# `:unavailable`, never zero and never blank (spec D3); a packet file named
|
|
22
|
+
# by `packet=` that is not on disk is counted and named by its sha, never
|
|
23
|
+
# silently treated as zero bytes (spec D4).
|
|
24
|
+
#
|
|
25
|
+
# Adds no second parser and no second estimator: `NodeLedger` owns the
|
|
26
|
+
# transition-line format, `NodeFile` owns the envelope's `budget:`,
|
|
27
|
+
# `NodePacket` owns packet path resolution and attempt numbering, and
|
|
28
|
+
# `PacketWrapper.estimate_tokens` is the one token formula, the same the
|
|
29
|
+
# packet builder enforced with (spec D8).
|
|
30
|
+
module GraphMeasureBudget
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
SAVEPOINT_FILE = "savepoint.md"
|
|
34
|
+
UNAVAILABLE = "unavailable"
|
|
35
|
+
|
|
36
|
+
# {ok:, nodes: {id => {declared_budget:, attempts: [...]}}, ceiling: {...}}.
|
|
37
|
+
# Never raises: a missing savepoint.md, a node with no envelope file, and a
|
|
38
|
+
# packet= naming a file that does not exist on disk all resolve to
|
|
39
|
+
# `:unavailable` fields rather than an exception.
|
|
40
|
+
def read(intent_dir, node_reader: NodeFile.method(:parse))
|
|
41
|
+
dir = File.expand_path(intent_dir.to_s)
|
|
42
|
+
content = read_content(File.join(dir, SAVEPOINT_FILE))
|
|
43
|
+
entries = NodeLedger.entries_from_content(content)
|
|
44
|
+
|
|
45
|
+
running_indices = Hash.new { |h, k| h[k] = [] }
|
|
46
|
+
entries.each_with_index do |e, idx|
|
|
47
|
+
next if e[:torn] || e[:state] != "running"
|
|
48
|
+
|
|
49
|
+
running_indices[e[:subject]] << idx
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
nodes = {}
|
|
53
|
+
running_indices.each do |subject, indices|
|
|
54
|
+
declared_budget = declared_budget_for(dir, subject, node_reader)
|
|
55
|
+
attempts = indices.map do |idx|
|
|
56
|
+
entry = entries[idx]
|
|
57
|
+
# Row 4.5: the attempt number is NodePacket's own count of non-torn
|
|
58
|
+
# `running` lines up to and including this one, never
|
|
59
|
+
# ReadySet.attempts_count (which resets after `done`, `superseded`
|
|
60
|
+
# or `abandoned` and answers "attempts left", not "which attempt was
|
|
61
|
+
# this line").
|
|
62
|
+
attempt_number = NodePacket.compute_attempt_number(
|
|
63
|
+
intent_dir: dir, node: subject, lease_flag_given: false, entries: entries[0..idx]
|
|
64
|
+
)
|
|
65
|
+
build_attempt(dir, subject, attempt_number, entry, declared_budget)
|
|
66
|
+
end
|
|
67
|
+
nodes[subject] = { declared_budget: declared_budget, attempts: attempts }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
record = { ok: true, nodes: nodes, ceiling: detect_ceiling(nodes) }
|
|
71
|
+
deep_freeze(record)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# --- rendering -----------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def render_json(record)
|
|
77
|
+
JSON.generate(model(record))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_text(record)
|
|
81
|
+
m = model(record)
|
|
82
|
+
lines = []
|
|
83
|
+
lines.concat(node_block(m["nodes"]))
|
|
84
|
+
lines << ""
|
|
85
|
+
lines.concat(ceiling_block(m["ceiling"]))
|
|
86
|
+
"#{lines.join("\n")}\n"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def model(record)
|
|
90
|
+
{
|
|
91
|
+
"nodes" => sort_ids(record[:nodes].keys).map { |id| node_model(id, record[:nodes][id]) },
|
|
92
|
+
"ceiling" => ceiling_model(record[:ceiling]),
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def node_model(id, node)
|
|
97
|
+
{
|
|
98
|
+
"id" => id,
|
|
99
|
+
"declared_budget" => av(node[:declared_budget]),
|
|
100
|
+
"attempts" => node[:attempts].map { |a| attempt_model(a) },
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
private_class_method :node_model
|
|
104
|
+
|
|
105
|
+
def attempt_model(a)
|
|
106
|
+
{
|
|
107
|
+
"attempt" => a[:attempt],
|
|
108
|
+
"packet_sha_declared" => av(a[:packet_sha_declared]),
|
|
109
|
+
"file_exists" => a[:file_exists],
|
|
110
|
+
"packet_sha_actual" => av(a[:packet_sha_actual]),
|
|
111
|
+
"sha_match" => a[:sha_match],
|
|
112
|
+
"bytes" => av(a[:bytes]),
|
|
113
|
+
"estimate_tokens" => av(a[:estimate_tokens]),
|
|
114
|
+
"hop" => a[:hop],
|
|
115
|
+
"effective_tokens" => av(a[:effective_tokens]),
|
|
116
|
+
"over_budget" => av(a[:over_budget]),
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
private_class_method :attempt_model
|
|
120
|
+
|
|
121
|
+
def ceiling_model(c)
|
|
122
|
+
{
|
|
123
|
+
"detected" => c[:detected],
|
|
124
|
+
"value" => c[:value] ? c[:value] : UNAVAILABLE,
|
|
125
|
+
"node_count" => c[:node_count],
|
|
126
|
+
"attempt_count" => c[:attempt_count],
|
|
127
|
+
"reason" => c[:reason].to_s,
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
private_class_method :ceiling_model
|
|
131
|
+
|
|
132
|
+
def node_block(nodes)
|
|
133
|
+
lines = ["== Budget =="]
|
|
134
|
+
if nodes.empty?
|
|
135
|
+
lines << "(no packet attempts recorded)"
|
|
136
|
+
return lines
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
lines << "| Node | Attempt | Declared budget | Bytes | Est tokens | Hop | Effective tokens | Over budget | Sha match |"
|
|
140
|
+
lines << "| --- | --- | --- | --- | --- | --- | --- | --- | --- |"
|
|
141
|
+
nodes.each do |n|
|
|
142
|
+
n["attempts"].each do |a|
|
|
143
|
+
lines << "| #{n['id']} | #{a['attempt']} | #{n['declared_budget']} | #{a['bytes']} | " \
|
|
144
|
+
"#{a['estimate_tokens']} | #{a['hop']} | #{a['effective_tokens']} | #{a['over_budget']} | #{a['sha_match']} |"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
lines
|
|
148
|
+
end
|
|
149
|
+
private_class_method :node_block
|
|
150
|
+
|
|
151
|
+
def ceiling_block(c)
|
|
152
|
+
[
|
|
153
|
+
"== Suspected ceiling ==",
|
|
154
|
+
"detected: #{c['detected']}",
|
|
155
|
+
"value: #{c['value']}#{c['value'] == UNAVAILABLE ? '' : ' tokens'}",
|
|
156
|
+
"based on: #{c['node_count']} nodes, #{c['attempt_count']} attempts (reason: #{c['reason']})",
|
|
157
|
+
]
|
|
158
|
+
end
|
|
159
|
+
private_class_method :ceiling_block
|
|
160
|
+
|
|
161
|
+
# --- reading ---------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def read_content(path)
|
|
164
|
+
return "" unless path && File.exist?(path)
|
|
165
|
+
|
|
166
|
+
File.read(path).scrub
|
|
167
|
+
end
|
|
168
|
+
private_class_method :read_content
|
|
169
|
+
|
|
170
|
+
# B6 (v1 review): `node_reader` (NodeFile.parse by default) reads its own
|
|
171
|
+
# path and does not scrub, so one bad byte anywhere in a node envelope
|
|
172
|
+
# raised out of NodeFile's regex matching and took the whole `budget` verb
|
|
173
|
+
# down (reproduced by appending an invalid UTF-8 byte to a copy of 340's
|
|
174
|
+
# nodes/n1.md: `budget` exited 1 with "internal error: invalid byte
|
|
175
|
+
# sequence in UTF-8", the same as `cohorts`, while `intent` - which never
|
|
176
|
+
# calls NodeFile.parse without GraphMeasure's own `with_safe_path` guard -
|
|
177
|
+
# stayed at exit 0). `GraphMeasure.with_safe_path` already solves exactly
|
|
178
|
+
# this, but it is `private_class_method` (carried from n1, same pattern
|
|
179
|
+
# n5's `GraphMeasureModels` already uses for its own `present?` and
|
|
180
|
+
# `resolve_kind`): a local copy, not a second parser or a reopen.
|
|
181
|
+
def declared_budget_for(dir, node, node_reader)
|
|
182
|
+
path = NodePacket.find_node_path(dir, node)
|
|
183
|
+
return :unavailable unless path
|
|
184
|
+
|
|
185
|
+
parsed = with_safe_path(path) { |p| p ? node_reader.call(p) : nil }
|
|
186
|
+
budget = parsed && parsed[:budget]
|
|
187
|
+
budget.is_a?(Integer) ? budget : :unavailable
|
|
188
|
+
end
|
|
189
|
+
private_class_method :declared_budget_for
|
|
190
|
+
|
|
191
|
+
# Own copy of GraphMeasure's `with_safe_path` (private there): scrub a
|
|
192
|
+
# bad-UTF-8 file into a throwaway Dir.mktmpdir copy before handing it to a
|
|
193
|
+
# reader that does not scrub itself, never writing into `intent_dir`
|
|
194
|
+
# (spec D2).
|
|
195
|
+
def with_safe_path(path)
|
|
196
|
+
return yield(nil) unless path && File.exist?(path)
|
|
197
|
+
|
|
198
|
+
raw = File.read(path)
|
|
199
|
+
scrubbed = raw.scrub
|
|
200
|
+
return yield(path) if scrubbed == raw
|
|
201
|
+
|
|
202
|
+
Dir.mktmpdir("graph-measure-budget-scrub") do |tmp|
|
|
203
|
+
safe_path = File.join(tmp, File.basename(path))
|
|
204
|
+
File.write(safe_path, scrubbed)
|
|
205
|
+
yield(safe_path)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
private_class_method :with_safe_path
|
|
209
|
+
|
|
210
|
+
def build_attempt(dir, subject, attempt_number, entry, declared_budget)
|
|
211
|
+
fields = entry[:fields] || {}
|
|
212
|
+
packet_sha_declared = present?(fields["packet"]) ? fields["packet"] : :unavailable
|
|
213
|
+
# D8: hop= is written once, on the running line; absent means no hop
|
|
214
|
+
# block was appended (the feature predates this line, or hop is off),
|
|
215
|
+
# never an unknown quantity to subtract.
|
|
216
|
+
hop = present?(fields["hop"]) ? fields["hop"].to_i : 0
|
|
217
|
+
path = NodePacket.packet_path(intent_dir: dir, node: subject, attempt: attempt_number)
|
|
218
|
+
file_exists = File.exist?(path)
|
|
219
|
+
|
|
220
|
+
if file_exists
|
|
221
|
+
raw = File.binread(path)
|
|
222
|
+
bytes = raw.bytesize
|
|
223
|
+
sha_actual = Digest::SHA256.hexdigest(raw)[0, 12]
|
|
224
|
+
estimate = PacketWrapper.estimate_tokens(raw)
|
|
225
|
+
effective = estimate - hop
|
|
226
|
+
over_budget = declared_budget.is_a?(Integer) ? effective > declared_budget : :unavailable
|
|
227
|
+
sha_match = sha_actual == packet_sha_declared
|
|
228
|
+
else
|
|
229
|
+
bytes = :unavailable
|
|
230
|
+
sha_actual = :unavailable
|
|
231
|
+
estimate = :unavailable
|
|
232
|
+
effective = :unavailable
|
|
233
|
+
over_budget = :unavailable
|
|
234
|
+
sha_match = false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
{
|
|
238
|
+
attempt: attempt_number,
|
|
239
|
+
packet_sha_declared: packet_sha_declared,
|
|
240
|
+
packet_path: path,
|
|
241
|
+
file_exists: file_exists,
|
|
242
|
+
packet_sha_actual: sha_actual,
|
|
243
|
+
sha_match: sha_match,
|
|
244
|
+
bytes: bytes,
|
|
245
|
+
estimate_tokens: estimate,
|
|
246
|
+
hop: hop,
|
|
247
|
+
effective_tokens: effective,
|
|
248
|
+
over_budget: over_budget,
|
|
249
|
+
raw: entry[:raw],
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
private_class_method :build_attempt
|
|
253
|
+
|
|
254
|
+
def present?(value)
|
|
255
|
+
!(value.nil? || value.to_s.strip.empty?)
|
|
256
|
+
end
|
|
257
|
+
private_class_method :present?
|
|
258
|
+
|
|
259
|
+
# --- the suspected ceiling: spec matrix rows 4.8, 4.9 -----------------------
|
|
260
|
+
|
|
261
|
+
# A run of packets whose estimates all fall under one value well below
|
|
262
|
+
# their own declared budgets (row 4.8): the candidate ceiling is the
|
|
263
|
+
# largest effective estimate among the usable attempts (every attempt
|
|
264
|
+
# whose declared budget and packet file are both known), and it is only
|
|
265
|
+
# reported when that value sits strictly under EVERY one of those
|
|
266
|
+
# attempts' own declared budgets - never a "cluster within a few percent"
|
|
267
|
+
# of each other, which never fires against a real spread of packet sizes.
|
|
268
|
+
# Row 4.9: fewer than two distinct nodes never reports a ceiling; one
|
|
269
|
+
# node's own repeated attempts are not evidence of a ceiling shared across
|
|
270
|
+
# the intent.
|
|
271
|
+
#
|
|
272
|
+
# B5 (v1 review): `candidate < u[:budget]` alone reads "nothing went over
|
|
273
|
+
# budget", which is true of every intent that simply stayed under its own
|
|
274
|
+
# declared budgets - reproduced with two nodes estimated at 1999 and 1998
|
|
275
|
+
# effective tokens against declared budgets of 2000 each, which the old
|
|
276
|
+
# code reported as "detected: true, value: 1999" though there was no
|
|
277
|
+
# ceiling there at all: both attempts used over 99.9% of their own
|
|
278
|
+
# declared allowance, they just never quite crossed it.
|
|
279
|
+
#
|
|
280
|
+
# "Well below" needs a stated threshold: WELL_BELOW_RATIO is that
|
|
281
|
+
# threshold, and a ceiling is reported only when the candidate uses no
|
|
282
|
+
# more than this fraction of EVERY usable attempt's own declared budget.
|
|
283
|
+
# Reproduced against 340's real packets: declared budgets there run from
|
|
284
|
+
# 50000 to 160000 tokens (327 D47's per-kind defaults), and every
|
|
285
|
+
# attempt's effective token count clusters between 3380 and 7717 - the
|
|
286
|
+
# candidate (7717) is at most 7717/50000 = 15.4% of even the SMALLEST
|
|
287
|
+
# declared budget among the usable attempts, so it clears this threshold
|
|
288
|
+
# by a wide margin and 340 still reports a detected ceiling. The
|
|
289
|
+
# synthetic 1999-vs-2000 case clears none of it (99.95%, not <= 50%) and
|
|
290
|
+
# correctly reports no ceiling.
|
|
291
|
+
WELL_BELOW_RATIO = 0.5
|
|
292
|
+
|
|
293
|
+
# NEW-2 (v2 review, D21): `candidate <= budget * WELL_BELOW_RATIO` checked
|
|
294
|
+
# against EVERY usable attempt in the intent, with no requirement that
|
|
295
|
+
# more than one of them actually sit anywhere near the candidate. That
|
|
296
|
+
# fires on any spread of generous, unrelated budgets (reproduced: nodes at
|
|
297
|
+
# 3000, 7000 and 1200 effective tokens against declared budgets of
|
|
298
|
+
# 100000, 100000 and 150000 reported "detected: true, value: 7000" though
|
|
299
|
+
# only the 7000 attempt sits anywhere near that number), and it misses a
|
|
300
|
+
# genuine shared ceiling under tight budgets, where the honest utilization
|
|
301
|
+
# of a real cluster crosses 50% (reproduced: three nodes converging
|
|
302
|
+
# tightly on 7717/7690/7650 against a declared 10000 each reported
|
|
303
|
+
# "not_well_below_declared_budgets"). D21's own fix: a candidate is
|
|
304
|
+
# evidence of a ceiling only once several distinct usable attempts
|
|
305
|
+
# actually approach it from below (CLUSTER_BAND_RATIO), and the "well
|
|
306
|
+
# below budget" bar only needs to be strict (WELL_BELOW_RATIO) when that
|
|
307
|
+
# cluster is thin (fewer than SEVERAL_CLUSTER_THRESHOLD distinct nodes);
|
|
308
|
+
# once several distinct nodes converge on the same value, the convergence
|
|
309
|
+
# itself is the primary evidence and the budget bar relaxes to
|
|
310
|
+
# CLUSTERED_WELL_BELOW_RATIO. 340's real cluster (five distinct nodes
|
|
311
|
+
# converging between 6237 and 7717) clears either bar by a wide margin;
|
|
312
|
+
# the two-node 1999/1998-vs-2000 case never reaches the cluster threshold
|
|
313
|
+
# of three, so it still runs against the strict 50% bar unchanged.
|
|
314
|
+
CLUSTER_BAND_RATIO = 0.8
|
|
315
|
+
SEVERAL_CLUSTER_THRESHOLD = 3
|
|
316
|
+
# D25 (after v3 M2): 0.9 let a cluster using ninety percent of its own
|
|
317
|
+
# declared budget report a ceiling - the case where the declared budget IS
|
|
318
|
+
# the ceiling, the opposite of what C19 asks for. 0.8 is "far under" and
|
|
319
|
+
# still clears the pinned cluster case (7717/10000 = 77.17%).
|
|
320
|
+
CLUSTERED_WELL_BELOW_RATIO = 0.8
|
|
321
|
+
|
|
322
|
+
def detect_ceiling(nodes)
|
|
323
|
+
usable = []
|
|
324
|
+
nodes.each do |id, node|
|
|
325
|
+
budget = node[:declared_budget]
|
|
326
|
+
# D21: an attempt whose declared budget is absent or zero carries no
|
|
327
|
+
# evidence either way and is disqualified from the comparison, never
|
|
328
|
+
# the whole intent (reproduced: two nodes declaring a budget of 0
|
|
329
|
+
# with 0 effective tokens each reported "detected: true, value: 0",
|
|
330
|
+
# an artifact of comparing a candidate against a budget of 0 rather
|
|
331
|
+
# than real evidence of a ceiling).
|
|
332
|
+
next unless budget.is_a?(Integer) && budget.positive?
|
|
333
|
+
|
|
334
|
+
node[:attempts].each do |a|
|
|
335
|
+
next unless a[:effective_tokens].is_a?(Integer)
|
|
336
|
+
|
|
337
|
+
usable << { node: id, effective: a[:effective_tokens], budget: budget }
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
distinct_nodes = usable.map { |u| u[:node] }.uniq
|
|
342
|
+
attempt_count = usable.length
|
|
343
|
+
|
|
344
|
+
if distinct_nodes.length < 2
|
|
345
|
+
return { detected: false, reason: :insufficient_nodes, value: nil,
|
|
346
|
+
node_count: distinct_nodes.length, attempt_count: attempt_count }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
candidate = usable.map { |u| u[:effective] }.max
|
|
350
|
+
cluster = usable.select { |u| u[:effective] >= candidate * CLUSTER_BAND_RATIO }
|
|
351
|
+
cluster_nodes = cluster.map { |u| u[:node] }.uniq
|
|
352
|
+
|
|
353
|
+
if cluster_nodes.length < 2
|
|
354
|
+
return { detected: false, reason: :no_cluster, value: nil,
|
|
355
|
+
node_count: distinct_nodes.length, attempt_count: attempt_count }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
ratio = cluster_nodes.length >= SEVERAL_CLUSTER_THRESHOLD ? CLUSTERED_WELL_BELOW_RATIO : WELL_BELOW_RATIO
|
|
359
|
+
well_below = cluster.all? { |u| candidate <= u[:budget] * ratio }
|
|
360
|
+
|
|
361
|
+
unless well_below
|
|
362
|
+
return { detected: false, reason: :not_well_below_declared_budgets, value: nil,
|
|
363
|
+
node_count: distinct_nodes.length, attempt_count: attempt_count }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# M1 (v3 review, D25): SEVERAL_CLUSTER_THRESHOLD was read only to pick
|
|
367
|
+
# which budget bar applies, never enforced as the bar for "several"
|
|
368
|
+
# itself, so any two-node band that cleared WELL_BELOW_RATIO still
|
|
369
|
+
# reported a ceiling. Clearing the budget bar is necessary but not
|
|
370
|
+
# sufficient: D21 asks for several distinct usable attempts to approach
|
|
371
|
+
# the candidate, and two is a band, not several.
|
|
372
|
+
if cluster_nodes.length < SEVERAL_CLUSTER_THRESHOLD
|
|
373
|
+
return { detected: false, reason: :insufficient_cluster, value: nil,
|
|
374
|
+
node_count: distinct_nodes.length, attempt_count: attempt_count }
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
{ detected: true, reason: :ok, value: candidate, node_count: distinct_nodes.length, attempt_count: attempt_count }
|
|
378
|
+
end
|
|
379
|
+
private_class_method :detect_ceiling
|
|
380
|
+
|
|
381
|
+
# --- formatting helpers ------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
def av(value)
|
|
384
|
+
return UNAVAILABLE if value.nil? || value == :unavailable
|
|
385
|
+
|
|
386
|
+
value
|
|
387
|
+
end
|
|
388
|
+
private_class_method :av
|
|
389
|
+
|
|
390
|
+
def sort_ids(ids)
|
|
391
|
+
ids.sort_by do |id|
|
|
392
|
+
m = id.to_s.match(/\A([A-Za-z]+)(\d+)\z/)
|
|
393
|
+
m ? [m[1], m[2].to_i] : [id.to_s, 0]
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
private_class_method :sort_ids
|
|
397
|
+
|
|
398
|
+
def deep_freeze(obj)
|
|
399
|
+
case obj
|
|
400
|
+
when Hash
|
|
401
|
+
obj.each { |k, v| deep_freeze(k); deep_freeze(v) }
|
|
402
|
+
when Array
|
|
403
|
+
obj.each { |v| deep_freeze(v) }
|
|
404
|
+
end
|
|
405
|
+
obj.freeze
|
|
406
|
+
end
|
|
407
|
+
private_class_method :deep_freeze
|
|
408
|
+
end
|