@aperant/framework 0.17.0 → 0.19.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/CHANGELOG.md +152 -0
- package/dist/cli/commands/ci-watch.mjs +49 -2
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/commit.mjs +3 -3
- package/dist/cli/commands/commit.mjs.map +1 -1
- package/dist/cli/commands/event.mjs +16 -16
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/merge-integrate.mjs +3 -3
- package/dist/cli/commands/merge-integrate.mjs.map +1 -1
- package/dist/cli/commands/produce.d.mts +9 -0
- package/dist/cli/commands/produce.d.mts.map +1 -0
- package/dist/cli/commands/produce.mjs +1345 -0
- package/dist/cli/commands/produce.mjs.map +1 -0
- package/dist/cli/commands/task.d.mts +16 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +434 -266
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
- package/dist/cli/coordination/event-schema.d.mts +5 -3
- package/dist/cli/coordination/event-schema.d.mts.map +1 -1
- package/dist/cli/coordination/event-schema.mjs +245 -21
- package/dist/cli/coordination/event-schema.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +2 -2
- package/dist/cli/coordination/store.mjs +4 -4
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +2 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +30 -0
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/produce/blind-probe.d.mts +85 -0
- package/dist/cli/produce/blind-probe.d.mts.map +1 -0
- package/dist/cli/produce/blind-probe.mjs +217 -0
- package/dist/cli/produce/blind-probe.mjs.map +1 -0
- package/dist/cli/produce/claim.d.mts +188 -0
- package/dist/cli/produce/claim.d.mts.map +1 -0
- package/dist/cli/produce/claim.mjs +518 -0
- package/dist/cli/produce/claim.mjs.map +1 -0
- package/dist/cli/produce/done-gate.d.mts +87 -0
- package/dist/cli/produce/done-gate.d.mts.map +1 -0
- package/dist/cli/produce/done-gate.mjs +200 -0
- package/dist/cli/produce/done-gate.mjs.map +1 -0
- package/dist/cli/produce/events.d.mts +77 -0
- package/dist/cli/produce/events.d.mts.map +1 -0
- package/dist/cli/produce/events.mjs +126 -0
- package/dist/cli/produce/events.mjs.map +1 -0
- package/dist/cli/produce/evidence-oracle.d.mts +63 -0
- package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
- package/dist/cli/produce/evidence-oracle.mjs +122 -0
- package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
- package/dist/cli/produce/ledger.d.mts +140 -0
- package/dist/cli/produce/ledger.d.mts.map +1 -0
- package/dist/cli/produce/ledger.mjs +272 -0
- package/dist/cli/produce/ledger.mjs.map +1 -0
- package/dist/cli/produce/probe-family.d.mts +53 -0
- package/dist/cli/produce/probe-family.d.mts.map +1 -0
- package/dist/cli/produce/probe-family.mjs +160 -0
- package/dist/cli/produce/probe-family.mjs.map +1 -0
- package/dist/cli/produce/projection.d.mts +55 -0
- package/dist/cli/produce/projection.d.mts.map +1 -0
- package/dist/cli/produce/projection.mjs +97 -0
- package/dist/cli/produce/projection.mjs.map +1 -0
- package/dist/cli/produce/run-id.d.mts +42 -0
- package/dist/cli/produce/run-id.d.mts.map +1 -0
- package/dist/cli/produce/run-id.mjs +79 -0
- package/dist/cli/produce/run-id.mjs.map +1 -0
- package/dist/cli/produce/saga.d.mts +180 -0
- package/dist/cli/produce/saga.d.mts.map +1 -0
- package/dist/cli/produce/saga.mjs +290 -0
- package/dist/cli/produce/saga.mjs.map +1 -0
- package/dist/cli/produce/scheduler.d.mts +165 -0
- package/dist/cli/produce/scheduler.d.mts.map +1 -0
- package/dist/cli/produce/scheduler.mjs +399 -0
- package/dist/cli/produce/scheduler.mjs.map +1 -0
- package/dist/cli/produce/setpoint.d.mts +52 -0
- package/dist/cli/produce/setpoint.d.mts.map +1 -0
- package/dist/cli/produce/setpoint.mjs +113 -0
- package/dist/cli/produce/setpoint.mjs.map +1 -0
- package/dist/cli/produce/verification-ttl.d.mts +75 -0
- package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
- package/dist/cli/produce/verification-ttl.mjs +169 -0
- package/dist/cli/produce/verification-ttl.mjs.map +1 -0
- package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
- package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
- package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
- package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/skills/apt/SKILL.md +112 -38
- package/dist/plugin/skills/apt-debug/SKILL.md +14 -24
- package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
- package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
- package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
- package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +14 -22
- package/dist/plugin/skills/apt-run/SKILL.md +126 -3
- package/dist/plugin/skills/apt-ship/SKILL.md +2 -0
- package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +4 -4
- package/package.json +4 -4
- package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
- package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
- package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
- package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
- package/skills/apt/SKILL.md +112 -38
- package/skills/apt-debug/SKILL.md +14 -24
- package/skills/apt-fan-out/SKILL.md +4 -4
- package/skills/apt-handoff/SKILL.md +1 -1
- package/skills/apt-plan/SKILL.md +5 -5
- package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/skills/apt-produce/SKILL.md +606 -0
- package/skills/apt-quick/SKILL.md +14 -22
- package/skills/apt-run/SKILL.md +126 -3
- package/skills/apt-ship/SKILL.md +2 -0
- package/skills/apt-spar/SKILL.md +5 -3
- package/skills/apt-watch-ci/SKILL.md +4 -4
- package/src/cli/commands/ci-watch.mjs +51 -2
- package/src/cli/commands/commit.mjs +3 -3
- package/src/cli/commands/event.mjs +16 -16
- package/src/cli/commands/merge-integrate.mjs +3 -3
- package/src/cli/commands/produce.mjs +1466 -0
- package/src/cli/commands/task.mjs +482 -285
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/src/cli/coordination/event-schema.d.ts +4 -2
- package/src/cli/coordination/event-schema.mjs +276 -21
- package/src/cli/coordination/store.mjs +4 -4
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/help.mjs +30 -0
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/produce/blind-probe.mjs +245 -0
- package/src/cli/produce/claim.mjs +543 -0
- package/src/cli/produce/done-gate.mjs +238 -0
- package/src/cli/produce/events.mjs +131 -0
- package/src/cli/produce/evidence-oracle.mjs +133 -0
- package/src/cli/produce/ledger.mjs +284 -0
- package/src/cli/produce/probe-family.mjs +168 -0
- package/src/cli/produce/projection.mjs +105 -0
- package/src/cli/produce/run-id.mjs +84 -0
- package/src/cli/produce/saga.mjs +303 -0
- package/src/cli/produce/scheduler.mjs +423 -0
- package/src/cli/produce/setpoint.mjs +122 -0
- package/src/cli/produce/verification-ttl.mjs +191 -0
- package/src/cli/roadmap/showrunner-view.d.ts +10 -0
- package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
- package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
- package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
- package/src/cli/roadmap/conductor-view.d.ts +0 -10
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* produce/blind-probe.mjs — the held-out cross-family blind-probe set (M2, AC4-6).
|
|
4
|
+
*
|
|
5
|
+
* A fixed 10–20 behavioral probe set the implementer/Showrunner NEVER sees
|
|
6
|
+
* (probe-blindness, US-4 / ID-06), authored + run by a provider family DISJOINT
|
|
7
|
+
* from the implementer's, re-run on a rotating sample of already-done milestones
|
|
8
|
+
* against HEAD (US-6).
|
|
9
|
+
*
|
|
10
|
+
* The probe SET is a frozen immutable artifact at
|
|
11
|
+
* `.aperant/produce/<run-id>/probes/<milestone>.json`, written ONCE via
|
|
12
|
+
* `openSync(path, 'wx')` exclusive-create — a second freeze throws EEXIST and
|
|
13
|
+
* returns the byte-identical stored set (mirrors setpoint.mjs::freezeSetpoint).
|
|
14
|
+
* Its CONTENT never enters the event log or the roadmap projection the
|
|
15
|
+
* Showrunner re-reads each tick (structural probe-blindness, ID-M2-03): the
|
|
16
|
+
* `produce.probe.frozen` event carries ONLY `{probe_set_hash, author_family}`
|
|
17
|
+
* and `produce.probe.run` carries ONLY `{result, probe_family, head_sha}`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
21
|
+
import {
|
|
22
|
+
closeSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
fsyncSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
openSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
writeSync,
|
|
29
|
+
} from 'node:fs'
|
|
30
|
+
import { join } from 'node:path'
|
|
31
|
+
import {
|
|
32
|
+
appendProduceEvent,
|
|
33
|
+
readImplementerFamily,
|
|
34
|
+
readProduceRunEventsMatching,
|
|
35
|
+
} from './events.mjs'
|
|
36
|
+
import { normalizeFamily } from './probe-family.mjs'
|
|
37
|
+
import { projectRoadmap } from './projection.mjs'
|
|
38
|
+
import { resolveRunDir } from './run-id.mjs'
|
|
39
|
+
|
|
40
|
+
/** sha256 of the canonical JSON of the probe content (the immutable hash stamp). */
|
|
41
|
+
function computeProbeSetHash(content) {
|
|
42
|
+
return createHash('sha256').update(JSON.stringify(content)).digest('hex')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The probes/<milestone>.json path for a run + milestone. */
|
|
46
|
+
export function probeSetPathFor(projectDir, runId, milestoneId) {
|
|
47
|
+
return join(resolveRunDir(projectDir, runId), 'probes', `${milestoneId}.json`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read + parse a frozen probe set. Returns null when absent or malformed. */
|
|
51
|
+
export function readProbeSet(projectDir, runId, milestoneId) {
|
|
52
|
+
const p = probeSetPathFor(projectDir, runId, milestoneId)
|
|
53
|
+
if (!existsSync(p)) return null
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(p, 'utf-8'))
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Exclusive-create write of the frozen probe set on the FINAL path (NOT
|
|
63
|
+
* tmp+rename), so a SECOND write throws EEXIST — the set is immutable.
|
|
64
|
+
*/
|
|
65
|
+
function writeProbeSetExclusive(probePath, probeSet) {
|
|
66
|
+
mkdirSync(join(probePath, '..'), { recursive: true })
|
|
67
|
+
const payload = `${JSON.stringify(probeSet, null, 2)}\n`
|
|
68
|
+
const fd = openSync(probePath, 'wx') // throws EEXIST when already frozen
|
|
69
|
+
try {
|
|
70
|
+
writeSync(fd, payload, 0, 'utf-8')
|
|
71
|
+
fsyncSync(fd)
|
|
72
|
+
} finally {
|
|
73
|
+
closeSync(fd)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Freeze a blind-probe set for a milestone, idempotently. REFUSES fail-closed
|
|
79
|
+
* when the author family equals the implementer family (a held-out probe an
|
|
80
|
+
* implementer authored is not held out — ID-06) or when no implementer family
|
|
81
|
+
* is recorded (ID-M2-02). On a FRESH freeze appends `produce.probe.frozen`
|
|
82
|
+
* carrying ONLY the hash + author family (never the content). A second freeze
|
|
83
|
+
* returns the byte-identical stored set (`reused: true`) and never re-logs.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} projectDir
|
|
86
|
+
* @param {string} runId
|
|
87
|
+
* @param {string} milestoneId
|
|
88
|
+
* @param {{ content: unknown, authorFamily: string, implementerFamily: string }} args
|
|
89
|
+
* @param {Date} [now]
|
|
90
|
+
* @returns {{ ok: true, reused: boolean, probe_set_hash: string, author_family: string }
|
|
91
|
+
* | { ok: false, reason: string }}
|
|
92
|
+
*/
|
|
93
|
+
export function freezeProbeSet(
|
|
94
|
+
projectDir,
|
|
95
|
+
runId,
|
|
96
|
+
milestoneId,
|
|
97
|
+
{ content, authorFamily, implementerFamily },
|
|
98
|
+
now = new Date(),
|
|
99
|
+
) {
|
|
100
|
+
if (!authorFamily) return { ok: false, reason: 'no_author_family' }
|
|
101
|
+
if (!implementerFamily) return { ok: false, reason: 'no_implementer_family' }
|
|
102
|
+
// Cross-family enforcement (normalized so claude-code === claude).
|
|
103
|
+
if (normalizeFamily(authorFamily) === normalizeFamily(implementerFamily))
|
|
104
|
+
return { ok: false, reason: 'same_family_author' }
|
|
105
|
+
|
|
106
|
+
const probePath = probeSetPathFor(projectDir, runId, milestoneId)
|
|
107
|
+
const existing = readProbeSet(projectDir, runId, milestoneId)
|
|
108
|
+
if (existing)
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
reused: true,
|
|
112
|
+
probe_set_hash: existing.probe_set_hash,
|
|
113
|
+
author_family: existing.author_family,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const probeSetHash = computeProbeSetHash(content ?? null)
|
|
117
|
+
const probeSet = {
|
|
118
|
+
run_id: runId,
|
|
119
|
+
milestone_id: milestoneId,
|
|
120
|
+
author_family: authorFamily,
|
|
121
|
+
frozen_at: now.toISOString(),
|
|
122
|
+
probe_set_hash: probeSetHash,
|
|
123
|
+
// The probe CONTENT lives in the frozen file (blind to the projection) —
|
|
124
|
+
// it NEVER enters the event log.
|
|
125
|
+
probes: content ?? null,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
writeProbeSetExclusive(probePath, probeSet)
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Lost a concurrent race — the winner's file is authoritative; re-read it
|
|
132
|
+
// byte-identically rather than overwriting (mirrors freezeSetpoint).
|
|
133
|
+
if (e && e.code === 'EEXIST') {
|
|
134
|
+
const winner = readProbeSet(projectDir, runId, milestoneId)
|
|
135
|
+
if (winner)
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
reused: true,
|
|
139
|
+
probe_set_hash: winner.probe_set_hash,
|
|
140
|
+
author_family: winner.author_family,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
throw e
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Announce by hash + author family ONLY — never the content (ID-M2-03).
|
|
147
|
+
appendProduceEvent(projectDir, 'produce.probe.frozen', {
|
|
148
|
+
run_id: runId,
|
|
149
|
+
milestone_id: milestoneId,
|
|
150
|
+
probe_set_hash: probeSetHash,
|
|
151
|
+
author_family: authorFamily,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return { ok: true, reused: false, probe_set_hash: probeSetHash, author_family: authorFamily }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Record a blind-probe RUN result. REFUSES fail-closed when the probe family
|
|
159
|
+
* equals the implementer family (a probe run BY the implementer family is not
|
|
160
|
+
* cross-family — ID-08) or when no implementer family is recorded. On
|
|
161
|
+
* acceptance appends `produce.probe.run {run_id, milestone_id, result,
|
|
162
|
+
* probe_family, head_sha}` — result/family/head_sha only, never content.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} projectDir
|
|
165
|
+
* @param {{ runId: string, milestoneId: string, result: string, probeFamily: string, headSha: string }} args
|
|
166
|
+
* @returns {{ ok: true, result: string, probe_family: string, head_sha: string }
|
|
167
|
+
* | { ok: false, reason: string }}
|
|
168
|
+
*/
|
|
169
|
+
export function recordProbeRun(projectDir, { runId, milestoneId, result, probeFamily, headSha }) {
|
|
170
|
+
if (result !== 'pass' && result !== 'fail') return { ok: false, reason: 'invalid_result' }
|
|
171
|
+
if (!probeFamily) return { ok: false, reason: 'no_probe_family' }
|
|
172
|
+
if (!headSha) return { ok: false, reason: 'no_head_sha' }
|
|
173
|
+
|
|
174
|
+
const implementerFamily = readImplementerFamily(projectDir, runId, milestoneId)
|
|
175
|
+
if (!implementerFamily) return { ok: false, reason: 'no_recorded_implementer' }
|
|
176
|
+
if (normalizeFamily(probeFamily) === normalizeFamily(implementerFamily))
|
|
177
|
+
return { ok: false, reason: 'same_family_probe' }
|
|
178
|
+
|
|
179
|
+
appendProduceEvent(projectDir, 'produce.probe.run', {
|
|
180
|
+
run_id: runId,
|
|
181
|
+
milestone_id: milestoneId,
|
|
182
|
+
result,
|
|
183
|
+
probe_family: probeFamily,
|
|
184
|
+
head_sha: headSha,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
return { ok: true, result, probe_family: probeFamily, head_sha: headSha }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Select a DETERMINISTIC rotating sample of already-`done` milestones for blind
|
|
192
|
+
* re-probing against HEAD (US-6). Rotates by fewest prior probe runs first,
|
|
193
|
+
* then oldest last-probe-run first (a never-probed done milestone sorts first),
|
|
194
|
+
* then first-seen roadmap order — so two folds of the same log select the same
|
|
195
|
+
* sample.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} projectDir
|
|
198
|
+
* @param {string} runId
|
|
199
|
+
* @param {{ sample?: number }} [opts]
|
|
200
|
+
* @returns {{ run_id: string, sample_size: number, selected: Array<{ milestone_id: string, title: string, probe_run_count: number, last_probe_run_at: string | null }> }}
|
|
201
|
+
*/
|
|
202
|
+
export function selectSweepSample(projectDir, runId, { sample = 1 } = {}) {
|
|
203
|
+
const roadmap = projectRoadmap(projectDir, runId)
|
|
204
|
+
const done = roadmap.milestones.filter((m) => m.status === 'done')
|
|
205
|
+
|
|
206
|
+
const runEvents = readProduceRunEventsMatching(
|
|
207
|
+
projectDir,
|
|
208
|
+
runId,
|
|
209
|
+
(op) => op === 'produce.probe.run',
|
|
210
|
+
)
|
|
211
|
+
const stats = new Map()
|
|
212
|
+
for (const e of runEvents) {
|
|
213
|
+
const id = e.data && typeof e.data.milestone_id === 'string' ? e.data.milestone_id : null
|
|
214
|
+
if (!id) continue
|
|
215
|
+
const s = stats.get(id) || { count: 0, last: null }
|
|
216
|
+
s.count += 1
|
|
217
|
+
if (typeof e.t === 'string' && (s.last === null || e.t > s.last)) s.last = e.t
|
|
218
|
+
stats.set(id, s)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const withOrder = done.map((m, idx) => {
|
|
222
|
+
const s = stats.get(m.id) || { count: 0, last: null }
|
|
223
|
+
return { milestone: m, order: idx, count: s.count, last: s.last }
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
withOrder.sort((a, b) => {
|
|
227
|
+
if (a.count !== b.count) return a.count - b.count // fewest probe runs first
|
|
228
|
+
// oldest last-probe-run first — a never-probed done milestone (null) sorts
|
|
229
|
+
// ahead of any dated one.
|
|
230
|
+
const al = a.last ?? ''
|
|
231
|
+
const bl = b.last ?? ''
|
|
232
|
+
if (al !== bl) return al < bl ? -1 : 1
|
|
233
|
+
return a.order - b.order // first-seen roadmap order (stable tiebreak)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const n = Number.isFinite(sample) && sample > 0 ? Math.floor(sample) : 1
|
|
237
|
+
const selected = withOrder.slice(0, n).map((w) => ({
|
|
238
|
+
milestone_id: w.milestone.id,
|
|
239
|
+
title: w.milestone.title,
|
|
240
|
+
probe_run_count: w.count,
|
|
241
|
+
last_probe_run_at: w.last,
|
|
242
|
+
}))
|
|
243
|
+
|
|
244
|
+
return { run_id: runId, sample_size: selected.length, selected }
|
|
245
|
+
}
|