@rulemetric/hooks 0.7.43 → 0.8.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulemetric/hooks",
3
- "version": "0.7.43",
3
+ "version": "0.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -136,49 +136,64 @@ _rulemetric_suggest_instructions() {
136
136
  # week of grooming would evict every adoption proposal and the additive feed
137
137
  # would silently go to zero. Over-fetch, then cap each direction separately
138
138
  # below so neither can starve the other.
139
+ # renderFor=session-start asks the API for the RENDER SET — already capped per
140
+ # direction, already ordered adopt-first, with counts that describe itself. The
141
+ # caps used to live in this script as jq slices, which meant the server could
142
+ # not tell a row it showed from a row it merely sent: `surfaced` events were
143
+ # logged for rows the caps never displayed, inflating the denominator of the
144
+ # acceptance rate this feed exists to measure.
145
+ #
146
+ # groupByKind is still sent so an API that predates renderFor degrades to the
147
+ # old mixed-pool response rather than to a score-ordered page that buries the
148
+ # subtractive directions.
149
+ #
150
+ # sessionId carries HOOK_SESSION_ID so the server can assign this session an
151
+ # ARM for any instruction under test — inject it, or deliberately withhold it —
152
+ # and record that decision BEFORE deciding what to return. The hook stays a
153
+ # dumb renderer: it never learns which arm it is in, and must not, or the
154
+ # decision would be re-derivable client-side and therefore forgeable.
155
+ #
156
+ # An API that predates arms ignores the parameter and returns the same render
157
+ # set as before, so an older server degrades to "always inject" rather than to
158
+ # a silently empty feed.
139
159
  response="$(curl -s --noproxy '*' --max-time 4 \
140
- "${api_url}/api/instruction-suggestions?projectPath=${encoded_path}&limit=10&includeContent=true&groupByKind=true" \
160
+ "${api_url}/api/instruction-suggestions?projectPath=${encoded_path}&limit=10&includeContent=true&groupByKind=true&renderFor=session-start&sessionId=${HOOK_SESSION_ID}" \
141
161
  -H "Authorization: Bearer ${auth_token}" 2>/dev/null)" || return 0
142
162
 
143
163
  local count
144
164
  count="$(printf '%s' "$response" | jq -r '.suggestions | length' 2>/dev/null)" || return 0
145
165
  [ "$count" != "0" ] && [ "$count" != "null" ] && [ -n "$count" ] || return 0
146
166
 
147
- # Per-direction index lists, capped independently. `kind` is absent on an older
148
- # API, so an untagged row is an adoption proposal the historical behaviour.
149
- local add_idx rem_idx trim_idx render_idx
150
- add_idx="$(printf '%s' "$response" | jq -r \
151
- '[.suggestions | to_entries[] | select((.value.kind // "add") == "add") | .key][0:3] | .[]' 2>/dev/null)" || add_idx=""
152
- rem_idx="$(printf '%s' "$response" | jq -r \
153
- '[.suggestions | to_entries[] | select(.value.kind == "remove") | .key][0:2] | .[]' 2>/dev/null)" || rem_idx=""
154
- trim_idx="$(printf '%s' "$response" | jq -r \
155
- '[.suggestions | to_entries[] | select(.value.kind == "trim") | .key][0:1] | .[]' 2>/dev/null)" || trim_idx=""
156
- render_idx="$(printf '%s\n%s\n%s\n' "$add_idx" "$rem_idx" "$trim_idx" | grep -v '^$' || true)"
167
+ local add_count cost_count high_count render_idx
168
+ add_count="$(printf '%s' "$response" | jq -r '.render.addCount // empty' 2>/dev/null)" || add_count=""
169
+
170
+ if [ -n "$add_count" ]; then
171
+ # Render-set response: print what we were given, in the order given.
172
+ cost_count="$(printf '%s' "$response" | jq -r '.render.costCount // 0' 2>/dev/null)" || cost_count=0
173
+ high_count="$(printf '%s' "$response" | jq -r '.render.highConfidenceCount // 0' 2>/dev/null)" || high_count=0
174
+ render_idx="$(seq 0 "$((count - 1))" 2>/dev/null)" || return 0
175
+ else
176
+ # COMPATIBILITY SHIM an API older than renderFor returned a mixed pool, so
177
+ # the caps have to be applied here. This is a second copy of a policy that
178
+ # now lives on the server, and a forked policy is one that eventually drifts:
179
+ # delete this branch once the minimum supported API understands renderFor.
180
+ local add_idx rem_idx trim_idx rem_count trim_count
181
+ add_idx="$(printf '%s' "$response" | jq -r \
182
+ '[.suggestions | to_entries[] | select((.value.kind // "add") == "add") | .key][0:3] | .[]' 2>/dev/null)" || add_idx=""
183
+ rem_idx="$(printf '%s' "$response" | jq -r \
184
+ '[.suggestions | to_entries[] | select(.value.kind == "remove") | .key][0:2] | .[]' 2>/dev/null)" || rem_idx=""
185
+ trim_idx="$(printf '%s' "$response" | jq -r \
186
+ '[.suggestions | to_entries[] | select(.value.kind == "trim") | .key][0:1] | .[]' 2>/dev/null)" || trim_idx=""
187
+ render_idx="$(printf '%s\n%s\n%s\n' "$add_idx" "$rem_idx" "$trim_idx" | grep -v '^$' || true)"
188
+ high_count="$(printf '%s' "$response" | jq -r \
189
+ '[[.suggestions | to_entries[] | select((.value.kind // "add") == "add")][0:3][] | select(.value.content != null)] | length' 2>/dev/null)" || high_count="0"
190
+ rem_count="$(printf '%s\n' "$rem_idx" | grep -c '[0-9]' 2>/dev/null)" || rem_count=0
191
+ trim_count="$(printf '%s\n' "$trim_idx" | grep -c '[0-9]' 2>/dev/null)" || trim_count=0
192
+ cost_count=$((rem_count + trim_count))
193
+ add_count="$(printf '%s\n' "$add_idx" | grep -c '[0-9]' 2>/dev/null)" || add_count=0
194
+ fi
157
195
  [ -n "$render_idx" ] || return 0
