diff --git a/apps/website/qa/agents-spec-contract.mjs b/apps/website/qa/agents-spec-contract.mjs index 41411423ed..35fde93405 100644 --- a/apps/website/qa/agents-spec-contract.mjs +++ b/apps/website/qa/agents-spec-contract.mjs @@ -21,7 +21,7 @@ import { join } from 'node:path' import { generate, SKILLS_OUT, CRONS_OUT, AGENTS_OUT, FUNCTIONS_OUT, TOOLS_OUT, FUNCTIONS_MANIFEST, EXPERIENCE_PATH, AGENT_COUNT, AGENT_LAYERS, HOSTS, CONTROLS, ON_FAILURE, FN_TRIGGERS, FN_ON_FAILURE, FN_SIDE_EFFECTS, FN_PROBE_RESULTS, FN_CONTROLS, functionDifferences, I18N_SPEC_DIR, I18N_FIELD_SOURCE, REPO_ROOT } from '../scripts/agents-from-specs.mjs' import { canonicalSpecEditUrl, vendoredSpecUrl, specSlug } from '../src/lib/agentSpecs.ts' import { MODULES } from '../src/lib/queenModules.ts' -import { HUD_VIEWS, HUD_KEYS, RAIL_VIEWS, SPEC_LAYERS, BOARD_VIEWS, railViewOf } from '../src/components/queenHud.ts' +import { HUD_VIEWS, HUD_KEYS, RAIL_VIEWS, SPEC_LAYERS, BOARD_VIEWS, PROJECT_VIEWS, railViewOf } from '../src/components/queenHud.ts' const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex') const skills = JSON.parse(readFileSync(SKILLS_OUT, 'utf8')) @@ -397,15 +397,18 @@ for (const tab of shown) { assert.ok(tab !== 'comb' && tab !== 'specs', `'${tab}' has a block of its own; showcasing it would render it twice`) } assert.match(app, //, 'the homepage does not mount the index, so every module the showcase leaves out is unreachable') -// The index draws three groups -- the rail's buttons, the ladder inside SPECS, -// the board inside KANBAN -- and they have to stay a partition of MODULES or the -// homepage silently drops a module. The predicates are asked of the shell's own +// The index draws four groups -- the rail's buttons, the ladder inside SPECS, +// the board inside KANBAN, the record inside PROJECT -- and they have to stay a +// partition of MODULES or the homepage silently drops a module. The fourth came +// with PASSPORT's move inside PROJECT (2026-09-21): until the index learned it, +// PASSPORT was a module with no card anywhere. The predicates are asked of the shell's own // lists, so this is checkable here rather than only readable there. const modulesBlock = src('components/ModulesBlock.tsx') assert.match(modulesBlock, /RAIL_VIEWS as readonly string\[\]\)\.includes\(tab\)/, 'the index no longer reads the rail from RAIL_VIEWS, so its groups can drift from the buttons') assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isRailModule\(m\.tab\)\)/, 'the rail group is no longer MODULES on the rail, so a module can fall off the homepage') assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isLadderRung\(m\.tab\)\)/, 'the ladder group is no longer the spec layers the rail does not draw') assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isBoardReading\(m\.tab\)\)/, 'the board group is no longer the board views the rail does not draw') +assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isProjectRecord\(m\.tab\)\)/, 'the project group is no longer the project views the rail does not draw') for (const m of MODULES) { const group = RAIL_VIEWS.includes(m.tab) ? 'rail' @@ -413,8 +416,10 @@ for (const m of MODULES) { ? 'ladder' : BOARD_VIEWS.includes(m.tab) ? 'board' - : null - assert.ok(group, `module '${m.tab}' is in none of the index's three groups, so its card is drawn nowhere`) + : PROJECT_VIEWS.includes(m.tab) + ? 'project' + : null + assert.ok(group, `module '${m.tab}' is in none of the index's four groups, so its card is drawn nowhere`) } assert.match(modulesBlock, /modules\.map\(\(module\)/, 'the index no longer draws a card for every module in a group') // lib/queenModules opens by calling itself "the only place their identity is @@ -520,9 +525,11 @@ assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprbwmlx', // member on both its family's list and the rail is drawn twice. assert.ok(SPEC_LAYERS.every((layer) => HUD_VIEWS.includes(layer)), 'a ladder layer is not a view, so it has no address and no key') assert.ok(BOARD_VIEWS.every((view) => HUD_VIEWS.includes(view)), 'a board view is not a view, so it has no address and no key') +assert.ok(PROJECT_VIEWS.every((view) => HUD_VIEWS.includes(view)), 'a project view is not a view, so it has no address and no key') assert.ok(RAIL_VIEWS.every((view) => HUD_VIEWS.includes(view)), 'the rail draws a button for something that is not a view') assert.equal(SPEC_LAYERS[0], 'specs', 'SPECS is the first rung: it is the module the rail opens and the ladder stands in') assert.equal(BOARD_VIEWS[0], 'kanban', 'KANBAN is the board the rail opens and the row stands in') +assert.equal(PROJECT_VIEWS[0], 'project', 'PROJECT is the module the rail opens and PASSPORT stands inside (2026-09-21)') assert.deepEqual( [...SPEC_LAYERS], ['specs', 'skills', 'crons', 'agents', 'tools', 'functions'], @@ -534,9 +541,9 @@ assert.deepEqual( 'the board is kanban, mission map, factory, tech tree -- the order of their keys, 3/4/5/6', ) assert.deepEqual( - [...new Set([...RAIL_VIEWS, ...SPEC_LAYERS, ...BOARD_VIEWS])].sort(), + [...new Set([...RAIL_VIEWS, ...SPEC_LAYERS, ...BOARD_VIEWS, ...PROJECT_VIEWS])].sort(), [...HUD_VIEWS].sort(), - 'rail plus ladder plus board is no longer every view: something is unreachable', + 'rail plus ladder plus board plus project is no longer every view: something is unreachable', ) assert.deepEqual( RAIL_VIEWS.filter((view) => SPEC_LAYERS.includes(view)), @@ -548,10 +555,15 @@ assert.deepEqual( ['kanban'], 'only KANBAN may be on the rail and in the board row; another reading on both is drawn twice', ) +assert.deepEqual( + RAIL_VIEWS.filter((view) => PROJECT_VIEWS.includes(view)), + ['project'], + 'only PROJECT may be on the rail and in the project family; PASSPORT on both is drawn twice', +) assert.equal( RAIL_VIEWS.length, - HUD_VIEWS.length - (SPEC_LAYERS.length - 1) - (BOARD_VIEWS.length - 1), - 'the rail is the views less the five layers folded into SPECS and the two readings folded into KANBAN', + HUD_VIEWS.length - (SPEC_LAYERS.length - 1) - (BOARD_VIEWS.length - 1) - (PROJECT_VIEWS.length - 1), + 'the rail is the views less the layers folded into SPECS, the readings folded into KANBAN and the record folded into PROJECT', ) // Every view still answers railViewOf, and answers with a button the rail draws // -- that is what lights the rail while a folded member is open. diff --git a/apps/website/qa/docs-spec-contract.mjs b/apps/website/qa/docs-spec-contract.mjs index e61d3c0fad..9bea432925 100644 --- a/apps/website/qa/docs-spec-contract.mjs +++ b/apps/website/qa/docs-spec-contract.mjs @@ -170,8 +170,11 @@ if (witnessTable) { const constitution = readFileSync(join(CORPUS, 'docs/T27-CONSTITUTION.md'), 'utf8') const alphabet = readFileSync(join(CORPUS, 'docs/agents/AGENTS_ALPHABET.md'), 'utf8') const laws = parseLawTable(constitution), phases = parsePhases(alphabet) -assert.equal(laws.rows.length, 7, 'the constitution defines L1-L7') -assert.deepEqual(laws.rows.map((l) => l.law), ['L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7']) +// L0 PURPOSE (adopted 2026-09-23) opens the table and sits outside the L1-L7 +// ordering by the constitution's own words; L1-L7 follow in order, unchanged. +const lawIds = laws.rows.map((l) => l.law) +const orderedLaws = lawIds[0] === 'L0' ? lawIds.slice(1) : lawIds +assert.deepEqual(orderedLaws, ['L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7'], 'the constitution defines L1-L7, optionally preceded by L0 PURPOSE') if (lawsTable) assert.deepEqual(lawsTable.rows, laws.rows) assert.deepEqual(docs.figures['law-hierarchy'].laws.map((l) => l.law), laws.rows.map((l) => l.law)) assert.equal(phases.length, 7, 'the alphabet draws six phases plus GIT WORKFLOW') diff --git a/apps/website/src/components/ModulesBlock.tsx b/apps/website/src/components/ModulesBlock.tsx index 7c1c71b23c..99b4cd07b1 100644 --- a/apps/website/src/components/ModulesBlock.tsx +++ b/apps/website/src/components/ModulesBlock.tsx @@ -1,6 +1,6 @@ import { useI18n } from '../i18n/context' import { MODULES } from '../lib/queenModules' -import { BOARD_VIEWS, RAIL_VIEWS, SPEC_LAYERS, isBoardView, isSpecLayer } from './queenHud' +import { BOARD_VIEWS, PROJECT_VIEWS, RAIL_VIEWS, SPEC_LAYERS, isBoardView, isProjectView, isSpecLayer } from './queenHud' import './ModulesBlock.css' // A compact index of the modules: one card each, linking to its tab. Not on the @@ -13,21 +13,23 @@ import './ModulesBlock.css' // FUNCTIONS — are rungs of the ladder inside SPECS, because each is a catalogue // generated from .t27 specs and each names the one below it. Two — MISSION MAP // and FACTORY — sit under KANBAN, because the three are three readings of the -// one board. A reader who scrolled past fourteen equal cards and then opened a -// rail of seven was reading a map of a shell that no longer existed. +// one board. PASSPORT sits inside PROJECT (2026-09-21), the record the project +// keeps beside itself. A reader who scrolled past fourteen equal cards and then +// opened the rail was reading a map of a shell that no longer existed. // // Which group a module is in is asked of the shell's own lists rather than // restated here: RAIL_VIEWS already is HUD_VIEWS less whatever has been folded, -// so folding a view under another moves its card by itself. The three groups +// so folding a view under another moves its card by itself. The four groups // are `MODULES.filter` of predicates that partition by construction -- every // module is in exactly one, and a new one lands in its group with no second // list to remember. /** On the rail: the shell's own list, so this cannot drift from the buttons. */ const isRailModule = (tab: string) => (RAIL_VIEWS as readonly string[]).includes(tab) -/** Folded into SPECS, and folded into KANBAN: the two families less their heads. */ +/** Folded into SPECS, KANBAN and PROJECT: the three families less their heads. */ const isLadderRung = (tab: string) => isSpecLayer(tab) && !isRailModule(tab) const isBoardReading = (tab: string) => isBoardView(tab) && !isRailModule(tab) +const isProjectRecord = (tab: string) => isProjectView(tab) && !isRailModule(tab) const RAIL_MODULES = MODULES.filter((m) => isRailModule(m.tab)) // Sorted into each family's own order, which is not the order of their keys: @@ -35,8 +37,10 @@ const RAIL_MODULES = MODULES.filter((m) => isRailModule(m.tab)) const order = (list: readonly string[]) => (tab: string) => list.indexOf(tab) const rung = order(SPEC_LAYERS) const seat = order(BOARD_VIEWS) +const shelf = order(PROJECT_VIEWS) const LADDER_MODULES = MODULES.filter((m) => isLadderRung(m.tab)).sort((a, b) => rung(a.tab) - rung(b.tab)) const BOARD_MODULES = MODULES.filter((m) => isBoardReading(m.tab)).sort((a, b) => seat(a.tab) - seat(b.tab)) +const PROJECT_MODULES = MODULES.filter((m) => isProjectRecord(m.tab)).sort((a, b) => shelf(a.tab) - shelf(b.tab)) // Every count is read, never a word typed here: the eyebrow once said "EIGHT" // while the title said "ten" and the rail showed ten. @@ -44,11 +48,12 @@ const N = MODULES.length const RAIL_N = RAIL_MODULES.length const LADDER_N = LADDER_MODULES.length const BOARD_N = BOARD_MODULES.length +const PROJECT_N = PROJECT_MODULES.length const COPY = { en: { eyebrow: `QUEEN / ${N} MODULES`, title: `One screen, ${N} ways to read it`, - lede: `The Queen is a single-page module: the 3D map is the ground and every panel floats on it. These ${N} are what that ground can be read as — each one opens on the same map, in the same shell, from the key beside it. ${RAIL_N} are buttons down the left edge; ${LADDER_N} are rungs of one ladder inside SPECS, where every catalogue generated from a spec lives together; the last ${BOARD_N} are the board's other two readings, under KANBAN.`, + lede: `The Queen is a single-page module: the 3D map is the ground and every panel floats on it. These ${N} are what that ground can be read as — each one opens on the same map, in the same shell, from the key beside it. ${RAIL_N} are buttons down the left edge; ${LADDER_N} are rungs of one ladder inside SPECS, where every catalogue generated from a spec lives together; ${BOARD_N} are the board's other two readings, under KANBAN; and ${PROJECT_N} is the record kept beside the project, inside PROJECT.`, railHead: `On the rail · ${RAIL_N}`, ladderHead: `Inside SPECS, on the ladder · ${LADDER_N}`, ladderNote: @@ -56,13 +61,16 @@ const COPY = { boardHead: `Inside KANBAN, on the board · ${BOARD_N}`, boardNote: 'The kanban, the mission map and the factory read one board: the same issues as columns, as a map, and as what the swarm is building from them. Same keys, one step inside KANBAN.', + projectHead: `Inside PROJECT, beside it · ${PROJECT_N}`, + projectNote: + 'The passport is part of how the project describes itself, not an instrument of its own: the record every result has to carry. Same key, one step inside PROJECT.', open: 'Open', all: 'Open the shell →', }, ru: { eyebrow: `КОРОЛЕВА / ${N} МОДУЛЕЙ`, title: `Один экран и ${N} способов его прочитать`, - lede: `Королева — одностраничный модуль: 3D-карта здесь основание, а каждая панель плавает над ним. Эти ${N} — то, чем это основание можно прочитать; любой открывается на той же карте, в том же шелле, по клавише рядом с ним. ${RAIL_N} — кнопки вдоль левого края, ${LADDER_N} — ступени одной лестницы внутри СПЕК, где лежат вместе все каталоги, порождённые из спек, а последние ${BOARD_N} — два других прочтения доски, внутри КАНБАНА.`, + lede: `Королева — одностраничный модуль: 3D-карта здесь основание, а каждая панель плавает над ним. Эти ${N} — то, чем это основание можно прочитать; любой открывается на той же карте, в том же шелле, по клавише рядом с ним. ${RAIL_N} — кнопки вдоль левого края, ${LADDER_N} — ступени одной лестницы внутри СПЕК, где лежат вместе все каталоги, порождённые из спек, ${BOARD_N} — два других прочтения доски, внутри КАНБАНА, и ${PROJECT_N} — запись, которую проект держит рядом с собой, внутри ПРОЕКТА.`, railHead: `На панели · ${RAIL_N}`, ladderHead: `Внутри СПЕК, на лестнице · ${LADDER_N}`, ladderNote: @@ -70,6 +78,9 @@ const COPY = { boardHead: `Внутри КАНБАНА, на доске · ${BOARD_N}`, boardNote: 'Канбан, карта миссий и фабрика читают одну доску: те же задачи — колонками, картой и тем, что рой из них строит. Клавиши прежние, просто на шаг внутрь КАНБАНА.', + projectHead: `Внутри ПРОЕКТА, рядом с ним · ${PROJECT_N}`, + projectNote: + 'Паспорт — часть того, как проект описывает себя, а не отдельный инструмент: запись, которую должен нести каждый результат. Клавиша прежняя, просто на шаг внутрь ПРОЕКТА.', open: 'Открыть', all: 'Открыть шелл →', }, @@ -130,6 +141,12 @@ export default function ModulesBlock() {

