@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
@@ -0,0 +1,377 @@
1
+ const AI_POLICY_ALLOWED = {
2
+ allowed: true,
3
+ matchedPhrase: null,
4
+ source: "none",
5
+ };
6
+ const AI_FATIGUE_NONE = {
7
+ level: "none",
8
+ priorityAdjustment: "none",
9
+ score: 0,
10
+ recheckAfterHours: 168,
11
+ evidence: [],
12
+ };
13
+ const BAN_PHRASES = [
14
+ {
15
+ phrase: "no ai-generated pull requests",
16
+ pattern: /\bno\s+ai[-\s]+generated\s+(?:pull\s+requests|prs|contributions)\b/i,
17
+ },
18
+ {
19
+ phrase: "ai-generated prs are rejected",
20
+ pattern: /\bai[-\s]+generated\s+(?:prs?|pull\s+requests|contributions?)\s+(?:are|will\s+be)\s+(?:banned|rejected|not\s+accepted)\b/i,
21
+ },
22
+ {
23
+ phrase: "do not submit ai-generated code",
24
+ pattern: /\bdo\s+not\s+(?:use|submit)\s+ai[-\s]+(?:written|generated)\s+code\b/i,
25
+ },
26
+ {
27
+ phrase: "llm-generated code is not accepted",
28
+ pattern: /\b(?:ai|llm)[-\s]+generated\s+code\s+(?:is|will\s+be)\s+(?:rejected|not\s+accepted)\b/i,
29
+ },
30
+ ];
31
+ const AI_ATTRIBUTION_PATTERNS = [
32
+ /\bai[-\s]?(?:generated|assisted|authored)\b/iu,
33
+ /\b(?:llm|chatgpt|copilot|codex|claude)[-\s]+(?:generated|assisted|authored)\b/iu,
34
+ /\b(?:automation|bot)[-\s]?(?:generated|submitted|authored)\b/iu,
35
+ ];
36
+ const AI_DOC_LANGUAGE_PATTERNS = [
37
+ /\bai\b/iu,
38
+ /\bllm\b/iu,
39
+ /\bautomation\b/iu,
40
+ /\bautomated\s+(?:prs?|pull\s+requests|contributions?)\b/iu,
41
+ /\bgenerated\s+(?:code|prs?|pull\s+requests|contributions?)\b/iu,
42
+ ];
43
+ const TERSE_REJECTION_RESPONSES = new Set(["terse_rejection", "template_rejection"]);
44
+ const FATIGUE_CACHE_TTL_HOURS = 24;
45
+ function parseInstant(value) {
46
+ if (value === null || value === undefined)
47
+ return null;
48
+ const ms = value instanceof Date ? value.getTime() : Date.parse(value);
49
+ if (!Number.isFinite(ms))
50
+ return null;
51
+ return new Date(ms).toISOString();
52
+ }
53
+ function requireNow(value) {
54
+ const parsed = parseInstant(value);
55
+ return parsed ? new Date(parsed) : new Date();
56
+ }
57
+ function hoursBetween(left, right) {
58
+ return Math.max(0, (right.getTime() - new Date(left).getTime()) / 3_600_000);
59
+ }
60
+ function recencyWeight(observedAt, now, halfLifeDays) {
61
+ if (!observedAt)
62
+ return 0.35;
63
+ const ageDays = hoursBetween(observedAt, now) / 24;
64
+ return Math.exp(-ageDays / halfLifeDays);
65
+ }
66
+ function roundScore(value) {
67
+ return Math.round(Math.min(1, Math.max(0, value)) * 1_000_000) / 1_000_000;
68
+ }
69
+ function roundHours(value) {
70
+ return Math.round(Math.max(0, value) * 1_000_000) / 1_000_000;
71
+ }
72
+ function collapseInline(value) {
73
+ return value.replace(/[\r\n\t]+/gu, " ").replace(/\s{2,}/gu, " ").trim();
74
+ }
75
+ function metadataText(pr) {
76
+ return [pr.title, ...(pr.labels ?? []), pr.authorLogin].filter((value) => Boolean(value)).join(" ");
77
+ }
78
+ function isAiAttributed(pr) {
79
+ const text = metadataText(pr);
80
+ return AI_ATTRIBUTION_PATTERNS.some((pattern) => pattern.test(text));
81
+ }
82
+ function isClosedWithoutMerge(pr) {
83
+ const state = pr.state.trim().toLowerCase();
84
+ if (state === "merged")
85
+ return false;
86
+ if (parseInstant(pr.mergedAt))
87
+ return false;
88
+ return state === "closed" || state === "rejected" || state === "declined";
89
+ }
90
+ function isTerseRejection(pr) {
91
+ const response = pr.maintainerResponse?.trim().toLowerCase();
92
+ const reviewDecision = pr.reviewDecision?.trim().toLowerCase();
93
+ const closeReason = pr.closeReason?.trim().toLowerCase();
94
+ return ((response ? TERSE_REJECTION_RESPONSES.has(response) : false) ||
95
+ reviewDecision === "changes_requested" ||
96
+ closeReason === "spam" ||
97
+ closeReason === "not_planned");
98
+ }
99
+ function docText(change) {
100
+ return [change.addedText, ...(change.addedPhrases ?? [])].filter((value) => Boolean(value)).join(" ");
101
+ }
102
+ function aiPolicyDocSource(path) {
103
+ const normalized = collapseInline(path);
104
+ if (normalized === "AI-USAGE.md")
105
+ return "AI-USAGE.md";
106
+ if (normalized === "CONTRIBUTING.md")
107
+ return "CONTRIBUTING.md";
108
+ return null;
109
+ }
110
+ function isAiDocLanguage(change) {
111
+ const source = aiPolicyDocSource(change.path);
112
+ if (!source)
113
+ return false;
114
+ const text = docText(change);
115
+ if (!text.trim())
116
+ return false;
117
+ if (!scanAiPolicyText(text, source).allowed) {
118
+ return false;
119
+ }
120
+ return AI_DOC_LANGUAGE_PATTERNS.some((pattern) => pattern.test(text));
121
+ }
122
+ function evidence(kind, weight, summary, observedAt) {
123
+ return {
124
+ kind,
125
+ weight: roundScore(weight),
126
+ summary: collapseInline(summary),
127
+ observedAt,
128
+ };
129
+ }
130
+ function fatigueLevel(score, hasCluster) {
131
+ if (score >= 0.72 || (hasCluster && score >= 0.58))
132
+ return "defer";
133
+ if (score >= 0.4)
134
+ return "deprioritize";
135
+ if (score >= 0.18)
136
+ return "watch";
137
+ return "none";
138
+ }
139
+ function priorityAdjustment(level) {
140
+ if (level === "defer")
141
+ return "defer";
142
+ if (level === "deprioritize" || level === "watch")
143
+ return "deprioritize";
144
+ return "none";
145
+ }
146
+ function recheckAfterHours(level) {
147
+ if (level === "defer")
148
+ return 12;
149
+ if (level === "deprioritize")
150
+ return 24;
151
+ if (level === "watch")
152
+ return 48;
153
+ return 168;
154
+ }
155
+ function fatigueMultiplier(level) {
156
+ if (level === "defer")
157
+ return 0.05;
158
+ if (level === "deprioritize")
159
+ return 0.35;
160
+ if (level === "watch")
161
+ return 0.7;
162
+ return 1;
163
+ }
164
+ function finiteScore(value, fallback) {
165
+ if (!Number.isFinite(value))
166
+ return fallback;
167
+ return Math.min(1, Math.max(0, value));
168
+ }
169
+ function isCacheFresh(cache, now) {
170
+ const computedAt = parseInstant(cache?.computedAt);
171
+ if (!cache || !computedAt)
172
+ return false;
173
+ return hoursBetween(computedAt, now) <= FATIGUE_CACHE_TTL_HOURS;
174
+ }
175
+ function cacheMatchesRepo(cache, repoFullName) {
176
+ if (!cache)
177
+ return false;
178
+ try {
179
+ return normalizeRepoFullName(cache.repoFullName) === normalizeRepoFullName(repoFullName);
180
+ }
181
+ catch {
182
+ return false;
183
+ }
184
+ }
185
+ function normalizeRepoFullName(value) {
186
+ const normalized = value.trim().toLowerCase();
187
+ if (!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/u.test(normalized)) {
188
+ throw new Error("AI policy fatigue cache entries require a repo full name in owner/name form.");
189
+ }
190
+ return normalized;
191
+ }
192
+ function addHours(instant, hours) {
193
+ return new Date(new Date(instant).getTime() + hours * 3_600_000).toISOString();
194
+ }
195
+ function cacheVerdict(cache) {
196
+ const computedAt = parseInstant(cache.computedAt);
197
+ return {
198
+ ...cache.verdict,
199
+ evidence: [
200
+ evidence("cache_fresh", 0, `cached fatigue verdict reused from ${computedAt ?? "unknown time"}`, computedAt),
201
+ ...cache.verdict.evidence,
202
+ ],
203
+ };
204
+ }
205
+ function markdownSafe(value) {
206
+ return collapseInline(value).replace(/[\\`*_[\]<>|]/gu, "\\$&");
207
+ }
208
+ function renderEvidenceItem(item) {
209
+ const observed = item.observedAt ? ` (${item.observedAt})` : "";
210
+ return `- ${markdownSafe(item.kind)}: ${item.weight.toFixed(6)}${observed} - ${markdownSafe(item.summary)}`;
211
+ }
212
+ /**
213
+ * Conservative by design (#2305): explicit ban phrases deny a repo, but ambiguous or absent policy text stays
214
+ * allowed. False negatives can be tightened with new literal phrases; false positives would hide valid work.
215
+ */
216
+ export function scanAiPolicyText(content, source) {
217
+ const text = content ?? "";
218
+ if (source === "none" || text.trim().length === 0) {
219
+ return { allowed: true, matchedPhrase: null, source };
220
+ }
221
+ for (const ban of BAN_PHRASES) {
222
+ if (ban.pattern.test(text)) {
223
+ return { allowed: false, matchedPhrase: ban.phrase, source };
224
+ }
225
+ }
226
+ return { allowed: true, matchedPhrase: null, source };
227
+ }
228
+ export function resolveAiPolicyVerdict(docs) {
229
+ // An empty or whitespace-only AI-USAGE.md carries no policy and must fall through to CONTRIBUTING.md,
230
+ // exactly as an absent (null/undefined) file does — otherwise a stub AI-USAGE.md silently fails open and
231
+ // swallows a real ban declared in CONTRIBUTING.md (#2305).
232
+ if (docs.aiUsage !== null && docs.aiUsage !== undefined && docs.aiUsage.trim().length > 0) {
233
+ return scanAiPolicyText(docs.aiUsage, "AI-USAGE.md");
234
+ }
235
+ if (docs.contributing !== null && docs.contributing !== undefined) {
236
+ return scanAiPolicyText(docs.contributing, "CONTRIBUTING.md");
237
+ }
238
+ return { ...AI_POLICY_ALLOWED };
239
+ }
240
+ export function resolveAiPolicyFatigueVerdict(input) {
241
+ const hardPolicy = resolveAiPolicyVerdict(input.docs);
242
+ const now = requireNow(input.now);
243
+ if (!hardPolicy.allowed) {
244
+ return {
245
+ ...hardPolicy,
246
+ fatigue: {
247
+ level: "none",
248
+ priorityAdjustment: "none",
249
+ score: 0,
250
+ recheckAfterHours: 168,
251
+ evidence: [
252
+ evidence("formal_ban_overrides", 0, `formal AI policy ban from ${hardPolicy.source} remains authoritative`, null),
253
+ ],
254
+ },
255
+ };
256
+ }
257
+ if (isCacheFresh(input.cache, now) && cacheMatchesRepo(input.cache, input.repoFullName)) {
258
+ return {
259
+ ...hardPolicy,
260
+ fatigue: cacheVerdict(input.cache),
261
+ };
262
+ }
263
+ const evidenceItems = [];
264
+ const aiAttributedClosed = (input.pullRequests ?? []).filter((pr) => isAiAttributed(pr) && isClosedWithoutMerge(pr));
265
+ const terseRejected = aiAttributedClosed.filter(isTerseRejection);
266
+ if (aiAttributedClosed.length > 0) {
267
+ const newest = aiAttributedClosed
268
+ .map((pr) => parseInstant(pr.closedAt) ?? parseInstant(pr.createdAt))
269
+ .filter((value) => Boolean(value))
270
+ .sort()
271
+ .at(-1) ?? null;
272
+ evidenceItems.push(evidence("ai_attributed_closed_pr", Math.min(0.35, 0.18 + aiAttributedClosed.length / 20), `${aiAttributedClosed.length} closed AI-attributed PR metadata row(s) observed`, newest));
273
+ }
274
+ if (terseRejected.length >= 2) {
275
+ const newest = terseRejected
276
+ .map((pr) => parseInstant(pr.closedAt) ?? parseInstant(pr.createdAt))
277
+ .filter((value) => Boolean(value))
278
+ .sort()
279
+ .at(-1) ?? null;
280
+ const ratio = terseRejected.length / Math.max(1, aiAttributedClosed.length);
281
+ evidenceItems.push(evidence("terse_ai_attributed_rejection_cluster", Math.min(0.62, ratio * 0.45 + Math.min(0.17, terseRejected.length / 20)), `${terseRejected.length}/${aiAttributedClosed.length} AI-attributed closed PRs have terse or templated rejection metadata`, newest));
282
+ }
283
+ for (const change of input.docChanges ?? []) {
284
+ if (!isAiDocLanguage(change))
285
+ continue;
286
+ const observedAt = parseInstant(change.changedAt);
287
+ const weight = 0.28 * recencyWeight(observedAt, now, 30);
288
+ evidenceItems.push(evidence("recent_ai_doc_language", weight, `${change.path} added AI/automation language short of a formal ban phrase`, observedAt));
289
+ }
290
+ const totalScore = roundScore(evidenceItems.reduce((sum, item) => sum + item.weight, 0));
291
+ const hasCluster = evidenceItems.some((item) => item.kind === "terse_ai_attributed_rejection_cluster");
292
+ const level = fatigueLevel(totalScore, hasCluster);
293
+ const fatigue = {
294
+ level,
295
+ priorityAdjustment: priorityAdjustment(level),
296
+ score: totalScore,
297
+ recheckAfterHours: recheckAfterHours(level),
298
+ evidence: evidenceItems,
299
+ };
300
+ return {
301
+ ...hardPolicy,
302
+ fatigue: fatigue.evidence.length === 0 ? { ...AI_FATIGUE_NONE } : fatigue,
303
+ };
304
+ }
305
+ export function renderAiPolicyFatigueMarkdown(verdict) {
306
+ const fatigue = verdict.fatigue ?? AI_FATIGUE_NONE;
307
+ const lines = [
308
+ "# AI Policy Fatigue",
309
+ "",
310
+ `Hard policy allowed: ${verdict.allowed ? "yes" : "no"}`,
311
+ `Policy source: ${markdownSafe(verdict.source)}`,
312
+ `Fatigue level: ${fatigue.level}`,
313
+ `Priority adjustment: ${fatigue.priorityAdjustment}`,
314
+ `Fatigue score: ${fatigue.score.toFixed(6)}`,
315
+ `Recheck after: ${fatigue.recheckAfterHours}h`,
316
+ "",
317
+ "## Evidence",
318
+ "",
319
+ fatigue.evidence.length === 0 ? "- none" : fatigue.evidence.map(renderEvidenceItem).join("\n"),
320
+ ];
321
+ return `${lines.join("\n")}\n`;
322
+ }
323
+ export function applyAiPolicyFatigueToRankInput(rankInput, verdict) {
324
+ const fatigue = verdict.fatigue ?? AI_FATIGUE_NONE;
325
+ const multiplier = verdict.allowed ? fatigueMultiplier(fatigue.level) : 0;
326
+ const reasons = [];
327
+ if (!verdict.allowed) {
328
+ reasons.push(`formal AI policy denial from ${verdict.source}`);
329
+ }
330
+ if (fatigue.priorityAdjustment !== "none") {
331
+ reasons.push(`AI-fatigue ${fatigue.priorityAdjustment} signal (${fatigue.level})`);
332
+ }
333
+ for (const item of fatigue.evidence.slice(0, 3)) {
334
+ reasons.push(item.summary);
335
+ }
336
+ return {
337
+ potential: roundScore(finiteScore(rankInput.potential, 0) * multiplier),
338
+ feasibility: finiteScore(rankInput.feasibility, 0),
339
+ laneFit: finiteScore(rankInput.laneFit, 0),
340
+ freshness: finiteScore(rankInput.freshness, 0),
341
+ dupRisk: verdict.allowed ? finiteScore(rankInput.dupRisk, 1) : 1,
342
+ fatigueLevel: fatigue.level,
343
+ priorityAdjustment: verdict.allowed ? fatigue.priorityAdjustment : "defer",
344
+ fatigueMultiplier: multiplier,
345
+ deferUntilHours: verdict.allowed && fatigue.priorityAdjustment === "defer" ? fatigue.recheckAfterHours : null,
346
+ reasons,
347
+ };
348
+ }
349
+ export function createAiPolicyFatigueCacheEntry(input) {
350
+ return {
351
+ repoFullName: normalizeRepoFullName(input.repoFullName),
352
+ computedAt: requireNow(input.computedAt).toISOString(),
353
+ verdict: input.verdict,
354
+ };
355
+ }
356
+ export function describeAiPolicyFatigueCache(cache, now) {
357
+ const computedAt = parseInstant(cache?.computedAt);
358
+ const current = requireNow(now);
359
+ if (!cache || !computedAt) {
360
+ return {
361
+ repoFullName: cache?.repoFullName ? collapseInline(cache.repoFullName).toLowerCase() : "unknown",
362
+ computedAt: null,
363
+ expiresAt: null,
364
+ ageHours: null,
365
+ fresh: false,
366
+ };
367
+ }
368
+ const ageHours = roundHours(hoursBetween(computedAt, current));
369
+ const expiresAt = addHours(computedAt, FATIGUE_CACHE_TTL_HOURS);
370
+ return {
371
+ repoFullName: normalizeRepoFullName(cache.repoFullName),
372
+ computedAt,
373
+ expiresAt,
374
+ ageHours,
375
+ fresh: ageHours <= FATIGUE_CACHE_TTL_HOURS,
376
+ };
377
+ }
@@ -0,0 +1,56 @@
1
+ import { type PortfolioConvergenceThresholds } from "./portfolio/non-convergence.js";
2
+ /** Whether a real attempt is allowed to actually submit (open a PR), or only compute + log its decision.
3
+ * Mirrors `src/settings/autonomy.ts`'s deny-by-default dial: "observe" still runs every real signal/decision,
4
+ * it just never lets `wouldBeAction` become a real write. */
5
+ export type AmsSubmissionMode = "observe" | "enforce";
6
+ /** The strictest self-review slop band still allowed to reach submission (`isSlopBandWithinThreshold`,
7
+ * submission-gate.ts). Lower = stricter: "clean" only lets the cleanest band through. */
8
+ export type AmsSlopThreshold = "clean" | "low" | "elevated" | "high";
9
+ /** The three Governor cap ceilings (`GovernorCapLimits`, budget-cap.ts) for one attempt. */
10
+ export type AmsCapLimits = {
11
+ /** Maximum cumulative budget/cost units (may be fractional, e.g. a dollar cost) permitted for one attempt. */
12
+ budget: number;
13
+ /** Maximum cumulative turns/iterations permitted for one attempt. */
14
+ turns: number;
15
+ /** Termination ceiling: maximum elapsed session time in milliseconds for one attempt. */
16
+ elapsedMs: number;
17
+ };
18
+ /** Per-operator AMS execution policy parsed from `.gittensory-ams.yml`. See {@link DEFAULT_AMS_POLICY_SPEC}. */
19
+ export type AmsPolicySpec = {
20
+ /** Whether a real attempt may actually submit. Default: "observe" (deny-by-default). */
21
+ submissionMode: AmsSubmissionMode;
22
+ /** The strictest self-review slop band still allowed to reach submission. Default: "low" (conservative). */
23
+ slopThreshold: AmsSlopThreshold;
24
+ /** Governor cap ceilings for one attempt. Default: { budget: 5, turns: 20, elapsedMs: 1_800_000 } (30 min). */
25
+ capLimits: AmsCapLimits;
26
+ /** Non-convergence detector thresholds. Default: {@link DEFAULT_PORTFOLIO_CONVERGENCE_THRESHOLDS}. */
27
+ convergenceThresholds: PortfolioConvergenceThresholds;
28
+ /** Hard ceiling on the iterate loop's own iteration count (IterateLoopInput.maxIterations). Default: 3. */
29
+ maxIterations: number;
30
+ /** Per-iteration turn budget passed to the coding-agent driver (IterateLoopInput.maxTurnsPerIteration).
31
+ * Default: 6. */
32
+ maxTurnsPerIteration: number;
33
+ };
34
+ /** The tolerant parser result for `.gittensory-ams.yml`. Mirrors `ParsedMinerGoalSpec`'s present/warnings shape. */
35
+ export type ParsedAmsPolicySpec = {
36
+ present: boolean;
37
+ spec: AmsPolicySpec;
38
+ warnings: string[];
39
+ };
40
+ /**
41
+ * The safe defaults applied when a field is absent from `.gittensory-ams.yml` (or the file itself is
42
+ * missing). Deep-frozen: a shared singleton, clone before layering overrides on top.
43
+ */
44
+ export declare const DEFAULT_AMS_POLICY_SPEC: Readonly<AmsPolicySpec>;
45
+ /**
46
+ * Tolerantly normalize an already-parsed `.gittensory-ams.yml` object into a {@link ParsedAmsPolicySpec}.
47
+ * Never throws: malformed shapes degrade to safe defaults and accumulate warnings.
48
+ */
49
+ export declare function parseAmsPolicySpec(raw: unknown): ParsedAmsPolicySpec;
50
+ /**
51
+ * Parse raw `.gittensory-ams.yml` file content (JSON or YAML). Malformed content degrades to an absent
52
+ * policy spec with a warning rather than throwing, mirroring `parseMinerGoalSpecContent`.
53
+ */
54
+ export declare function parseAmsPolicySpecContent(content: string | null | undefined): ParsedAmsPolicySpec;
55
+ /** The documented `.gittensory-ams` file-discovery order (first match wins), mirroring `MINER_GOAL_SPEC_FILENAMES`. */
56
+ export declare const AMS_POLICY_SPEC_FILENAMES: readonly [".gittensory-ams.yml", ".github/gittensory-ams.yml", ".gittensory-ams.json", ".github/gittensory-ams.json"];
@@ -0,0 +1,165 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ import { DEFAULT_PORTFOLIO_CONVERGENCE_THRESHOLDS } from "./portfolio/non-convergence.js";
3
+ /**
4
+ * The safe defaults applied when a field is absent from `.gittensory-ams.yml` (or the file itself is
5
+ * missing). Deep-frozen: a shared singleton, clone before layering overrides on top.
6
+ */
7
+ export const DEFAULT_AMS_POLICY_SPEC = Object.freeze({
8
+ submissionMode: "observe",
9
+ slopThreshold: "low",
10
+ capLimits: Object.freeze({ budget: 5, turns: 20, elapsedMs: 1_800_000 }),
11
+ convergenceThresholds: Object.freeze({ ...DEFAULT_PORTFOLIO_CONVERGENCE_THRESHOLDS }),
12
+ maxIterations: 3,
13
+ maxTurnsPerIteration: 6,
14
+ });
15
+ const MAX_AMS_POLICY_SPEC_BYTES = 8_192;
16
+ function cloneDefaultAmsPolicySpec() {
17
+ return {
18
+ submissionMode: DEFAULT_AMS_POLICY_SPEC.submissionMode,
19
+ slopThreshold: DEFAULT_AMS_POLICY_SPEC.slopThreshold,
20
+ capLimits: { ...DEFAULT_AMS_POLICY_SPEC.capLimits },
21
+ convergenceThresholds: { ...DEFAULT_AMS_POLICY_SPEC.convergenceThresholds },
22
+ maxIterations: DEFAULT_AMS_POLICY_SPEC.maxIterations,
23
+ maxTurnsPerIteration: DEFAULT_AMS_POLICY_SPEC.maxTurnsPerIteration,
24
+ };
25
+ }
26
+ function emptyAmsPolicySpec(warnings = []) {
27
+ return { present: false, spec: cloneDefaultAmsPolicySpec(), warnings };
28
+ }
29
+ function normalizeSubmissionMode(value, fallback, warnings) {
30
+ if (value === undefined || value === null)
31
+ return fallback;
32
+ if (value === "observe" || value === "enforce")
33
+ return value;
34
+ warnings.push(`AmsPolicySpec field "submissionMode" must be one of observe, enforce; falling back to "${fallback}".`);
35
+ return fallback;
36
+ }
37
+ function normalizeSlopThreshold(value, fallback, warnings) {
38
+ if (value === undefined || value === null)
39
+ return fallback;
40
+ if (value === "clean" || value === "low" || value === "elevated" || value === "high")
41
+ return value;
42
+ warnings.push(`AmsPolicySpec field "slopThreshold" must be one of clean, low, elevated, high; falling back to "${fallback}".`);
43
+ return fallback;
44
+ }
45
+ function normalizePositiveNumber(value, field, fallback, warnings) {
46
+ if (value === undefined || value === null)
47
+ return fallback;
48
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
49
+ warnings.push(`AmsPolicySpec field "${field}" must be a non-negative number; falling back to ${fallback}.`);
50
+ return fallback;
51
+ }
52
+ return value;
53
+ }
54
+ /** Like normalizePositiveNumber, but floors to a whole count -- for fields that are semantically integer
55
+ * counts (an iteration/turn budget), matching MinerGoalSpec's own normalizePositiveInteger convention. */
56
+ function normalizeNonNegativeInteger(value, field, fallback, warnings) {
57
+ const normalized = normalizePositiveNumber(value, field, fallback, warnings);
58
+ return Math.floor(normalized);
59
+ }
60
+ function normalizeCapLimits(value, fallback, warnings) {
61
+ if (value === undefined || value === null)
62
+ return fallback;
63
+ if (typeof value !== "object" || Array.isArray(value)) {
64
+ warnings.push('AmsPolicySpec field "capLimits" must be a mapping; falling back to defaults.');
65
+ return fallback;
66
+ }
67
+ const record = value;
68
+ return {
69
+ budget: normalizePositiveNumber(record.budget, "capLimits.budget", fallback.budget, warnings),
70
+ turns: normalizePositiveNumber(record.turns, "capLimits.turns", fallback.turns, warnings),
71
+ elapsedMs: normalizePositiveNumber(record.elapsedMs, "capLimits.elapsedMs", fallback.elapsedMs, warnings),
72
+ };
73
+ }
74
+ function normalizeConvergenceThresholds(value, fallback, warnings) {
75
+ if (value === undefined || value === null)
76
+ return fallback;
77
+ if (typeof value !== "object" || Array.isArray(value)) {
78
+ warnings.push('AmsPolicySpec field "convergenceThresholds" must be a mapping; falling back to defaults.');
79
+ return fallback;
80
+ }
81
+ const record = value;
82
+ return {
83
+ maxConsecutiveFailures: normalizePositiveNumber(record.maxConsecutiveFailures, "convergenceThresholds.maxConsecutiveFailures", fallback.maxConsecutiveFailures, warnings),
84
+ maxReenqueues: normalizePositiveNumber(record.maxReenqueues, "convergenceThresholds.maxReenqueues", fallback.maxReenqueues, warnings),
85
+ };
86
+ }
87
+ function hasConfiguredPolicyFields(spec) {
88
+ return (spec.submissionMode !== DEFAULT_AMS_POLICY_SPEC.submissionMode ||
89
+ spec.slopThreshold !== DEFAULT_AMS_POLICY_SPEC.slopThreshold ||
90
+ spec.capLimits.budget !== DEFAULT_AMS_POLICY_SPEC.capLimits.budget ||
91
+ spec.capLimits.turns !== DEFAULT_AMS_POLICY_SPEC.capLimits.turns ||
92
+ spec.capLimits.elapsedMs !== DEFAULT_AMS_POLICY_SPEC.capLimits.elapsedMs ||
93
+ spec.convergenceThresholds.maxConsecutiveFailures !== DEFAULT_AMS_POLICY_SPEC.convergenceThresholds.maxConsecutiveFailures ||
94
+ spec.convergenceThresholds.maxReenqueues !== DEFAULT_AMS_POLICY_SPEC.convergenceThresholds.maxReenqueues ||
95
+ spec.maxIterations !== DEFAULT_AMS_POLICY_SPEC.maxIterations ||
96
+ spec.maxTurnsPerIteration !== DEFAULT_AMS_POLICY_SPEC.maxTurnsPerIteration);
97
+ }
98
+ function utf8ByteLength(value) {
99
+ let bytes = 0;
100
+ for (const char of value) {
101
+ const codePoint = char.codePointAt(0);
102
+ if (codePoint <= 0x7f)
103
+ bytes += 1;
104
+ else if (codePoint <= 0x7ff)
105
+ bytes += 2;
106
+ else if (codePoint <= 0xffff)
107
+ bytes += 3;
108
+ else
109
+ bytes += 4;
110
+ }
111
+ return bytes;
112
+ }
113
+ /**
114
+ * Tolerantly normalize an already-parsed `.gittensory-ams.yml` object into a {@link ParsedAmsPolicySpec}.
115
+ * Never throws: malformed shapes degrade to safe defaults and accumulate warnings.
116
+ */
117
+ export function parseAmsPolicySpec(raw) {
118
+ if (raw === undefined || raw === null)
119
+ return emptyAmsPolicySpec();
120
+ if (typeof raw !== "object" || Array.isArray(raw)) {
121
+ return emptyAmsPolicySpec(["AmsPolicySpec must be a mapping of fields; ignoring malformed config and falling back to safe defaults."]);
122
+ }
123
+ const record = raw;
124
+ const warnings = [];
125
+ const spec = {
126
+ submissionMode: normalizeSubmissionMode(record.submissionMode, DEFAULT_AMS_POLICY_SPEC.submissionMode, warnings),
127
+ slopThreshold: normalizeSlopThreshold(record.slopThreshold, DEFAULT_AMS_POLICY_SPEC.slopThreshold, warnings),
128
+ capLimits: normalizeCapLimits(record.capLimits, DEFAULT_AMS_POLICY_SPEC.capLimits, warnings),
129
+ convergenceThresholds: normalizeConvergenceThresholds(record.convergenceThresholds, DEFAULT_AMS_POLICY_SPEC.convergenceThresholds, warnings),
130
+ maxIterations: normalizeNonNegativeInteger(record.maxIterations, "maxIterations", DEFAULT_AMS_POLICY_SPEC.maxIterations, warnings),
131
+ maxTurnsPerIteration: normalizeNonNegativeInteger(record.maxTurnsPerIteration, "maxTurnsPerIteration", DEFAULT_AMS_POLICY_SPEC.maxTurnsPerIteration, warnings),
132
+ };
133
+ if (!hasConfiguredPolicyFields(spec)) {
134
+ warnings.push("AmsPolicySpec contained no recognized non-default policy fields; falling back to safe defaults.");
135
+ return { present: false, spec: cloneDefaultAmsPolicySpec(), warnings };
136
+ }
137
+ return { present: true, spec, warnings };
138
+ }
139
+ /**
140
+ * Parse raw `.gittensory-ams.yml` file content (JSON or YAML). Malformed content degrades to an absent
141
+ * policy spec with a warning rather than throwing, mirroring `parseMinerGoalSpecContent`.
142
+ */
143
+ export function parseAmsPolicySpecContent(content) {
144
+ if (content === undefined || content === null || content.trim() === "")
145
+ return emptyAmsPolicySpec();
146
+ if (utf8ByteLength(content) > MAX_AMS_POLICY_SPEC_BYTES) {
147
+ return emptyAmsPolicySpec([`AmsPolicySpec content exceeded ${MAX_AMS_POLICY_SPEC_BYTES} bytes; ignoring it and falling back to safe defaults.`]);
148
+ }
149
+ const trimmed = content.trim();
150
+ const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("[");
151
+ let parsed;
152
+ try {
153
+ parsed = looksLikeJson ? JSON.parse(trimmed) : parseYaml(trimmed);
154
+ }
155
+ catch {
156
+ return emptyAmsPolicySpec([
157
+ looksLikeJson
158
+ ? "AmsPolicySpec content was not valid JSON; ignoring it and falling back to safe defaults."
159
+ : "AmsPolicySpec content was not valid YAML; ignoring it and falling back to safe defaults.",
160
+ ]);
161
+ }
162
+ return parseAmsPolicySpec(parsed);
163
+ }
164
+ /** The documented `.gittensory-ams` file-discovery order (first match wins), mirroring `MINER_GOAL_SPEC_FILENAMES`. */
165
+ export const AMS_POLICY_SPEC_FILENAMES = [".gittensory-ams.yml", ".github/gittensory-ams.yml", ".gittensory-ams.json", ".github/gittensory-ams.json"];
@@ -0,0 +1,23 @@
1
+ import { type Phase7CalibrationLoopResult } from "./phase7-calibration-loop.js";
2
+ export type CalibrationDashboardStatus = "on_track" | "below_baseline" | "insufficient_signal" | "disabled";
3
+ /** One labeled row in the dashboard: a metric name, its formatted value, and a short detail line. */
4
+ export type CalibrationDashboardRow = {
5
+ label: string;
6
+ value: string;
7
+ detail: string;
8
+ };
9
+ /** The read-only dashboard projection of a calibration-loop result. */
10
+ export type CalibrationDashboardView = {
11
+ status: CalibrationDashboardStatus;
12
+ headline: string;
13
+ rows: readonly CalibrationDashboardRow[];
14
+ holdReasons: readonly string[];
15
+ };
16
+ /** Classify the overall calibration state for the dashboard's headline banner. */
17
+ export declare function resolveCalibrationDashboardStatus(result: Phase7CalibrationLoopResult): CalibrationDashboardStatus;
18
+ /**
19
+ * Project a computed {@link Phase7CalibrationLoopResult} into a read-only dashboard view. Pure and deterministic;
20
+ * adds no computation of its own. `holdReasons` are surfaced verbatim so an operator can see why an autonomy
21
+ * increase is (or isn't) permitted.
22
+ */
23
+ export declare function buildCalibrationDashboardView(result: Phase7CalibrationLoopResult): CalibrationDashboardView;