RuleService.vala 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Invercargill.Expressions;
  4. using InvercargillSql.Orm;
  5. using Inversion;
  6. using ClankerSaddle.Data;
  7. namespace ClankerSaddle.Services {
  8. /**
  9. * Rule storage: CRUD over `rules` plus their project and task
  10. * attachments, and the effective-rule computation the dispatcher
  11. * ships in the `/start` payload.
  12. */
  13. public class RuleService : Object {
  14. private OrmSession db = inject<OrmSession>();
  15. /**
  16. * Inserts a rule; names are unique. Returns the stored row (with
  17. * its id).
  18. */
  19. public async Rule create_rule(string name, string body) throws Error {
  20. if (name.strip().length == 0) {
  21. throw new Error(Quark.from_string("clanker-saddle"), 30,
  22. "A rule name is required");
  23. }
  24. if (body.strip().length == 0) {
  25. throw new Error(Quark.from_string("clanker-saddle"), 31,
  26. "A rule body is required");
  27. }
  28. var existing = yield db.query<Rule>()
  29. .where(expr("name == $0", elem(name.strip())))
  30. .first_async();
  31. if (existing != null) {
  32. throw new Error(Quark.from_string("clanker-saddle"), 32,
  33. @"A rule named '$(name)' already exists");
  34. }
  35. var rule = new Rule() {
  36. name = name.strip(),
  37. body = body,
  38. created_at = new DateTime.now_local()
  39. };
  40. yield db.insert_async<Rule>(rule);
  41. return rule;
  42. }
  43. /** Loads the rule identified by `id`, or null. */
  44. public async Rule? get_rule(int64 id) throws Error {
  45. return yield db.query<Rule>()
  46. .where(expr("id == $0", elem<int64?>(id)))
  47. .first_async();
  48. }
  49. /** Every rule ordered by name. */
  50. public async ImmutableLot<Rule> list_rules() throws Error {
  51. return yield db.query<Rule>()
  52. .order_by(expr("name"))
  53. .materialise_async();
  54. }
  55. /**
  56. * Saves the editable fields of the rule identified by `id`; null
  57. * or empty arguments keep their current values. An edit stamps
  58. * `updated_at` — the resume-time context_update delta keys on
  59. * it, so a rule edited while a workspace was parked reaches
  60. * the agent as a turn. Returns null when the rule does not
  61. * exist.
  62. */
  63. public async Rule? update_rule(int64 id, string? name, string? body) throws Error {
  64. var rule = yield get_rule(id);
  65. if (rule == null) {
  66. return null;
  67. }
  68. bool edited = false;
  69. if (name != null && name.strip().length > 0) {
  70. var clash = yield db.query<Rule>()
  71. .where(expr("name == $0 && id != $1", elem(name.strip()), elem<int64?>(id)))
  72. .first_async();
  73. if (clash != null) {
  74. throw new Error(Quark.from_string("clanker-saddle"), 32,
  75. @"A rule named '$(name)' already exists");
  76. }
  77. if (((!)rule).name != name.strip()) {
  78. ((!)rule).name = name.strip();
  79. edited = true;
  80. }
  81. }
  82. if (body != null && body.length > 0) {
  83. if (((!)rule).body != body) {
  84. ((!)rule).body = body;
  85. edited = true;
  86. }
  87. }
  88. if (edited) {
  89. ((!)rule).updated_at = new DateTime.now_local();
  90. }
  91. yield db.update_async<Rule>((!)rule);
  92. return rule;
  93. }
  94. /**
  95. * Deletes a rule and every attachment referencing it (task and
  96. * project links go with it).
  97. */
  98. public async void delete_rule(int64 id) throws Error {
  99. foreach (var link in yield db.query<TaskRule>()
  100. .where(expr("rule_id == $0", elem<int64?>(id)))
  101. .materialise_async()) {
  102. yield db.delete_async<TaskRule>(link);
  103. }
  104. foreach (var link in yield db.query<RuleProject>()
  105. .where(expr("rule_id == $0", elem<int64?>(id)))
  106. .materialise_async()) {
  107. yield db.delete_async<RuleProject>(link);
  108. }
  109. var rule = yield get_rule(id);
  110. if (rule != null) {
  111. yield db.delete_async<Rule>((!)rule);
  112. }
  113. }
  114. /**
  115. * Attaches `rule_id` to `project_id`; duplicates are rejected
  116. * (the pair is unique).
  117. */
  118. public async void attach_rule_to_project(int64 rule_id, int64 project_id) throws Error {
  119. var rule = yield get_rule(rule_id);
  120. if (rule == null) {
  121. throw new Error(Quark.from_string("clanker-saddle"), 33,
  122. @"No rule exists with id $(rule_id)");
  123. }
  124. var existing = yield db.query<RuleProject>()
  125. .where(expr("rule_id == $0 && project_id == $1",
  126. elem<int64?>(rule_id), elem<int64?>(project_id)))
  127. .materialise_async();
  128. if (existing.length > 0) {
  129. return;
  130. }
  131. yield db.insert_async<RuleProject>(new RuleProject() {
  132. rule_id = rule_id,
  133. project_id = project_id,
  134. created_at = new DateTime.now_local()
  135. });
  136. }
  137. /** Detaches the `rule_projects` row identified by `id`. */
  138. public async void detach_rule_from_project(int64 link_id) throws Error {
  139. var link = yield db.query<RuleProject>()
  140. .where(expr("id == $0", elem<int64?>(link_id)))
  141. .first_async();
  142. if (link != null) {
  143. yield db.delete_async<RuleProject>((!)link);
  144. }
  145. }
  146. /** The `rule_projects` attachments of `project_id`, oldest first. */
  147. public async ImmutableLot<RuleProject> list_project_attachments(int64 project_id) throws Error {
  148. return yield db.query<RuleProject>()
  149. .where(expr("project_id == $0", elem<int64?>(project_id)))
  150. .order_by(expr("id"))
  151. .materialise_async();
  152. }
  153. /** The rules attached to `project_id`, attachment order. */
  154. public async ImmutableLot<Rule> list_rules_for_project(int64 project_id) throws Error {
  155. var rows = new Vector<Rule>();
  156. foreach (var link in yield list_project_attachments(project_id)) {
  157. var rule = yield get_rule(link.rule_id);
  158. if (rule != null) {
  159. rows.add((!)rule);
  160. }
  161. }
  162. return rows.to_immutable_buffer();
  163. }
  164. /**
  165. * Attaches `rule_id` to `task_id`; duplicates are rejected (the
  166. * pair is unique).
  167. */
  168. public async void attach_rule_to_task(int64 rule_id, int64 task_id) throws Error {
  169. var rule = yield get_rule(rule_id);
  170. if (rule == null) {
  171. throw new Error(Quark.from_string("clanker-saddle"), 33,
  172. @"No rule exists with id $(rule_id)");
  173. }
  174. var existing = yield db.query<TaskRule>()
  175. .where(expr("rule_id == $0 && task_id == $1",
  176. elem<int64?>(rule_id), elem<int64?>(task_id)))
  177. .materialise_async();
  178. if (existing.length > 0) {
  179. return;
  180. }
  181. yield db.insert_async<TaskRule>(new TaskRule() {
  182. rule_id = rule_id,
  183. task_id = task_id,
  184. created_at = new DateTime.now_local()
  185. });
  186. }
  187. /** Detaches the `task_rules` row identified by `id`. */
  188. public async void detach_rule_from_task(int64 link_id) throws Error {
  189. var link = yield db.query<TaskRule>()
  190. .where(expr("id == $0", elem<int64?>(link_id)))
  191. .first_async();
  192. if (link != null) {
  193. yield db.delete_async<TaskRule>((!)link);
  194. }
  195. }
  196. /** The `task_rules` attachments of `task_id`, oldest first. */
  197. public async ImmutableLot<TaskRule> list_task_attachments(int64 task_id) throws Error {
  198. return yield db.query<TaskRule>()
  199. .where(expr("task_id == $0", elem<int64?>(task_id)))
  200. .order_by(expr("id"))
  201. .materialise_async();
  202. }
  203. /** The rules attached to `task_id`, attachment order. */
  204. public async ImmutableLot<Rule> list_rules_for_task(int64 task_id) throws Error {
  205. var rows = new Vector<Rule>();
  206. foreach (var link in yield list_task_attachments(task_id)) {
  207. var rule = yield get_rule(link.rule_id);
  208. if (rule != null) {
  209. rows.add((!)rule);
  210. }
  211. }
  212. return rows.to_immutable_buffer();
  213. }
  214. /**
  215. * The effective rule set for a dispatched task, in the wire
  216. * contract's stable order: the task's own rules, then the primary
  217. * project's, then the co-developed projects' in link order —
  218. * deduped by id (reference projects contribute nothing).
  219. */
  220. public async ImmutableLot<Rule> effective_rules(TaskEntity task,
  221. ProjectService projects) throws Error {
  222. return yield effective_rules_for(task, projects, 0);
  223. }
  224. /**
  225. * The goal-aware variant (plan 12): with `goal_id` > 0 the
  226. * co-developed expansion walks the GOAL's links instead of the
  227. * project's — goal work (the agent's run and its worker
  228. * tasks) scopes its involved repos through the goal.
  229. */
  230. public async ImmutableLot<Rule> effective_rules_for(TaskEntity task,
  231. ProjectService projects, int64 goal_id) throws Error {
  232. var ordered = new Vector<Rule>();
  233. // Ids are stringified: the Invercargill collections cannot
  234. // key (or hash) on value types.
  235. var seen = new HashSet<string>();
  236. foreach (var rule in yield list_rules_for_task(task.id)) {
  237. if (seen.add(rule.id.to_string())) {
  238. ordered.add(rule);
  239. }
  240. }
  241. foreach (var rule in yield list_rules_for_project(task.project_id)) {
  242. if (seen.add(rule.id.to_string())) {
  243. ordered.add(rule);
  244. }
  245. }
  246. if (goal_id > 0) {
  247. foreach (var link in yield db.query<GoalLink>()
  248. .where(expr("goal_id == $0", elem<int64?>(goal_id)))
  249. .order_by(expr("id"))
  250. .materialise_async()) {
  251. if (link.relationship != LINK_CO_DEVELOPED) {
  252. continue;
  253. }
  254. foreach (var rule in yield list_rules_for_project(link.project_id)) {
  255. if (seen.add(rule.id.to_string())) {
  256. ordered.add(rule);
  257. }
  258. }
  259. }
  260. } else {
  261. foreach (var link in yield projects.list_links(task.project_id)) {
  262. if (link.relationship != LINK_CO_DEVELOPED) {
  263. continue;
  264. }
  265. foreach (var rule in yield list_rules_for_project(link.linked_project_id)) {
  266. if (seen.add(rule.id.to_string())) {
  267. ordered.add(rule);
  268. }
  269. }
  270. }
  271. }
  272. return ordered.to_immutable_buffer();
  273. }
  274. }
  275. }