| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Expressions;
- using InvercargillSql.Orm;
- using Inversion;
- using ClankerSaddle.Data;
- namespace ClankerSaddle.Services {
- /**
- * Rule storage: CRUD over `rules` plus their project and task
- * attachments, and the effective-rule computation the dispatcher
- * ships in the `/start` payload.
- */
- public class RuleService : Object {
- private OrmSession db = inject<OrmSession>();
- /**
- * Inserts a rule; names are unique. Returns the stored row (with
- * its id).
- */
- public async Rule create_rule(string name, string body) throws Error {
- if (name.strip().length == 0) {
- throw new Error(Quark.from_string("clanker-saddle"), 30,
- "A rule name is required");
- }
- if (body.strip().length == 0) {
- throw new Error(Quark.from_string("clanker-saddle"), 31,
- "A rule body is required");
- }
- var existing = yield db.query<Rule>()
- .where(expr("name == $0", elem(name.strip())))
- .first_async();
- if (existing != null) {
- throw new Error(Quark.from_string("clanker-saddle"), 32,
- @"A rule named '$(name)' already exists");
- }
- var rule = new Rule() {
- name = name.strip(),
- body = body,
- created_at = new DateTime.now_local()
- };
- yield db.insert_async<Rule>(rule);
- return rule;
- }
- /** Loads the rule identified by `id`, or null. */
- public async Rule? get_rule(int64 id) throws Error {
- return yield db.query<Rule>()
- .where(expr("id == $0", elem<int64?>(id)))
- .first_async();
- }
- /** Every rule ordered by name. */
- public async ImmutableLot<Rule> list_rules() throws Error {
- return yield db.query<Rule>()
- .order_by(expr("name"))
- .materialise_async();
- }
- /**
- * Saves the editable fields of the rule identified by `id`; null
- * or empty arguments keep their current values. An edit stamps
- * `updated_at` — the resume-time context_update delta keys on
- * it, so a rule edited while a workspace was parked reaches
- * the agent as a turn. Returns null when the rule does not
- * exist.
- */
- public async Rule? update_rule(int64 id, string? name, string? body) throws Error {
- var rule = yield get_rule(id);
- if (rule == null) {
- return null;
- }
- bool edited = false;
- if (name != null && name.strip().length > 0) {
- var clash = yield db.query<Rule>()
- .where(expr("name == $0 && id != $1", elem(name.strip()), elem<int64?>(id)))
- .first_async();
- if (clash != null) {
- throw new Error(Quark.from_string("clanker-saddle"), 32,
- @"A rule named '$(name)' already exists");
- }
- if (((!)rule).name != name.strip()) {
- ((!)rule).name = name.strip();
- edited = true;
- }
- }
- if (body != null && body.length > 0) {
- if (((!)rule).body != body) {
- ((!)rule).body = body;
- edited = true;
- }
- }
- if (edited) {
- ((!)rule).updated_at = new DateTime.now_local();
- }
- yield db.update_async<Rule>((!)rule);
- return rule;
- }
- /**
- * Deletes a rule and every attachment referencing it (task and
- * project links go with it).
- */
- public async void delete_rule(int64 id) throws Error {
- foreach (var link in yield db.query<TaskRule>()
- .where(expr("rule_id == $0", elem<int64?>(id)))
- .materialise_async()) {
- yield db.delete_async<TaskRule>(link);
- }
- foreach (var link in yield db.query<RuleProject>()
- .where(expr("rule_id == $0", elem<int64?>(id)))
- .materialise_async()) {
- yield db.delete_async<RuleProject>(link);
- }
- var rule = yield get_rule(id);
- if (rule != null) {
- yield db.delete_async<Rule>((!)rule);
- }
- }
- /**
- * Attaches `rule_id` to `project_id`; duplicates are rejected
- * (the pair is unique).
- */
- public async void attach_rule_to_project(int64 rule_id, int64 project_id) throws Error {
- var rule = yield get_rule(rule_id);
- if (rule == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 33,
- @"No rule exists with id $(rule_id)");
- }
- var existing = yield db.query<RuleProject>()
- .where(expr("rule_id == $0 && project_id == $1",
- elem<int64?>(rule_id), elem<int64?>(project_id)))
- .materialise_async();
- if (existing.length > 0) {
- return;
- }
- yield db.insert_async<RuleProject>(new RuleProject() {
- rule_id = rule_id,
- project_id = project_id,
- created_at = new DateTime.now_local()
- });
- }
- /** Detaches the `rule_projects` row identified by `id`. */
- public async void detach_rule_from_project(int64 link_id) throws Error {
- var link = yield db.query<RuleProject>()
- .where(expr("id == $0", elem<int64?>(link_id)))
- .first_async();
- if (link != null) {
- yield db.delete_async<RuleProject>((!)link);
- }
- }
- /** The `rule_projects` attachments of `project_id`, oldest first. */
- public async ImmutableLot<RuleProject> list_project_attachments(int64 project_id) throws Error {
- return yield db.query<RuleProject>()
- .where(expr("project_id == $0", elem<int64?>(project_id)))
- .order_by(expr("id"))
- .materialise_async();
- }
- /** The rules attached to `project_id`, attachment order. */
- public async ImmutableLot<Rule> list_rules_for_project(int64 project_id) throws Error {
- var rows = new Vector<Rule>();
- foreach (var link in yield list_project_attachments(project_id)) {
- var rule = yield get_rule(link.rule_id);
- if (rule != null) {
- rows.add((!)rule);
- }
- }
- return rows.to_immutable_buffer();
- }
- /**
- * Attaches `rule_id` to `task_id`; duplicates are rejected (the
- * pair is unique).
- */
- public async void attach_rule_to_task(int64 rule_id, int64 task_id) throws Error {
- var rule = yield get_rule(rule_id);
- if (rule == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 33,
- @"No rule exists with id $(rule_id)");
- }
- var existing = yield db.query<TaskRule>()
- .where(expr("rule_id == $0 && task_id == $1",
- elem<int64?>(rule_id), elem<int64?>(task_id)))
- .materialise_async();
- if (existing.length > 0) {
- return;
- }
- yield db.insert_async<TaskRule>(new TaskRule() {
- rule_id = rule_id,
- task_id = task_id,
- created_at = new DateTime.now_local()
- });
- }
- /** Detaches the `task_rules` row identified by `id`. */
- public async void detach_rule_from_task(int64 link_id) throws Error {
- var link = yield db.query<TaskRule>()
- .where(expr("id == $0", elem<int64?>(link_id)))
- .first_async();
- if (link != null) {
- yield db.delete_async<TaskRule>((!)link);
- }
- }
- /** The `task_rules` attachments of `task_id`, oldest first. */
- public async ImmutableLot<TaskRule> list_task_attachments(int64 task_id) throws Error {
- return yield db.query<TaskRule>()
- .where(expr("task_id == $0", elem<int64?>(task_id)))
- .order_by(expr("id"))
- .materialise_async();
- }
- /** The rules attached to `task_id`, attachment order. */
- public async ImmutableLot<Rule> list_rules_for_task(int64 task_id) throws Error {
- var rows = new Vector<Rule>();
- foreach (var link in yield list_task_attachments(task_id)) {
- var rule = yield get_rule(link.rule_id);
- if (rule != null) {
- rows.add((!)rule);
- }
- }
- return rows.to_immutable_buffer();
- }
- /**
- * The effective rule set for a dispatched task, in the wire
- * contract's stable order: the task's own rules, then the primary
- * project's, then the co-developed projects' in link order —
- * deduped by id (reference projects contribute nothing).
- */
- public async ImmutableLot<Rule> effective_rules(TaskEntity task,
- ProjectService projects) throws Error {
- return yield effective_rules_for(task, projects, 0);
- }
- /**
- * The goal-aware variant (plan 12): with `goal_id` > 0 the
- * co-developed expansion walks the GOAL's links instead of the
- * project's — goal work (the agent's run and its worker
- * tasks) scopes its involved repos through the goal.
- */
- public async ImmutableLot<Rule> effective_rules_for(TaskEntity task,
- ProjectService projects, int64 goal_id) throws Error {
- var ordered = new Vector<Rule>();
- // Ids are stringified: the Invercargill collections cannot
- // key (or hash) on value types.
- var seen = new HashSet<string>();
- foreach (var rule in yield list_rules_for_task(task.id)) {
- if (seen.add(rule.id.to_string())) {
- ordered.add(rule);
- }
- }
- // Goal 8 task 4: standalone tasks (project 0) carry only
- // their own task rules — no project exists to expand.
- if (task.project_id > 0) {
- foreach (var rule in yield list_rules_for_project(task.project_id)) {
- if (seen.add(rule.id.to_string())) {
- ordered.add(rule);
- }
- }
- }
- if (goal_id > 0) {
- foreach (var link in yield db.query<GoalLink>()
- .where(expr("goal_id == $0", elem<int64?>(goal_id)))
- .order_by(expr("id"))
- .materialise_async()) {
- if (link.relationship != LINK_CO_DEVELOPED) {
- continue;
- }
- foreach (var rule in yield list_rules_for_project(link.project_id)) {
- if (seen.add(rule.id.to_string())) {
- ordered.add(rule);
- }
- }
- }
- } else if (task.project_id > 0) {
- foreach (var link in yield projects.list_links(task.project_id)) {
- if (link.relationship != LINK_CO_DEVELOPED) {
- continue;
- }
- foreach (var rule in yield list_rules_for_project(link.linked_project_id)) {
- if (seen.add(rule.id.to_string())) {
- ordered.add(rule);
- }
- }
- }
- }
- return ordered.to_immutable_buffer();
- }
- }
- }
|