@aperant/framework 0.17.0 → 0.20.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.
Files changed (242) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/agents/apt-executor.md +6 -0
  3. package/agents/apt-planner.md +3 -2
  4. package/bin/features-reconcile-check.mjs +77 -0
  5. package/dist/cli/artifacts/self-stage.d.mts.map +1 -1
  6. package/dist/cli/artifacts/self-stage.mjs +2 -1
  7. package/dist/cli/artifacts/self-stage.mjs.map +1 -1
  8. package/dist/cli/cli-wrappers/features-reconcile.d.mts +2 -0
  9. package/dist/cli/cli-wrappers/features-reconcile.d.mts.map +1 -0
  10. package/dist/cli/cli-wrappers/features-reconcile.mjs +9 -0
  11. package/dist/cli/cli-wrappers/features-reconcile.mjs.map +1 -0
  12. package/dist/cli/commands/ci-watch.mjs +49 -2
  13. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  14. package/dist/cli/commands/commit.mjs +3 -3
  15. package/dist/cli/commands/commit.mjs.map +1 -1
  16. package/dist/cli/commands/event.mjs +16 -16
  17. package/dist/cli/commands/event.mjs.map +1 -1
  18. package/dist/cli/commands/features-reconcile.d.mts +2 -0
  19. package/dist/cli/commands/features-reconcile.d.mts.map +1 -0
  20. package/dist/cli/commands/features-reconcile.mjs +143 -0
  21. package/dist/cli/commands/features-reconcile.mjs.map +1 -0
  22. package/dist/cli/commands/health-check.d.mts +2 -2
  23. package/dist/cli/commands/health-check.mjs +2 -2
  24. package/dist/cli/commands/merge-integrate.mjs +3 -3
  25. package/dist/cli/commands/merge-integrate.mjs.map +1 -1
  26. package/dist/cli/commands/produce.d.mts +9 -0
  27. package/dist/cli/commands/produce.d.mts.map +1 -0
  28. package/dist/cli/commands/produce.mjs +1345 -0
  29. package/dist/cli/commands/produce.mjs.map +1 -0
  30. package/dist/cli/commands/task.d.mts +16 -1
  31. package/dist/cli/commands/task.d.mts.map +1 -1
  32. package/dist/cli/commands/task.mjs +434 -266
  33. package/dist/cli/commands/task.mjs.map +1 -1
  34. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  35. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
  36. package/dist/cli/coordination/event-schema.d.mts +5 -3
  37. package/dist/cli/coordination/event-schema.d.mts.map +1 -1
  38. package/dist/cli/coordination/event-schema.mjs +245 -21
  39. package/dist/cli/coordination/event-schema.mjs.map +1 -1
  40. package/dist/cli/coordination/store.d.mts +2 -2
  41. package/dist/cli/coordination/store.mjs +4 -4
  42. package/dist/cli/coordination/store.mjs.map +1 -1
  43. package/dist/cli/dispatch.d.mts.map +1 -1
  44. package/dist/cli/dispatch.mjs +4 -2
  45. package/dist/cli/dispatch.mjs.map +1 -1
  46. package/dist/cli/features/area-shape.d.mts +11 -0
  47. package/dist/cli/features/area-shape.d.mts.map +1 -0
  48. package/dist/cli/features/area-shape.mjs +141 -0
  49. package/dist/cli/features/area-shape.mjs.map +1 -0
  50. package/dist/cli/features/delta.d.mts +78 -0
  51. package/dist/cli/features/delta.d.mts.map +1 -0
  52. package/dist/cli/features/delta.mjs +591 -0
  53. package/dist/cli/features/delta.mjs.map +1 -0
  54. package/dist/cli/features/reconcile.d.mts +236 -0
  55. package/dist/cli/features/reconcile.d.mts.map +1 -0
  56. package/dist/cli/features/reconcile.mjs +1126 -0
  57. package/dist/cli/features/reconcile.mjs.map +1 -0
  58. package/dist/cli/features/surface-map.d.mts +63 -0
  59. package/dist/cli/features/surface-map.d.mts.map +1 -0
  60. package/dist/cli/features/surface-map.mjs +191 -0
  61. package/dist/cli/features/surface-map.mjs.map +1 -0
  62. package/dist/cli/{commands/features-audit.d.mts → features/write-root.d.mts} +4 -3
  63. package/dist/cli/features/write-root.d.mts.map +1 -0
  64. package/dist/cli/features/write-root.mjs +137 -0
  65. package/dist/cli/features/write-root.mjs.map +1 -0
  66. package/dist/cli/gate/gates/features-reconciled.d.mts +36 -0
  67. package/dist/cli/gate/gates/features-reconciled.d.mts.map +1 -0
  68. package/dist/cli/gate/gates/features-reconciled.mjs +67 -0
  69. package/dist/cli/gate/gates/features-reconciled.mjs.map +1 -0
  70. package/dist/cli/gate/gates/index.mjs +1 -0
  71. package/dist/cli/gate/gates/index.mjs.map +1 -1
  72. package/dist/cli/help.d.mts.map +1 -1
  73. package/dist/cli/help.mjs +31 -1
  74. package/dist/cli/help.mjs.map +1 -1
  75. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  76. package/dist/cli/install/legacy-paths.mjs +2 -0
  77. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  78. package/dist/cli/produce/blind-probe.d.mts +85 -0
  79. package/dist/cli/produce/blind-probe.d.mts.map +1 -0
  80. package/dist/cli/produce/blind-probe.mjs +217 -0
  81. package/dist/cli/produce/blind-probe.mjs.map +1 -0
  82. package/dist/cli/produce/claim.d.mts +188 -0
  83. package/dist/cli/produce/claim.d.mts.map +1 -0
  84. package/dist/cli/produce/claim.mjs +518 -0
  85. package/dist/cli/produce/claim.mjs.map +1 -0
  86. package/dist/cli/produce/done-gate.d.mts +87 -0
  87. package/dist/cli/produce/done-gate.d.mts.map +1 -0
  88. package/dist/cli/produce/done-gate.mjs +200 -0
  89. package/dist/cli/produce/done-gate.mjs.map +1 -0
  90. package/dist/cli/produce/events.d.mts +77 -0
  91. package/dist/cli/produce/events.d.mts.map +1 -0
  92. package/dist/cli/produce/events.mjs +126 -0
  93. package/dist/cli/produce/events.mjs.map +1 -0
  94. package/dist/cli/produce/evidence-oracle.d.mts +63 -0
  95. package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
  96. package/dist/cli/produce/evidence-oracle.mjs +122 -0
  97. package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
  98. package/dist/cli/produce/ledger.d.mts +140 -0
  99. package/dist/cli/produce/ledger.d.mts.map +1 -0
  100. package/dist/cli/produce/ledger.mjs +272 -0
  101. package/dist/cli/produce/ledger.mjs.map +1 -0
  102. package/dist/cli/produce/probe-family.d.mts +53 -0
  103. package/dist/cli/produce/probe-family.d.mts.map +1 -0
  104. package/dist/cli/produce/probe-family.mjs +160 -0
  105. package/dist/cli/produce/probe-family.mjs.map +1 -0
  106. package/dist/cli/produce/projection.d.mts +55 -0
  107. package/dist/cli/produce/projection.d.mts.map +1 -0
  108. package/dist/cli/produce/projection.mjs +97 -0
  109. package/dist/cli/produce/projection.mjs.map +1 -0
  110. package/dist/cli/produce/run-id.d.mts +42 -0
  111. package/dist/cli/produce/run-id.d.mts.map +1 -0
  112. package/dist/cli/produce/run-id.mjs +79 -0
  113. package/dist/cli/produce/run-id.mjs.map +1 -0
  114. package/dist/cli/produce/saga.d.mts +180 -0
  115. package/dist/cli/produce/saga.d.mts.map +1 -0
  116. package/dist/cli/produce/saga.mjs +290 -0
  117. package/dist/cli/produce/saga.mjs.map +1 -0
  118. package/dist/cli/produce/scheduler.d.mts +165 -0
  119. package/dist/cli/produce/scheduler.d.mts.map +1 -0
  120. package/dist/cli/produce/scheduler.mjs +399 -0
  121. package/dist/cli/produce/scheduler.mjs.map +1 -0
  122. package/dist/cli/produce/setpoint.d.mts +52 -0
  123. package/dist/cli/produce/setpoint.d.mts.map +1 -0
  124. package/dist/cli/produce/setpoint.mjs +113 -0
  125. package/dist/cli/produce/setpoint.mjs.map +1 -0
  126. package/dist/cli/produce/verification-ttl.d.mts +75 -0
  127. package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
  128. package/dist/cli/produce/verification-ttl.mjs +169 -0
  129. package/dist/cli/produce/verification-ttl.mjs.map +1 -0
  130. package/dist/cli/release-notes/ship-autodraft.mjs +2 -2
  131. package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
  132. package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
  133. package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
  134. package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
  135. package/dist/cli/variant/gallery.d.mts.map +1 -1
  136. package/dist/cli/variant/gallery.mjs +7 -3
  137. package/dist/cli/variant/gallery.mjs.map +1 -1
  138. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  139. package/dist/plugin/agents/apt-executor.md +6 -0
  140. package/dist/plugin/agents/apt-planner.md +3 -2
  141. package/dist/plugin/skills/apt/SKILL.md +112 -38
  142. package/dist/plugin/skills/apt-close-task/SKILL.md +2 -2
  143. package/dist/plugin/skills/apt-debug/SKILL.md +22 -24
  144. package/dist/plugin/skills/apt-execute/SKILL.md +35 -26
  145. package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
  146. package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
  147. package/dist/plugin/skills/apt-mockup/SKILL.md +1 -1
  148. package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
  149. package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  150. package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
  151. package/dist/plugin/skills/apt-quick/SKILL.md +27 -33
  152. package/dist/plugin/skills/apt-review/SKILL.md +14 -11
  153. package/dist/plugin/skills/apt-run/SKILL.md +132 -3
  154. package/dist/plugin/skills/apt-ship/SKILL.md +29 -5
  155. package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
  156. package/dist/plugin/skills/apt-watch-ci/SKILL.md +6 -4
  157. package/dist/schemas/feature-registry.d.ts.map +1 -1
  158. package/dist/schemas/feature-registry.js +34 -3
  159. package/dist/schemas/feature-registry.js.map +1 -1
  160. package/dist/types/config.d.ts +8 -5
  161. package/dist/types/config.d.ts.map +1 -1
  162. package/package.json +4 -4
  163. package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
  164. package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
  165. package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
  166. package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
  167. package/skills/apt/SKILL.md +112 -38
  168. package/skills/apt-close-task/SKILL.md +2 -2
  169. package/skills/apt-debug/SKILL.md +22 -24
  170. package/skills/apt-execute/SKILL.md +35 -26
  171. package/skills/apt-fan-out/SKILL.md +4 -4
  172. package/skills/apt-handoff/SKILL.md +1 -1
  173. package/skills/apt-mockup/SKILL.md +1 -1
  174. package/skills/apt-plan/SKILL.md +5 -5
  175. package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  176. package/skills/apt-produce/SKILL.md +606 -0
  177. package/skills/apt-quick/SKILL.md +27 -33
  178. package/skills/apt-review/SKILL.md +14 -11
  179. package/skills/apt-run/SKILL.md +132 -3
  180. package/skills/apt-ship/SKILL.md +29 -5
  181. package/skills/apt-spar/SKILL.md +5 -3
  182. package/skills/apt-watch-ci/SKILL.md +6 -4
  183. package/src/cli/artifacts/self-stage.mjs +2 -1
  184. package/src/cli/cli-wrappers/features-reconcile.mjs +9 -0
  185. package/src/cli/commands/ci-watch.mjs +51 -2
  186. package/src/cli/commands/commit.mjs +3 -3
  187. package/src/cli/commands/event.mjs +16 -16
  188. package/src/cli/commands/features-reconcile.mjs +157 -0
  189. package/src/cli/commands/health-check.mjs +2 -2
  190. package/src/cli/commands/merge-integrate.mjs +3 -3
  191. package/src/cli/commands/produce.mjs +1466 -0
  192. package/src/cli/commands/task.mjs +482 -285
  193. package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  194. package/src/cli/coordination/event-schema.d.ts +4 -2
  195. package/src/cli/coordination/event-schema.mjs +276 -21
  196. package/src/cli/coordination/store.mjs +4 -4
  197. package/src/cli/dispatch.mjs +4 -2
  198. package/src/cli/features/area-shape.mjs +140 -0
  199. package/src/cli/features/delta.mjs +625 -0
  200. package/src/cli/features/reconcile.mjs +1169 -0
  201. package/src/cli/features/surface-map.mjs +192 -0
  202. package/src/cli/features/write-root.mjs +140 -0
  203. package/src/cli/gate/gates/features-reconciled.mjs +70 -0
  204. package/src/cli/gate/gates/index.mjs +1 -0
  205. package/src/cli/help.mjs +31 -1
  206. package/src/cli/install/legacy-paths.mjs +2 -0
  207. package/src/cli/produce/blind-probe.mjs +245 -0
  208. package/src/cli/produce/claim.mjs +543 -0
  209. package/src/cli/produce/done-gate.mjs +238 -0
  210. package/src/cli/produce/events.mjs +131 -0
  211. package/src/cli/produce/evidence-oracle.mjs +133 -0
  212. package/src/cli/produce/ledger.mjs +284 -0
  213. package/src/cli/produce/probe-family.mjs +168 -0
  214. package/src/cli/produce/projection.mjs +105 -0
  215. package/src/cli/produce/run-id.mjs +84 -0
  216. package/src/cli/produce/saga.mjs +303 -0
  217. package/src/cli/produce/scheduler.mjs +423 -0
  218. package/src/cli/produce/setpoint.mjs +122 -0
  219. package/src/cli/produce/verification-ttl.mjs +191 -0
  220. package/src/cli/release-notes/ship-autodraft.mjs +2 -2
  221. package/src/cli/roadmap/showrunner-view.d.ts +10 -0
  222. package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
  223. package/src/cli/variant/gallery.mjs +7 -3
  224. package/templates/aperant-claude-md-appendix.md +2 -2
  225. package/workflows/scan-features.md +17 -11
  226. package/dist/cli/cli-wrappers/features-audit.d.mts +0 -2
  227. package/dist/cli/cli-wrappers/features-audit.d.mts.map +0 -1
  228. package/dist/cli/cli-wrappers/features-audit.mjs +0 -8
  229. package/dist/cli/cli-wrappers/features-audit.mjs.map +0 -1
  230. package/dist/cli/commands/features-audit.d.mts.map +0 -1
  231. package/dist/cli/commands/features-audit.mjs +0 -293
  232. package/dist/cli/commands/features-audit.mjs.map +0 -1
  233. package/dist/cli/features/registry-audit.d.mts +0 -56
  234. package/dist/cli/features/registry-audit.d.mts.map +0 -1
  235. package/dist/cli/features/registry-audit.mjs +0 -264
  236. package/dist/cli/features/registry-audit.mjs.map +0 -1
  237. package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
  238. package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
  239. package/src/cli/cli-wrappers/features-audit.mjs +0 -8
  240. package/src/cli/commands/features-audit.mjs +0 -302
  241. package/src/cli/features/registry-audit.mjs +0 -254
  242. 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
+ }