@loopover/engine 1.0.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 (272) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +661 -0
  3. package/README.md +676 -0
  4. package/dist/advisory/gate-advisory.d.ts +151 -0
  5. package/dist/advisory/gate-advisory.js +512 -0
  6. package/dist/ai-policy-map.d.ts +97 -0
  7. package/dist/ai-policy-map.js +377 -0
  8. package/dist/ams-policy-spec.d.ts +56 -0
  9. package/dist/ams-policy-spec.js +165 -0
  10. package/dist/calibration-dashboard.d.ts +23 -0
  11. package/dist/calibration-dashboard.js +65 -0
  12. package/dist/calibration-trend.d.ts +40 -0
  13. package/dist/calibration-trend.js +60 -0
  14. package/dist/contributor-fit.d.ts +24 -0
  15. package/dist/contributor-fit.js +50 -0
  16. package/dist/discovery-index-contract.d.ts +74 -0
  17. package/dist/discovery-index-contract.js +184 -0
  18. package/dist/discovery-soft-claim.d.ts +35 -0
  19. package/dist/discovery-soft-claim.js +50 -0
  20. package/dist/duplicate-winner.d.ts +57 -0
  21. package/dist/duplicate-winner.js +98 -0
  22. package/dist/feasibility.d.ts +27 -0
  23. package/dist/feasibility.js +86 -0
  24. package/dist/finding-severity-calibration.d.ts +116 -0
  25. package/dist/finding-severity-calibration.js +457 -0
  26. package/dist/fleet-run-manifest.d.ts +41 -0
  27. package/dist/fleet-run-manifest.js +151 -0
  28. package/dist/focus-manifest/guidance.d.ts +21 -0
  29. package/dist/focus-manifest/guidance.js +222 -0
  30. package/dist/focus-manifest.d.ts +826 -0
  31. package/dist/focus-manifest.js +2730 -0
  32. package/dist/gate-verdict-calibration.d.ts +111 -0
  33. package/dist/gate-verdict-calibration.js +411 -0
  34. package/dist/github/constants.d.ts +1 -0
  35. package/dist/github/constants.js +1 -0
  36. package/dist/github/sanitize-public-comment.d.ts +1 -0
  37. package/dist/github/sanitize-public-comment.js +26 -0
  38. package/dist/goal-model.d.ts +7 -0
  39. package/dist/goal-model.js +87 -0
  40. package/dist/governor/action-mode.d.ts +55 -0
  41. package/dist/governor/action-mode.js +74 -0
  42. package/dist/governor/budget-cap.d.ts +49 -0
  43. package/dist/governor/budget-cap.js +34 -0
  44. package/dist/governor/chokepoint.d.ts +58 -0
  45. package/dist/governor/chokepoint.js +269 -0
  46. package/dist/governor/kill-switch.d.ts +38 -0
  47. package/dist/governor/kill-switch.js +62 -0
  48. package/dist/governor/rate-limit.d.ts +40 -0
  49. package/dist/governor/rate-limit.js +63 -0
  50. package/dist/governor/reputation-throttle.d.ts +50 -0
  51. package/dist/governor/reputation-throttle.js +94 -0
  52. package/dist/governor/run-halt.d.ts +39 -0
  53. package/dist/governor/run-halt.js +83 -0
  54. package/dist/governor/self-plagiarism.d.ts +47 -0
  55. package/dist/governor/self-plagiarism.js +200 -0
  56. package/dist/governor/write-rate-limit.d.ts +52 -0
  57. package/dist/governor/write-rate-limit.js +132 -0
  58. package/dist/governor-ledger.d.ts +25 -0
  59. package/dist/governor-ledger.js +91 -0
  60. package/dist/index.d.ts +99 -0
  61. package/dist/index.js +120 -0
  62. package/dist/issue-plan-decomposition.d.ts +18 -0
  63. package/dist/issue-plan-decomposition.js +56 -0
  64. package/dist/issue-rag-query.d.ts +12 -0
  65. package/dist/issue-rag-query.js +35 -0
  66. package/dist/local-scorer.d.ts +34 -0
  67. package/dist/local-scorer.js +36 -0
  68. package/dist/metadata-best-min-score.d.ts +9 -0
  69. package/dist/metadata-best-min-score.js +9 -0
  70. package/dist/metadata-best-pick.d.ts +9 -0
  71. package/dist/metadata-best-pick.js +9 -0
  72. package/dist/metadata-min-score.d.ts +9 -0
  73. package/dist/metadata-min-score.js +13 -0
  74. package/dist/metadata-top-min-score.d.ts +9 -0
  75. package/dist/metadata-top-min-score.js +13 -0
  76. package/dist/metadata-top-pick.d.ts +9 -0
  77. package/dist/metadata-top-pick.js +13 -0
  78. package/dist/miner/acceptance-criteria.d.ts +43 -0
  79. package/dist/miner/acceptance-criteria.js +77 -0
  80. package/dist/miner/agent-sdk-driver.d.ts +39 -0
  81. package/dist/miner/agent-sdk-driver.js +162 -0
  82. package/dist/miner/attempt-log.d.ts +29 -0
  83. package/dist/miner/attempt-log.js +100 -0
  84. package/dist/miner/attempt-metering.d.ts +39 -0
  85. package/dist/miner/attempt-metering.js +67 -0
  86. package/dist/miner/cli-subprocess-driver.d.ts +68 -0
  87. package/dist/miner/cli-subprocess-driver.js +272 -0
  88. package/dist/miner/coding-agent-driver.d.ts +33 -0
  89. package/dist/miner/coding-agent-driver.js +36 -0
  90. package/dist/miner/coding-agent-invoke.d.ts +11 -0
  91. package/dist/miner/coding-agent-invoke.js +80 -0
  92. package/dist/miner/coding-agent-mode.d.ts +25 -0
  93. package/dist/miner/coding-agent-mode.js +43 -0
  94. package/dist/miner/driver-factory.d.ts +82 -0
  95. package/dist/miner/driver-factory.js +169 -0
  96. package/dist/miner/harness-submission-trigger.d.ts +37 -0
  97. package/dist/miner/harness-submission-trigger.js +46 -0
  98. package/dist/miner/iterate-loop.d.ts +97 -0
  99. package/dist/miner/iterate-loop.js +278 -0
  100. package/dist/miner/iterate-policy.d.ts +98 -0
  101. package/dist/miner/iterate-policy.js +98 -0
  102. package/dist/miner/lint-guard.d.ts +50 -0
  103. package/dist/miner/lint-guard.js +68 -0
  104. package/dist/miner/local-write-tools.d.ts +81 -0
  105. package/dist/miner/local-write-tools.js +113 -0
  106. package/dist/miner/loop-reentry-policy.d.ts +36 -0
  107. package/dist/miner/loop-reentry-policy.js +53 -0
  108. package/dist/miner/repo-map.d.ts +51 -0
  109. package/dist/miner/repo-map.js +268 -0
  110. package/dist/miner/self-review-adapter.d.ts +106 -0
  111. package/dist/miner/self-review-adapter.js +83 -0
  112. package/dist/miner/submission-gate.d.ts +40 -0
  113. package/dist/miner/submission-gate.js +89 -0
  114. package/dist/miner/worktree-allocator.d.ts +59 -0
  115. package/dist/miner/worktree-allocator.js +60 -0
  116. package/dist/miner/worktree-pool.d.ts +55 -0
  117. package/dist/miner/worktree-pool.js +65 -0
  118. package/dist/miner-goal-lane-fit.d.ts +18 -0
  119. package/dist/miner-goal-lane-fit.js +76 -0
  120. package/dist/miner-goal-spec.d.ts +139 -0
  121. package/dist/miner-goal-spec.js +281 -0
  122. package/dist/miner-prediction-metrics.d.ts +16 -0
  123. package/dist/miner-prediction-metrics.js +61 -0
  124. package/dist/miner-telemetry.d.ts +32 -0
  125. package/dist/miner-telemetry.js +87 -0
  126. package/dist/objective-anchor.d.ts +125 -0
  127. package/dist/objective-anchor.js +401 -0
  128. package/dist/opportunity-competition.d.ts +6 -0
  129. package/dist/opportunity-competition.js +29 -0
  130. package/dist/opportunity-freshness.d.ts +18 -0
  131. package/dist/opportunity-freshness.js +55 -0
  132. package/dist/opportunity-metadata.d.ts +53 -0
  133. package/dist/opportunity-metadata.js +204 -0
  134. package/dist/opportunity-ranker.d.ts +46 -0
  135. package/dist/opportunity-ranker.js +74 -0
  136. package/dist/pairwise-calibration.d.ts +49 -0
  137. package/dist/pairwise-calibration.js +106 -0
  138. package/dist/phase7-calibration-loop.d.ts +136 -0
  139. package/dist/phase7-calibration-loop.js +437 -0
  140. package/dist/plan-blocked.d.ts +6 -0
  141. package/dist/plan-blocked.js +24 -0
  142. package/dist/plan-completed.d.ts +5 -0
  143. package/dist/plan-completed.js +6 -0
  144. package/dist/plan-completion.d.ts +6 -0
  145. package/dist/plan-completion.js +7 -0
  146. package/dist/plan-empty.d.ts +5 -0
  147. package/dist/plan-empty.js +6 -0
  148. package/dist/plan-export.d.ts +27 -0
  149. package/dist/plan-export.js +86 -0
  150. package/dist/plan-failure.d.ts +5 -0
  151. package/dist/plan-failure.js +6 -0
  152. package/dist/plan-overall-status.d.ts +6 -0
  153. package/dist/plan-overall-status.js +25 -0
  154. package/dist/plan-pending.d.ts +5 -0
  155. package/dist/plan-pending.js +6 -0
  156. package/dist/plan-progress-complete.d.ts +7 -0
  157. package/dist/plan-progress-complete.js +10 -0
  158. package/dist/plan-ready.d.ts +6 -0
  159. package/dist/plan-ready.js +12 -0
  160. package/dist/plan-running.d.ts +5 -0
  161. package/dist/plan-running.js +6 -0
  162. package/dist/plan-skipped.d.ts +5 -0
  163. package/dist/plan-skipped.js +6 -0
  164. package/dist/plan-step-count.d.ts +5 -0
  165. package/dist/plan-step-count.js +6 -0
  166. package/dist/plan-step-stats.d.ts +5 -0
  167. package/dist/plan-step-stats.js +6 -0
  168. package/dist/plan-templates.d.ts +25 -0
  169. package/dist/plan-templates.js +107 -0
  170. package/dist/plan-terminated.d.ts +6 -0
  171. package/dist/plan-terminated.js +9 -0
  172. package/dist/portfolio/non-convergence.d.ts +37 -0
  173. package/dist/portfolio/non-convergence.js +57 -0
  174. package/dist/portfolio/queue.d.ts +30 -0
  175. package/dist/portfolio/queue.js +150 -0
  176. package/dist/predicted-gate.d.ts +103 -0
  177. package/dist/predicted-gate.js +218 -0
  178. package/dist/prompt-packet.d.ts +11 -0
  179. package/dist/prompt-packet.js +34 -0
  180. package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
  181. package/dist/ranked-opportunity-best-min-score.js +9 -0
  182. package/dist/ranked-opportunity-best-pick.d.ts +8 -0
  183. package/dist/ranked-opportunity-best-pick.js +9 -0
  184. package/dist/ranked-opportunity-min-score.d.ts +8 -0
  185. package/dist/ranked-opportunity-min-score.js +13 -0
  186. package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
  187. package/dist/ranked-opportunity-top-min-score.js +13 -0
  188. package/dist/review/advisory-ai-routing-config.d.ts +8 -0
  189. package/dist/review/advisory-ai-routing-config.js +40 -0
  190. package/dist/review/check-names.d.ts +1 -0
  191. package/dist/review/check-names.js +1 -0
  192. package/dist/review/cla-check.d.ts +49 -0
  193. package/dist/review/cla-check.js +71 -0
  194. package/dist/review/diff-file-priority.d.ts +1 -0
  195. package/dist/review/diff-file-priority.js +12 -0
  196. package/dist/review/enrichment-analyzer-names.d.ts +3 -0
  197. package/dist/review/enrichment-analyzer-names.js +63 -0
  198. package/dist/review/guardrail-config.d.ts +10 -0
  199. package/dist/review/guardrail-config.js +62 -0
  200. package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
  201. package/dist/review/linked-issue-hard-rules-config.js +79 -0
  202. package/dist/review/linked-issue-label-propagation.d.ts +9 -0
  203. package/dist/review/linked-issue-label-propagation.js +114 -0
  204. package/dist/review/pre-merge-checks.d.ts +28 -0
  205. package/dist/review/pre-merge-checks.js +66 -0
  206. package/dist/review/review-thread-findings.d.ts +1 -0
  207. package/dist/review/review-thread-findings.js +1 -0
  208. package/dist/review/safe-url.d.ts +5 -0
  209. package/dist/review/safe-url.js +137 -0
  210. package/dist/review/screenshot-table-gate.d.ts +87 -0
  211. package/dist/review/screenshot-table-gate.js +340 -0
  212. package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
  213. package/dist/review/unlinked-issue-guardrail-config.js +44 -0
  214. package/dist/reviewer-consensus-calibration.d.ts +113 -0
  215. package/dist/reviewer-consensus-calibration.js +493 -0
  216. package/dist/reward-risk.d.ts +223 -0
  217. package/dist/reward-risk.js +618 -0
  218. package/dist/scoring/label-match.d.ts +4 -0
  219. package/dist/scoring/label-match.js +145 -0
  220. package/dist/scoring/model.d.ts +31 -0
  221. package/dist/scoring/model.js +157 -0
  222. package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
  223. package/dist/scoring/pending-pr-scenarios.js +145 -0
  224. package/dist/scoring/preview.d.ts +246 -0
  225. package/dist/scoring/preview.js +1108 -0
  226. package/dist/scoring/types.d.ts +131 -0
  227. package/dist/scoring/types.js +7 -0
  228. package/dist/settings/auto-close-exempt.d.ts +10 -0
  229. package/dist/settings/auto-close-exempt.js +58 -0
  230. package/dist/settings/autonomy.d.ts +33 -0
  231. package/dist/settings/autonomy.js +80 -0
  232. package/dist/settings/command-authorization.d.ts +49 -0
  233. package/dist/settings/command-authorization.js +222 -0
  234. package/dist/settings/contributor-blacklist.d.ts +17 -0
  235. package/dist/settings/contributor-blacklist.js +86 -0
  236. package/dist/settings/moderation-rules.d.ts +51 -0
  237. package/dist/settings/moderation-rules.js +104 -0
  238. package/dist/settings/pr-type-label.d.ts +70 -0
  239. package/dist/settings/pr-type-label.js +149 -0
  240. package/dist/signals/change-guardrail.d.ts +49 -0
  241. package/dist/signals/change-guardrail.js +164 -0
  242. package/dist/signals/check-summary.d.ts +9 -0
  243. package/dist/signals/check-summary.js +12 -0
  244. package/dist/signals/contributor-calibration.d.ts +23 -0
  245. package/dist/signals/contributor-calibration.js +47 -0
  246. package/dist/signals/duplicate-winner.d.ts +9 -0
  247. package/dist/signals/duplicate-winner.js +9 -0
  248. package/dist/signals/path-matchers.d.ts +36 -0
  249. package/dist/signals/path-matchers.js +371 -0
  250. package/dist/signals/predicted-gate-engine.d.ts +62 -0
  251. package/dist/signals/predicted-gate-engine.js +895 -0
  252. package/dist/signals/preflight-limits.d.ts +14 -0
  253. package/dist/signals/preflight-limits.js +14 -0
  254. package/dist/signals/slop.d.ts +56 -0
  255. package/dist/signals/slop.js +324 -0
  256. package/dist/signals/test-evidence.d.ts +25 -0
  257. package/dist/signals/test-evidence.js +108 -0
  258. package/dist/subprocess-env.d.ts +23 -0
  259. package/dist/subprocess-env.js +74 -0
  260. package/dist/track-record-summary.d.ts +92 -0
  261. package/dist/track-record-summary.js +301 -0
  262. package/dist/types/manifest-deps-types.d.ts +514 -0
  263. package/dist/types/manifest-deps-types.js +4 -0
  264. package/dist/types/predicted-gate-types.d.ts +338 -0
  265. package/dist/types/predicted-gate-types.js +3 -0
  266. package/dist/types/reward-risk-types.d.ts +319 -0
  267. package/dist/types/reward-risk-types.js +11 -0
  268. package/dist/utils/json.d.ts +1 -0
  269. package/dist/utils/json.js +3 -0
  270. package/dist/version.d.ts +2 -0
  271. package/dist/version.js +3 -0
  272. package/package.json +80 -0
