diff --git a/.github/workflows/conventions.yml b/.github/workflows/conventions.yml index ef7855b..fccd772 100644 --- a/.github/workflows/conventions.yml +++ b/.github/workflows/conventions.yml @@ -5,4 +5,4 @@ on: pull_request: jobs: conventions: - uses: robertblust/conventions/.github/workflows/check.yml@v1.46.0 + uses: robertblust/conventions/.github/workflows/check.yml@v1.51.0 diff --git a/AGENTS.md b/AGENTS.md index 303ecff..34fc83d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ - + Shared conventions of the robertblust, guestgraph and companygraph organizations live in `conventions/`, vendored from robertblust/conventions at the release `conventions.json` names. Read them before writing or committing anything here. - `conventions/WRITING.md` — how we write: one voice, three registers, English and German. diff --git a/blog/deciding-well-solved/og.sha b/blog/deciding-well-solved/og.sha index 15aca41..b162239 100644 --- a/blog/deciding-well-solved/og.sha +++ b/blog/deciding-well-solved/og.sha @@ -1 +1 @@ -6535f8b21c3602a1b317ff70b6fa4e7f43b5fd622aca9e0c0e04c78851336d80 +fe9d91db2eb5d86f0de1770586f0ced6bdfb756b77a050ce824a81efc75dbed0 diff --git a/blog/og.sha b/blog/og.sha index 72a11eb..149f0d1 100644 --- a/blog/og.sha +++ b/blog/og.sha @@ -1 +1 @@ -b276dbe150c4de571fe6437f09b4f3e32e9061e804bd02fbe81f6a27f72d8816 +1538846499546c6beb3abcee489d4e1f8ec2adfb357281e84e779e8dc8583162 diff --git a/blog/one-question-answered/og.sha b/blog/one-question-answered/og.sha index 0e9a520..dc83259 100644 --- a/blog/one-question-answered/og.sha +++ b/blog/one-question-answered/og.sha @@ -1 +1 @@ -a2110254675a84d80f24d22812f89551827e0c180e3162110164be92d72137c9 +cbf859ae36d91cafd897c8599e1bc3c29894efca94e7c19d6132757e1dad471b diff --git a/chat.css b/chat.css index f78c782..02be536 100644 --- a/chat.css +++ b/chat.css @@ -214,7 +214,12 @@ html[data-chat-waiting]::after{content:"ask \00b7 " attr(data-chat-waiting);pos Processes page draws them, and both beat the linked label's own color; a system's element mark is the firm brightness too, since a system is a thing and not an agent. An open position is a dashed box with no fill, as is a scenario's extension, and the frame the agents of a group - stand in is shaded. */ + stand in is shaded. A frame that stands on the ground, a use case's scope and a requirement + kind's, is shaded the same way with the accent as its border: Mermaid fills a frame with the + ground, the modal's own color, and in the dark theme draws its border black, so the frame + would not read. The host names a frame by its subgraph id, which Mermaid keeps at the end of + the cluster's own, and that is what the selector reads; an organization's groups, named + g0 and on, stay on the ground so that the agents' frame inside them stands apart. */ .rbchat-diagram-box svg .label-icon{width:1.15em;height:1.15em;vertical-align:-.2em;margin-right:.3em} .rbchat-diagram-box svg .rbchat-mark.human{color:var(--c-firm,var(--ink))!important} .rbchat-diagram-box svg .rbchat-mark.agent{color:var(--c-mid)!important} @@ -223,6 +228,7 @@ html[data-chat-waiting]::after{content:"ask \00b7 " attr(data-chat-waiting);pos .rbchat-diagram-box svg .node.open rect{fill:none!important} .rbchat-diagram-box svg .node.ext rect{fill:none!important} .rbchat-diagram-box svg .cluster.agents rect{fill:var(--press)!important} +.rbchat-diagram-box svg .cluster:is([id$="-scope"],[id$="-functional"],[id$="-quality"],[id$="-constraint"]) rect{fill:var(--press)!important;stroke:var(--c-mid)!important} .rbchat-diagram-reading{margin:.35rem .1rem 0;font-size:.8rem;line-height:1.4;color:var(--dim)} /* A page's own picture is a preview: fitted to the column, never past its own size, and opened by a click to be read; chat.js marks it. The rule reaches the box only while it is in diff --git a/chat.js b/chat.js index 23b70db..650e6b7 100644 --- a/chat.js +++ b/chat.js @@ -51,7 +51,7 @@ // rbChat.iconOf(doc) the page's icon, for the head of that line // rbChat.pick(list, n, random) n items of list, uniformly at random and without repeats // rbChat.unasked(list, messages) the titles no visitor message in the conversation has asked -// rbChat.spread(items, n, random) the titles offered, one per kind where the model groups them +// rbChat.spread(items, n, random) the titles offered, one per kind where the model groups them, its first-ranked kind first // rbChat.mentioned(content, names, items, messages) the entities an answer writes, in its order, not yet asked about // rbChat.follow(cites, items, messages, lang, random, named) the three that follow an answer, or null // rbChat.answerLang(turns, page) the language what follows the last answer is offered in @@ -62,6 +62,8 @@ // rbChat.lockupOf(doc) the header's mark and name, split as the page splits it, or null // rbChat.command(text) "new", "help" or null, for the command line's own words // rbChat.picked(text, rows) the row a bare number picks, or the text as typed +// rbChat.tryPick(facts, random) what an opening offers after the meta-model, two of what the model holds +// rbChat.connected(entities, edges) the systems a landscape row may name // rbChat.tryRows(facts, lang, random) the intro's asks, each with what comes back // rbChat.commitOf(cites) the commit the answer was read at, or null // rbChat.seconds(ms) the spinner's count @@ -122,6 +124,10 @@ try: { metaModel: "Show me the meta-model", metaModelGets: "a diagram of the types and how they refer to each other", process: "Walk me through the {name} process", processGets: "its steps as a flow, the loops back included", + organization: "Show me the organization", organizationGets: "its units as a chart, with who leads each and who sits in it", + landscape: "Show me the landscape of {name}", landscapeGets: "what it runs on, what it connects to and the data it keeps", + usecase: "Show me the use case {name}", usecaseGets: "who it serves, what it includes and what it asks of the product", + requirements: "Show me the requirements of {name}", requirementsGets: "what it is held to, every requirement a link into the model", list: "List {list} as a table", listGets: "one row each, every name a link into the model", lists: { kpi: "the KPIs", seat: "the seats", product: "the products", decision: "the decisions", value: "the values" } }, @@ -170,6 +176,10 @@ try: { metaModel: "Zeig mir das Meta-Modell", metaModelGets: "ein Diagramm der Typen und wie sie aufeinander verweisen", process: "Zeig mir den Prozess {name} Schritt für Schritt", processGets: "die Schritte als Ablauf, samt Rücksprüngen", + organization: "Zeig mir die Organisation", organizationGets: "die Einheiten als Organigramm: wer sie jeweils leitet und wer dazugehört", + landscape: "Zeig mir die Landschaft von {name}", landscapeGets: "worauf das System läuft, womit es verbunden ist und welche Daten es hält", + usecase: "Zeig mir den Anwendungsfall {name}", usecaseGets: "wem er dient, was er einbindet und was er vom Produkt verlangt", + requirements: "Zeig mir die Anforderungen an {name}", requirementsGets: "woran das Produkt gemessen wird, jede Anforderung ein Link ins Modell", list: "Liste {list} als Tabelle auf", listGets: "eine Zeile je Eintrag, jeder Name ein Link ins Modell", lists: { kpi: "die KPIs", seat: "die Sitze", product: "die Produkte", decision: "die Entscheide", value: "die Werte" } }, @@ -601,7 +611,9 @@ // The chips as a span of what the model answers: where its questions name a kind, three kinds // are picked at random and one question from each, so a visitor sees three sorts of question - // rather than three that may all be about one thing. Fewer kinds than chips fill from what is + // rather than three that may all be about one thing. Where the model ranks its kinds, the first + // chip is of the kind it ranks first among those still open, the one its owner says a visitor + // asks first, and the others span the rest at random. Fewer kinds than chips fill from what is // left, and a model whose questions name no kind is offered exactly as pick() offers it, so a // site that takes this before its model groups anything loses nothing. function spread(items, n, random){ @@ -609,9 +621,16 @@ var list = (items || []).filter(function(q){ return q && typeof q.title === "string"; }); var kinded = list.filter(function(q){ return typeof q.kind === "string" && q.kind; }); if (!kinded.length) return pick(list.map(function(q){ return q.title; }), n, rnd); - var byKind = {}, kinds = []; - kinded.forEach(function(q){ if (!byKind[q.kind]) { byKind[q.kind] = []; kinds.push(q.kind); } byKind[q.kind].push(q.title); }); - var chosen = pick(kinds, n, rnd).map(function(k){ return pick(byKind[k], 1, rnd)[0]; }); + var byKind = {}, kinds = [], rankOf = Object.create(null), first = null; + kinded.forEach(function(q){ + if (!byKind[q.kind]) { byKind[q.kind] = []; kinds.push(q.kind); } + byKind[q.kind].push(q.title); + if (typeof q.rank === "number" && isFinite(q.rank) && !(rankOf[q.kind] <= q.rank)) rankOf[q.kind] = q.rank; + }); + kinds.forEach(function(k){ if (k in rankOf && (first === null || rankOf[k] < rankOf[first])) first = k; }); + var count = Math.max(0, Number(n) || 0); + var order = first !== null && count > 0 ? [first].concat(pick(kinds.filter(function(k){ return k !== first; }), count - 1, rnd)) : pick(kinds, n, rnd); + var chosen = order.map(function(k){ return pick(byKind[k], 1, rnd)[0]; }); var taken = {}; chosen.forEach(function(t){ taken[t] = true; }); var rest = list.map(function(q){ return q.title; }).filter(function(t){ return !taken[t]; }); @@ -759,6 +778,11 @@ return { // The classic look is flat, as the family draws: the default draws shadows and gradients. startOnLoad: false, securityLevel: "strict", theme: "base", look: "classic", fontFamily: font, + // Mermaid draws a rounded node and a cylinder from a seed, and where this is left at its + // default of 0 it takes a random one for each render, so the same picture came out a few + // pixels different every time. A fixed seed makes a picture the same wherever and whenever + // it is drawn, which a deck's PDF is compared by. + handDrawnSeed: 1, // At its own size in a box that scrolls: fitted to a bubble, a wide picture's words shrink // below reading. A concept carries no attributes or methods, so its class has no empty bars. flowchart: { useMaxWidth: false }, class: { useMaxWidth: false, hideEmptyMembersBox: true }, @@ -869,17 +893,71 @@ return i >= 0 && i < rows.length ? rows[i] : t; } // The Try rows: what the chat is built to answer, each with what comes back. The meta-model - // always; a process only where the model holds one, picked at random; a list only of a kind - // the model holds at least three of, the first in this order, so no row names what is not there. + // always, then two of what the model holds, picked at random: one of its processes, its + // organization where it holds a group, the landscape of one of its systems, one of its use + // cases, what one of its products is held to where it holds a requirement, and a list of a + // kind it holds at least three of, the first in LIST_TYPES' order. No row names what the model + // does not have, and a model that holds only a process and a list is offered both, as before. var LIST_TYPES = ["kpi", "seat", "product", "decision", "value"]; - function tryRows(facts, lang, random){ - var t = strings(lang).try, rows = [[t.metaModel, t.metaModelGets]]; - var ps = facts && Array.isArray(facts.processes) ? facts.processes : []; - if (ps.length) rows.push([t.process.replace("{name}", pick(ps, 1, random)[0]), t.processGets]); - var counts = facts && facts.counts || {}; + var TRY_ROWS = 3; + // What a row asks about by name, a name short enough to stand in a row: a use case and a + // product as the model names them, and a system as connected() chooses it. + var NAMED_TYPES = ["use-case", "product"], NAME_LIMIT = 120; + // The systems a landscape row may name: those the model writes a connection on, since a + // landscape is what a system takes its data from and one that takes none draws alone; where + // the model writes no connection at all, every system. + function connected(entities, edges){ + var nameOf = {}, all = [], on = {}; + (Array.isArray(entities) ? entities : []).forEach(function(e){ + if (!e || e.type !== "system" || typeof e.name !== "string" || !e.name.length || e.name.length > NAME_LIMIT) return; + all.push(e.name); + if (typeof e.id === "string") nameOf[e.id] = e.name; + }); + (Array.isArray(edges) ? edges : []).forEach(function(g){ + if (g && typeof g.via === "string" && g.via.indexOf("Connects to.") === 0 && typeof nameOf[g.from] === "string") on[nameOf[g.from]] = true; + }); + var linked = all.filter(function(n){ return on[n] === true; }); + return linked.length ? linked : all; + } + // What an opening offers after the meta-model, as { key, name }: `key` names the row's words in + // strings().try and `name` what it asks about, so the picks are kept with the conversation and + // drawn again in either language. They stand in one order however the random falls. + function tryPick(facts, random){ + var rnd = typeof random === "function" ? random : Math.random; + var f = facts || {}, counts = f.counts || {}, names = f.names || {}, pool = []; + function named(type){ return Array.isArray(names[type]) ? names[type] : []; } + function one(list){ return pick(list, 1, rnd)[0]; } + var ps = Array.isArray(f.processes) ? f.processes : []; + if (ps.length) pool.push({ key: "process", name: one(ps) }); + if ((counts.group || 0) >= 1) pool.push({ key: "organization" }); + if (named("system").length) pool.push({ key: "landscape", name: one(named("system")) }); + if (named("use-case").length) pool.push({ key: "usecase", name: one(named("use-case")) }); + if ((counts.requirement || 0) >= 1 && named("product").length) pool.push({ key: "requirements", name: one(named("product")) }); for (var i = 0; i < LIST_TYPES.length; i++) { - if ((counts[LIST_TYPES[i]] || 0) >= 3) { rows.push([t.list.replace("{list}", t.lists[LIST_TYPES[i]]), t.listGets]); break; } + if ((counts[LIST_TYPES[i]] || 0) >= 3) { pool.push({ key: "list", name: LIST_TYPES[i] }); break; } } + var at = pick(pool.map(function(_, n){ return n; }), TRY_ROWS - 1, rnd).sort(function(x, y){ return x - y; }); + return at.map(function(n){ return pool[n]; }); + } + // The rows as the page's language words them. `facts.tries` is the picks an opening already + // made, drawn as they stand; without it the picks are made here. A pick this widget has no + // words for, kept in the tab by another release, is left out rather than drawn half. + function tryRows(facts, lang, random){ + var t = strings(lang).try, rows = [[t.metaModel, t.metaModelGets]]; + var tries = facts && Array.isArray(facts.tries) ? facts.tries : tryPick(facts, random); + tries.forEach(function(p){ + if (!p || typeof p.key !== "string") return; + var ask = t[p.key], gets = t[p.key + "Gets"]; + if (typeof ask !== "string" || typeof gets !== "string") return; + if (p.key === "list") { + if (typeof t.lists[p.name] !== "string") return; + ask = ask.replace("{list}", function(){ return t.lists[p.name]; }); + } else if (ask.indexOf("{name}") >= 0) { + if (typeof p.name !== "string" || !p.name) return; + ask = ask.replace("{name}", function(){ return p.name; }); + } + rows.push([ask, gets]); + }); return rows; } // The commit the answer was read at: the first cite whose URL names one. @@ -895,7 +973,7 @@ // The rows a number picks, as the keys line and /help name them: none, one, or a range. function rangeOf(n){ return n > 1 ? "1-" + n : n === 1 ? "1" : ""; } - window.rbChat = { md: md, readEvents: readEvents, strings: strings, link: link, refocus: refocus, asked: asked, linkQuestion: linkQuestion, nameLinks: nameLinks, heard: heard, when: when, refusalText: refusalText, citeLine: citeLine, iconOf: iconOf, pick: pick, unasked: unasked, sayIn: sayIn, asTitles: asTitles, answerLang: answerLang, spread: spread, mentioned: mentioned, mermaidConfig: mermaidConfig, nodeElement: nodeElement, diagramCaption: diagramCaption, nodeHref: nodeHref, oriented: oriented, follow: follow, place: place, placed: placed, lockupOf: lockupOf, command: command, picked: picked, tryRows: tryRows, commitOf: commitOf, seconds: seconds, rangeOf: rangeOf, versionsOf: versionsOf, graphHref: graphHref, entityOf: entityOf, graphTarget: graphTarget }; + window.rbChat = { md: md, readEvents: readEvents, strings: strings, link: link, refocus: refocus, asked: asked, linkQuestion: linkQuestion, nameLinks: nameLinks, heard: heard, when: when, refusalText: refusalText, citeLine: citeLine, iconOf: iconOf, pick: pick, unasked: unasked, sayIn: sayIn, asTitles: asTitles, answerLang: answerLang, spread: spread, mentioned: mentioned, mermaidConfig: mermaidConfig, nodeElement: nodeElement, diagramCaption: diagramCaption, nodeHref: nodeHref, oriented: oriented, follow: follow, place: place, placed: placed, lockupOf: lockupOf, command: command, picked: picked, tryPick: tryPick, connected: connected, tryRows: tryRows, commitOf: commitOf, seconds: seconds, rangeOf: rangeOf, versionsOf: versionsOf, graphHref: graphHref, entityOf: entityOf, graphTarget: graphTarget }; // ─── The page ───────────────────────────────────────────────────────────────────────────── var tag = document.currentScript; @@ -1477,7 +1555,7 @@ // container currently in the log, if any, and `qNext` whether it follows an answer rather than // opening an empty conversation, which is all that tells its two names apart. `qPicked` is // the titles that box offers, kept with the conversation while it stands. - var qList = null, qFetch = null, qBox = null, qPicked = null, qNext = false, qFacts = { processes: [], counts: {}, versions: null }; + var qList = null, qFetch = null, qBox = null, qPicked = null, qNext = false, qFacts = { processes: [], counts: {}, names: {}, versions: null }; var Q_TIMEOUT = 8000; // The titles to offer, read from the site's own parsed model rather than asked of the chat @@ -1516,14 +1594,22 @@ // of each type the model holds, so a row never names what the model does not have. var counts = {}; entities.forEach(function(e){ if (e && typeof e.type === "string") counts[e.type] = (counts[e.type] || 0) + 1; }); - qFacts = { processes: entities.filter(function(e){ return e && e.type === "process" && typeof e.name === "string" && e.name.length > 0; }).map(function(e){ return e.name; }), counts: counts, versions: versionsOf(j) }; + // What a row asks about by name, and the rank the model gives each kind of question. + var names = {}, ranks = Object.create(null); + entities.forEach(function(e){ + if (!e || typeof e.name !== "string" || !e.name.length) return; + if (NAMED_TYPES.indexOf(e.type) >= 0 && e.name.length <= NAME_LIMIT) (names[e.type] = names[e.type] || []).push(e.name); + if (e.type === "question-kind" && e.fields && e.fields.rank != null && String(e.fields.rank).trim() !== "" && isFinite(Number(e.fields.rank))) ranks[e.name] = Number(e.fields.rank); + }); + names.system = connected(entities, j && j.edges); + qFacts = { processes: entities.filter(function(e){ return e && e.type === "process" && typeof e.name === "string" && e.name.length > 0; }).map(function(e){ return e.name; }), counts: counts, names: names, versions: versionsOf(j) }; (j && Array.isArray(j.edges) ? j.edges : []).forEach(function(g){ if (!g || types[g.from] !== "question" || typeof g.via !== "string" || g.via.indexOf("Rests on.") !== 0) return; (rests[g.from] = rests[g.from] || []).push({ id: g.to, type: types[g.to] || null }); }); return entities .filter(function(e){ return e && e.type === "question" && typeof e.name === "string" && e.name.length > 0; }) - .map(function(e){ return { id: typeof e.id === "string" ? e.id : null, title: e.name, kind: e.fields && typeof e.fields.kind === "string" ? e.fields.kind : null, rests: rests[e.id] || [] }; }) + .map(function(e){ var kind = e.fields && typeof e.fields.kind === "string" ? e.fields.kind : null; return { id: typeof e.id === "string" ? e.id : null, title: e.name, kind: kind, rank: kind !== null && kind in ranks ? ranks[kind] : null, rests: rests[e.id] || [] }; }) .filter(function(q){ return q.title.length <= LIMIT; }); }); }) @@ -1845,8 +1931,10 @@ if (mine !== introRun || !introEl) return; var t = strings(langNow()); var seen = asTitles(list, messages); - if (!introPick) introPick = { processes: f.processes.length ? pick(f.processes, 1) : [], questions: spread(list.filter(function(q){ return unasked([q.title], seen).length; }), 3) }; - var rows = tryRows({ processes: introPick.processes, counts: f.counts }, langNow()); + if (!introPick) introPick = { tries: tryPick(f), questions: spread(list.filter(function(q){ return unasked([q.title], seen).length; }), 3) }; + // Picks kept by a release that kept a process alone are made again, once. + if (!Array.isArray(introPick.tries)) introPick.tries = tryPick(f); + var rows = tryRows({ tries: introPick.tries }, langNow()); groups.appendChild(el("p", "rbchat-label", t.tryLabel)); menu(groups, rows, 0); var picked = introPick.questions.map(function(q){ return [asOffered(q)]; }); @@ -2326,8 +2414,8 @@ // Only a panel restored open shows its intro now; a closed one gets it from open(), since // the intro reads the model file and that read waits for the panel. reading = was.at && typeof was.at.turn === "number" ? was.at : { end: true }; - // The intro's picks, so it offers the same process and questions it offered on the last page. - if (was.pick && Array.isArray(was.pick.processes) && Array.isArray(was.pick.questions)) introPick = was.pick; + // The intro's picks, so it offers the same rows and questions it offered on the last page. + if (was.pick && Array.isArray(was.pick.questions)) introPick = was.pick; was.turns.forEach(function(t){ if (t.role === "user") { var u = bubble("user"); u.textContent = t.content; u.setAttribute("data-turn", turns.length); messages.push({ role: "user", content: t.content }); turns.push({ role: "user", content: t.content }); return; } var ans = bubble("assistant"), body = el("div", "rbchat-body"); diff --git a/conventions.json b/conventions.json index eed39a7..c4059d6 100644 --- a/conventions.json +++ b/conventions.json @@ -1 +1 @@ -{ "repo": "robertblust/conventions", "tag": "v1.46.0", "exclude": ["node_modules", "docs/superpowers"], "format-exclude": ["node_modules"] } +{ "repo": "robertblust/conventions", "tag": "v1.51.0", "exclude": ["node_modules", "docs/superpowers"], "format-exclude": ["node_modules"] } diff --git a/conventions/AGENTS.md b/conventions/AGENTS.md index 2568465..9521da8 100644 --- a/conventions/AGENTS.md +++ b/conventions/AGENTS.md @@ -1,4 +1,4 @@ - + Shared conventions of the robertblust, guestgraph and companygraph organizations live in `conventions/`, vendored from robertblust/conventions at the release `conventions.json` names. Read them before writing or committing anything here. - `conventions/WRITING.md` — how we write: one voice, three registers, English and German. diff --git a/conventions/GERMAN.md b/conventions/GERMAN.md index a919913..363988f 100644 --- a/conventions/GERMAN.md +++ b/conventions/GERMAN.md @@ -45,7 +45,6 @@ selbstständig → selbständig Offener Kern → Open Core Open Source → quelloffen Takeaway → Fazit -Requirements → Anforderungen ``` A bare count of the years, «fünfundzwanzig Jahre» without «über», is refused by a rule of its own in the check, because a phrase list cannot say "unless preceded by". @@ -60,6 +59,7 @@ Each row is a sentence the editor flagged and the owner settled, kept where it g | «eine einzige Quelle der Wahrheit» | «eine einzige verbindliche Quelle» | The English picture read as a calque. | | «Ein Konzept, erklärt» | «Ein Konzept im Überblick» | The label copied the English. | | «Der Haken» | «Der Aufhänger» | The hook of a talk; «Haken» is a catch. | +| «das Anforderungs-Pack» | «das Requirements-Pack» | A pack keeps its English name, as «das Landscape-Pack» and «das Software-Pack» do; the generic noun stays «Anforderungen», and the type is `Anforderung`, the glossary's row. The refused form that turned every Requirements into Anforderungen was wrong in this sentence, so it left the fence. | | «wie Arbeit fliesst» | «wie die Arbeit abläuft» | The English picture of flow. | | «Menschen richten sich schneller aus» | «Menschen finden schneller eine gemeinsame Linie» | *Align* word for word. | | «Zusammenführen ist einfach. Falsch liegen ist teuer.» | «Treffer finden ist einfach. Danebenliegen ist teuer.» | *Matching* is finding a match, not merging. | diff --git a/conventions/GLOSSARY.md b/conventions/GLOSSARY.md index ad2b657..df1c48b 100644 --- a/conventions/GLOSSARY.md +++ b/conventions/GLOSSARY.md @@ -42,6 +42,7 @@ The German cells are inline code because the prose check reads no language and a | MCP server | MCP server | `MCP-Server` | The server that answers over MCP; `companygraph/mcp-server` is the package. Masculine, `der MCP-Server`. | | MCP Registry | MCP Registry | `MCP Registry` | The public index a server is published to. A proper name, English in both languages, and feminine: `in der MCP Registry`, never `im`. No page carried German for it before, which is why the gender is recorded here rather than left to the next sentence. | | AI agent | AI agent | `KI-Agent` | The reader a model serves beside a person. Masculine, `der KI-Agent`, plural `die KI-Agenten`, the form blust.ch's ideas page already uses. | +| AI Act | AI Act | `AI Act` | The EU's regulation on artificial intelligence, whose Article 50 asks a chat to say that an AI answers. A name, English in both languages, as `MCP` and `core` are, so a reader matches the German page to the English one; masculine, `der AI Act`. Not `KI-Verordnung`, the German press's term, set aside on 2026-10-11 so both pages carry the one name. | | read-only | read-only | `nur lesend` | Of a server that answers and never writes. Not `schreibgeschützt`, which says write-protected and describes what may be done to a file rather than what a server offers. | | shipped | shipped | `ausgeliefert` | Of a thing released and running, as against designed or built. Not `gebaut`, which says only built, and not `fertig`, which says finished. | | entity | entity | `Entität` | One file in the model, of whatever type. The form blust.ch's model page uses. | @@ -83,6 +84,13 @@ The German cells are inline code because the prose check reads no language and a | actor claim | actor claim | `Actor-Claim` | A claim in a JWT that names who acts. Masculine, `der Actor-Claim`. | | steward | steward | `verantwortliche Person` | Who reviews a proposed match; plural `die Mitarbeitenden, die prüfen`. A hotel reader does not know Steward. | | API (surface) | surface | `API` | An API as what a caller reaches, `die API`. Not `Schnittstelle`, and not `Surface`, which is a page the model is published on. | +| interface (between systems) | interface | `Schnittstelle` | What a connection between two systems goes through, as a landscape's `As` column names it: an API, a file transfer, a queue. Feminine, `die Schnittstelle`. An API a caller reaches stays `API`, the row above. | +| service (of a system) | service | `Service` | What a system exposes to other systems, the landscape pack's type. Masculine, `der Service`, as Swiss IT writes it; not `Dienst`, which beside `System` and `Plattform` reads as a Windows service or a public one. | +| landscape (the picture) | landscape | `Landschaft` | The picture of one system among those it runs on, takes its data from and gives data to, and the chat's caption for it: «Zeig mir die Landschaft von Chat server». Feminine, `die Landschaft`. Not `Systemlandschaft`, the commoner word in Swiss IT, because the caption and the pack say landscape alone; the pack itself keeps its English name, `das Landscape-Pack`. | +| organization chart | organization chart | `Organigramm` | The picture of a company's units, with who leads each and who belongs to it: «die Einheiten als Organigramm». Neuter, `das Organigramm`. The caption over the picture stays `Organisation`, and `Diagramm` is any drawn figure. | +| requirements pack | requirements pack | `Requirements-Pack` | The pack for a company that says what its products must do before it builds them, with the types use case and requirement. Neuter, `das Requirements-Pack`, the English name kept as every pack's is. | +| use case | use case | `Anwendungsfall` | The requirements pack's type: one goal for one actor and every way of reaching it, in Cockburn's form. Masculine, `der Anwendungsfall`, Cockburn's German; its scope is `der Geltungsbereich`, its trigger `der Auslöser`, an extension `die Erweiterung`, its primary actor `der Hauptakteur`. | +| requirement (the type) | requirement | `Anforderung` | The requirements pack's type: one statement a product is held to, with its kind, `die Art`, its modality, `muss`, `sollte` or `darf`, and its fit criterion, `das Erfüllungskriterium`. Feminine, `die Anforderung`; a constraint is `die Randbedingung`. | | credit meter | credit meter | `Abrechnung nach Guthaben` | Billing against a prepaid balance. | | backfill | backfill | `Historischer Import` | The first import of the history before live data. | | walk-in | walk-in | `Walk-in` | A guest without a reservation, as Swiss hotels say it; plural `Walk-ins`. | diff --git a/conventions/conventions-check b/conventions/conventions-check index a7f53fe..b00a070 100755 --- a/conventions/conventions-check +++ b/conventions/conventions-check @@ -1,6 +1,7 @@ #!/bin/sh # conventions-check — hold a member's own Markdown to WRITING.md: American English and the -# spaced em-dash. Fenced code and inline code are not prose and are dropped before the scan; +# spaced em-dash. Fenced code and inline code are not prose and are dropped before the scan; the +# stem scan also skips frontmatter, the first H1 of a page and quoted text (see below); # folders under "exclude" in conventions.json are someone else's prose and are not read, nor is # anything git ignores, which the shared job never sees either. # @@ -31,6 +32,12 @@ PIN=$ROOT/conventions.json # way but on the letter that follows, since British and American diverge right there. Case is # folded by lowering the line before matching, not by the pattern, since IGNORECASE is a GNU # extension awk on macOS does not have. +# What the stem scan does not read: text in backticks, text in guillemets «…», the page's H1 line +# and the YAML frontmatter. A name is the company's and is quoted, not spelled, so a German name +# that holds a stem, or a product's own feature name, is not a spelling. Only the stem scan skips +# these spans; the closed-dash check below still reads them. A quote is cut within its line; a +# pair spanning two lines is read. Curly double quotes “…” are cut like guillemets, since an +# English name is quoted the en-US way. STEMS='organis(e|a|i)|colour|behaviour|centre|licence|recognis|modelling|catalogue|grey([^a-z]|$)|judgement|artefact|whilst|amongst|favour|honour|analys(e|ed|ing)([^a-z]|$)|optimis(e|a|i)|normalis|serialis|initialis(e|a|i)|visualis|minimis(e|a|i)|prioritis|customis|summaris|categoris|parameteris|rasteris|unrecognis|cancelled|travelled|labelled|aluminium|instalment|neighbour|totalled|defence|offence|programmes?([^a-z]|$)|fulfil([^a-z]|$)|enrol([^a-z]|$)|skilful|ageing|cosy([^a-z]|$)|focuss|cheque([^a-z]|$)|tyre([^a-z]|$)|kerb([^a-z]|$)|storey|mould|plough|draught|sceptic|generalis(e|ed|es|ing|ation|ations)|specialis(e|ed|es|ing|ation)|realis(e|ed|es|ing|ation)|materialis(e|ed|es|ing|ation)|penalis(e|ed|es|ing|ation)|singularis(e|ed|es|ing|ation)|authoris(e|ed|es|ing|ation)|emphasis(e|ed|es|ing)|criticis(e|ed|es|ing)|capitalis(e|ed|es|ing|ation)|formalis(e|ed|es|ing|ation)|finalis(e|ed|es|ing|ation)|stabilis(e|ed|es|ing|ation)|utilis(e|ed|es|ing|ation)|harmonis(e|ed|es|ing|ation)|standardis(e|ed|es|ing|ation)|synchronis(e|ed|es|ing|ation)|mobilis(e|ed|es|ing|ation)|memoris(e|ed|es|ing|ation)|apologis(e|ed|es|ing)|synthesis(e|ed|es|ing)|hypothesis(e|ed|es|ing)|paralys(e|ed|es|ing)|catalys(e|ed|es|ing)|modelled|modeller|flavour|harbour|labour|rumour|humour|vigour|rigour|armour|endeavour|savour|odour|valour|metre([^a-z]|$)|metres|litre|fibre|theatre|calibre|travelling|signalling|cancelling|fuelled|levelled|channelled|counsellor|marvellous|jewellery' DASH='[^ ]—|—[^ ]' @@ -76,8 +83,12 @@ if [ -z "$files" ]; then fi hits=$(printf '%s\n' "$files" | while IFS= read -r f; do - # Drop fenced code and inline code, then scan what is left of each line for a stem or a - # closed dash; the match is against the line's own content, never the path awk read it from. + # Drop fenced code, inline code and URLs, then scan what is left of each line for a stem or a + # closed dash. The stem scan alone also skips frontmatter, the page's first H1 and quoted + # text; the dash check reads those as it did before, with backticks and URLs stripped. The match is against the line's own content, never + # the path awk read it from. + # An address is fixed by the site it points to, as a name is, so it ends at a space, a closing + # parenthesis or angle bracket, or a table pipe, and a Markdown link's text is still read. # A fence left open at end of file is a hit of its own, not a silent skip of the rest. awk -v stems="$STEMS" -v dash="$DASH" -v root="$ROOT" ' function relpath( p, prefix) { @@ -86,19 +97,46 @@ hits=$(printf '%s\n' "$files" | while IFS= read -r f; do if (index(p, prefix) == 1) p = substr(p, length(prefix) + 1) return p } - /^```/ { fence = !fence; next } - fence { next } + # Cut the text between o and c from s. The glyph lengths are taken with length(), so the + # cut is right in a byte awk and in a character awk alike. + function cut(s, o, c, a, b, lo, lc) { + lo = length(o); lc = length(c) + while ((a = index(s, o)) > 0 && (b = index(substr(s, a + lo), c)) > 0) + s = substr(s, 1, a - 1) substr(s, a + lo + b - 1 + lc) + return s + } + # A byte order mark is dropped by its bytes in a string, which every awk reads alike. + FNR == 1 && index($0, "\357\273\277") == 1 { $0 = substr($0, length("\357\273\277") + 1) } + FNR == 1 && /^---[ \t\r]*$/ { fm = 1; next } + fm && /^---[ \t\r]*$/ { fm = 0; next } + # A frontmatter that is never closed ends at the first line that is none of these: blank, + # indented, a "- item" at column 0, a "#" comment or a key line. A stray opener cannot hide + # the whole page, though a malformed page whose --- is never closed may still hide a line + # that looks like a key. + fm && !/^[ \t\r]*$/ && !/^[ \t]/ && !/^- / && !/^#/ && !/^["\047]?[$@A-Za-z0-9_][A-Za-z0-9_ .-]*["\047]?:( |$)/ { fm = 0 } + !fm && /^```/ { fence = !fence; next } + fence { next } { line = $0 sub(/\r$/, "", line) + raw = line + # Only the stem scan skips frontmatter, the first H1 of the page and quoted text; the + # dash check below reads the line as it did before. + skip = fm + if (!fm && !h1 && line ~ /^# /) { h1 = 1; skip = 1 } gsub(/`[^`]*`/, "", line) + line = cut(line, "«", "»") + line = cut(line, "“", "”") + gsub(/https?:\/\/[^ )>|]*/, "", line) low = tolower(line) - if (match(low, stems)) { + if (!skip && match(low, stems)) { w = substr(low, RSTART, RLENGTH) sub(/[^a-z]$/, "", w) printf "✗ %s:%d: %s\n", relpath(), FNR, w } - if (match(line, dash)) printf "✗ %s:%d: closed em-dash\n", relpath(), FNR + gsub(/`[^`]*`/, "", raw) + gsub(/https?:\/\/[^ )>|]*/, "", raw) + if (match(raw, dash)) printf "✗ %s:%d: closed em-dash\n", relpath(), FNR } END { if (fence) printf "✗ %s:%d: unclosed code fence\n", relpath(), FNR } ' "$f" diff --git a/conventions/manifest.json b/conventions/manifest.json index e36c1e3..f304766 100644 --- a/conventions/manifest.json +++ b/conventions/manifest.json @@ -1,6 +1,6 @@ { "repo": "robertblust/conventions", - "tag": "v1.46.0", + "tag": "v1.51.0", "files": { "conventions/WRITING.md": "sha256:51d9a902ec3038392160df07f86aba98d34db1d4dbfaa5f4a647590eaccfb390", "conventions/WORKING.md": "sha256:3a7a6b4ab1bd23f2407fc6ada2dd0512710a49d233fa7c3342951f6fc526ab23", @@ -8,13 +8,13 @@ "conventions/PINS.md": "sha256:68df67ae820a8673d3130582aa829d4283176d3c8ec0a69adda3132a1a755b93", "conventions/WRITER.md": "sha256:bc3b92683130b75993f824f0e34073d4344846c91be2009fd90c39099fd4fd48", "conventions/TRANSLATOR.md": "sha256:3a3d2d0916d7230cbc760bced1be249b11cc516956fb41ad010b3216f23024ba", - "conventions/GLOSSARY.md": "sha256:429733de608d68142cc261dc48aa4fdf0dde3dd002902f0af413a63e6467f9b9", - "conventions/GERMAN.md": "sha256:e8b311ed4b48cc2ded964bbaa0cfcf70178cdbd8072cd467b586c2bfa0cb949b", + "conventions/GLOSSARY.md": "sha256:f29b2a31e171f9666e70e3c0ab3290876fcd90846f99c86a12a6ac56c16acdf1", + "conventions/GERMAN.md": "sha256:6a0ab79fd52f93d1361aceb83a1771ce6448124d5ea707af3d71baf9efe35e11", "conventions/EDITOR.md": "sha256:e65d0b2893ac841faefede9b873f7e33c1fd7b02bedf97d37d5362c0e83c08ea", "conventions/BACKREADER.md": "sha256:36b12d4bee8e4fe190469e2f550d50d8a7f6dcab6bec573da17eb1c165c417cd", - "conventions/AGENTS.md": "sha256:861c1ec80624afb6e87a993922a18ea101cc718ace94e4237c7da89bf6367f2b", + "conventions/AGENTS.md": "sha256:7fba72d9d4c76e269c29fbe014adfb2e03861ad9e3e4c73af594929167ceeac4", "conventions/conventions-sync": "sha256:a190ff79c2d8d3f6f6d9a597e9f261654f2419d30e2ac43abf6c64ebe2c47ed9", - "conventions/conventions-check": "sha256:fb55ca4b4cfc5caa0d346ad6c705d4402125155f95db6c974099a8112e275c69", + "conventions/conventions-check": "sha256:4a633cc53579f4577939702350c439bbb58ebed4ccbf6736d94c01e5fc160147", "conventions/conventions-format": "sha256:2911a0d30dda1bc0f621522a9538b3e39704eee03458e65ba320eccbf39b5c7f", "conventions/markdown-rules.cjs": "sha256:5cbf6120bf16e358b3a4e362015d054f0cd6e47ddfb0e5fe5ac17143b2bdef26", "conventions/vscode-settings.json": "sha256:86d3c951f7c06d86ee3bc828ad6e49fac3531be98117a7a590cc75da7ba3600a", diff --git a/id/01a12457-c64f-7a57-97e5-b36e80289743/index.html b/id/01a12457-c64f-7a57-97e5-b36e80289743/index.html new file mode 100644 index 0000000..bc5a890 --- /dev/null +++ b/id/01a12457-c64f-7a57-97e5-b36e80289743/index.html @@ -0,0 +1,13 @@ + + + + + +Model + + + + +

Go on to Model.

+ + diff --git a/ideas/og.sha b/ideas/og.sha index 94e6bd1..cceb5ce 100644 --- a/ideas/og.sha +++ b/ideas/og.sha @@ -1 +1 @@ -a45e7c0933de69896b1392ca5837271000140f124031dc00b8d5dd86c84ab359 +05d5094e89d27f9354fbd275ce230670f5581b967d4c0b92fad793a005ae78b2 diff --git a/model.json b/model.json index c21d093..423cd05 100644 --- a/model.json +++ b/model.json @@ -1,6 +1,6 @@ { - "commit": "8b29e0a2635c91818c4e1e9518845c7886e2293a", - "core": "0.64.0", + "commit": "43dfa37bb4fa92ca6fe2a6a311b3f80c1d135158", + "core": "0.69.0", "root": "Robert Blust", "rootId": "01a04c85-bc20-7849-9ec1-d877215d70b4", "types": [ @@ -124,6 +124,12 @@ "owner": null, "singular": false }, + { + "type": "product-kind", + "folder": "product-kinds", + "owner": null, + "singular": false + }, { "type": "proficiency-level", "folder": "proficiency-levels", @@ -7278,6 +7284,27 @@ "owner": null, "path": "model/processing-activities/keeping-the-chat-s-questions.md" }, + { + "id": "01a12457-c64f-7a57-97e5-b36e80289743", + "address": "product-kinds/model", + "type": "product-kind", + "name": "Model", + "tagline": "A company described once, published as a site, an MCP server and a chat.", + "fields": { + "id": "01a12457-c64f-7a57-97e5-b36e80289743", + "source": "Local", + "rank": "10" + }, + "sections": [ + { + "heading": "What it means", + "text": "A product of this kind is opened by whoever wants to know what the company is and does, a reader on the site, a search engine, an agent over MCP, a visitor asking the chat, and every one of them reads the same model. The Mental Model is this kind. There is no other kind yet; a second arrives with the first product that is not a published model.", + "tables": [] + } + ], + "owner": null, + "path": "model/product-kinds/model.md" + }, { "id": "01a0c5d1-a530-7cf0-a47d-80e069b18bf0", "address": "products/mental-model", @@ -7287,7 +7314,8 @@ "fields": { "id": "01a0c5d1-a530-7cf0-a47d-80e069b18bf0", "source": "Local", - "domain": "Portfolio" + "domain": "Portfolio", + "kind": "Model" }, "sections": [], "owner": null, @@ -27575,6 +27603,12 @@ "via": "domain", "attrs": {} }, + { + "from": "01a0c5d1-a530-7cf0-a47d-80e069b18bf0", + "to": "01a12457-c64f-7a57-97e5-b36e80289743", + "via": "kind", + "attrs": {} + }, { "from": "01a0c5d1-a530-7cf0-a47d-80e069b18bf0", "to": "01a03d8e-dd00-7372-ac62-ba79f12bf81a", @@ -31793,6 +31827,12 @@ "to": "01a0dd50-37a8-753d-8ace-143372d9cbac", "via": "status", "attrs": {} + }, + { + "from": "01a12457-c64f-7a57-97e5-b36e80289743", + "to": "01a03d8e-dd00-7372-ac62-ba79f12bf81a", + "via": "source", + "attrs": {} } ], "repo": "robertblust/mental-model" diff --git a/model/og.png b/model/og.png index 3de0ae7..47fce31 100644 Binary files a/model/og.png and b/model/og.png differ diff --git a/model/og.sha b/model/og.sha index 10f583d..24d0fea 100644 --- a/model/og.sha +++ b/model/og.sha @@ -1 +1 @@ -59f3901f8d7def8cc96f3afb7fce56362d56db4c573ce868162096842ec9aa7e +f6a3199e9fab9db40f1524064b85ed98c40ce1d6bd5530ce5af2785f879b2f47 diff --git a/og.sha b/og.sha index 22a3956..ec8e663 100644 --- a/og.sha +++ b/og.sha @@ -1 +1 @@ -8b1242c11c39f2af81ff4086eac33d0a7d9117234b74312b38373cccb1f5ec48 +2aa70fd96dd24c32a5b54e6b3e524805cd6a6f1262611aa0170be90ec75ec029 diff --git a/package-lock.json b/package-lock.json index f69c099..85e5411 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "blust-ch", "version": "1.0.0", "dependencies": { - "companygraph-meta-model": "github:companygraph/meta-model#v0.88.0" + "companygraph-meta-model": "github:companygraph/meta-model#v0.94.0" }, "devDependencies": { - "@robertblust/design": "github:robertblust/design#v0.151.0", - "companygraph-mcp-server": "github:companygraph/mcp-server#v0.62.0", + "@robertblust/design": "github:robertblust/design#v0.154.0", + "companygraph-mcp-server": "github:companygraph/mcp-server#v0.69.0", "pdf-lib": "^1.17.1", "playwright": "^1.63.0" } @@ -95,8 +95,8 @@ } }, "node_modules/@robertblust/design": { - "version": "0.151.0", - "resolved": "git+ssh://git@github.com/robertblust/design.git#65b78c6310af99370e594d48de0f8bff6a682448", + "version": "0.154.0", + "resolved": "git+ssh://git@github.com/robertblust/design.git#812a39bab81d0f67e0a5a953d569b1b397e4a93f", "dev": true, "license": "Apache-2.0", "bin": { @@ -107,14 +107,14 @@ } }, "node_modules/companygraph-mcp-server": { - "version": "0.62.0", - "resolved": "git+ssh://git@github.com/companygraph/mcp-server.git#8e91cb3594a87826a8909c0cd536475a510d77cd", + "version": "0.69.0", + "resolved": "git+ssh://git@github.com/companygraph/mcp-server.git#1815507b4bb4bf1fb31ecb984bc848cc602e666b", "dev": true, "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/node": "^2.1.1", "@modelcontextprotocol/server": "^2.0.0", - "companygraph-meta-model": "github:companygraph/meta-model#v0.88.0", + "companygraph-meta-model": "github:companygraph/meta-model#v0.94.0", "zod": "^4.2.0" }, "bin": { @@ -128,8 +128,8 @@ } }, "node_modules/companygraph-meta-model": { - "version": "0.88.0", - "resolved": "git+ssh://git@github.com/companygraph/meta-model.git#93e13f7288312730e7a053d2718b8ec5b4335ef1", + "version": "0.94.0", + "resolved": "git+ssh://git@github.com/companygraph/meta-model.git#4088f5a1d23be63f43953ed1eae5eb1740f6eb8d", "license": "Apache-2.0", "bin": { "companygraph": "bin/companygraph.mjs" diff --git a/package.json b/package.json index e2949b2..20c05c6 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,12 @@ "pin:check": "node pin-check.mjs" }, "devDependencies": { - "@robertblust/design": "github:robertblust/design#v0.151.0", - "companygraph-mcp-server": "github:companygraph/mcp-server#v0.62.0", + "@robertblust/design": "github:robertblust/design#v0.154.0", + "companygraph-mcp-server": "github:companygraph/mcp-server#v0.69.0", "pdf-lib": "^1.17.1", "playwright": "^1.63.0" }, "dependencies": { - "companygraph-meta-model": "github:companygraph/meta-model#v0.88.0" + "companygraph-meta-model": "github:companygraph/meta-model#v0.94.0" } } diff --git a/page.css b/page.css index 969f431..ee7c5f1 100644 --- a/page.css +++ b/page.css @@ -1,5 +1,5 @@ /* @robertblust/design — page.css, assembled from the shared blocks: reset v3 · link v1 · - header v18 · title v2 · lines v3 · footer v3 · principles v1 · processes v1 · surfaces v1 · + header v18 · title v2 · lines v3 · footer v3 · principles v1 · processes v1 · surfaces v2 · index v2 · home v1. Editing this file in a site does nothing: the next npm run design overwrites it. */ @@ -386,7 +386,10 @@ footer a[aria-current="page"]{color:var(--c-firm)} .lineage{position:relative; display:grid; grid-template-columns:minmax(0,13rem) minmax(0,1fr); column-gap:clamp(3rem,7vw,6rem); align-items:center; margin-top:1.4rem} .wires{position:absolute; inset:0; width:100%; height:100%; overflow:visible; pointer-events:none} -.wires path{fill:none; stroke:var(--rule); stroke-width:1.5; transition:stroke .2s ease} +/* A wire at rest is the candidate color, as the stage's resting edge is. --rule draws the + boxes, which their text holds in place; a wire in it was a hairline with nothing beside it, + and on the dark ground it was lost. */ +.wires path{fill:none; stroke:var(--c-weak); stroke-width:1.5; transition:stroke .2s ease} .wires path.hand{stroke-dasharray:5 5} /* --c-path is the token for exactly this: the way back to the root from what is focused. */ .wires path.on{stroke:var(--c-path); stroke-width:2} @@ -424,9 +427,9 @@ footer a[aria-current="page"]{color:var(--c-firm)} .wires{display:none} .lineage{grid-template-columns:minmax(0,1fr); row-gap:0} .ln-model{justify-self:start} - .ln-groups{gap:0; margin-left:1rem; padding:1.2rem 0 0 1.1rem; border-left:1.5px solid var(--rule)} + .ln-groups{gap:0; margin-left:1rem; padding:1.2rem 0 0 1.1rem; border-left:1.5px solid var(--c-weak)} .ln-group{grid-template-columns:minmax(0,1fr); padding-bottom:1.2rem} - .ln-surfaces{margin-left:.7rem; padding-left:1rem; border-left:1.5px solid var(--rule)} + .ln-surfaces{margin-left:.7rem; padding-left:1rem; border-left:1.5px solid var(--c-weak)} .ln-group.hand .ln-surfaces{border-left-style:dashed} } diff --git a/principles/index.html b/principles/index.html index 3a59282..60eca47 100644 --- a/principles/index.html +++ b/principles/index.html @@ -334,7 +334,7 @@

Production is the finish line

-

Generated from robertblust/mental-model@8b29e0a — model/vision.md and model/values/. That repository is an instance of CompanyGraph, a meta-model for describing a company as a graph of Markdown.

+

Generated from robertblust/mental-model@43dfa37 — model/vision.md and model/values/. That repository is an instance of CompanyGraph, a meta-model for describing a company as a graph of Markdown.

diff --git a/principles/og.sha b/principles/og.sha index 0aa4af5..004c95c 100644 --- a/principles/og.sha +++ b/principles/og.sha @@ -1 +1 @@ -42955012ca6c878dd69060ca9c1e68bb2468772b466f9d64c0ab37a729475b2f +5819fa39a014fb311bd191a62938e99fccb04806c9a16cb8460242d81f924a1b diff --git a/privacy/index.html b/privacy/index.html index 6b26285..5130f5b 100644 --- a/privacy/index.html +++ b/privacy/index.html @@ -332,7 +332,7 @@

Where your data goes

-
On blust.ch
Your visit
@8b29e0a
+
On blust.ch
Your visit
@43dfa37