gitoriaLog in with ident

antcolony

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit3a4d03243a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmre3a4d0324/lib/relations.hl

9.0 KB

  1. // relations.hl — mission 022 (ticket antcolony#1; concept docs/scheduler-agent.md §2 "Picking work",
  2. // "Creator questions are tickets", "Rejections are work"): what the ticket relations (tickets#4, mission 017:
  3. // `parent`, `children`, `blockedBy`, `blocks` on every row) and the history mean for the scheduler.
  4. //
  5. // * a ticket with any blocker that is not `confirmed` is not eligible ("blocked by X#n (state)");
  6. // * a ticket with children is a parent → not work — EXCEPT children that are creator questions (the scheduler's
  7. // own: source `colony…:question…`; mission 023: also the architect's hand-filed ones, subject starting with
  8. // `Question`): they hang below the ticket they belong to (parent = the ticket) and must not turn it into a
  9. // parent (else a ticket with a question could never be reworked);
  10. // * rejections: the last `→ rejected` event against the last colony session start (the `→ in progress`
  11. // lease event of `work`: `colony-lease:` marker, or v0's "session <S> started on <H>"). Newer → eligible
  12. // again. The brief starts with the rejection while no colony report came after it.
  13. import { baseUrl } from './tickets.hl'
  14. import { NL } from './util.hl'
  15. // a creator question the scheduler filed (next --post, report questions / unknown-project issues, Nth controller fail)
  16. static isQuestionSource = (src) => {
  17. if (src == null || src == '') { return false }
  18. return src.startsWith('colony') && src.includes(':question')
  19. }
  20. // a creator question: filed by the scheduler (source) OR by hand — the architect's convention is a subject that
  21. // starts with `Question` (mission 023, creator 2026-09-24). Such children never make their ticket a parent.
  22. static isCreatorQuestion = (row) => {
  23. if (row == null) { return false }
  24. if (isQuestionSource(row.source)) { return true }
  25. if (row.subject == null || !row.subject.startsWith('Question')) { return false }
  26. if (row.subject.length == 8) { return true }
  27. // the whole word: `Question: …`, `Question (x#1): …`, `Questions …` — not `Questionnaire page`
  28. let c = row.subject.charCodeAt(8)
  29. if (c == 115) { c = row.subject.length == 9 ? 32 : row.subject.charCodeAt(9) }
  30. return !((c >= 65 && c <= 90) || (c >= 97 && c <= 122))
  31. }
  32. // mission 030 (antcolony#1): a question ticket is NEVER work — in no state (open, rejected, on hold, …). The live run
  33. // (pre-030) took rejected question tickets ("already answered") as rework: ident#17 once, gitoria#3 twice, gitoria#4
  34. // five times. `next` / `run` / `cycle` list it with this reason, `brief` / `work` / `salvage` refuse it, a parked session
  35. // on it is not resumed. Its answer reaches workers only through the librarian (DECISIONS.md in every brief).
  36. static QUESTION_WHY = 'a question for the creator — the librarian takes its answer'
  37. static questionWhy = (row) => { return isCreatorQuestion(row) ? QUESTION_WHY : null }
  38. static listOf = (v) => { return v == null ? [] : v }
  39. // a relation ref → `alpha#3 [open] subject`
  40. static refText = (r) => { return r.project + r.ref + ' [' + r.state + '] ' + r.subject }
  41. static refUrl = (r) => { return baseUrl() + r.href }
  42. // row = a ticket row; lookup(id) → the child's row (for its `source`) or null
  43. // → { has (tickets serves relations), parent, work (children that are work), questions (question children),
  44. // blockedBy, openBlockers (not confirmed), blocks }
  45. static relationsOf = (row, lookup) => {
  46. let workKids = []
  47. let questionKids = []
  48. for (c of listOf(row.children)) {
  49. let cr = lookup(c.id)
  50. if (isCreatorQuestion(cr) || isCreatorQuestion(c)) { questionKids.push(c) } else { workKids.push(c) }
  51. }
  52. let openB = []
  53. for (b of listOf(row.blockedBy)) { if (b.state != 'confirmed') { openB.push(b) } }
  54. let hasRel = row.keys().includes('children')
  55. return { has = hasRel parent = row.parent work = workKids questions = questionKids blockedBy = listOf(row.blockedBy) openBlockers = openB blocks = listOf(row.blocks) }
  56. }
  57. // why the ticket is not work because of its relations, or null
  58. // `rework` = a rejection newer than the last session: the creator's rejection outranks a pending question
  59. static blockReason = (rel, rework) => {
  60. if (rel.work.length > 0) {
  61. let k = 0
  62. for (c of rel.work) { if (c.state == 'confirmed') { k = k + 1 } }
  63. return 'a parent (' + rel.work.length + ' child ticket' + (rel.work.length == 1 ? '' : 's') + ', ' + k + ' confirmed) — not work itself, its children are'
  64. }
  65. // antcolony#30: a creator question of the ticket that is not answered yet (confirmed) or refused (rejected) → the ticket
  66. // waits for it — a half-done ticket whose worker asked the creator must not be restarted every minute
  67. let waiting = []
  68. if (rework != true) { for (q of rel.questions) { if (q.state != 'confirmed' && q.state != 'rejected') { waiting.push(q.project + q.ref + ' (' + q.state + ')') } } }
  69. if (waiting.length > 0) { return 'waiting for the creator\'s answer on ' + waiting.join(', ') }
  70. if (rel.openBlockers.length > 0) {
  71. let parts = []
  72. for (b of rel.openBlockers) { parts.push(b.project + b.ref + ' (' + b.state + ')') }
  73. return 'blocked by ' + parts.join(', ')
  74. }
  75. return null
  76. }
  77. // the start of a colony session in a ticket's history: the lease event of `work`
  78. static isSessionStart = (e) => {
  79. if (e.kind != 'state' || e.to != 'in progress' || e.text == null) { return false }
  80. return e.text.includes('colony-lease: ') || (e.text.startsWith('session ') && e.text.includes(' started on '))
  81. }
  82. // events (oldest first) → { rejection (the last `→ rejected` event or null), lastSessionSeq, eligible (the last
  83. // rejection is newer than the last colony session start), pending (a rejection no colony report answered yet),
  84. // comments (the rejecting creator's comments since the last colony session before the rejection) }
  85. static rejectionOf = (events) => {
  86. let rej = null
  87. let lastStart = 0
  88. let startBefore = 0
  89. let answered = false
  90. for (e of events) {
  91. if (isSessionStart(e)) { lastStart = e.seq }
  92. if (e.kind == 'state' && e.to == 'rejected') {
  93. rej = e
  94. startBefore = lastStart
  95. answered = false
  96. } else if (rej != null && e.kind == 'comment' && e.text != null && e.text.includes('colony-report: ') && !e.text.includes(' · half done')) {
  97. // mission 026: a HALF DONE report does not answer the rejection — the next worker continues and still reads it
  98. answered = true
  99. }
  100. }
  101. let cs = []
  102. if (rej != null) {
  103. for (e of events) {
  104. if (e.seq > startBefore && e.kind == 'comment' && e.author == rej.author) { cs.push(e) }
  105. }
  106. }
  107. let elig = rej != null && rej.seq > lastStart
  108. let pend = rej != null && !answered
  109. return { rejection = rej lastSessionSeq = lastStart eligible = elig pending = pend comments = cs }
  110. }
  111. // antcolony#30 (safety net): how many colony sessions started on the ticket within the last hour (events oldest first)
  112. static startsInLastHour = (events, nowMs) => {
  113. let n = 0
  114. for (e of events) { if (isSessionStart(e) && e.createdMs != null && nowMs - e.createdMs < 3600000) { n = n + 1 } }
  115. return n
  116. }
  117. static quoted = (text) => {
  118. let out = []
  119. for (l of text.trim().split(NL)) { out.push('> ' + l) }
  120. return out.join(NL)
  121. }
  122. // the brief's first lines for a rejected ticket (list of lines)
  123. static rejectedLines = (rj, ref) => {
  124. let o = []
  125. let e = rj.rejection
  126. let reason = e.text == null || e.text.trim() == '' ? '_(no reason given with the rejection — see the comments / the history)_' : e.text.trim()
  127. o.push('REJECTED by the creator: ' + reason)
  128. o.push('')
  129. o.push('_(' + e.author + ' set ' + ref + ' to `rejected` on ' + e.when + ', event ' + e.seq + ' of the history below.)_')
  130. if (rj.comments.length > 0) {
  131. o.push('')
  132. o.push("The creator's comments since the last session:")
  133. for (c of rj.comments) {
  134. o.push('')
  135. o.push('**' + c.when + '** (event ' + c.seq + '):')
  136. o.push('')
  137. o.push(quoted(c.text))
  138. }
  139. }
  140. o.push('')
  141. o.push('**Build what was described, not a workaround; if impossible, stop and explain** (in the report: `open` + `questions`) — never deliver a different design than the ticket and the creator describe.')
  142. o.push('')
  143. o.push('---')
  144. o.push('')
  145. return o
  146. }
  147. // the brief's "Relations" section (list of lines)
  148. static relationLines = (rel) => {
  149. let o = []
  150. o.push('## Relations (parent / children / blocked by)')
  151. if (!rel.has) {
  152. o.push('- _(this tickets server serves no relations)_')
  153. return o
  154. }
  155. o.push('- Parent: ' + (rel.parent == null ? 'none' : refText(rel.parent) + ' — ' + refUrl(rel.parent)))
  156. let n = rel.work.length + rel.questions.length
  157. if (n == 0) { o.push('- Children: none') } else {
  158. o.push('- Children (' + n + '):')
  159. for (c of rel.work) { o.push(' - ' + refText(c) + ' — ' + refUrl(c)) }
  160. for (c of rel.questions) { o.push(' - ' + refText(c) + ' — creator question — ' + refUrl(c)) }
  161. }
  162. if (rel.blockedBy.length == 0) { o.push('- Blocked by: none') } else {
  163. o.push('- Blocked by (' + rel.openBlockers.length + ' not confirmed):')
  164. for (b of rel.blockedBy) { o.push(' - ' + refText(b) + (b.state == 'confirmed' ? '' : ' — **blocking**') + ' — ' + refUrl(b)) }
  165. }
  166. if (rel.blocks.length == 0) { o.push('- Blocks: none') } else {
  167. o.push('- Blocks:')
  168. for (b of rel.blocks) { o.push(' - ' + refText(b) + ' — ' + refUrl(b)) }
  169. }
  170. return o
  171. }

Branches

Latest commits

  • 3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmre
  • a6af7883tracker: worker box sees calendar.worldapi.org (login to copy)mre
  • c613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mre
  • 9062978ctracker: worker box sees /media/STORAGE/projects/old-tracker read-only (tracker#2 source data)mre
  • 7f9660eeState of 2026-09-27, before the move to gitoriamre