package/README.md ADDED
@@ -0,0 +1,676 @@
1
+ # @loopover/engine
2
+
3
+ Shared, deterministic engine logic for the Gittensory review stack and the `gittensory-miner`.
4
+
5
+ This package houses pure, side-effect-free logic (scoring preview/model, predicted-gate types, reward-risk,
6
+ slop signals, focus-manifest parse/compile core, duplicate-winner adjudication, and their engine-parity
7
+ fixtures) so the exact same code runs identically in the hosted review backend and in a local miner. It is
8
+ versioned independently of the app and published to npm as `@loopover/engine`.
9
+
10
+ The logic is extracted from the app's `src/` in follow-up issues; this skeleton keeps the package buildable in
11
+ the meantime. The root `package.json` already globs `packages/*` in its `workspaces` field, so `npm ci`
12
+ discovers this package with no additional wiring.
13
+
14
+ ## Version pin
15
+
16
+ `ENGINE_VERSION` mirrors `package.json`'s `version` field and is exported from the package barrel so consumers can
17
+ log or assert which engine build produced a deterministic result.
18
+
19
+ ## Build
20
+
21
+ ```
22
+ npm run build --workspace @loopover/engine
23
+ ```
24
+
25
+ This runs `tsc -p tsconfig.json`, emitting `dist/` (the only published output alongside `CHANGELOG.md`).
26
+
27
+ ## Test
28
+
29
+ ```
30
+ npm test --workspace @loopover/engine
31
+ ```
32
+
33
+ Compiles the package and the `test/` suite (`node:test`) to plain JS and runs it — no experimental runtime
34
+ flags, so it works on the whole declared `engines` range.
35
+
36
+ ## `opportunity-ranker`
37
+
38
+ The Phase-1 miner-discovery ranker. It composes five already-normalized `[0, 1]` signals into one ordinal score:
39
+
40
+ ```
41
+ score = potential * feasibility * laneFit * freshness * (1 - dupRisk)
42
+ ```
43
+
44
+ Every field is normalized before use, so a malformed upstream signal always degrades the score toward `0` rather
45
+ than inverting or overflowing it — but the two directions are handled asymmetrically:
46
+
47
+ - The four **positive** factors (`potential`, `feasibility`, `laneFit`, `freshness`) clamp into `[0, 1]`; a
48
+ non-finite value (`NaN`/`±Infinity`) maps to `0`.
49
+ - **`dupRisk`** is clamped into `[0, 1]` like the others (below-range → `0`, above-range → `1`), so `-0.1` reads as
50
+ no contention. The one exception: a **non-finite** `dupRisk` (`NaN`/`±Infinity`) can't be clamped, so it **fails
51
+ closed** to `1` (maximum risk) rather than `0` — a broken contention signal must never masquerade as safe.
52
+
53
+ Any single factor at `0` (or a `dupRisk` of `1`) collapses the whole score to `0`.
54
+
55
+ ```ts
56
+ import { rankOpportunities, rankOpportunityScore } from "@loopover/engine";
57
+
58
+ rankOpportunityScore({ potential: 0.9, feasibility: 0.8, laneFit: 1, freshness: 0.7, dupRisk: 0.1 }); // → 0.4536
59
+
60
+ rankOpportunities(candidates); // sorted by descending score, each annotated with `rankScore`
61
+ ```
62
+
63
+ `rankOpportunities` is a stable sort with an explicit index tie-break: candidates with an equal score keep their
64
+ input order.
65
+
66
+ ## Objective-anchor calibration
67
+
68
+ `scoreObjectiveAnchor()` provides the deterministic half of historical replay calibration. It compares the structural
69
+ features of a miner replay against the revealed post-snapshot history without any model call, network call, wall-clock
70
+ read, or random input.
71
+
72
+ The score is intended for replay harnesses that need an auditable floor before a pairwise judge runs. Callers pass
73
+ the replayed plan or PR target data and the revealed history target data:
74
+
75
+ ```ts
76
+ import { scoreObjectiveAnchor } from "@loopover/engine";
77
+
78
+ const result = scoreObjectiveAnchor({
79
+ replayed: {
80
+ paths: ["packages/gittensory-engine/src/opportunity-ranker.ts"],
81
+ labels: ["feature"],
82
+ titles: ["feat(miner): add deterministic opportunity ranking"],
83
+ },
84
+ revealed: {
85
+ paths: ["packages/gittensory-engine/src/objective-anchor.ts"],
86
+ labels: ["feature"],
87
+ titles: ["feat(miner): add objective-anchor calibration scoring"],
88
+ },
89
+ });
90
+ ```
91
+
92
+ The returned object includes:
93
+
94
+ - `score`: a composite value in `[0, 1]`.
95
+ - `dimensions.paths`: exact/tight path overlap.
96
+ - `dimensions.modules`: coarser module overlap, so a replay that targets the right package but the wrong file receives
97
+ visible partial credit.
98
+ - `dimensions.changeKinds`: overlap between caller-supplied or inferred change classes.
99
+ - `audit`: normalized replayed/revealed feature sets, intersections, misses, and normalized weights.
100
+
101
+ The default weight split is path-heavy but still gives module-level and kind-level signal:
102
+
103
+ ```ts
104
+ {
105
+ paths: 0.45,
106
+ modules: 0.4,
107
+ changeKinds: 0.15
108
+ }
109
+ ```
110
+
111
+ Custom weights are normalized to sum to `1`. Negative, non-finite, or otherwise invalid weights are treated as `0`;
112
+ if every provided weight is unusable, the defaults are restored.
113
+
114
+ Feature extraction is intentionally conservative:
115
+
116
+ - Paths are normalized to lowercase slash paths, deduplicated, and sorted.
117
+ - Modules are derived only from paths, never guessed from free text.
118
+ - Change kinds can come from explicit `changeKinds`, issue/PR labels, titles, notes, and path conventions.
119
+ - If no change-kind signal exists, the kind is `unknown` so an opaque replay and opaque revealed history can still be
120
+ compared deterministically.
121
+
122
+ Given the same inputs, `JSON.stringify(scoreObjectiveAnchor(input))` is byte-stable across runs.
123
+
124
+ Replay harnesses that already represent the two sides as arrays of plans, PRs, or commits can use the history
125
+ helpers instead:
126
+
127
+ ```ts
128
+ import { scoreObjectiveAnchorHistory } from "@loopover/engine";
129
+
130
+ const result = scoreObjectiveAnchorHistory({
131
+ replayed: [
132
+ {
133
+ id: "plan:objective-anchor",
134
+ source: "plan",
135
+ paths: ["packages/gittensory-engine/src/objective-anchor.ts"],
136
+ labels: ["feature"],
137
+ },
138
+ ],
139
+ revealed: [
140
+ {
141
+ id: "pr:3142",
142
+ source: "pull_request",
143
+ paths: ["packages/gittensory-engine/src/objective-anchor.ts"],
144
+ labels: ["feature"],
145
+ },
146
+ ],
147
+ });
148
+ ```
149
+
150
+ `result.history.replayed.items` and `result.history.revealed.items` preserve the per-record normalized features, while
151
+ `result.audit` shows the aggregate intersections and misses used for the score. Empty histories remain valid inputs:
152
+ they produce empty path/module sets and an `unknown` change kind rather than throwing, so a replay batch can record a
153
+ low-information calibration row without special casing.
154
+
155
+ For local replay artifacts, `renderObjectiveAnchorAuditMarkdown(result)` turns either score shape into a deterministic
156
+ Markdown report. It includes dimensions, weights, normalized feature sets, intersections, misses, and per-item history
157
+ evidence when present. Report values are escaped and collapsed to one line so caller-supplied ids or paths cannot
158
+ reshape the artifact.
159
+
160
+ ## Pairwise calibration
161
+
162
+ `computePairwiseCalibrationScore()` is the deterministic half of the order-swapped pairwise judge layer. The miner
163
+ runtime owns the model calls; the engine package owns the stable post-processing contract:
164
+
165
+ - run a judge attempt in both presentation orders,
166
+ - accept only outcomes that agree after inverting the swapped-order verdict,
167
+ - discard `incomparable` and order-flipping attempts,
168
+ - cap retries,
169
+ - track order-instability rate,
170
+ - combine the surviving pairwise average with the objective-anchor score.
171
+
172
+ ```ts
173
+ import { computePairwiseCalibrationScore } from "@loopover/engine";
174
+
175
+ const result = computePairwiseCalibrationScore({
176
+ objectiveAnchor: 0.55,
177
+ samples: [
178
+ {
179
+ attempts: [
180
+ {
181
+ replayFirst: "replay_better",
182
+ revealedFirst: "revealed_better",
183
+ },
184
+ ],
185
+ },
186
+ ],
187
+ });
188
+ ```
189
+
190
+ If every pairwise sample is unstable, the composite falls back to the objective-anchor score and records the failed
191
+ samples in `metrics` rather than averaging noise into the calibration signal.
192
+
193
+ ## Structured gate-verdict calibration
194
+
195
+ `resolveGateVerdictCalibrationConfig()`, `ingestGateVerdictCalibrationSignals()`, and
196
+ `computeGateVerdictCompositeCalibrationScore()` provide the pure engine contract for opt-in cross-product calibration.
197
+ The hosted review stack remains responsible for loading the repo's current `.loopover.yml` (or legacy
198
+ `.gittensory.yml`, dual-read indefinitely — #4773) or private config; the engine contract is
199
+ deliberately default-off and safe to call at ingestion time.
200
+
201
+ The preferred config-as-code surface is:
202
+
203
+ ```yaml
204
+ miner:
205
+ calibration:
206
+ shareStructuredGateVerdicts: true
207
+ structuredGateVerdictWeight: 0.2
208
+ ```
209
+
210
+ Only `shareStructuredGateVerdicts: true` enables ingestion. Missing, malformed, or falsey values all fail closed to no
211
+ sharing. The optional weight is non-negative and finite; malformed values fall back to the default.
212
+
213
+ The accepted signal is intentionally narrow. It contains repo/run ids plus structured dimension outcomes such as
214
+ `correctness`, `tests`, `security`, `scope`, `freshness`, `ci`, and `policy`. It has no fields for raw review text,
215
+ secrets, trust scores, reward values, private rankings, or maintainer evidence.
216
+
217
+ ```ts
218
+ import {
219
+ computeGateVerdictCompositeCalibrationScore,
220
+ ingestGateVerdictCalibrationSignals,
221
+ } from "@loopover/engine";
222
+
223
+ const gateVerdicts = ingestGateVerdictCalibrationSignals([
224
+ {
225
+ repoFullName: "jsonbored/gittensory",
226
+ replayRunId: "replay-2026-07-04",
227
+ gateRunId: "gate-123",
228
+ optedIn: true,
229
+ dimensions: [
230
+ { dimension: "correctness", outcome: "pass" },
231
+ { dimension: "tests", outcome: "warn" },
232
+ { dimension: "security", outcome: "pass" },
233
+ ],
234
+ },
235
+ ]);
236
+
237
+ const score = computeGateVerdictCompositeCalibrationScore({
238
+ objectiveAnchor: 0.65,
239
+ pairwise: 0.8,
240
+ gateVerdicts,
241
+ });
242
+ ```
243
+
244
+ The composite scorer renormalizes weights when a signal is absent. For example, if a repo opts out or no valid
245
+ structured dimensions remain, the structured gate-verdict weight drops to zero and the objective/pairwise signals are
246
+ renormalized. The returned audit trail records which opted-in repos contributed to the replay run and which rows were
247
+ rejected because the repo was not opted in, had invalid ids, or exposed no recognized structured dimensions.
248
+
249
+ `renderGateVerdictCalibrationAuditMarkdown(result)` turns the composite result into a deterministic local artifact with
250
+ component scores, effective weights, contributing repos, dimension tables, rejected rows, and a contributing-repo
251
+ summary. All caller-supplied ids and repo names are Markdown-escaped and newline-collapsed before rendering.
252
+
253
+ ## Phase 7 calibration loop
254
+
255
+ `computePhase7CalibrationLoop()` wires the historical-replay composite score into the live Phase 7 calibration loop
256
+ alongside the passive pr_outcome signal. The module tracks a combined calibration-accuracy metric against the
257
+ documented 62% baseline, records provenance per source, recommends replay-run cadence, and fail-closes autonomy-level
258
+ increases when the replay harness is missing, stale, degraded, or below the configured threshold.
259
+
260
+ The loop is default-off and must be enabled explicitly:
261
+
262
+ ```yaml
263
+ miner:
264
+ calibration:
265
+ phase7LoopEnabled: true
266
+ autonomyIncreaseMinAccuracy: 0.70
267
+ replayFreshnessMaxAgeHours: 168
268
+ historicalReplayWeight: 0.5
269
+ prOutcomeWeight: 0.5
270
+ ```
271
+
272
+ When enabled, autonomy-level increases require a fresh healthy historical-replay run plus enough live pr_outcome samples.
273
+ If the replay harness is degraded or unavailable, the loop sets an explicit hold flag instead of silently falling back
274
+ to pr_outcome-only gating.
275
+
276
+ ```ts
277
+ import {
278
+ computePhase7CalibrationLoop,
279
+ shouldScheduleHistoricalReplayRun,
280
+ } from "@loopover/engine";
281
+
282
+ const prOutcome = {
283
+ mergeConfirmed: 74,
284
+ mergeFalse: 26,
285
+ closeConfirmed: 0,
286
+ closeFalse: 0,
287
+ observedAt: "2026-07-04T18:00:00Z",
288
+ };
289
+
290
+ const loop = computePhase7CalibrationLoop({
291
+ config: {
292
+ phase7LoopEnabled: true,
293
+ autonomyIncreaseMinAccuracy: 0.7,
294
+ replayFreshnessMaxAgeHours: 168,
295
+ historicalReplayWeight: 0.5,
296
+ prOutcomeWeight: 0.5,
297
+ prOutcomeMinDecided: 10,
298
+ warnings: [],
299
+ },
300
+ prOutcome,
301
+ historicalReplay: {
302
+ compositeScore: 0.82,
303
+ replayRunId: "replay-2026-07-04",
304
+ observedAt: "2026-07-04T12:00:00Z",
305
+ harnessStatus: "healthy",
306
+ },
307
+ now: "2026-07-04T18:00:00Z",
308
+ });
309
+
310
+ const schedule = shouldScheduleHistoricalReplayRun({
311
+ config: {
312
+ phase7LoopEnabled: true,
313
+ autonomyIncreaseMinAccuracy: 0.7,
314
+ replayFreshnessMaxAgeHours: 168,
315
+ historicalReplayWeight: 0.5,
316
+ prOutcomeWeight: 0.5,
317
+ prOutcomeMinDecided: 10,
318
+ warnings: [],
319
+ },
320
+ lastReplayObservedAt: loop.bySource.historical_replay.observedAt,
321
+ harnessStatus: loop.replayHarnessStatus,
322
+ now: "2026-07-04T18:00:00Z",
323
+ });
324
+ ```
325
+
326
+ `renderPhase7CalibrationAuditMarkdown(loop)` turns the result into a deterministic local artifact with the combined
327
+ metric, baseline delta, per-source breakdown, hold reasons, and replay cadence state.
328
+
329
+ `computePrOutcomeCalibrationAccuracy()` is a read-only helper for inspecting derived accuracy from raw gate-eval
330
+ counters; pass the counters themselves into `computePhase7CalibrationLoop()`, not the helper result.
331
+
332
+ ## Track-record summary
333
+
334
+ `computeTrackRecordSummary()` and `renderTrackRecordSummaryMarkdown()` provide a portable first-contact summary for a
335
+ miner identity. The summary is computed client-side from already-public PR outcomes plus public conduct/moderation
336
+ records, then rendered as a short Markdown block for a PR body or first comment.
337
+
338
+ The feature is default-off and must be enabled explicitly:
339
+
340
+ ```yaml
341
+ miner:
342
+ trackRecordSummary:
343
+ enabled: true
344
+ ```
345
+
346
+ The computation only counts resolved PR outcomes attributable to the requested login. Merged PRs contribute to the
347
+ numerator, closed-without-merge PRs contribute to the denominator, and open PRs are reported as ignored so in-flight
348
+ work cannot inflate or deflate the public rate. Tenure is derived from the earliest observed public PR timestamp, and a
349
+ clean conduct line is emitted only when no active public incident record is present for the login.
350
+
351
+ ```ts
352
+ import {
353
+ computeTrackRecordSummary,
354
+ renderTrackRecordSummaryMarkdown,
355
+ resolveTrackRecordSummaryConfig,
356
+ } from "@loopover/engine";
357
+
358
+ const config = resolveTrackRecordSummaryConfig({
359
+ miner: { trackRecordSummary: { enabled: true } },
360
+ });
361
+
362
+ const summary = computeTrackRecordSummary({
363
+ login: "octo-miner",
364
+ config,
365
+ now: "2026-07-04T18:00:00Z",
366
+ outcomes: [
367
+ {
368
+ repoFullName: "JSONbored/gittensory",
369
+ authorLogin: "octo-miner",
370
+ state: "merged",
371
+ createdAt: "2026-06-01T00:00:00Z",
372
+ mergedAt: "2026-06-02T00:00:00Z",
373
+ },
374
+ ],
375
+ incidents: [],
376
+ });
377
+
378
+ const markdown = renderTrackRecordSummaryMarkdown(summary);
379
+ ```
380
+
381
+ The rendered block is intentionally narrow: login, resolved public PR counts, public merge rate, public tenure, conduct
382
+ status, and optional public evidence URLs for active incidents. Caller-provided ids, PR URLs, and arbitrary metadata are
383
+ never copied into the Markdown, and the renderer fails closed if a blocked private-field name is introduced.
384
+
385
+ ## Structured finding-severity calibration
386
+
387
+ `resolveFindingSeverityCalibrationConfig()`, `ingestFindingSeverityCalibrationSignals()`, and
388
+ `computeFindingSeverityCompositeCalibrationScore()` provide the pure engine contract for the opt-in finding-severity
389
+ calibration signal. It sits in the same family as objective-anchor and pairwise-judge: the hosted review stack decides
390
+ whether a repo is opted in from its resolved `.loopover.yml`/private config (legacy `.gittensory.yml` also still
391
+ works, #4773), and the engine contract is deliberately default-off and safe to call at ingestion time.
392
+
393
+ The preferred config-as-code surface is:
394
+
395
+ ```yaml
396
+ miner:
397
+ calibration:
398
+ shareStructuredFindingSeverity: true
399
+ structuredFindingSeverityWeight: 0.2
400
+ ```
401
+
402
+ Only `shareStructuredFindingSeverity: true` enables ingestion. Missing, malformed, or falsey values all fail closed to
403
+ no sharing. `calibration.shareStructuredFindingSeverity` is accepted as a narrow top-level alias. The optional weight is
404
+ non-negative and finite; malformed values fall back to the default.
405
+
406
+ The accepted signal is intentionally narrow: repo/run ids plus, per severity tier (`blocker`, `warning`, `advisory`,
407
+ `nit`), how many findings the review raised and how many were subsequently CONFIRMED (true positives). It has no fields
408
+ for raw review text, secrets, trust scores, reward values, private rankings, or maintainer evidence.
409
+
410
+ ```ts
411
+ import {
412
+ computeFindingSeverityCompositeCalibrationScore,
413
+ ingestFindingSeverityCalibrationSignals,
414
+ } from "@loopover/engine";
415
+
416
+ const findingSeverity = ingestFindingSeverityCalibrationSignals([
417
+ {
418
+ repoFullName: "jsonbored/gittensory",
419
+ replayRunId: "replay-2026-07-04",
420
+ reviewRunId: "review-123",
421
+ optedIn: true,
422
+ tiers: [
423
+ { tier: "blocker", total: 2, confirmed: 2 },
424
+ { tier: "warning", total: 5, confirmed: 3 },
425
+ { tier: "nit", total: 9, confirmed: 1 },
426
+ ],
427
+ },
428
+ ]);
429
+
430
+ const score = computeFindingSeverityCompositeCalibrationScore({
431
+ objectiveAnchor: 0.65,
432
+ pairwise: 0.8,
433
+ findingSeverity,
434
+ });
435
+ ```
436
+
437
+ The per-PR score is the severity-and-volume-weighted mean of the per-tier confirmation rates, so a confirmed blocker
438
+ moves calibration far more than a confirmed nit, and a review that raises blockers which are then dismissed (false
439
+ positives at the most disruptive tier) calibrates poorly. Each tier's `confirmed` count is clamped to its `total` and
440
+ discounted by an optional per-tier `confidence`, so an unverified "all confirmed" claim cannot inflate the score.
441
+
442
+ The composite scorer renormalizes weights when a signal is absent: if a repo opts out or no tier survives
443
+ normalization, the structured finding-severity weight drops to zero and the objective/pairwise signals are
444
+ renormalized. The returned audit trail records which opted-in repos contributed and which rows were rejected because the
445
+ repo was not opted in, had invalid ids, or exposed no recognized non-empty tiers.
446
+
447
+ `renderFindingSeverityCalibrationAuditMarkdown(result)` turns the composite result into a deterministic local artifact
448
+ with component scores, effective weights, contributing repos, per-tier tables, rejected rows, and a contributing-repo
449
+ summary. All caller-supplied ids and repo names are Markdown-escaped and newline-collapsed before rendering.
450
+
451
+ ## Structured reviewer-consensus calibration
452
+
453
+ `resolveReviewerConsensusCalibrationConfig()`, `ingestReviewerConsensusCalibrationSignals()`, and
454
+ `computeReviewerConsensusCompositeCalibrationScore()` provide the pure engine contract for the opt-in
455
+ reviewer-consensus calibration signal. When a review runs more than one independent reviewer (multiple models, or the
456
+ same model sampled multiple times), each reviewer casts a per-dimension verdict; this signal measures how much they
457
+ **agree**. It is a companion to the pairwise judge (which measures order-stability of a single judge) at the level of
458
+ independent reviewers, and — like the rest of the family — the engine contract is deliberately default-off and safe to
459
+ call at ingestion time.
460
+
461
+ The preferred config-as-code surface is:
462
+
463
+ ```yaml
464
+ miner:
465
+ calibration:
466
+ shareStructuredReviewerConsensus: true
467
+ structuredReviewerConsensusWeight: 0.2
468
+ ```
469
+
470
+ Only `shareStructuredReviewerConsensus: true` enables ingestion. Missing, malformed, or falsey values all fail closed to
471
+ no sharing. `calibration.shareStructuredReviewerConsensus` is accepted as a narrow top-level alias. The optional weight
472
+ is non-negative and finite; malformed values fall back to the default.
473
+
474
+ The accepted signal is intentionally narrow: repo/run ids plus, per dimension (`correctness`, `tests`, `security`,
475
+ `maintainability`, `scope`, `freshness`, `ci`, `policy`), the set of independent reviewer votes (`pass`/`warn`/`fail`).
476
+ It has no fields for raw review text, secrets, trust scores, reward values, private rankings, or maintainer evidence.
477
+
478
+ ```ts
479
+ import {
480
+ computeReviewerConsensusCompositeCalibrationScore,
481
+ ingestReviewerConsensusCalibrationSignals,
482
+ } from "@loopover/engine";
483
+
484
+ const reviewerConsensus = ingestReviewerConsensusCalibrationSignals([
485
+ {
486
+ repoFullName: "jsonbored/gittensory",
487
+ replayRunId: "replay-2026-07-05",
488
+ reviewRunId: "review-123",
489
+ optedIn: true,
490
+ dimensions: [
491
+ { dimension: "correctness", votes: ["pass", "pass", "pass"] },
492
+ { dimension: "security", votes: ["fail", "warn", "fail"] },
493
+ ],
494
+ },
495
+ ]);
496
+
497
+ const score = computeReviewerConsensusCompositeCalibrationScore({
498
+ objectiveAnchor: 0.65,
499
+ pairwise: 0.8,
500
+ reviewerConsensus,
501
+ });
502
+ ```
503
+
504
+ Per dimension, unrecognized and abstention votes are dropped, the remaining votes are tallied, the plurality outcome is
505
+ chosen (ties broken toward the more severe outcome so a genuine split never rounds a real `fail`/`warn` down to `pass`),
506
+ and the **agreement** fraction is the plurality's share of the definite votes. The per-PR score is the
507
+ **vote-count-weighted** mean of the per-dimension agreements, so a dimension reviewed by more reviewers carries more
508
+ weight than one seen by a single reviewer.
509
+
510
+ The composite scorer renormalizes weights when a signal is absent: if a repo opts out or no dimension carries a definite
511
+ vote, the structured reviewer-consensus weight drops to zero and the objective/pairwise signals are renormalized. The
512
+ returned audit trail records which opted-in repos contributed and which rows were rejected because the repo was not
513
+ opted in, had invalid ids, or exposed no definite per-dimension votes.
514
+
515
+ `renderReviewerConsensusCalibrationAuditMarkdown(result)` turns the composite result into a deterministic local artifact
516
+ with component scores, effective weights, contributing repos, per-dimension agreement tables, rejected rows, and a
517
+ contributing-repo summary. All caller-supplied ids and repo names are Markdown-escaped and newline-collapsed before
518
+ rendering.
519
+
520
+ ## Plan templates
521
+
522
+ `plan-templates.ts` exports one builder per miner lifecycle stage (`analyze`, `plan`, `prepare`, `create`, `manage`).
523
+ Each builder returns `RawPlanStep[]` in the shape accepted by `gittensory_build_plan`. Templates are pure data — they
524
+ describe step ordering via `dependsOn` but never actuate anything.
525
+
526
+ ## Plan DAG status helpers
527
+
528
+ `plan-export.ts` renders a validated `PlanDag`; the helpers below are pure predicates over that shape for miner and
529
+ dashboard progress summaries:
530
+
531
+ - `countPlanSteps(plan)` — total step count
532
+ - `countPlanStepsByStatus(plan, status)` — steps matching a `PlanStepStatus`
533
+ - `isPlanEmpty(plan)` — whether the plan has no steps
534
+ - `isPlanFullyCompleted(plan)` — every step is `completed` (empty plans are not complete)
535
+ - `hasPlanFailedSteps(plan)` — any step is `failed`
536
+ - `hasPlanPendingSteps(plan)` — any step is `pending`
537
+ - `hasPlanRunningSteps(plan)` — any step is `running`
538
+ - `hasPlanSkippedSteps(plan)` — any step is `skipped`
539
+ - `hasPlanCompletedSteps(plan)` — any step is `completed`
540
+ - `isPlanBlocked(plan)` — pending steps remain but none are runnable (deadlock; mirrors `planProgress`'s `blocked` status)
541
+ - `isPlanProgressComplete(plan)` — every step is `completed` or `skipped` (empty plans are not complete; mirrors `planProgress`'s `completed` status)
542
+ - `resolvePlanOverallStatus(plan)` — coarse status (`pending` | `running` | `completed` | `failed` | `blocked`); mirrors `planProgress`'s `status`
543
+ - `hasPlanReadySteps(plan)` — any step is runnable now (`pending` with satisfied dependencies; mirrors `nextReadySteps(plan).length > 0`)
544
+ - `isPlanTerminated(plan)` — plan reached a terminal outcome (`failed` step or every step `completed`/`skipped`; empty plans are not terminated)
545
+
546
+ ## Opportunity competition
547
+
548
+ `computeOpportunityCompetition(highRiskDuplicateClusters, openPullRequests)` mirrors the hosted
549
+ `opportunityCompetitionFactor` in `src/signals/reward-risk.ts`, producing a `[0, 1]` signal suitable for the ranker's
550
+ `dupRisk` input.
551
+
552
+ ## Metadata opportunity signals
553
+
554
+ `opportunity-metadata.ts` turns fan-out issue metadata into the five normalized ranker inputs:
555
+
556
+ - `computeMetadataPotential` — label-based upside estimate
557
+ - `computeMetadataFeasibility` — comment load + issue age + title quality
558
+ - `computeMetadataDupRisk` — same-repo title overlap inside a candidate batch
559
+ - `computeMetadataLaneFit` — label-only lane fit by default; honors optional `candidatePaths` via `computeLaneFit`
560
+ - `buildMetadataRankInput` — composes freshness, competition, lane fit, and the metadata heuristics
561
+ - `rankMetadataOpportunities` — sorts candidates with `rankOpportunities`
562
+
563
+ `computeOpportunityFreshness` and `computeOpportunityCompetition` mirror the hosted reward-risk helpers with pure,
564
+ injected-clock semantics for local miners.
565
+
566
+ ## AI Policy Map
567
+
568
+ `scanAiPolicyText` and `resolveAiPolicyVerdict` provide the deterministic policy gate used by miner discovery.
569
+ They only deny on small, explicit AI-contribution ban phrases in `AI-USAGE.md` or `CONTRIBUTING.md`; ambiguous,
570
+ missing, or empty policy text stays allowed so discovery does not invent a ban.
571
+
572
+ `resolveAiPolicyFatigueVerdict()` adds a softer metadata-only tier for repos that show signs of AI-contribution
573
+ fatigue before a formal ban lands. It keeps the hard-ban verdict authoritative and attaches a separate `fatigue`
574
+ object instead of changing `allowed`:
575
+
576
+ ```ts
577
+ import { resolveAiPolicyFatigueVerdict } from "@loopover/engine";
578
+
579
+ const verdict = resolveAiPolicyFatigueVerdict({
580
+ now: "2026-07-05T00:00:00Z",
581
+ docs: {
582
+ aiUsage: null,
583
+ contributing: "Please keep pull requests focused.",
584
+ },
585
+ pullRequests: [
586
+ {
587
+ state: "closed",
588
+ title: "AI-assisted parser cleanup",
589
+ labels: ["ai-generated"],
590
+ closedAt: "2026-07-04T00:00:00Z",
591
+ reviewDecision: "changes_requested",
592
+ maintainerResponse: "terse_rejection",
593
+ },
594
+ ],
595
+ docChanges: [
596
+ {
597
+ path: "CONTRIBUTING.md",
598
+ changedAt: "2026-07-04T12:00:00Z",
599
+ addedPhrases: ["Disclose AI or automation assistance."],
600
+ },
601
+ ],
602
+ });
603
+ ```
604
+
605
+ The fatigue verdict has four levels:
606
+
607
+ - `none` leaves ranking unchanged and uses a long recheck interval.
608
+ - `watch` and `deprioritize` return `priorityAdjustment: "deprioritize"` so miners can rank the repo lower without
609
+ treating it as banned.
610
+ - `defer` returns `priorityAdjustment: "defer"` with a short recheck interval for stronger but still reversible
611
+ signals.
612
+
613
+ Evidence is deliberately metadata-only: AI-attributed closed PR rows, terse/template rejection metadata, and recent
614
+ AI/automation language added to policy docs that does not match a formal ban phrase. `renderAiPolicyFatigueMarkdown()`
615
+ turns the verdict into a deterministic observability artifact. Fresh cache entries can be passed back through
616
+ `cache`; expired entries are recomputed.
617
+
618
+ Miner ranking/fanout code can pass the verdict to `applyAiPolicyFatigueToRankInput()`. Formal bans still zero the
619
+ candidate, but fatigue-only verdicts merely reduce `potential` (`watch` = 0.7x, `deprioritize` = 0.35x, `defer` =
620
+ 0.05x) and carry a `deferUntilHours` hint when the repo should be revisited later.
621
+
622
+ `createAiPolicyFatigueCacheEntry()` and `describeAiPolicyFatigueCache()` provide the cache surface for the miner's
623
+ shorter fatigue recheck interval. Cache keys normalize repo names to lowercase `owner/name`, record an ISO
624
+ `computedAt`, and are considered fresh for 24 hours.
625
+
626
+ ## Governor ledger
627
+
628
+ `normalizeGovernorLedgerEvent` validates append-only governor decision rows before the local miner persists them.
629
+ The vocabulary is fixed (`allowed`, `denied`, `throttled`, `kill_switch`) and unknown event types fail closed. This
630
+ module defines the storage contract only — it does not wire into live governor enforcement yet. (#2328)
631
+
632
+ ## MinerGoalSpec
633
+
634
+ `MinerGoalSpec` is the type surface for a repo's `.gittensory-miner.yml` (miner-side analogue of `.gittensory.yml`).
635
+ `DEFAULT_MINER_GOAL_SPEC` is the safe default a repo with no file behaves as — minable (`minerEnabled: true`, an
636
+ explicit opt-out), no path/label preferences, one concurrent claim, `neutral` discovery.
637
+
638
+ `parseMinerGoalSpec(raw)` and `parseMinerGoalSpecContent(content)` are the tolerant parser pair for that file. They
639
+ never throw on malformed JSON/YAML; instead they return `{ present, spec, warnings }`, where `spec` is normalized to
640
+ safe defaults and `warnings` explains any dropped or invalid fields.
641
+
642
+ `discoverMinerGoalSpecPath(exists)` returns the first present file in the documented order (`MINER_GOAL_SPEC_FILENAMES`:
643
+ `.gittensory-miner.yml` → `.github/gittensory-miner.yml` → the `.json` variants). It is IO-free — the caller injects
644
+ the existence check — so a caller reads the returned path and feeds its content to `parseMinerGoalSpecContent`. See
645
+ `.gittensory-miner.yml.example` for the documented fields.
646
+
647
+ ## AmsPolicySpec
648
+
649
+ `AmsPolicySpec` is the type surface for `.gittensory-ams.yml` — the OPERATOR's own execution-risk policy for
650
+ their miner (`submissionMode`, `slopThreshold`, `capLimits`, `convergenceThresholds`, `maxIterations`,
651
+ `maxTurnsPerIteration`), a deliberate structural sibling to `MinerGoalSpec` but answering a different question: `MinerGoalSpec` is what the target repo wants
652
+ from being mined; `AmsPolicySpec` is how aggressive the operator wants their own agent to be. No field on this
653
+ type lets a target repo's own file loosen what an operator's agent is willing to do — see the type's own header
654
+ comment for why that boundary is load-bearing. `DEFAULT_AMS_POLICY_SPEC` is deny-by-default: `"observe"`
655
+ submission mode (computes real decisions but never actually submits) and a `"low"` (strict) slop threshold.
656
+
657
+ `parseAmsPolicySpec(raw)` / `parseAmsPolicySpecContent(content)` are the same tolerant-parser pair shape as
658
+ `MinerGoalSpec`'s — never throw, return `{ present, spec, warnings }`.
659
+
660
+ Unlike `MinerGoalSpec`, this package does not resolve `.gittensory-ams.yml` from the filesystem itself (this
661
+ package is IO-free) — `packages/gittensory-miner/lib/ams-policy.js`'s `resolveAmsPolicy` is the real caller.
662
+ That resolver reads only the operator's local policy and otherwise uses safe defaults; it intentionally does not
663
+ fetch a target repo's checked-in AMS policy, because untrusted repo content must not loosen operator-side budget,
664
+ turn, slop, or submission controls. See `.gittensory-ams.yml.example`.
665
+
666
+ ## Repo map builder
667
+
668
+ `buildRepoMap(files)` gives a coding-agent driver (or the acceptance-criteria/prompt-packet builders upstream of
669
+ it) a compact, structural view of a target repository — function/class/method/interface/type signatures — without
670
+ paying the token cost of dumping full file contents into a prompt. It parses with `web-tree-sitter` (the WASM
671
+ binding, not a native addon, since this package also ships a Cloudflare Workers deployment target) using prebuilt
672
+ grammars from `tree-sitter-wasms`. Supported today: JavaScript/TypeScript/TSX. A file with an unsupported extension
673
+ or a grammar that fails to load/parse is reported via `skipped` on its `RepoMapFileEntry`, never thrown — this
674
+ module's contract is "extract what it safely can," not "block the whole driver invocation." `renderRepoMap(entries)`
675
+ renders a bounded plain-text outline, truncating (with a marker) once a configurable char budget is exceeded so it
676
+ can't blow out a prompt budget on a large repo. (#4280)