antcolony
All repositories: gitoria
9.0 KB
// relations.hl — mission 022 (ticket antcolony#1; concept docs/scheduler-agent.md §2 "Picking work",// "Creator questions are tickets", "Rejections are work"): what the ticket relations (tickets#4, mission 017:// `parent`, `children`, `blockedBy`, `blocks` on every row) and the history mean for the scheduler.//// * a ticket with any blocker that is not `confirmed` is not eligible ("blocked by X#n (state)");// * a ticket with children is a parent → not work — EXCEPT children that are creator questions (the scheduler's// own: source `colony…:question…`; mission 023: also the architect's hand-filed ones, subject starting with// `Question`): they hang below the ticket they belong to (parent = the ticket) and must not turn it into a// parent (else a ticket with a question could never be reworked);// * rejections: the last `→ rejected` event against the last colony session start (the `→ in progress`// lease event of `work`: `colony-lease:` marker, or v0's "session <S> started on <H>"). Newer → eligible// again. The brief starts with the rejection while no colony report came after it.import { baseUrl } from './tickets.hl'import { NL } from './util.hl'// a creator question the scheduler filed (next --post, report questions / unknown-project issues, Nth controller fail)static isQuestionSource = (src) => {if (src == null || src == '') { return false }return src.startsWith('colony') && src.includes(':question')}// a creator question: filed by the scheduler (source) OR by hand — the architect's convention is a subject that// starts with `Question` (mission 023, creator 2026-09-24). Such children never make their ticket a parent.static isCreatorQuestion = (row) => {if (row == null) { return false }if (isQuestionSource(row.source)) { return true }if (row.subject == null || !row.subject.startsWith('Question')) { return false }if (row.subject.length == 8) { return true }// the whole word: `Question: …`, `Question (x#1): …`, `Questions …` — not `Questionnaire page`let c = row.subject.charCodeAt(8)if (c == 115) { c = row.subject.length == 9 ? 32 : row.subject.charCodeAt(9) }return !((c >= 65 && c <= 90) || (c >= 97 && c <= 122))}// mission 030 (antcolony#1): a question ticket is NEVER work — in no state (open, rejected, on hold, …). The live run// (pre-030) took rejected question tickets ("already answered") as rework: ident#17 once, gitoria#3 twice, gitoria#4// five times. `next` / `run` / `cycle` list it with this reason, `brief` / `work` / `salvage` refuse it, a parked session// on it is not resumed. Its answer reaches workers only through the librarian (DECISIONS.md in every brief).static QUESTION_WHY = 'a question for the creator — the librarian takes its answer'static questionWhy = (row) => { return isCreatorQuestion(row) ? QUESTION_WHY : null }static listOf = (v) => { return v == null ? [] : v }// a relation ref → `alpha#3 [open] subject`static refText = (r) => { return r.project + r.ref + ' [' + r.state + '] ' + r.subject }static refUrl = (r) => { return baseUrl() + r.href }// row = a ticket row; lookup(id) → the child's row (for its `source`) or null// → { has (tickets serves relations), parent, work (children that are work), questions (question children),// blockedBy, openBlockers (not confirmed), blocks }static relationsOf = (row, lookup) => {let workKids = []let questionKids = []for (c of listOf(row.children)) {let cr = lookup(c.id)if (isCreatorQuestion(cr) || isCreatorQuestion(c)) { questionKids.push(c) } else { workKids.push(c) }}let openB = []for (b of listOf(row.blockedBy)) { if (b.state != 'confirmed') { openB.push(b) } }let hasRel = row.keys().includes('children')return { has = hasRel parent = row.parent work = workKids questions = questionKids blockedBy = listOf(row.blockedBy) openBlockers = openB blocks = listOf(row.blocks) }}// why the ticket is not work because of its relations, or null// `rework` = a rejection newer than the last session: the creator's rejection outranks a pending questionstatic blockReason = (rel, rework) => {if (rel.work.length > 0) {let k = 0for (c of rel.work) { if (c.state == 'confirmed') { k = k + 1 } }return 'a parent (' + rel.work.length + ' child ticket' + (rel.work.length == 1 ? '' : 's') + ', ' + k + ' confirmed) — not work itself, its children are'}// antcolony#30: a creator question of the ticket that is not answered yet (confirmed) or refused (rejected) → the ticket// waits for it — a half-done ticket whose worker asked the creator must not be restarted every minutelet waiting = []if (rework != true) { for (q of rel.questions) { if (q.state != 'confirmed' && q.state != 'rejected') { waiting.push(q.project + q.ref + ' (' + q.state + ')') } } }if (waiting.length > 0) { return 'waiting for the creator\'s answer on ' + waiting.join(', ') }if (rel.openBlockers.length > 0) {let parts = []for (b of rel.openBlockers) { parts.push(b.project + b.ref + ' (' + b.state + ')') }return 'blocked by ' + parts.join(', ')}return null}// the start of a colony session in a ticket's history: the lease event of `work`static isSessionStart = (e) => {if (e.kind != 'state' || e.to != 'in progress' || e.text == null) { return false }return e.text.includes('colony-lease: ') || (e.text.startsWith('session ') && e.text.includes(' started on '))}// events (oldest first) → { rejection (the last `→ rejected` event or null), lastSessionSeq, eligible (the last// rejection is newer than the last colony session start), pending (a rejection no colony report answered yet),// comments (the rejecting creator's comments since the last colony session before the rejection) }static rejectionOf = (events) => {let rej = nulllet lastStart = 0let startBefore = 0let answered = falsefor (e of events) {if (isSessionStart(e)) { lastStart = e.seq }if (e.kind == 'state' && e.to == 'rejected') {rej = estartBefore = lastStartanswered = false} else if (rej != null && e.kind == 'comment' && e.text != null && e.text.includes('colony-report: ') && !e.text.includes(' · half done')) {// mission 026: a HALF DONE report does not answer the rejection — the next worker continues and still reads itanswered = true}}let cs = []if (rej != null) {for (e of events) {if (e.seq > startBefore && e.kind == 'comment' && e.author == rej.author) { cs.push(e) }}}let elig = rej != null && rej.seq > lastStartlet pend = rej != null && !answeredreturn { rejection = rej lastSessionSeq = lastStart eligible = elig pending = pend comments = cs }}// antcolony#30 (safety net): how many colony sessions started on the ticket within the last hour (events oldest first)static startsInLastHour = (events, nowMs) => {let n = 0for (e of events) { if (isSessionStart(e) && e.createdMs != null && nowMs - e.createdMs < 3600000) { n = n + 1 } }return n}static quoted = (text) => {let out = []for (l of text.trim().split(NL)) { out.push('> ' + l) }return out.join(NL)}// the brief's first lines for a rejected ticket (list of lines)static rejectedLines = (rj, ref) => {let o = []let e = rj.rejectionlet reason = e.text == null || e.text.trim() == '' ? '_(no reason given with the rejection — see the comments / the history)_' : e.text.trim()o.push('REJECTED by the creator: ' + reason)o.push('')o.push('_(' + e.author + ' set ' + ref + ' to `rejected` on ' + e.when + ', event ' + e.seq + ' of the history below.)_')if (rj.comments.length > 0) {o.push('')o.push("The creator's comments since the last session:")for (c of rj.comments) {o.push('')o.push('**' + c.when + '** (event ' + c.seq + '):')o.push('')o.push(quoted(c.text))}}o.push('')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.')o.push('')o.push('---')o.push('')return o}// the brief's "Relations" section (list of lines)static relationLines = (rel) => {let o = []o.push('## Relations (parent / children / blocked by)')if (!rel.has) {o.push('- _(this tickets server serves no relations)_')return o}o.push('- Parent: ' + (rel.parent == null ? 'none' : refText(rel.parent) + ' — ' + refUrl(rel.parent)))let n = rel.work.length + rel.questions.lengthif (n == 0) { o.push('- Children: none') } else {o.push('- Children (' + n + '):')for (c of rel.work) { o.push(' - ' + refText(c) + ' — ' + refUrl(c)) }for (c of rel.questions) { o.push(' - ' + refText(c) + ' — creator question — ' + refUrl(c)) }}if (rel.blockedBy.length == 0) { o.push('- Blocked by: none') } else {o.push('- Blocked by (' + rel.openBlockers.length + ' not confirmed):')for (b of rel.blockedBy) { o.push(' - ' + refText(b) + (b.state == 'confirmed' ? '' : ' — **blocking**') + ' — ' + refUrl(b)) }}if (rel.blocks.length == 0) { o.push('- Blocks: none') } else {o.push('- Blocks:')for (b of rel.blocks) { o.push(' - ' + refText(b) + ' — ' + refUrl(b)) }}return o}
Branches
- mainmain branch
Latest commits
- 7f9660eeState of 2026-09-27, before the move to gitoriamre