{t.boardNote}

{cards(BOARD_MODULES)} +

+ {t.projectHead} +

+

{t.projectNote}

+ {cards(PROJECT_MODULES)} + {t.all} diff --git a/apps/website/src/components/SystemDocsFigures.tsx b/apps/website/src/components/SystemDocsFigures.tsx index e6b358ad7d..a38b19acb2 100644 --- a/apps/website/src/components/SystemDocsFigures.tsx +++ b/apps/website/src/components/SystemDocsFigures.tsx @@ -24,8 +24,8 @@ const CAPTIONS: Record = { } const UI = { - en: { source: 'Data source', figure: 'Figure', unowned: 'no owner', external: 'external', crons: 'crons', skills: 'skills', agents: 'agents', tools: 'tools', functions: 'functions', noEdges: 'without a drawn edge', specs: 'specs', chapters: 'docs chapters', tri: 'tri commands', mcp: 'MCP servers', repo: 'repository', priority: 'priority', steps: 'steps' }, - ru: { source: 'Источник данных', figure: 'Рисунок', unowned: 'без владельца', external: 'внешний', crons: 'расписания', skills: 'навыки', agents: 'агенты', tools: 'инструменты', functions: 'функции', noEdges: 'без нарисованных связей', specs: 'спеки', chapters: 'главы документации', tri: 'команды tri', mcp: 'MCP-серверы', repo: 'репозиторий', priority: 'приоритет', steps: 'шагов' }, + en: { source: 'Data source', figure: 'Figure', unowned: 'no owner', external: 'external', crons: 'crons', skills: 'skills', agents: 'agents', tools: 'tools', functions: 'functions', noEdges: 'without a drawn edge', specs: 'specs', chapters: 'docs chapters', tri: 'tri commands', mcp: 'MCP servers', repo: 'repository', priority: 'priority', steps: 'steps', purpose: 'the goal, outside the ordering' }, + ru: { source: 'Источник данных', figure: 'Рисунок', unowned: 'без владельца', external: 'внешний', crons: 'расписания', skills: 'навыки', agents: 'агенты', tools: 'инструменты', functions: 'функции', noEdges: 'без нарисованных связей', specs: 'спеки', chapters: 'главы документации', tri: 'команды tri', mcp: 'MCP-серверы', repo: 'репозиторий', priority: 'приоритет', steps: 'шагов', purpose: 'цель, вне порядка приоритета' }, } const mono: CSSProperties = { fontFamily: C.mono } @@ -161,23 +161,37 @@ export function PhaseCycleFigure({ data, lang }: { data: DocFigures['phase-cycle export function LawHierarchyFigure({ data, lang }: { data: DocFigures['law-hierarchy']; lang: string }) { const ui = UI[lang === 'ru' ? 'ru' : 'en'] - const W = 560, rowH = 30, H = data.laws.length * rowH + 44 - const n = data.laws.length || 1 + // L0 PURPOSE (2026-09-23) is the destination, not a rung: the constitution + // puts it outside the L1 > ... > L7 ordering, so it is drawn above the + // pyramid as a dashed band and never joins the '>' chain. + const purpose = data.laws.filter((l) => l.law === 'L0') + const ranked = data.laws.filter((l) => l.law !== 'L0') + const W = 560, rowH = 30, top = purpose.length ? rowH + 8 : 0, H = top + ranked.length * rowH + 44 + const n = ranked.length || 1 return ( - {data.laws.map((l, i) => { + {purpose.map((l) => ( + + + {l.law} + {l.name} + {ui.purpose} + + ))} + {ranked.map((l, i) => { const w = 500 - (i * (300 / n)) const x = (W - w) / 2 + const y = top + 8 + i * rowH return ( - - {l.law} - {l.name} - {i > 0 && {'>'}} + + {l.law} + {l.name} + {i > 0 && {'>'}} ) })} - {ui.priority}: {data.priority ?? data.laws.map((l) => l.law).join(' > ')} + {ui.priority}: {data.priority ?? ranked.map((l) => l.law).join(' > ')} ) }