tickets
All repositories: gitoria
6.7 KB
// mdview.hl — THE MARKDOWN READ VIEW FOR LLM READERS (ticket #6, mission 014; CONCEPT.md "Data// formats": a Markdown read view for LLM readers). The ticket GET endpoints answer// `Accept: text/markdown` with a compact Markdown document instead of JSON (project.hl// `wantsMarkdown`); JSON stays the default and is unchanged. Statics only.//// one ticket → # <project> #<n>: <subject>// state · opened (by) · updated · formerly · URL// the summary as written (it IS Markdown)// ## History — one `### <seq> · <when> · <author> <what>` per event, its text below// a list → # Tickets (<filters>): <count>// one line per ticket: `- <project> #<n> [<state>] <subject> · updated <when> · <url>`// Texts are embedded as written (they are Markdown already), so a text's own headings sit// inside the document — readers should take the `### <seq> ·` lines as the event boundaries.import { Response } from 'hl:http1'import { publicUrl } from './users.hl'static NL = ""// does the request ask for Markdown? `Accept` names text/markdown with q > 0 and prefers it to// application/json (a higher q, or the same q and listed first). No Accept / */* → JSON.static qOf = (part) => {let q = 1for (p of part.split(';')) {let kv = p.trim()if (kv.startsWith('q=')) { q = toNumber(kv.slice(2)) }}return q == null ? 0 : q}static wantsMarkdown = (req) => {let h = req.headers['accept']if (h == null || hlTypeName(h) != 'String') { return false }let md = -1let js = -1let mdAt = -1let jsAt = -1let i = 0for (part of h.toLowerCase().split(',')) {let type = part.split(';')[0].trim()if (type == 'text/markdown' && mdAt < 0) {md = qOf(part)mdAt = i}if (type == 'application/json' && jsAt < 0) {js = qOf(part)jsAt = i}i = i + 1}if (md <= 0) { return false }if (js < 0) { return true }return md > js || (md == js && mdAt < jsAt)}static markdownReply = (text) => {return new Response(text, { status = 200 headers = { 'Content-Type' = 'text/markdown; charset=utf-8' 'Vary' = 'Accept' } })}// one line: line breaks and runs of spaces become one spacestatic oneLine = (s) => {if (s == null) { return '' }let parts = []for (w of ('' + s).replaceAll(NL, ' ').split(' ')) { if (w != '') { parts.push(w) } }return parts.join(' ')}// `> ` in front of every line (a previous summary inside an edit event)static quoted = (s) => {let out = []for (l of ('' + s).split(NL)) { out.push(l == '' ? '>' : '> ' + l) }return out.join(NL)}static ticketLine = (row) => {return '- ' + row.project + ' ' + row.ref + ' [' + row.state + '] ' + oneLine(row.subject) + ' · updated ' + row.updated + ' · ' + publicUrl + row.href + relationTail(row)}// ---- relations (ticket #4, mission 017) --------------------------------------------------------// one ticket's document gets "Parent:", "Children (k of n done):", "Blocked by:", "Blocks:"// blocks under its URL; a list line gets ` · parent … · children n, k done · blocked by … · blocks …`static refName = (r) => { return r.project + ' ' + r.ref }static refLine = (r) => { return '- ' + refName(r) + ' [' + r.state + '] ' + oneLine(r.subject) + ' · ' + publicUrl + r.href }static confirmedOf = (refs) => {let n = 0for (r of refs) { if (r.state == 'done') { n = n + 1 } }return n}static relationTail = (row) => {let parts = []if (row.parent != null) { parts.push('parent ' + refName(row.parent)) }let total = row.children.lengthif (total > 0) { parts.push('children ' + total + (row.allChildrenConfirmed ? ', all done' : ', ' + confirmedOf(row.children) + ' done')) }if (row.blockedBy.length > 0) {let bs = []for (r of row.blockedBy) { bs.push(refName(r) + ' [' + r.state + ']') }parts.push('blocked by ' + bs.join(', '))}if (row.blocks.length > 0) {let bs = []for (r of row.blocks) { bs.push(refName(r)) }parts.push('blocks ' + bs.join(', '))}return parts.length == 0 ? '' : ' · ' + parts.join(' · ')}static relationLines = (t) => {let lines = []if (t.parent != null) {lines.push('')lines.push('Parent: ' + refName(t.parent) + ' [' + t.parent.state + '] ' + oneLine(t.parent.subject) + ' · ' + publicUrl + t.parent.href)}let total = t.children.lengthif (total > 0) {lines.push('')lines.push('Children (' + (t.allChildrenConfirmed ? 'all ' + total + ' done' : confirmedOf(t.children) + ' of ' + total + ' done') + '):')for (r of t.children) { lines.push(refLine(r)) }}if (t.blockedBy.length > 0) {lines.push('')lines.push('Blocked by:')for (r of t.blockedBy) { lines.push(refLine(r)) }}if (t.blocks.length > 0) {lines.push('')lines.push('Blocks:')for (r of t.blocks) { lines.push(refLine(r)) }}return lines}// a list: { tickets } (rows, as the JSON answer) + a label of its filters ('' = all)static listDocument = (rows, filters) => {let lines = []lines.push('# Tickets' + (filters != '' ? ' (' + filters + ')' : '') + ': ' + rows.length)lines.push('')for (r of rows) { lines.push(ticketLine(r)) }return lines.join(NL) + NL}static eventHeadline = (e) => {let what = e.labelif (e.kind == 'state') { what = 'changed the state: ' + (e.from != '' ? e.from : '?') + ' → ' + e.to }if (e.kind == 'edit') {let parts = []if (e.subjectChanged) { parts.push('subject') }if (e.summaryChanged) { parts.push('summary') }what = 'edited the ' + parts.join(' and ')}return '### ' + e.seq + ' · ' + e.when + ' · ' + oneLine(e.author) + ' ' + what}// one ticket: { ticket (row), events (rows, oldest first) } as the JSON answerstatic ticketDocument = (data) => {let t = data.ticketlet opener = ''for (e of data.events) { if (e.kind == 'created' && opener == '') { opener = oneLine(e.author) } }let lines = []lines.push('# ' + t.project + ' ' + t.ref + ': ' + oneLine(t.subject))lines.push('')let meta = 'state: ' + t.state + ' · opened ' + t.created + (opener != '' ? ' by ' + opener : '') + ' · updated ' + t.updatedif (t.hasOld) { meta = meta + ' · formerly ' + t.oldRef }lines.push(meta)lines.push(publicUrl + t.href)for (l of relationLines(t)) { lines.push(l) }lines.push('')lines.push(t.summary != '' ? t.summary : '(no summary)')lines.push('')lines.push('## History')for (e of data.events) {lines.push('')lines.push(eventHeadline(e))if (e.kind == 'edit') {if (e.subjectChanged) {lines.push('')lines.push('Subject before: ' + oneLine(e.oldSubject))}if (e.summaryChanged) {lines.push('')lines.push('Summary before:')lines.push('')lines.push(e.oldSummary != '' ? quoted(e.oldSummary) : '> (empty)')}}if (e.hasText) {lines.push('')lines.push(e.text)}}return lines.join(NL) + NL}
Branches
- mainmain branch
Latest commits
- bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
- 4137be0fantcolony#40: mission references point to the moved missionsmre
- 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
- 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
- e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
- 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
- f12fa1bcState of 2026-09-27, before the move to gitoriamre