158
196
 
159
- # Count high-confidence rows among the ADDS WE WILL ACTUALLY RENDER. Counting
160
- # the whole response over-counts now that the fetch pulls 6 to cap 3+2: the
161
- # header announced "5 high-confidence instruction(s) included below" with 3
162
- # below it. A header that miscounts what follows it is how a reader stops
163
- # trusting the block.
164
- local high_count
165
- high_count="$(printf '%s' "$response" | jq -r \
166
- '[[.suggestions | to_entries[] | select((.value.kind // "add") == "add")][0:3][] | select(.value.content != null)] | length' 2>/dev/null)" || high_count="0"
167
- # ONE count for the shared "costing you" header. Counting only removals would
168
- # under-report the moment a trim renders beneath the same heading — the same
169
- # header-miscounts-its-own-list bug just fixed above, reintroduced by a third
170
- # direction.
171
- local rem_count trim_count cost_count
172
- rem_count="$(printf '%s\n' "$rem_idx" | grep -c '[0-9]' 2>/dev/null)" || rem_count=0
173
- trim_count="$(printf '%s\n' "$trim_idx" | grep -c '[0-9]' 2>/dev/null)" || trim_count=0
174
- cost_count=$((rem_count + trim_count))
175
-
176
- # The header describes the ADDITIVE rows only. `count` is now a mixed total, so
177
- # using it here would announce "N instructions may help with this project" and
178
- # then list retirements underneath it.
179
- local add_count
180
- add_count="$(printf '%s\n' "$add_idx" | grep -c '[0-9]' 2>/dev/null)" || add_count=0
181
-
182
197
  printf '\n---\n'
183
198
  if [ "$high_count" != "0" ] && [ -n "$high_count" ]; then
184
199
  printf '[rulemetric] %s high-confidence instruction(s) included below as session context.\n' "$high_count"
@@ -253,6 +268,11 @@ _rulemetric_suggest_instructions() {
253
268
  # reader (human or model) could act without going to look an id up. With
254
269
  # several suggestions on screen one shared CTA is ambiguous even once it
255
270
  # carries an id, so the command belongs to the row it accepts.
271
+ #
272
+ # A `graduated:`-prefixed id is a RULE, not a proposal (ticket 04): its
273
+ # question is settled by measurement, so there is nothing to accept and the
274
+ # CTA would be an instruction to act on a decision already made.
275
+ case "$sid" in graduated:*|experiment:*) continue ;; esac
256
276
  if [ -n "$sid" ] && [ "$sid" != "null" ]; then
257
277
  printf ' Accept: rulemetric suggestions accept %s\n' "$sid"
258
278
  fi
@@ -270,6 +290,11 @@ _rulemetric_suggest_instructions() {
270
290
  for i in $render_idx; do
271
291
  local sid has_content event_type
272
292
  sid="$(printf '%s' "$response" | jq -r ".suggestions[$i].id" 2>/dev/null)" || break
293
+ # A graduated rule (ticket 04) has no suggestion row — its id is synthetic
294
+ # — and its surfacing is not a funnel event: the budget and acceptance
295
+ # metrics both measure QUESTIONS, and a rule is an answer. Posting would
296
+ # 404 anyway; skipping keeps the log clean and the denominators honest.
297
+ case "$sid" in graduated:*|experiment:*) continue ;; esac
273
298
  has_content="$(printf '%s' "$response" | jq -r ".suggestions[$i].content // \"null\"" 2>/dev/null)" || has_content="null"
274
299
  if [ "$has_content" != "null" ] && [ -n "$has_content" ]; then
275
300
  event_type="surfaced_with_content"