@aragon-agent/cli 0.6.44 → 0.6.46

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 (255) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +120 -2
  3. package/dist/agent/controller.d.ts +69 -0
  4. package/dist/agent/controller.js +224 -11
  5. package/dist/agent/governance-view.d.ts +40 -0
  6. package/dist/agent/governance-view.js +40 -0
  7. package/dist/agent/governance-wiring.d.ts +162 -0
  8. package/dist/agent/governance-wiring.js +470 -0
  9. package/dist/agent/invocation-card.d.ts +77 -0
  10. package/dist/agent/invocation-card.js +169 -0
  11. package/dist/agent/plan-prompt.d.ts +7 -0
  12. package/dist/agent/plan-prompt.js +2 -1
  13. package/dist/agent/prompt-options.d.ts +6 -0
  14. package/dist/agent/queued-messages.d.ts +3 -0
  15. package/dist/agent/queued-messages.js +4 -1
  16. package/dist/agent/reducer.d.ts +38 -1
  17. package/dist/agent/reducer.js +57 -12
  18. package/dist/agent/system-prompt.d.ts +19 -0
  19. package/dist/agent/system-prompt.js +9 -1
  20. package/dist/cli.js +45 -1
  21. package/dist/commands/builtins.js +22 -4
  22. package/dist/commands/governance-command.d.ts +35 -0
  23. package/dist/commands/governance-command.js +304 -0
  24. package/dist/commands/governance-runs-table.d.ts +40 -0
  25. package/dist/commands/governance-runs-table.js +130 -0
  26. package/dist/commands/index-build.d.ts +8 -0
  27. package/dist/commands/index-build.js +11 -0
  28. package/dist/commands/registry.d.ts +35 -0
  29. package/dist/commands/registry.js +20 -7
  30. package/dist/commands/skills.js +36 -2
  31. package/dist/commands/subagent-command.js +3 -1
  32. package/dist/commands/team-command.d.ts +6 -0
  33. package/dist/commands/team-command.js +40 -3
  34. package/dist/config/app-paths.d.ts +10 -0
  35. package/dist/config/app-paths.js +12 -0
  36. package/dist/config/cli-commands.d.ts +18 -0
  37. package/dist/config/cli-commands.js +47 -1
  38. package/dist/config/env.d.ts +2 -1
  39. package/dist/config/env.js +10 -1
  40. package/dist/config/governance-config.d.ts +125 -0
  41. package/dist/config/governance-config.js +164 -0
  42. package/dist/config/governance-launch.d.ts +71 -0
  43. package/dist/config/governance-launch.js +58 -0
  44. package/dist/config/load.d.ts +15 -0
  45. package/dist/config/load.js +26 -1
  46. package/dist/config/model-profile-store.d.ts +4 -2
  47. package/dist/config/schema.d.ts +22 -0
  48. package/dist/config/schema.js +6 -0
  49. package/dist/config/store.js +14 -1
  50. package/dist/config/ui-state.d.ts +11 -0
  51. package/dist/config/ui-state.js +11 -0
  52. package/dist/diagnostics/doctor.js +20 -0
  53. package/dist/governance/card-normalize.d.ts +24 -0
  54. package/dist/governance/card-normalize.js +76 -0
  55. package/dist/governance/copy.d.ts +109 -0
  56. package/dist/governance/copy.js +223 -0
  57. package/dist/governance/engine/aftermath.d.ts +49 -0
  58. package/dist/governance/engine/aftermath.js +134 -0
  59. package/dist/governance/engine/artifact-persist.d.ts +35 -0
  60. package/dist/governance/engine/artifact-persist.js +86 -0
  61. package/dist/governance/engine/budget.d.ts +52 -0
  62. package/dist/governance/engine/budget.js +112 -0
  63. package/dist/governance/engine/cards.d.ts +18 -0
  64. package/dist/governance/engine/cards.js +186 -0
  65. package/dist/governance/engine/digest.d.ts +32 -0
  66. package/dist/governance/engine/digest.js +63 -0
  67. package/dist/governance/engine/engine-core.d.ts +219 -0
  68. package/dist/governance/engine/engine-core.js +562 -0
  69. package/dist/governance/engine/engine.d.ts +68 -0
  70. package/dist/governance/engine/engine.js +314 -0
  71. package/dist/governance/engine/estimate.d.ts +38 -0
  72. package/dist/governance/engine/estimate.js +46 -0
  73. package/dist/governance/engine/inspector.d.ts +16 -0
  74. package/dist/governance/engine/inspector.js +396 -0
  75. package/dist/governance/engine/integration.d.ts +35 -0
  76. package/dist/governance/engine/integration.js +220 -0
  77. package/dist/governance/engine/lifecycle.d.ts +29 -0
  78. package/dist/governance/engine/lifecycle.js +656 -0
  79. package/dist/governance/engine/pause.d.ts +56 -0
  80. package/dist/governance/engine/pause.js +107 -0
  81. package/dist/governance/engine/phase-members.d.ts +15 -0
  82. package/dist/governance/engine/phase-members.js +34 -0
  83. package/dist/governance/engine/phase-planner.d.ts +53 -0
  84. package/dist/governance/engine/phase-planner.js +282 -0
  85. package/dist/governance/engine/rebuild.d.ts +27 -0
  86. package/dist/governance/engine/rebuild.js +57 -0
  87. package/dist/governance/engine/report.d.ts +66 -0
  88. package/dist/governance/engine/report.js +220 -0
  89. package/dist/governance/engine/run-digest.d.ts +20 -0
  90. package/dist/governance/engine/run-digest.js +42 -0
  91. package/dist/governance/engine/spec-builder.d.ts +117 -0
  92. package/dist/governance/engine/spec-builder.js +321 -0
  93. package/dist/governance/engine/stage-runner.d.ts +22 -0
  94. package/dist/governance/engine/stage-runner.js +664 -0
  95. package/dist/governance/engine/start-input.d.ts +19 -0
  96. package/dist/governance/engine/start-input.js +111 -0
  97. package/dist/governance/engine/state-machine.d.ts +101 -0
  98. package/dist/governance/engine/state-machine.js +586 -0
  99. package/dist/governance/engine/submission-queries.d.ts +42 -0
  100. package/dist/governance/engine/submission-queries.js +97 -0
  101. package/dist/governance/engine/submission-render.d.ts +12 -0
  102. package/dist/governance/engine/submission-render.js +92 -0
  103. package/dist/governance/engine/submissions.d.ts +27 -0
  104. package/dist/governance/engine/submissions.js +455 -0
  105. package/dist/governance/engine/visibility.d.ts +104 -0
  106. package/dist/governance/engine/visibility.js +223 -0
  107. package/dist/governance/governed-dispatch.d.ts +19 -0
  108. package/dist/governance/governed-dispatch.js +17 -0
  109. package/dist/governance/limits.d.ts +107 -0
  110. package/dist/governance/limits.js +131 -0
  111. package/dist/governance/posture-transition.d.ts +38 -0
  112. package/dist/governance/posture-transition.js +44 -0
  113. package/dist/governance/posture.d.ts +35 -0
  114. package/dist/governance/posture.js +59 -0
  115. package/dist/governance/profiles.d.ts +48 -0
  116. package/dist/governance/profiles.js +140 -0
  117. package/dist/governance/prompts/orchestrator-block.d.ts +40 -0
  118. package/dist/governance/prompts/orchestrator-block.js +128 -0
  119. package/dist/governance/prompts/plan-addendum.d.ts +22 -0
  120. package/dist/governance/prompts/plan-addendum.js +60 -0
  121. package/dist/governance/prompts/refusals.d.ts +48 -0
  122. package/dist/governance/prompts/refusals.js +82 -0
  123. package/dist/governance/prompts/role-blocks.d.ts +40 -0
  124. package/dist/governance/prompts/role-blocks.js +255 -0
  125. package/dist/governance/report-reader.d.ts +31 -0
  126. package/dist/governance/report-reader.js +57 -0
  127. package/dist/governance/roles.d.ts +83 -0
  128. package/dist/governance/roles.js +142 -0
  129. package/dist/governance/rules/claims.d.ts +83 -0
  130. package/dist/governance/rules/claims.js +250 -0
  131. package/dist/governance/rules/elimination.d.ts +66 -0
  132. package/dist/governance/rules/elimination.js +105 -0
  133. package/dist/governance/rules/events-table.d.ts +46 -0
  134. package/dist/governance/rules/events-table.js +94 -0
  135. package/dist/governance/rules/failure-analysis.d.ts +28 -0
  136. package/dist/governance/rules/failure-analysis.js +93 -0
  137. package/dist/governance/rules/ledger-replay.d.ts +53 -0
  138. package/dist/governance/rules/ledger-replay.js +53 -0
  139. package/dist/governance/rules/ledger-state.d.ts +85 -0
  140. package/dist/governance/rules/ledger-state.js +175 -0
  141. package/dist/governance/rules/ledger.d.ts +228 -0
  142. package/dist/governance/rules/ledger.js +560 -0
  143. package/dist/governance/rules/rca.d.ts +68 -0
  144. package/dist/governance/rules/rca.js +220 -0
  145. package/dist/governance/rules/settlement.d.ts +129 -0
  146. package/dist/governance/rules/settlement.js +363 -0
  147. package/dist/governance/rules/suspicion.d.ts +66 -0
  148. package/dist/governance/rules/suspicion.js +169 -0
  149. package/dist/governance/store/artifacts.d.ts +81 -0
  150. package/dist/governance/store/artifacts.js +173 -0
  151. package/dist/governance/store/atomic.d.ts +79 -0
  152. package/dist/governance/store/atomic.js +119 -0
  153. package/dist/governance/store/events-log.d.ts +35 -0
  154. package/dist/governance/store/events-log.js +86 -0
  155. package/dist/governance/store/run-lock.d.ts +51 -0
  156. package/dist/governance/store/run-lock.js +122 -0
  157. package/dist/governance/store/run-store.d.ts +229 -0
  158. package/dist/governance/store/run-store.js +644 -0
  159. package/dist/governance/store/scoreboard-export.d.ts +53 -0
  160. package/dist/governance/store/scoreboard-export.js +65 -0
  161. package/dist/governance/tools/child-tools.d.ts +31 -0
  162. package/dist/governance/tools/child-tools.js +65 -0
  163. package/dist/governance/tools/guard.d.ts +71 -0
  164. package/dist/governance/tools/guard.js +194 -0
  165. package/dist/governance/tools/inspector-tools.d.ts +63 -0
  166. package/dist/governance/tools/inspector-tools.js +424 -0
  167. package/dist/governance/tools/lead-tools.d.ts +42 -0
  168. package/dist/governance/tools/lead-tools.js +226 -0
  169. package/dist/governance/tools/normalize-util.d.ts +101 -0
  170. package/dist/governance/tools/normalize-util.js +230 -0
  171. package/dist/governance/tools/scan-incomplete.d.ts +79 -0
  172. package/dist/governance/tools/scan-incomplete.js +486 -0
  173. package/dist/governance/tools/shared-tools.d.ts +44 -0
  174. package/dist/governance/tools/shared-tools.js +125 -0
  175. package/dist/governance/tools/submit-normalizers.d.ts +28 -0
  176. package/dist/governance/tools/submit-normalizers.js +474 -0
  177. package/dist/governance/tools/submit-tools.d.ts +26 -0
  178. package/dist/governance/tools/submit-tools.js +171 -0
  179. package/dist/governance/types.d.ts +661 -0
  180. package/dist/governance/types.js +27 -0
  181. package/dist/session/persist.js +10 -0
  182. package/dist/session/validate-session.js +36 -3
  183. package/dist/team/forced-mode.d.ts +19 -0
  184. package/dist/team/forced-mode.js +42 -20
  185. package/dist/team/runtime.d.ts +8 -0
  186. package/dist/team/runtime.js +9 -0
  187. package/dist/team/subagent.d.ts +23 -0
  188. package/dist/team/subagent.js +14 -3
  189. package/dist/team/types.d.ts +6 -0
  190. package/dist/tools/index.d.ts +30 -7
  191. package/dist/tools/index.js +14 -0
  192. package/dist/tools/plan-tools.d.ts +14 -2
  193. package/dist/tools/plan-tools.js +5 -2
  194. package/dist/ui/App.js +170 -31
  195. package/dist/ui/BottomStatusRow.js +1 -1
  196. package/dist/ui/Composer.d.ts +7 -1
  197. package/dist/ui/Composer.js +9 -6
  198. package/dist/ui/GovernancePanel.d.ts +50 -0
  199. package/dist/ui/GovernancePanel.js +77 -0
  200. package/dist/ui/GovernanceRun.d.ts +18 -0
  201. package/dist/ui/GovernanceRun.js +11 -0
  202. package/dist/ui/StatusBar.d.ts +15 -2
  203. package/dist/ui/StatusBar.js +10 -4
  204. package/dist/ui/Transcript.js +7 -1
  205. package/dist/ui/entries/GovernanceCard.d.ts +30 -0
  206. package/dist/ui/entries/GovernanceCard.js +32 -0
  207. package/dist/ui/entries/InvocationCard.d.ts +20 -0
  208. package/dist/ui/entries/InvocationCard.js +32 -0
  209. package/dist/ui/governance-copy.d.ts +69 -0
  210. package/dist/ui/governance-copy.js +104 -0
  211. package/dist/ui/governance-status.d.ts +145 -0
  212. package/dist/ui/governance-status.js +326 -0
  213. package/dist/ui/interaction-copy.d.ts +4 -0
  214. package/dist/ui/interaction-copy.js +5 -0
  215. package/dist/ui/interaction-hints.d.ts +17 -0
  216. package/dist/ui/interaction-hints.js +14 -2
  217. package/dist/ui/layout/governance-board.d.ts +121 -0
  218. package/dist/ui/layout/governance-board.js +382 -0
  219. package/dist/ui/layout/governance-card.d.ts +53 -0
  220. package/dist/ui/layout/governance-card.js +134 -0
  221. package/dist/ui/layout/governance-chip.d.ts +38 -0
  222. package/dist/ui/layout/governance-chip.js +64 -0
  223. package/dist/ui/layout/governance-panel.d.ts +85 -0
  224. package/dist/ui/layout/governance-panel.js +358 -0
  225. package/dist/ui/layout/governance-roster.d.ts +65 -0
  226. package/dist/ui/layout/governance-roster.js +275 -0
  227. package/dist/ui/layout/invocation-card.d.ts +41 -0
  228. package/dist/ui/layout/invocation-card.js +91 -0
  229. package/dist/ui/layout/status-detail-layout.js +3 -3
  230. package/dist/ui/layout/status-layout.d.ts +5 -2
  231. package/dist/ui/layout/status-layout.js +10 -6
  232. package/dist/ui/layout/virtual-window.js +22 -1
  233. package/dist/ui/overlays/GovernanceBoardOverlay.d.ts +41 -0
  234. package/dist/ui/overlays/GovernanceBoardOverlay.js +76 -0
  235. package/dist/ui/overlays/GovernanceSettingsPage.d.ts +48 -0
  236. package/dist/ui/overlays/GovernanceSettingsPage.js +93 -0
  237. package/dist/ui/overlays/HelpOverlay.js +4 -3
  238. package/dist/ui/overlays/QuestionOverlay.d.ts +9 -1
  239. package/dist/ui/overlays/QuestionOverlay.js +4 -2
  240. package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
  241. package/dist/ui/overlays/SettingsScreen.js +21 -1
  242. package/dist/ui/overlays/governance-settings.d.ts +79 -0
  243. package/dist/ui/overlays/governance-settings.js +140 -0
  244. package/dist/ui/overlays/model-profile-state.d.ts +6 -1
  245. package/dist/ui/overlays/model-profile-state.js +10 -0
  246. package/dist/ui/palettes.d.ts +17 -0
  247. package/dist/ui/palettes.js +9 -0
  248. package/dist/ui/scrollback/live-preview.js +2 -0
  249. package/dist/ui/scrollback/settled-boundary.js +3 -0
  250. package/dist/ui/shortcuts.js +2 -2
  251. package/dist/ui/theme.d.ts +7 -0
  252. package/dist/ui/theme.js +1 -0
  253. package/dist/ui/transcript-text.js +13 -1
  254. package/dist/ui/use-wheel-routing.js +3 -1
  255. package/package.json +2 -2
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Mechanical settlement - structured verdicts -> automatic bookings and routing
3
+ * (governance-mode 5.5).
4
+ *
5
+ * ASCII ONLY (`governance/` is glyph-scanned).
6
+ *
7
+ * Every function here is a PURE DECISION: it returns the events to book, the next route
8
+ * for the work package and any claims to open, and touches no state. The engine books the
9
+ * events through the ledger (I-G7: one accounting path) and applies the route.
10
+ *
11
+ * VALIDATION BEFORE ACCOUNTING (D-15). A submission that is incomplete or contradicts
12
+ * itself comes back as a `refusal` with NOTHING booked and no route - the child fixes the
13
+ * submission and tries again. A refusal text starts with `NOT ACCEPTED:` and says what is
14
+ * wrong and how to fix it; it is plain text, never an error, because an error would trip
15
+ * the model's retry loop. The tool layer decides how many refusals a child gets.
16
+ */
17
+ import { GOV_LIMITS } from '../limits.js';
18
+ import { claimsFromReview, claimsFromTest, findingsIntersect, fingerprintFinding, } from './claims.js';
19
+ import { eventDef } from './events-table.js';
20
+ import { hasBooked } from './ledger.js';
21
+ import { detectOwnershipViolations, detectRubberStamp, detectScopeNarrowing, } from './suspicion.js';
22
+ const NOT_ACCEPTED = 'NOT ACCEPTED: ';
23
+ function emptyResult(route) {
24
+ return {
25
+ ok: true,
26
+ refusal: null,
27
+ events: [],
28
+ route,
29
+ claims: [],
30
+ reversals: [],
31
+ newRejections: [],
32
+ rejection: null,
33
+ notes: [],
34
+ };
35
+ }
36
+ function engineEvent(ctx, code, agentId, detail) {
37
+ return {
38
+ code,
39
+ agentId,
40
+ at: ctx.at,
41
+ phase: ctx.phase,
42
+ by: 'engine',
43
+ reason: `${eventDef(code).title}: ${detail}`,
44
+ wpId: ctx.wp.id,
45
+ };
46
+ }
47
+ function isBlocking(finding) {
48
+ return finding.severity === 'blocker' || finding.severity === 'major' || finding.mustFix === true;
49
+ }
50
+ /** The findings that make a rework mandatory: blocker, major or flagged `mustFix`. */
51
+ export function blockingFindings(findings) {
52
+ return findings.filter(isBlocking);
53
+ }
54
+ /** The only feedback surface a DEV sees (D-7): one row per blocking finding. */
55
+ export function deriveRequiredChanges(findings) {
56
+ return blockingFindings(findings).map((f) => ({
57
+ file: f.file,
58
+ issue: f.issue,
59
+ change: f.requiredChange,
60
+ }));
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Work submission (D17)
64
+ // ---------------------------------------------------------------------------
65
+ /**
66
+ * A DEV submission: route to review, flag D17 (scope narrowing) on the mechanical
67
+ * signature, and report files touched outside the WP's ownership (evidence only).
68
+ * D17 is appealable once; the booking carries that in its reason.
69
+ */
70
+ export function settleWork(ctx, submission) {
71
+ const result = emptyResult({ to: 'review' });
72
+ const narrowing = detectScopeNarrowing({
73
+ wpId: ctx.wp.id,
74
+ contracts: ctx.contracts,
75
+ interfaceContracts: submission.interfaceContracts,
76
+ });
77
+ if (narrowing.flagged) {
78
+ const detail = `${narrowing.reason} (mechanical signature; appealable once)`;
79
+ result.events.push(engineEvent(ctx, 'D17', submission.identity, detail));
80
+ }
81
+ const stray = detectOwnershipViolations(submission.filesChanged, ctx.wp.files);
82
+ if (stray.length > 0) {
83
+ result.notes.push(`Ownership: ${stray.length} file(s) outside the WP: ${stray.join(', ')}.`);
84
+ }
85
+ return result;
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Review (5.5 table 1)
89
+ // ---------------------------------------------------------------------------
90
+ function validateReview(ctx, sub) {
91
+ if (ctx.wp.assignee === null) {
92
+ return refuse('the work package has no developer yet.', 'Dispatch the dev stage first.');
93
+ }
94
+ const blocking = blockingFindings(sub.findings);
95
+ if (sub.verdict === 'PASS' && (blocking.length > 0 || sub.fixesApplied.length > 0)) {
96
+ return refuse('verdict PASS contradicts blocking findings or fixesApplied.', 'Use NEEDS_REWORK for blocking findings or PASS_WITH_FIXES when you fixed files.');
97
+ }
98
+ if (sub.verdict === 'PASS_WITH_FIXES') {
99
+ const bare = sub.fixesApplied.length === 0 || sub.fixesApplied.some((f) => !f.rationale.trim());
100
+ if (bare) {
101
+ return refuse('PASS_WITH_FIXES needs fixesApplied, each with a rationale.', 'List every file you changed with the reason, or use PASS.');
102
+ }
103
+ }
104
+ if (sub.verdict === 'NEEDS_REWORK' && blocking.length === 0) {
105
+ return refuse('NEEDS_REWORK needs at least one blocker, major or mustFix finding.', 'Add a finding with file, issue and requiredChange, or change the verdict.');
106
+ }
107
+ return unansweredAppeal(ctx, sub);
108
+ }
109
+ function refuse(problem, fix) {
110
+ return `${NOT_ACCEPTED}${problem} ${fix}`;
111
+ }
112
+ function unansweredAppeal(ctx, sub) {
113
+ const answered = new Set(sub.appealResponses.map((r) => r.findingId));
114
+ const missing = (ctx.appealFindingIds ?? []).filter((id) => !answered.has(id));
115
+ if (missing.length === 0)
116
+ return null;
117
+ return refuse(`the developer appealed finding(s) ${missing.join(', ')} and you did not answer.`, 'Add an appealResponses entry (uphold or withdraw) for each.');
118
+ }
119
+ function appealEffects(ctx, sub, out) {
120
+ const withdrawn = sub.appealResponses.filter((r) => r.decision === 'withdraw');
121
+ if (withdrawn.length === 0)
122
+ return;
123
+ const ids = withdrawn.map((r) => r.findingId).join(', ');
124
+ out.events.push(engineEvent(ctx, 'R5', sub.identity, `withdrew finding(s) ${ids} on appeal`));
125
+ if (ctx.wp.assignee !== null) {
126
+ out.reversals.push({
127
+ agentId: ctx.wp.assignee,
128
+ code: 'D5',
129
+ wpId: ctx.wp.id,
130
+ reason: `finding(s) ${ids} withdrawn on appeal`,
131
+ });
132
+ }
133
+ out.notes.push(`Appeal succeeded for ${ids}: R5 booked and the earlier D5 struck.`);
134
+ }
135
+ function reworkOutcome(ctx, sub, out) {
136
+ const developer = ctx.wp.assignee;
137
+ const required = sub.requiredChanges.length > 0 ? sub.requiredChanges : deriveRequiredChanges(sub.findings);
138
+ const fingerprints = [
139
+ ...new Set(blockingFindings(sub.findings).map((f) => fingerprintFinding(f))),
140
+ ];
141
+ out.events.push(engineEvent(ctx, 'D5', developer, `review ${sub.verdict} on ${ctx.wp.id}`));
142
+ if (findingsIntersect(fingerprints, ctx.wp.rejections)) {
143
+ out.events.push(engineEvent(ctx, 'D8', developer, 'a finding repeats an earlier rejection'));
144
+ }
145
+ out.newRejections = fingerprints.filter((fp) => !ctx.wp.rejections.includes(fp));
146
+ out.route = { to: 'dev', requiredChanges: required };
147
+ out.rejection = {
148
+ id: sub.id,
149
+ wpId: ctx.wp.id,
150
+ reviewer: sub.identity,
151
+ hasBlockerOrMajor: sub.findings.some((f) => f.severity === 'blocker' || f.severity === 'major'),
152
+ withdrawn: false,
153
+ };
154
+ }
155
+ /**
156
+ * A REVIEW submission. PASS -> D1 (+15 P) -> test; PASS_WITH_FIXES -> D2 (+5 P) -> test;
157
+ * NEEDS_REWORK -> D5 (-20) -> dev, plus D8 (-30) when a blocking finding repeats an
158
+ * earlier rejection. A withdrawn appeal finding books R5 on the reviewer and strikes the
159
+ * earlier D5 (one R5 per submission, not per finding, so withdrawing is not a punishment
160
+ * multiplier). R12 is checked on every verdict.
161
+ */
162
+ export function settleReview(ctx, sub) {
163
+ const problem = validateReview(ctx, sub);
164
+ if (problem !== null)
165
+ return { ...emptyResult(null), ok: false, refusal: problem };
166
+ const out = emptyResult({ to: 'test' });
167
+ const developer = ctx.wp.assignee;
168
+ appealEffects(ctx, sub, out);
169
+ if (sub.verdict === 'PASS') {
170
+ out.events.push(engineEvent(ctx, 'D1', developer, 'review PASS with no changes'));
171
+ }
172
+ else if (sub.verdict === 'PASS_WITH_FIXES') {
173
+ out.events.push(engineEvent(ctx, 'D2', developer, 'review PASS_WITH_FIXES'));
174
+ }
175
+ else {
176
+ reworkOutcome(ctx, sub, out);
177
+ }
178
+ const stamp = detectRubberStamp({
179
+ assessment: sub.integrationAssessment,
180
+ contracts: ctx.contracts,
181
+ wpId: ctx.wp.id,
182
+ });
183
+ if (stamp.flagged) {
184
+ const detail = `${stamp.reason} (mechanical signature; appealable)`;
185
+ out.events.push(engineEvent(ctx, 'R12', sub.identity, detail));
186
+ }
187
+ out.claims = claimsFromReview({ wpId: ctx.wp.id, phase: ctx.phase, developer: ctx.wp.assignee, reviewer: sub.identity }, sub);
188
+ if (sub.excellence !== undefined) {
189
+ out.notes.push('Excellence nominated (D10/R8): the inspector confirms at settle.');
190
+ }
191
+ return out;
192
+ }
193
+ // ---------------------------------------------------------------------------
194
+ // Test (5.5 table 2)
195
+ // ---------------------------------------------------------------------------
196
+ const PLACEHOLDER_REQUIREMENT = /^(n\/?a|none|tbd|todo|unknown|missing|gap|requirement|[-. ]*)$/i;
197
+ const MIN_REQUIREMENT_CHARS = 8;
198
+ const MIN_SKELETON_EVIDENCE_CHARS = 10;
199
+ /** True when a traceability row names a real requirement (not a placeholder). */
200
+ export function isConcreteRequirement(text) {
201
+ const trimmed = text.trim();
202
+ return trimmed.length >= MIN_REQUIREMENT_CHARS && !PLACEHOLDER_REQUIREMENT.test(trimmed);
203
+ }
204
+ function gapRows(sub) {
205
+ return sub.traceability.filter((row) => row.status === 'gap');
206
+ }
207
+ function validateSkeleton(sub) {
208
+ const ratio = sub.skeletonRatio;
209
+ if (ratio === undefined)
210
+ return null;
211
+ if (!(ratio >= 0 && ratio <= 1)) {
212
+ return refuse('skeletonRatio must be between 0 and 1.', 'Report the fraction of stub bodies.');
213
+ }
214
+ const evidence = (sub.skeletonEvidence ?? '').trim();
215
+ if (ratio > GOV_LIMITS.skeletonRatioThreshold && evidence.length < MIN_SKELETON_EVIDENCE_CHARS) {
216
+ return refuse(`skeletonRatio ${ratio} is above ${GOV_LIMITS.skeletonRatioThreshold} but has no evidence.`, 'Add skeletonEvidence with the stub count (use gov_scan), or lower the ratio.');
217
+ }
218
+ return null;
219
+ }
220
+ function validateTest(ctx, sub) {
221
+ if (ctx.wp.assignee === null) {
222
+ return refuse('the work package has no developer yet.', 'Dispatch the dev stage first.');
223
+ }
224
+ const gaps = gapRows(sub);
225
+ if (sub.verdict === 'SPEC_MISMATCH' && !gaps.some((g) => isConcreteRequirement(g.requirement))) {
226
+ return refuse('SPEC_MISMATCH needs a traceability row with status gap and a concrete requirement.', 'Quote the missing requirement text, or use ISSUES_FOUND.');
227
+ }
228
+ if (sub.verdict === 'ALL_PASS' && gaps.length > 0) {
229
+ return refuse('ALL_PASS contradicts traceability rows with status gap.', 'Use SPEC_MISMATCH for missing requirements.');
230
+ }
231
+ if (sub.verdict === 'ISSUES_FOUND' && sub.issues.length === 0) {
232
+ return refuse('ISSUES_FOUND lists no issues.', 'Describe each defect in issues[].');
233
+ }
234
+ return validateSkeleton(sub);
235
+ }
236
+ function testRoute(sub) {
237
+ if (sub.verdict === 'ALL_PASS')
238
+ return { to: 'passed' };
239
+ const fromGaps = gapRows(sub).map((g) => ({
240
+ file: '(spec)',
241
+ issue: g.requirement,
242
+ change: `Implement: ${g.requirement}`,
243
+ }));
244
+ const fromIssues = sub.issues.map((i) => ({
245
+ file: '(unspecified)',
246
+ issue: i.description,
247
+ change: 'Fix the defect and add a regression test.',
248
+ }));
249
+ const chosen = sub.verdict === 'SPEC_MISMATCH' ? fromGaps : fromIssues;
250
+ const fallback = [
251
+ { file: '(unspecified)', issue: sub.verdict, change: 'Replace stubs with real code.' },
252
+ ];
253
+ return { to: 'dev', requiredChanges: chosen.length > 0 ? chosen : fallback };
254
+ }
255
+ function testVerdictEvents(ctx, sub, out) {
256
+ const developer = ctx.wp.assignee;
257
+ switch (sub.verdict) {
258
+ case 'ALL_PASS': {
259
+ out.events.push(engineEvent(ctx, 'D3', developer, 'all tests passed'));
260
+ const rows = sub.traceability;
261
+ if (rows.length > 0 && rows.every((r) => r.status === 'covered')) {
262
+ out.events.push(engineEvent(ctx, 'D4', developer, 'every requirement covered'));
263
+ out.events.push(engineEvent(ctx, 'T1', sub.identity, 'full requirement traceability'));
264
+ }
265
+ else {
266
+ out.notes.push('D4/T1 withheld: traceability is not 100 percent covered.');
267
+ }
268
+ return;
269
+ }
270
+ case 'SPEC_MISMATCH':
271
+ out.events.push(engineEvent(ctx, 'D7', developer, 'a requirement is missing'));
272
+ return;
273
+ case 'INCOMPLETE_IMPL': {
274
+ const skeleton = (sub.skeletonRatio ?? 0) > GOV_LIMITS.skeletonRatioThreshold;
275
+ const code = skeleton ? 'D11' : 'D9';
276
+ const detail = skeleton ? `skeleton ratio ${sub.skeletonRatio}` : 'incomplete implementation';
277
+ out.events.push(engineEvent(ctx, code, developer, detail));
278
+ return;
279
+ }
280
+ case 'ISSUES_FOUND':
281
+ out.notes.push('Defects go back to the developer; rework drives D5/D8, not the test.');
282
+ return;
283
+ }
284
+ }
285
+ /**
286
+ * A TEST submission. ALL_PASS -> D3, and when every traceability row is covered D4 + T1;
287
+ * SPEC_MISMATCH -> D7 (needs a real `gap` row); INCOMPLETE_IMPL -> D9, or D11 instead when
288
+ * the skeleton ratio is above 0.30 with evidence; ISSUES_FOUND books nothing itself. Any
289
+ * `unverified` row books T6 on the tester. Claims are opened, never booked here.
290
+ */
291
+ export function settleTest(ctx, sub) {
292
+ const problem = validateTest(ctx, sub);
293
+ if (problem !== null)
294
+ return { ...emptyResult(null), ok: false, refusal: problem };
295
+ const out = emptyResult(testRoute(sub));
296
+ testVerdictEvents(ctx, sub, out);
297
+ if (sub.traceability.some((row) => row.status === 'unverified')) {
298
+ out.events.push(engineEvent(ctx, 'T6', sub.identity, 'a requirement is unverified'));
299
+ }
300
+ out.claims = claimsFromTest({ wpId: ctx.wp.id, phase: ctx.phase, developer: ctx.wp.assignee, reviewer: ctx.wp.reviewer }, sub);
301
+ if (sub.boundaryTestsWritten) {
302
+ out.notes.push('Boundary tests written: the inspector may award T8.');
303
+ }
304
+ return out;
305
+ }
306
+ const MISSED_KINDS = new Set(['missed_by_review', 'incomplete_impl']);
307
+ function phaseEvent(input, code, who, wpId) {
308
+ const event = {
309
+ code,
310
+ agentId: who,
311
+ at: input.at,
312
+ phase: input.phase,
313
+ by: 'engine',
314
+ reason: `${eventDef(code).title} on ${wpId}`,
315
+ wpId,
316
+ };
317
+ return event;
318
+ }
319
+ function reviewBonus(input, out) {
320
+ for (const { wpId, reviewer } of input.reviewedWps) {
321
+ const missed = input.claims.filter((c) => c.wpId === wpId && c.phase === input.phase && MISSED_KINDS.has(c.kind));
322
+ if (missed.some((c) => c.status === 'open')) {
323
+ out.notes.push(`R1 for ${wpId} waits: a missed-defect claim is still open.`);
324
+ continue;
325
+ }
326
+ if (missed.some((c) => c.status === 'upheld'))
327
+ continue;
328
+ const key = `r1:${wpId}`;
329
+ if (hasBooked(input.state, reviewer, 'R1', key))
330
+ continue;
331
+ out.events.push({ ...phaseEvent(input, 'R1', reviewer, wpId), incidentId: key });
332
+ }
333
+ }
334
+ function validRejectionBonus(input, out) {
335
+ const testedOk = new Set(input.testedOkWpIds);
336
+ for (const r of input.rejections) {
337
+ if (r.withdrawn || !r.hasBlockerOrMajor || !testedOk.has(r.wpId))
338
+ continue;
339
+ const overturned = input.claims.some((c) => c.wpId === r.wpId &&
340
+ c.kind === 'false_positive' &&
341
+ c.status === 'upheld' &&
342
+ c.against === r.reviewer);
343
+ const key = `r2:${r.id}`;
344
+ if (overturned || hasBooked(input.state, r.reviewer, 'R2', key))
345
+ continue;
346
+ out.events.push({ ...phaseEvent(input, 'R2', r.reviewer, r.wpId), incidentId: key });
347
+ }
348
+ }
349
+ /**
350
+ * R1 and R2, which can only be decided once the phase's claims are ruled.
351
+ *
352
+ * - R1 +10 (P): a WP passed review and no missed-defect / incomplete-impl claim against
353
+ * that review was upheld. A still-open claim defers R1 (never pay before the ruling).
354
+ * - R2 +10: a rejection with a blocker/major finding that was not withdrawn or overturned,
355
+ * and whose WP later reached ALL_PASS - rework confirmed the problem was real. Paid
356
+ * once per rejection (`r2:<review id>`), which is what stops a reviewer farming it.
357
+ */
358
+ export function settlePhaseR1R2(input) {
359
+ const out = { events: [], notes: [] };
360
+ reviewBonus(input, out);
361
+ validRejectionBonus(input, out);
362
+ return out;
363
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Mechanical suspicion signatures (governance-mode 5.9): scope narrowing (D17),
3
+ * rubber-stamp integration check (R12) and file-ownership violations.
4
+ *
5
+ * ASCII ONLY (`governance/` is glyph-scanned).
6
+ *
7
+ * WHY MECHANICAL. These are the cheap, objectively checkable shadows of two cheats the
8
+ * reference spec calls out: a developer who avoids shared interfaces to dodge integration
9
+ * blame, and a reviewer who waves integration through with a one-liner. A signature that
10
+ * a script can test books automatically and is appealable once; everything fuzzier is
11
+ * left to the inspector. The functions here only DECIDE - booking goes through the
12
+ * ledger like any other event.
13
+ *
14
+ * Pure: no I/O, no clock, no randomness.
15
+ */
16
+ import type { Contract, InterfaceContractCheck } from '../types.js';
17
+ export interface Suspicion {
18
+ flagged: boolean;
19
+ /** Why it fired, or why it was exempt. English, ASCII. */
20
+ reason: string;
21
+ }
22
+ /** The only exemption phrase a reviewer may use when no contract touches the WP. */
23
+ export declare const NO_CONTRACTS_MARKER = "NOT_APPLICABLE: no cross-module contracts";
24
+ /** Contracts that bind `wpId` (as producer or consumer); voided contracts are ignored. */
25
+ export declare function contractsInvolving(wpId: string, contracts: readonly Contract[]): Contract[];
26
+ /**
27
+ * D17: the WP appears in `contracts`, yet the work submission's `interfaceContracts` is
28
+ * missing, empty, or entirely NOT_APPLICABLE.
29
+ */
30
+ export declare function detectScopeNarrowing(input: {
31
+ wpId: string;
32
+ contracts: readonly Contract[];
33
+ interfaceContracts: readonly InterfaceContractCheck[] | undefined;
34
+ }): Suspicion;
35
+ /** Count sentences: segments split on terminal punctuation or newlines with real content. */
36
+ export declare function countSentences(text: string): number;
37
+ /** True when the text names a file path, a file name or a function signature. */
38
+ export declare function hasConcreteReference(text: string): boolean;
39
+ /**
40
+ * R12: a contract binds the reviewed WP, the integration assessment is shorter than two
41
+ * sentences, AND it cites no contract, path or signature. All three together - a short but
42
+ * specific assessment is fine, and a long vague one is the inspector's call, not a script's.
43
+ *
44
+ * A run (or WP) without contracts is exempt outright. The agreed marker phrase
45
+ * `NOT_APPLICABLE: no cross-module contracts` is how a reviewer says so honestly; the
46
+ * exemption does not DEPEND on the phrase because penalising a one-WP run's reviewer for
47
+ * phrasing would be a false positive of the mechanism itself.
48
+ */
49
+ export declare function detectRubberStamp(input: {
50
+ assessment: string;
51
+ contracts: readonly Contract[];
52
+ /** The reviewed WP; absent => every live contract counts as relevant. */
53
+ wpId?: string;
54
+ }): Suspicion;
55
+ /**
56
+ * True when `file` is covered by one owned pattern: an exact path, a directory prefix
57
+ * (`src/a`, `src/a/`), or a glob. Prefix matching is on whole path segments so owning
58
+ * `src/a` never covers `src/ab/x.ts`.
59
+ */
60
+ export declare function isOwnedPath(file: string, ownedPatterns: readonly string[]): boolean;
61
+ /**
62
+ * Files the agent touched that its work package does not own. Returned in input order,
63
+ * de-duplicated. Evidence for the inspector, never an automatic deduction: the touched-file
64
+ * list is best-effort (it comes from the run's tool trace).
65
+ */
66
+ export declare function detectOwnershipViolations(filesTouched: readonly string[], ownedPatterns: readonly string[]): string[];
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Mechanical suspicion signatures (governance-mode 5.9): scope narrowing (D17),
3
+ * rubber-stamp integration check (R12) and file-ownership violations.
4
+ *
5
+ * ASCII ONLY (`governance/` is glyph-scanned).
6
+ *
7
+ * WHY MECHANICAL. These are the cheap, objectively checkable shadows of two cheats the
8
+ * reference spec calls out: a developer who avoids shared interfaces to dodge integration
9
+ * blame, and a reviewer who waves integration through with a one-liner. A signature that
10
+ * a script can test books automatically and is appealable once; everything fuzzier is
11
+ * left to the inspector. The functions here only DECIDE - booking goes through the
12
+ * ledger like any other event.
13
+ *
14
+ * Pure: no I/O, no clock, no randomness.
15
+ */
16
+ /** The only exemption phrase a reviewer may use when no contract touches the WP. */
17
+ export const NO_CONTRACTS_MARKER = 'NOT_APPLICABLE: no cross-module contracts';
18
+ /** Contracts that bind `wpId` (as producer or consumer); voided contracts are ignored. */
19
+ export function contractsInvolving(wpId, contracts) {
20
+ return contracts.filter((c) => c.voided !== true && (c.producer === wpId || c.consumers.includes(wpId)));
21
+ }
22
+ /**
23
+ * D17: the WP appears in `contracts`, yet the work submission's `interfaceContracts` is
24
+ * missing, empty, or entirely NOT_APPLICABLE.
25
+ */
26
+ export function detectScopeNarrowing(input) {
27
+ const bound = contractsInvolving(input.wpId, input.contracts);
28
+ if (bound.length === 0) {
29
+ return { flagged: false, reason: 'No contract involves this work package.' };
30
+ }
31
+ const checks = input.interfaceContracts ?? [];
32
+ if (checks.length === 0) {
33
+ return {
34
+ flagged: true,
35
+ reason: `Bound by ${bound.length} contract(s) but declares no interface checks.`,
36
+ };
37
+ }
38
+ if (checks.every((c) => c.verified === 'NOT_APPLICABLE')) {
39
+ return {
40
+ flagged: true,
41
+ reason: 'Every interface check is NOT_APPLICABLE although contracts bind this work package.',
42
+ };
43
+ }
44
+ return { flagged: false, reason: 'Interface contracts were declared and verified.' };
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // R12
48
+ // ---------------------------------------------------------------------------
49
+ const PATH_PATTERN = /(?:^|[\s`'"(])(?:[\w.-]+\/)+[\w.-]+/;
50
+ const FILE_EXTENSIONS = 'tsx?|jsx?|mjs|cjs|py|go|rs|java|kt|rb|php|cs|cpp|json|ya?ml|toml|md|sql';
51
+ const FILENAME_PATTERN = new RegExp(`\\b[\\w-]+\\.(?:${FILE_EXTENSIONS})\\b`);
52
+ const SIGNATURE_PATTERN = /[A-Za-z_$][\w$.]*\([^()\n]*\)/;
53
+ /** Count sentences: segments split on terminal punctuation or newlines with real content. */
54
+ export function countSentences(text) {
55
+ return text
56
+ .split(/(?<=[.!?])\s+|\n+/)
57
+ .filter((segment) => (segment.match(/[A-Za-z0-9]/g) ?? []).length >= 3).length;
58
+ }
59
+ function mentionsContract(text, contracts) {
60
+ const lower = text.toLowerCase();
61
+ return contracts.some((c) => {
62
+ const name = c.name.trim().toLowerCase();
63
+ const id = c.id.trim().toLowerCase();
64
+ return (name.length >= 3 && lower.includes(name)) || (id.length >= 2 && lower.includes(id));
65
+ });
66
+ }
67
+ /** True when the text names a file path, a file name or a function signature. */
68
+ export function hasConcreteReference(text) {
69
+ return PATH_PATTERN.test(text) || FILENAME_PATTERN.test(text) || SIGNATURE_PATTERN.test(text);
70
+ }
71
+ /**
72
+ * R12: a contract binds the reviewed WP, the integration assessment is shorter than two
73
+ * sentences, AND it cites no contract, path or signature. All three together - a short but
74
+ * specific assessment is fine, and a long vague one is the inspector's call, not a script's.
75
+ *
76
+ * A run (or WP) without contracts is exempt outright. The agreed marker phrase
77
+ * `NOT_APPLICABLE: no cross-module contracts` is how a reviewer says so honestly; the
78
+ * exemption does not DEPEND on the phrase because penalising a one-WP run's reviewer for
79
+ * phrasing would be a false positive of the mechanism itself.
80
+ */
81
+ export function detectRubberStamp(input) {
82
+ const live = input.contracts.filter((c) => c.voided !== true);
83
+ const relevant = input.wpId === undefined ? live : contractsInvolving(input.wpId, input.contracts);
84
+ if (relevant.length === 0) {
85
+ const marked = input.assessment.trim().startsWith(NO_CONTRACTS_MARKER);
86
+ return {
87
+ flagged: false,
88
+ reason: marked
89
+ ? 'Exempt: no cross-module contracts (declared).'
90
+ : 'Exempt: no contract binds this work package.',
91
+ };
92
+ }
93
+ const text = input.assessment;
94
+ const sentences = countSentences(text);
95
+ if (sentences >= 2) {
96
+ return { flagged: false, reason: `Assessment has ${sentences} sentences.` };
97
+ }
98
+ if (mentionsContract(text, live) || hasConcreteReference(text)) {
99
+ return { flagged: false, reason: 'Assessment cites a contract, file or signature.' };
100
+ }
101
+ return {
102
+ flagged: true,
103
+ reason: `Assessment has ${sentences} sentence(s) and cites no contract, file or signature.`,
104
+ };
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Ownership
108
+ // ---------------------------------------------------------------------------
109
+ function normalizePath(path) {
110
+ return path.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/{2,}/g, '/');
111
+ }
112
+ const GLOB_CHARS = /[*?[]/;
113
+ /** Translate a glob (`**`, `*`, `?`) into an anchored RegExp. `**\/` may match nothing. */
114
+ function globToRegExp(glob) {
115
+ let out = '';
116
+ for (let i = 0; i < glob.length; i++) {
117
+ const ch = glob[i];
118
+ if (ch === '*' && glob[i + 1] === '*') {
119
+ const slash = glob[i + 2] === '/';
120
+ out += slash ? '(?:.*/)?' : '.*';
121
+ i += slash ? 2 : 1;
122
+ }
123
+ else if (ch === '*') {
124
+ out += '[^/]*';
125
+ }
126
+ else if (ch === '?') {
127
+ out += '[^/]';
128
+ }
129
+ else {
130
+ out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
131
+ }
132
+ }
133
+ return new RegExp(`^${out}$`);
134
+ }
135
+ /**
136
+ * True when `file` is covered by one owned pattern: an exact path, a directory prefix
137
+ * (`src/a`, `src/a/`), or a glob. Prefix matching is on whole path segments so owning
138
+ * `src/a` never covers `src/ab/x.ts`.
139
+ */
140
+ export function isOwnedPath(file, ownedPatterns) {
141
+ const target = normalizePath(file);
142
+ return ownedPatterns.some((raw) => {
143
+ const pattern = normalizePath(raw);
144
+ if (pattern === target)
145
+ return true;
146
+ if (GLOB_CHARS.test(pattern))
147
+ return globToRegExp(pattern).test(target);
148
+ const dir = pattern.endsWith('/') ? pattern : `${pattern}/`;
149
+ return target.startsWith(dir);
150
+ });
151
+ }
152
+ /**
153
+ * Files the agent touched that its work package does not own. Returned in input order,
154
+ * de-duplicated. Evidence for the inspector, never an automatic deduction: the touched-file
155
+ * list is best-effort (it comes from the run's tool trace).
156
+ */
157
+ export function detectOwnershipViolations(filesTouched, ownedPatterns) {
158
+ const seen = new Set();
159
+ const violations = [];
160
+ for (const file of filesTouched) {
161
+ const key = normalizePath(file);
162
+ if (seen.has(key))
163
+ continue;
164
+ seen.add(key);
165
+ if (!isOwnedPath(file, ownedPatterns))
166
+ violations.push(file);
167
+ }
168
+ return violations;
169
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Artifact storage: validated submissions and reports under
3
+ * `<run>/artifacts/<phase>/<wpId>/<stage>-<identity>-<n>.json|.md` (governance-mode
4
+ * sections 8.3 and 12.1).
5
+ *
6
+ * ASCII ONLY (`governance/` is glyph-scanned).
7
+ *
8
+ * IDS ARE STABLE STRINGS: `p1/dev1/review-rev1-1` = phase 1 (1-based, `p` prefix), work
9
+ * package `dev1`, stage `review`, written by identity `rev1`, its 1st artifact. The id is
10
+ * also the relative path (minus extension), so `gov_read` can resolve an id without an
11
+ * index and a human can find the file by eye.
12
+ *
13
+ * EVERY ARTIFACT CARRIES A VISIBILITY TAG (`work | review | test | inspector`), derived
14
+ * from the stage and never from content. `engine/visibility.ts` filters on it; keeping the
15
+ * tag in the id's stage segment means the information boundary cannot be defeated by an
16
+ * artifact whose body claims to be something else.
17
+ *
18
+ * Phase-level artifacts (settle / integrate reports) have no work package; they use the
19
+ * reserved wp segment `_phase`.
20
+ */
21
+ import type { StageKind } from '../types.js';
22
+ import type { RunStore } from './run-store.js';
23
+ export type ArtifactKind = 'work' | 'review' | 'test' | 'inspector';
24
+ export declare const PHASE_WP_SEGMENT = "_phase";
25
+ export interface ArtifactRef {
26
+ id: string;
27
+ /** 1-based phase number, as shown in the id. */
28
+ phase: number;
29
+ /** Work package id, or `_phase` for phase-level reports. */
30
+ wpId: string;
31
+ stage: StageKind;
32
+ identity: string;
33
+ /** 1-based per (phase, wp, stage, identity). */
34
+ n: number;
35
+ kind: ArtifactKind;
36
+ }
37
+ export type ArtifactRefInput = Omit<ArtifactRef, 'id' | 'kind'>;
38
+ /** The visibility tag of an artifact is a pure function of the stage that produced it. */
39
+ export declare function artifactKindForStage(stage: StageKind): ArtifactKind;
40
+ export declare function buildArtifactId(ref: ArtifactRefInput): string;
41
+ /** Inverse of `buildArtifactId`. Returns null for anything that is not a well-formed id. */
42
+ export declare function parseArtifactId(id: string): ArtifactRef | null;
43
+ /** Cut `text` to at most `maxBytes` UTF-8 bytes without leaving a broken character. */
44
+ export declare function truncateUtf8(text: string, maxBytes: number): string;
45
+ /**
46
+ * Serialize a machine-form submission within `submissionBytes`. JSON cannot be cut
47
+ * mid-text, so an oversized body is replaced by a small valid envelope carrying a head
48
+ * excerpt and the original size; the marker keeps the cut visible to every reader.
49
+ */
50
+ export declare function boundedJson(value: unknown, maxBytes?: 64000): string;
51
+ export interface WriteArtifactInput extends Omit<ArtifactRefInput, 'n'> {
52
+ /** The validated machine form. */
53
+ json: unknown;
54
+ /** Human-readable rendering of the same submission, if any. */
55
+ markdown?: string;
56
+ }
57
+ export type WriteArtifactResult = {
58
+ ok: true;
59
+ ref: ArtifactRef;
60
+ bytes: number;
61
+ } | {
62
+ ok: false;
63
+ exhausted: boolean;
64
+ message: string;
65
+ };
66
+ export interface ReadArtifactResult {
67
+ ref: ArtifactRef;
68
+ json: unknown;
69
+ markdown: string | null;
70
+ }
71
+ /** Thin domain layer over `RunStore`'s budgeted file primitives. */
72
+ export declare class ArtifactStore {
73
+ private readonly store;
74
+ constructor(store: RunStore);
75
+ /** Write `.json` (and `.md`) as artifact number n+1 for this (phase, wp, stage, who). */
76
+ write(runId: string, input: WriteArtifactInput): Promise<WriteArtifactResult>;
77
+ /** Every artifact in the run, ordered by phase, wp, stage, identity, n. */
78
+ list(runId: string): Promise<ArtifactRef[]>;
79
+ /** Resolve an id (never a path) to its content, or null if absent / malformed. */
80
+ read(runId: string, id: string): Promise<ReadArtifactResult | null>;
81
+ }