@marver-design/marver 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -15
- package/README.md +17 -4
- package/dist/{bake-BID6mo-N.mjs → bake-CvY8QR3L.mjs} +1 -1
- package/dist/board-status-CWGHIdo_.mjs +1307 -0
- package/dist/boards-BwKI0qWd.mjs +188 -0
- package/dist/{build-C7MqQ7hq.mjs → build-B1kavcpc.mjs} +74 -13
- package/dist/cli.mjs +69 -9
- package/dist/config-DJxMRVD8.mjs +373 -0
- package/dist/context-D4t59wDi.mjs +600 -0
- package/dist/{daemon-CRZFpl6K.mjs → daemon-PmNqoOAk.mjs} +1 -1
- package/dist/{dev-BNZF4Mup.mjs → dev-Cf4wLGe2.mjs} +15 -7
- package/dist/{init-C34BY3R4.mjs → init-C_K-cjBQ.mjs} +41 -45
- package/dist/managed-write-Bo-oPc-i.mjs +71 -0
- package/dist/{manifest-mMfUhPtL.mjs → manifest-CcdWx7ud.mjs} +42 -376
- package/dist/{plugin-omHLCn91.mjs → plugin-C5u07Yhv.mjs} +234 -105
- package/dist/{poster-BvxiAzy1.mjs → poster-BduzQBYz.mjs} +1 -1
- package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-b0jUwvM_.mjs} +2 -2
- package/dist/{shot-DswS4iRK.mjs → shot-BAR8hmU9.mjs} +2 -2
- package/docs/boards-and-folders.md +161 -0
- package/docs/context.md +117 -0
- package/docs/sharing.md +12 -2
- package/package.json +1 -1
- package/src/client/shell/BoardList.tsx +33 -3
- package/src/client/shell/ContextMenu.tsx +16 -5
- package/src/client/shell/StatusPicker.tsx +91 -0
- package/src/client/shell/board-icons.tsx +75 -0
- package/src/client/shell/store.ts +88 -9
- package/src/client/shell/styles.css +34 -3
- package/src/shared/board-tree.ts +30 -13
- package/src/shared/board-types.ts +103 -0
- package/src/shared/context.ts +265 -0
- package/src/shared/status.ts +157 -0
- package/templates/AGENTS-embedded.md +12 -0
- package/templates/AGENTS-studio.md +12 -0
- package/templates/context/INDEX.md +50 -0
- package/templates/context/map.json +6 -0
- package/templates/context/shipped-knowledge.md +19 -0
- package/templates/context/shipped.md +28 -0
- package/templates/instructions/boards.md +33 -0
- package/templates/instructions/context.md +123 -0
- package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
- package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
- package/templates/playbooks/reorganize-context/eval.md +93 -0
- package/dist/boards-BwiDAmPf.mjs +0 -337
- package/dist/boards-DnLewfj8.mjs +0 -71
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading `context/` (spec 19) - pure parsers over file text, shared by the dev server (which
|
|
3
|
+
* reads status off them, spec 20) and `marver context check` (which keeps them true). No fs, no
|
|
4
|
+
* git: callers read the files and hand the text in, so every reader parses one way.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The evidence levels a shipped-record cell carries. */
|
|
8
|
+
export type Level = 'confirmed' | 'reported' | 'unknown'
|
|
9
|
+
export const LEVEL = /`(confirmed|reported|unknown)`/g
|
|
10
|
+
/** What counts as a citation beside a level: a `path:line`, a run id, a cited file, or a link to a
|
|
11
|
+
* system of record (a deploy run's page). A cited file must also resolve - the check sees to it. */
|
|
12
|
+
export const CITATION = /`[^`\s]+:\d+(-\d+)?`|\bruns? \d{6,}(, \d{6,})*|`[^`\s]+\.(md|json|ts|tsx|js|mjs|yml|yaml|sql)`|\]\(https?:\/\/[^)\s]+\)/
|
|
13
|
+
/** A cited repository file inside a cell, with or without a line: `path/to/x.ts`, `CHANGELOG.md:12`. */
|
|
14
|
+
export const CITED_FILE = /`((?:\.{0,2}[\w@.-]+\/)*[\w@.$-]+\.(?:md|json|ts|tsx|js|jsx|mjs|yml|yaml|sql|txt))(?::(\d+)(?:-(\d+))?)?`/g
|
|
15
|
+
|
|
16
|
+
/** The audiences a context file declares (spec 19, Audiences): `team` when it says nothing. */
|
|
17
|
+
export type Audience = 'publishable' | 'team' | 'restricted'
|
|
18
|
+
export const readAudience = (v: unknown): Audience => (v === 'publishable' || v === 'restricted' ? v : 'team')
|
|
19
|
+
const AUDIENCE_RANK: Record<Audience, number> = { publishable: 0, team: 1, restricted: 2 }
|
|
20
|
+
/** The strictest of several audiences - what a conclusion drawn from them may be shown to. */
|
|
21
|
+
export const strictest = (...a: Audience[]): Audience => a.reduce((x, y) => (AUDIENCE_RANK[y] > AUDIENCE_RANK[x] ? y : x), 'publishable' as Audience)
|
|
22
|
+
|
|
23
|
+
/** Line endings normalized: every reader parses LF, so a CRLF file means the same thing. */
|
|
24
|
+
export const lf = (text: string): string => text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n')
|
|
25
|
+
/** The headers whose cells carry evidence in a shipped table. */
|
|
26
|
+
export const EVIDENCE_COLUMN = /^(evidence|verified|available|delivered)$/i
|
|
27
|
+
|
|
28
|
+
/** The context files' own front matter: a YAML subset - scalars, flow maps `{ a: b }`, flow
|
|
29
|
+
* lists `[a, b]`, block lists. `error` names a block that opens and never closes; a file
|
|
30
|
+
* with no front matter has `data: null`. */
|
|
31
|
+
export interface FrontMatter { data: Record<string, unknown> | null; body: string; offset: number; error?: string }
|
|
32
|
+
/** A Marver-managed file's first line (the playbooks Marver maintains) - front matter follows it. */
|
|
33
|
+
const MANAGED_LINE = /^<!-- marver:managed [^\n]*-->\n/
|
|
34
|
+
export function frontMatter(raw: string): FrontMatter {
|
|
35
|
+
const text = lf(raw)
|
|
36
|
+
const managed = MANAGED_LINE.exec(text)
|
|
37
|
+
if (managed) {
|
|
38
|
+
const r = frontMatter(text.slice(managed[0].length))
|
|
39
|
+
return { ...r, offset: r.offset + 1 }
|
|
40
|
+
}
|
|
41
|
+
if (!text.startsWith('---\n')) return { data: null, body: text, offset: 0 }
|
|
42
|
+
const m = text.match(/^---\n([\s\S]*?)\n---[ \t]*(\n|$)/)
|
|
43
|
+
if (!m) return { data: null, body: text, offset: 0, error: 'front matter opens with --- and never closes' }
|
|
44
|
+
const data: Record<string, unknown> = {}
|
|
45
|
+
let key: string | null = null
|
|
46
|
+
for (const line of m[1].split('\n')) {
|
|
47
|
+
const item = line.match(/^\s+-\s+(.*)$/)
|
|
48
|
+
if (item && key) {
|
|
49
|
+
if (!Array.isArray(data[key])) data[key] = []
|
|
50
|
+
;(data[key] as unknown[]).push(scalar(item[1]))
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
54
|
+
if (!kv) continue
|
|
55
|
+
key = kv[1]
|
|
56
|
+
data[key] = kv[2] === '' ? [] : value(kv[2])
|
|
57
|
+
}
|
|
58
|
+
return { data, body: text.slice(m[0].length), offset: m[0].split('\n').length - 1 }
|
|
59
|
+
}
|
|
60
|
+
function scalar(s: string): string {
|
|
61
|
+
s = s.trim().replace(/\s+#.*$/, '')
|
|
62
|
+
if (/^".*"$|^'.*'$/.test(s)) return s.slice(1, -1)
|
|
63
|
+
return s
|
|
64
|
+
}
|
|
65
|
+
function splitTop(s: string): string[] {
|
|
66
|
+
const out: string[] = []
|
|
67
|
+
let depth = 0, cur = '', q: string | null = null
|
|
68
|
+
for (const ch of s) {
|
|
69
|
+
if (q) { cur += ch; if (ch === q) q = null; continue }
|
|
70
|
+
if (ch === '"' || ch === "'") { q = ch; cur += ch; continue }
|
|
71
|
+
if (ch === '{' || ch === '[') depth++
|
|
72
|
+
if (ch === '}' || ch === ']') depth--
|
|
73
|
+
if (ch === ',' && depth === 0) { out.push(cur); cur = ''; continue }
|
|
74
|
+
cur += ch
|
|
75
|
+
}
|
|
76
|
+
if (cur.trim()) out.push(cur)
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
function value(s: string): unknown {
|
|
80
|
+
s = s.trim().replace(/\s+#[^"'}\]]*$/, '')
|
|
81
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
82
|
+
const o: Record<string, unknown> = {}
|
|
83
|
+
for (const part of splitTop(s.slice(1, -1))) {
|
|
84
|
+
const i = part.indexOf(':')
|
|
85
|
+
if (i > 0) o[part.slice(0, i).trim()] = value(part.slice(i + 1))
|
|
86
|
+
}
|
|
87
|
+
return o
|
|
88
|
+
}
|
|
89
|
+
if (s.startsWith('[') && s.endsWith(']')) return splitTop(s.slice(1, -1)).map((x) => value(x))
|
|
90
|
+
return scalar(s)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Lines outside fenced code blocks, with their 1-based numbers in the whole file. */
|
|
94
|
+
export function proseLines(text: string, offset = 0): [number, string][] {
|
|
95
|
+
const out: [number, string][] = []
|
|
96
|
+
let fenced = false
|
|
97
|
+
lf(text).split('\n').forEach((line, i) => {
|
|
98
|
+
if (/^\s*```/.test(line)) { fenced = !fenced; return }
|
|
99
|
+
if (!fenced) out.push([i + 1 + offset, line])
|
|
100
|
+
})
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Markdown tables outside code: each header and its rows, cells trimmed, with line numbers. */
|
|
105
|
+
export interface Table { header: string[]; headerLine: number; rows: { line: number; cells: string[] }[] }
|
|
106
|
+
export function tables(text: string): Table[] {
|
|
107
|
+
const out: Table[] = []
|
|
108
|
+
let cur: Table | null = null
|
|
109
|
+
for (const [n, line] of proseLines(text)) {
|
|
110
|
+
if (!/^\s*\|/.test(line)) { cur = null; continue }
|
|
111
|
+
if (/^\s*\|[\s|:-]+\|\s*$/.test(line)) continue
|
|
112
|
+
const cells = splitRow(line)
|
|
113
|
+
if (!cur) { cur = { header: cells, headerLine: n, rows: [] }; out.push(cur); continue }
|
|
114
|
+
cur.rows.push({ line: n, cells })
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
/** A table row's cells - pipes inside backticks stay in their cell. */
|
|
119
|
+
function splitRow(line: string): string[] {
|
|
120
|
+
const s = line.trim().replace(/^\|/, '').replace(/\|$/, '')
|
|
121
|
+
const out: string[] = []
|
|
122
|
+
let cur = '', code = false
|
|
123
|
+
for (const ch of s) {
|
|
124
|
+
if (ch === '`') code = !code
|
|
125
|
+
if (ch === '|' && !code) { out.push(cur.trim()); cur = ''; continue }
|
|
126
|
+
cur += ch
|
|
127
|
+
}
|
|
128
|
+
out.push(cur.trim())
|
|
129
|
+
return out
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const levelsIn = (cell: string): Level[] => [...cell.matchAll(LEVEL)].map((m) => m[1] as Level)
|
|
133
|
+
|
|
134
|
+
/** A table whose header row has no outer pipes - Markdown renders it, the readers here do not. The
|
|
135
|
+
* check rejects it rather than read past it. Returns the separator rows' line numbers. */
|
|
136
|
+
export function looseTables(text: string): number[] {
|
|
137
|
+
return proseLines(text).filter(([, l]) => !/^\s*\|/.test(l) && /^\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(l)).map(([n]) => n)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The levels an availability cell grants, clause by clause (`;` separates them). A clause counts
|
|
141
|
+
* only when it claims availability: never one carrying a negation anywhere ("nowhere", "not
|
|
142
|
+
* available", "rolled back", "withdrawn") - and, for a product's Available cell, never one scoped to
|
|
143
|
+
* a pre-production place (staging, preview, sandbox) that does not also name production. A
|
|
144
|
+
* knowledge-work Delivered cell has no environments: only negations void it. */
|
|
145
|
+
const NEGATION = /\b(nowhere|none|never|no longer|not (yet )?(available|live|deployed|delivered|on|in|shipped|released|out)|not yet|pending|planned|scheduled|upcoming|awaiting|withdrawn|rolled back|reverted|removed|retired|pulled)\b|^\s*not\b/i
|
|
146
|
+
/** The places a product is NOT yet available to its users, unless the clause also names production. */
|
|
147
|
+
const PRE_PRODUCTION = /\b(staging|preview|sandbox|dev|development|local|locally|testing|test environment|qa)\b/i
|
|
148
|
+
export function availableLevels(cell: string, kind: 'available' | 'delivered' = 'available'): Level[] {
|
|
149
|
+
const out: Level[] = []
|
|
150
|
+
for (const clause of cell.split(';')) {
|
|
151
|
+
const c = clause.replace(/\*\*/g, '').trim()
|
|
152
|
+
if (NEGATION.test(c)) continue
|
|
153
|
+
if (kind === 'available' && PRE_PRODUCTION.test(c) && !/\b(production|prod)\b/i.test(c)) continue
|
|
154
|
+
out.push(...levelsIn(c))
|
|
155
|
+
}
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The shipped record's capability rows: every table whose first column is "Capability" (a
|
|
160
|
+
* product) or "Project" / "Deliverable" (knowledge work) and that has an "Available" or
|
|
161
|
+
* "Delivered" column. The capability is the first backticked slug in the row's first cell;
|
|
162
|
+
* `levels` are the ones its availability clauses grant (availableLevels). */
|
|
163
|
+
export interface ShippedRow { capability: string; line: number; available: string; levels: Level[] }
|
|
164
|
+
export const RECORD_KEY = /^(capability|project|deliverable)$/i
|
|
165
|
+
export const RECORD_AVAILABLE = /^(available|delivered)$/i
|
|
166
|
+
export const isRecordTable = (t: Table): boolean => RECORD_KEY.test(t.header[0] ?? '') && t.header.some((h) => RECORD_AVAILABLE.test(h))
|
|
167
|
+
export function shippedRows(text: string): ShippedRow[] {
|
|
168
|
+
const out: ShippedRow[] = []
|
|
169
|
+
for (const t of tables(text)) {
|
|
170
|
+
if (!isRecordTable(t)) continue
|
|
171
|
+
const a = t.header.findIndex((h) => RECORD_AVAILABLE.test(h))
|
|
172
|
+
const kind = /^delivered$/i.test(t.header[a]) ? 'delivered' : 'available'
|
|
173
|
+
for (const r of t.rows) {
|
|
174
|
+
const slug = /`([a-z0-9][a-z0-9-]*)`/.exec(r.cells[0] ?? '')?.[1]
|
|
175
|
+
if (!slug) continue
|
|
176
|
+
const available = r.cells[a] ?? ''
|
|
177
|
+
out.push({ capability: slug, line: r.line, available, levels: availableLevels(available, kind) })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Is this a glob the matcher reads? Balanced, unnested braces; no character classes. */
|
|
184
|
+
export const validGlob = (g: string): boolean => {
|
|
185
|
+
if (typeof g !== 'string' || !g || /[[\]]/.test(g)) return false
|
|
186
|
+
let depth = 0
|
|
187
|
+
for (const c of g) { if (c === '{') { if (++depth > 1) return false } else if (c === '}') { if (--depth < 0) return false } }
|
|
188
|
+
return depth === 0
|
|
189
|
+
}
|
|
190
|
+
export const hasGlob = (g: string): boolean => /[*?{]/.test(g)
|
|
191
|
+
/** A glob with **, *, ? and {a,b} as a RegExp over repo-relative paths (validGlob first). */
|
|
192
|
+
export function globRe(glob: string): RegExp {
|
|
193
|
+
let re = '', brace = 0
|
|
194
|
+
for (let i = 0; i < glob.length; i++) {
|
|
195
|
+
const c = glob[i]
|
|
196
|
+
if (c === '*') {
|
|
197
|
+
if (glob[i + 1] === '*') {
|
|
198
|
+
i++
|
|
199
|
+
if (glob[i + 1] === '/') { i++; re += '(?:[\\s\\S]*/)?' } else re += '[\\s\\S]*'
|
|
200
|
+
} else re += '[^/]*'
|
|
201
|
+
} else if (c === '?') re += '[^/]'
|
|
202
|
+
else if (c === '{') { brace++; re += '(?:' }
|
|
203
|
+
else if (c === '}' && brace) { brace--; re += ')' }
|
|
204
|
+
else if (c === ',' && brace) re += '|'
|
|
205
|
+
else re += c.replace(/[.+^$()|[\]\\]/g, '\\$&')
|
|
206
|
+
}
|
|
207
|
+
return new RegExp(`^${re}$`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `context/map.json`'s shape. */
|
|
211
|
+
export interface CapabilityMap {
|
|
212
|
+
/** `summary` - a few words the index shows beside the slug, so a reader routes without opening the contract */
|
|
213
|
+
capabilities: Record<string, { contract?: string; summary?: string; paths: string[]; tests?: string[] }>
|
|
214
|
+
excluded: { path: string; reason: string }[]
|
|
215
|
+
/** the globs whose files must be mapped or excluded - the check reports any that is neither */
|
|
216
|
+
source?: string[]
|
|
217
|
+
}
|
|
218
|
+
/** Read a map's text: the map, or what is wrong with it - every field checked, every glob valid. */
|
|
219
|
+
export function parseMap(text: string): CapabilityMap | string {
|
|
220
|
+
let raw: unknown
|
|
221
|
+
try { raw = JSON.parse(text) } catch { return 'context/map.json is not valid JSON' }
|
|
222
|
+
const m = raw as Partial<CapabilityMap> | null
|
|
223
|
+
if (!m || typeof m !== 'object' || !m.capabilities || typeof m.capabilities !== 'object' || Array.isArray(m.capabilities)) return 'context/map.json needs a "capabilities" object'
|
|
224
|
+
const globs = (where: string, v: unknown, required: boolean): string | null => {
|
|
225
|
+
if (v === undefined && !required) return null
|
|
226
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) return `context/map.json: ${where} must be a list of paths`
|
|
227
|
+
const bad = (v as string[]).findIndex((g) => !validGlob(g))
|
|
228
|
+
return bad >= 0 ? `context/map.json: ${where} has an invalid glob "${(v as string[])[bad]}" (not empty; balanced, unnested {a,b}; no [...])` : null
|
|
229
|
+
}
|
|
230
|
+
for (const [k, c] of Object.entries(m.capabilities)) {
|
|
231
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(k)) return `context/map.json: "${k}" is not a capability slug`
|
|
232
|
+
const cc = c as Record<string, unknown> | null
|
|
233
|
+
if (!cc || typeof cc !== 'object') return `context/map.json: "${k}" must be an object`
|
|
234
|
+
const e = globs(`"${k}".paths`, cc.paths, true) ?? globs(`"${k}".tests`, cc.tests, false)
|
|
235
|
+
if (e) return e
|
|
236
|
+
if (cc.contract !== undefined && typeof cc.contract !== 'string') return `context/map.json: "${k}".contract must be a path`
|
|
237
|
+
if (cc.summary !== undefined && typeof cc.summary !== 'string') return `context/map.json: "${k}".summary must be text`
|
|
238
|
+
}
|
|
239
|
+
if (m.excluded !== undefined) {
|
|
240
|
+
if (!Array.isArray(m.excluded)) return 'context/map.json: "excluded" must be a list'
|
|
241
|
+
for (const x of m.excluded as unknown[]) {
|
|
242
|
+
const e = x as { path?: unknown; reason?: unknown } | null
|
|
243
|
+
if (!e || typeof e.path !== 'string' || !validGlob(e.path)) return 'context/map.json: every "excluded" entry needs a valid "path" glob'
|
|
244
|
+
if (typeof e.reason !== 'string' || !e.reason.trim()) return `context/map.json: excluded "${e.path}" needs a "reason"`
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
{ const e = globs('"source"', (m as { source?: unknown }).source, false); if (e) return e }
|
|
248
|
+
const source = Array.isArray(m.source) ? m.source.filter((x): x is string => typeof x === 'string') : undefined
|
|
249
|
+
return { capabilities: m.capabilities as CapabilityMap['capabilities'], excluded: Array.isArray(m.excluded) ? m.excluded : [], ...(source ? { source } : {}) }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The index's generated capability table, between `<!-- generated ... -->` fences. */
|
|
253
|
+
export const GENERATED = /<!-- generated[^>]*-->([\s\S]*?)<!-- \/generated -->/
|
|
254
|
+
/** The table the index generates from the map: one row per capability, its contract linked. */
|
|
255
|
+
export function capabilityTable(map: CapabilityMap): string {
|
|
256
|
+
const rows = Object.entries(map.capabilities).map(([k, c]) => {
|
|
257
|
+
const name = `\`${k}\`${typeof c.summary === 'string' && c.summary.trim() ? ` - ${c.summary.trim().replace(/\|/g, '/')}` : ''}`
|
|
258
|
+
const rel = c.contract?.replace(/^context\//, '')
|
|
259
|
+
return rel ? `| ${name} | [\`${rel}\`](${rel}) |` : `| ${name} | none yet - see the map |`
|
|
260
|
+
})
|
|
261
|
+
return ['| Capability | Contract |', '|---|---|', ...rows].join('\n')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Words in a document's body (front matter excluded) - the index's budget counts these. */
|
|
265
|
+
export const wordCount = (text: string): number => frontMatter(text).body.split(/\s+/).filter(Boolean).length
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status (spec 20) - one resolver, shared by the dev server and the build: files in, a status and
|
|
3
|
+
* its evidence out, pure. Feature and project boards wear it; read top to bottom, the first row
|
|
4
|
+
* that matches wins:
|
|
5
|
+
*
|
|
6
|
+
* 1-3 the board says archived, paused or blocked (with its reason) - a decision, by hand
|
|
7
|
+
* 4 the evidence it needs cannot be read - Unknown, never a stale Done
|
|
8
|
+
* 5 an open plan names the capability - In progress, filling by phase
|
|
9
|
+
* 6 the shipped record shows it available, `confirmed` - Done
|
|
10
|
+
* 7 ... `reported` only - Done, reported
|
|
11
|
+
* 8 an accepted contract (`state: current`), no availability - To do
|
|
12
|
+
* 9 anything else - Backlog
|
|
13
|
+
*
|
|
14
|
+
* Without `context/`, a board may also say in-progress, todo or backlog by hand. Done is never
|
|
15
|
+
* set by hand: `"status": "done"` is ignored here and reported by `marver context check`.
|
|
16
|
+
*/
|
|
17
|
+
import { DECISIONS, HAS_STATUS, type BoardType, type StatusWord } from './board-types.ts'
|
|
18
|
+
import { strictest, type Audience, type Level } from './context.ts'
|
|
19
|
+
|
|
20
|
+
export type Status = 'archived' | 'paused' | 'blocked' | 'unknown' | 'in-progress' | 'done' | 'done-reported' | 'todo' | 'backlog'
|
|
21
|
+
export const STATUS_LABEL: Record<Status, string> = {
|
|
22
|
+
archived: 'Archived', paused: 'Paused', blocked: 'Blocked', unknown: 'Unknown', 'in-progress': 'In progress',
|
|
23
|
+
done: 'Done', 'done-reported': 'Done, reported', todo: 'To do', backlog: 'Backlog',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A phase, as the fill reads it: 1 spec, 2 lo-fi, 3 hi-fi. */
|
|
27
|
+
export type Phase = 1 | 2 | 3
|
|
28
|
+
export const PHASE_LABEL: Record<Phase, string> = { 1: 'spec', 2: 'lo-fi', 3: 'hi-fi' }
|
|
29
|
+
|
|
30
|
+
/** What the resolver knows about `context/` - read once per pass by the caller. */
|
|
31
|
+
export interface ContextFacts {
|
|
32
|
+
/** a `context/` directory exists */
|
|
33
|
+
present: boolean
|
|
34
|
+
/** evidence that could not be read: `'*'` for the whole record (shipped.md unreadable), else per capability */
|
|
35
|
+
unreadable: Map<string, string>
|
|
36
|
+
/** capability -> the levels its shipped row's availability grants, where, and the record's audience */
|
|
37
|
+
shipped: Map<string, { levels: Level[]; where: string; audience: Audience }>
|
|
38
|
+
/** capability -> its contract's state, where, and its audience */
|
|
39
|
+
contracts: Map<string, { state: string; where: string; audience: Audience }>
|
|
40
|
+
/** capability -> its open plans, each with its audience */
|
|
41
|
+
plans: Map<string, { where: string; audience: Audience }[]>
|
|
42
|
+
}
|
|
43
|
+
export const NO_CONTEXT: ContextFacts = { present: false, unreadable: new Map(), shipped: new Map(), contracts: new Map(), plans: new Map() }
|
|
44
|
+
|
|
45
|
+
export interface BoardInput {
|
|
46
|
+
name: string
|
|
47
|
+
type: BoardType
|
|
48
|
+
capability?: string
|
|
49
|
+
status?: StatusWord
|
|
50
|
+
reason?: string
|
|
51
|
+
/** the scenes the board shows, with any `phase` their brief declares and that brief's audience */
|
|
52
|
+
scenes: { name: string; phase?: string; audience?: Audience }[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StatusResult {
|
|
56
|
+
status: Status
|
|
57
|
+
/** the table row that decided it */
|
|
58
|
+
row: number
|
|
59
|
+
/** In progress: the latest phase present */
|
|
60
|
+
fill?: Phase
|
|
61
|
+
/** Blocked: why */
|
|
62
|
+
reason?: string
|
|
63
|
+
/** the capability the evidence was read for */
|
|
64
|
+
capability: string
|
|
65
|
+
/** one line per piece of evidence, for the tooltip */
|
|
66
|
+
evidence: string[]
|
|
67
|
+
/** the strictest audience of the evidence the status was drawn from - a published canvas shows a
|
|
68
|
+
* status only when this is `publishable` (spec 20, Publishing status) */
|
|
69
|
+
audience: Audience
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PHASE_WORDS: Record<string, Phase> = { spec: 1, specs: 1, lofi: 2, 'lo-fi': 2, hifi: 3, 'hi-fi': 3 }
|
|
73
|
+
|
|
74
|
+
/** The latest phase a board's scenes show: a scene named `<cap>-specs` (spec), `<cap>-lofi`
|
|
75
|
+
* (lo-fi) or `<cap>` itself (hi-fi), or a `phase` in a scene's brief. Never geometry. */
|
|
76
|
+
export function phaseOf(capability: string, scenes: BoardInput['scenes']): Phase | undefined {
|
|
77
|
+
return phaseSource(capability, scenes)?.phase
|
|
78
|
+
}
|
|
79
|
+
/** The deciding phase and where it came from - a brief's phase carries that brief's audience. */
|
|
80
|
+
function phaseSource(capability: string, scenes: BoardInput['scenes']): { phase: Phase; scene: string; fromBrief: boolean; audience: Audience } | undefined {
|
|
81
|
+
let best: { phase: Phase; scene: string; fromBrief: boolean; audience: Audience } | undefined
|
|
82
|
+
for (const s of scenes) {
|
|
83
|
+
const declared = s.phase ? PHASE_WORDS[s.phase.toLowerCase()] : undefined
|
|
84
|
+
const named = s.name === `${capability}-specs` || s.name === `${capability}-spec` ? 1 : s.name === `${capability}-lofi` ? 2 : s.name === capability ? 3 : undefined
|
|
85
|
+
const p = (declared ?? named) as Phase | undefined
|
|
86
|
+
if (p && (!best || p > best.phase)) best = { phase: p, scene: s.name, fromBrief: !!declared, audience: declared ? (s.audience ?? 'team') : 'publishable' }
|
|
87
|
+
}
|
|
88
|
+
return best
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A board's status, or null when its type carries none. */
|
|
92
|
+
export function resolveStatus(b: BoardInput, ctx: ContextFacts): StatusResult | null {
|
|
93
|
+
if (!HAS_STATUS.includes(b.type)) return null
|
|
94
|
+
const capability = b.capability ?? b.name
|
|
95
|
+
const out = (status: Status, row: number, evidence: string[], audience: Audience, extra: Partial<StatusResult> = {}): StatusResult => ({ status, row, capability, evidence, audience, ...extra })
|
|
96
|
+
|
|
97
|
+
// 1-3: a decision on the board
|
|
98
|
+
if (b.status && (DECISIONS as readonly string[]).includes(b.status)) {
|
|
99
|
+
const by = `design/boards/${b.name}.json: "status": "${b.status}"`
|
|
100
|
+
return out(b.status as Status, DECISIONS.indexOf(b.status as (typeof DECISIONS)[number]) + 1, [by], 'team', b.status === 'blocked' && b.reason ? { reason: b.reason } : {})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!ctx.present) {
|
|
104
|
+
// without context/ a project sets To do, Backlog and In progress by hand; Done needs a record
|
|
105
|
+
// the board's own word, and a board ships as written - publishable
|
|
106
|
+
if (b.status === 'in-progress') {
|
|
107
|
+
const f = fillOf(capability, b.scenes)
|
|
108
|
+
return out('in-progress', 5, [`design/boards/${b.name}.json: by hand`, ...f.evidence], strictest('publishable', ...f.audience), f.fill)
|
|
109
|
+
}
|
|
110
|
+
if (b.status === 'todo') return out('todo', 8, [`design/boards/${b.name}.json: by hand`], 'publishable')
|
|
111
|
+
return out('backlog', 9, [b.status === 'backlog' ? `design/boards/${b.name}.json: by hand` : 'no context/ - nothing records it'], 'publishable')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4: evidence that cannot be read is Unknown - never the last value seen
|
|
115
|
+
const bad = ctx.unreadable.get('*') ?? ctx.unreadable.get(capability)
|
|
116
|
+
if (bad) return out('unknown', 4, [bad], 'team')
|
|
117
|
+
|
|
118
|
+
const row = ctx.shipped.get(capability)
|
|
119
|
+
const live = row?.levels.includes('confirmed') ? 'confirmed' : row?.levels.includes('reported') ? 'reported' : null
|
|
120
|
+
|
|
121
|
+
// 5: an open plan - work on a shipped capability is version two being built, and says so
|
|
122
|
+
const plans = ctx.plans.get(capability)
|
|
123
|
+
if (plans?.length) {
|
|
124
|
+
const f = fillOf(capability, b.scenes)
|
|
125
|
+
const ev = [...plans.map((p) => `${p.where}: an open plan`), ...f.evidence]
|
|
126
|
+
if (live && row) ev.push(`${row.where}: available, \`${live}\` - this is the next version`)
|
|
127
|
+
return out('in-progress', 5, ev, strictest(...plans.map((p) => p.audience), ...f.audience, ...(live && row ? [row.audience] : [])), f.fill)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 6-7: the shipped record
|
|
131
|
+
if (live === 'confirmed') return out('done', 6, [`${row!.where}: available, \`confirmed\``], row!.audience)
|
|
132
|
+
if (live === 'reported') return out('done-reported', 7, [`${row!.where}: available, \`reported\` only`], row!.audience)
|
|
133
|
+
|
|
134
|
+
// 8: an accepted contract
|
|
135
|
+
const c = ctx.contracts.get(capability)
|
|
136
|
+
if (c?.state === 'current') return out('todo', 8, [`${c.where}: state current, nothing available yet`], strictest(c.audience, ...(row ? [row.audience] : [])))
|
|
137
|
+
|
|
138
|
+
// 9: nothing records it - which says nothing private
|
|
139
|
+
return out('backlog', 9, [c ? `${c.where}: state ${c.state}` : `no contract for ${capability}`], c ? c.audience : 'publishable')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The fill, the line that says where it came from, and the audience of that source. */
|
|
143
|
+
const fillOf = (capability: string, scenes: BoardInput['scenes']): { fill: Partial<StatusResult>; evidence: string[]; audience: Audience[] } => {
|
|
144
|
+
const src = phaseSource(capability, scenes)
|
|
145
|
+
if (!src) return { fill: {}, evidence: [], audience: [] }
|
|
146
|
+
const where = src.fromBrief ? `design/scenes/${src.scene}/_brief.md: phase ${PHASE_LABEL[src.phase]}` : `design/scenes/${src.scene}: the ${PHASE_LABEL[src.phase]} scene`
|
|
147
|
+
return { fill: { fill: src.phase }, evidence: [where], audience: [src.audience] }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** What a published canvas may show of a status (spec 20, Publishing status): rows 5-9 only, drawn
|
|
151
|
+
* from `publishable` evidence only, and never a reason, a record path or other evidence. */
|
|
152
|
+
export function publishableStatus(r: { status: Status; row?: number; fill?: Phase; audience?: Audience } | null | undefined): { status: Status; fill?: Phase } | null {
|
|
153
|
+
if (!r || !PUBLISHABLE.has(r.status) || r.audience !== 'publishable') return null
|
|
154
|
+
return { status: r.status, ...(r.fill ? { fill: r.fill } : {}) }
|
|
155
|
+
}
|
|
156
|
+
/** The statuses rows 5-9 produce - the only ones a published canvas may show. */
|
|
157
|
+
export const PUBLISHABLE: ReadonlySet<Status> = new Set(['in-progress', 'done', 'done-reported', 'todo', 'backlog'])
|
|
@@ -5,6 +5,17 @@ Never drive or automate the canvas UI; read and write files only. (Starting
|
|
|
5
5
|
`npx marver dev` so the human has a live canvas - first session, or on request -
|
|
6
6
|
is the one allowed touch.)
|
|
7
7
|
|
|
8
|
+
## Before the method: a project with no `context/`
|
|
9
|
+
|
|
10
|
+
This repository has no `context/` directory? Then **every reply in which you made, specced or changed
|
|
11
|
+
a feature or project board ends with this line**, word for word, until the human answers it:
|
|
12
|
+
|
|
13
|
+
> This project has no `context/` yet, so every feature board reads Backlog and nothing records what
|
|
14
|
+
> shipped. Want me to set it up? A few questions, then a draft from the code and the deploy history.
|
|
15
|
+
|
|
16
|
+
A yes: instructions/context.md, "Setting up". A no stands for the session - stop asking. Never set
|
|
17
|
+
it up unasked.
|
|
18
|
+
|
|
8
19
|
## The method (binding)
|
|
9
20
|
|
|
10
21
|
Design work moves through phases. BEFORE working in a phase, read its instruction
|
|
@@ -22,6 +33,7 @@ file in design/instructions/ - they are short, strict, and part of this contract
|
|
|
22
33
|
| Iterate | changing a frame the human has seen, a round of feedback on a scene, or retiring explorations | instructions/iterate.md |
|
|
23
34
|
| Review | before presenting anything | instructions/review.md |
|
|
24
35
|
| Boards | creating a board, choosing what ships | instructions/boards.md |
|
|
36
|
+
| Context | any question about what the product does or what is live; a behaviour change; a deploy; no `context/` yet | instructions/context.md |
|
|
25
37
|
| Slides | a deck is asked for, or `slide: true` frames exist | instructions/slides.md + design/slides.md |
|
|
26
38
|
| Publish | deploying the canvas: gate, volume, accounts, invites | instructions/publish.md |
|
|
27
39
|
| Live Jam | responding to an `@marver` comment (a spawned job); on by default - confirm it names YOUR tool | instructions/jam.md |
|
|
@@ -5,6 +5,17 @@ Never drive or automate the canvas UI; read and write files only. (Starting
|
|
|
5
5
|
`npx marver dev` so the human has a live canvas - first session, or on request -
|
|
6
6
|
is the one allowed touch.)
|
|
7
7
|
|
|
8
|
+
## Before the method: a project with no `context/`
|
|
9
|
+
|
|
10
|
+
This repository has no `context/` directory? Then **every reply in which you made, specced or changed
|
|
11
|
+
a feature or project board ends with this line**, word for word, until the human answers it:
|
|
12
|
+
|
|
13
|
+
> This project has no `context/` yet, so every feature board reads Backlog and nothing records what
|
|
14
|
+
> shipped. Want me to set it up? A few questions, then a draft from the code and the deploy history.
|
|
15
|
+
|
|
16
|
+
A yes: instructions/context.md, "Setting up". A no stands for the session - stop asking. Never set
|
|
17
|
+
it up unasked.
|
|
18
|
+
|
|
8
19
|
## The method (binding)
|
|
9
20
|
|
|
10
21
|
Design work moves through phases. BEFORE working in a phase, read its instruction
|
|
@@ -22,6 +33,7 @@ file in design/instructions/ - they are short, strict, and part of this contract
|
|
|
22
33
|
| Iterate | changing a frame the human has seen, a round of feedback on a scene, or retiring explorations | instructions/iterate.md |
|
|
23
34
|
| Review | before presenting anything | instructions/review.md |
|
|
24
35
|
| Boards | creating a board, choosing what ships | instructions/boards.md |
|
|
36
|
+
| Context | any question about what the product does or what is live; a behaviour change; a deploy; no `context/` yet | instructions/context.md |
|
|
25
37
|
| Slides | a deck is asked for, or `slide: true` frames exist | instructions/slides.md + design/slides.md |
|
|
26
38
|
| Publish | deploying the canvas: gate, volume, accounts, invites | instructions/publish.md |
|
|
27
39
|
| Live Jam | responding to an `@marver` comment (a spawned job); on by default - confirm it names YOUR tool | instructions/jam.md |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
audience: team
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# {{NAME}} - the index
|
|
6
|
+
|
|
7
|
+
Routing only. Read this, then at most two more files per question. It stays under 800 words -
|
|
8
|
+
`npx marver context check` counts.
|
|
9
|
+
|
|
10
|
+
## The product
|
|
11
|
+
|
|
12
|
+
<!-- Five lines at most: what it is and for whom; what runs where; what is happening now. -->
|
|
13
|
+
|
|
14
|
+
## Questions, and where they are answered
|
|
15
|
+
|
|
16
|
+
| Question | Read |
|
|
17
|
+
|---|---|
|
|
18
|
+
| {{RECORD_QUESTION}} | [`shipped.md`](shipped.md) |
|
|
19
|
+
| How does it work today? | its contract below; without one, [`map.json`](map.json) names the code and tests |
|
|
20
|
+
| Why was it decided? | the decision log - say where this project keeps it |
|
|
21
|
+
| What is next, and what counts as done? | the roadmap - say where |
|
|
22
|
+
| Who asked for it, and what happened to it? | `feedback/` |
|
|
23
|
+
| How do we do it? | "How we do things", below |
|
|
24
|
+
|
|
25
|
+
## Capabilities
|
|
26
|
+
|
|
27
|
+
<!-- generated: `npx marver context index` keeps this table equal to context/map.json -->
|
|
28
|
+
| Capability | Contract |
|
|
29
|
+
|---|---|
|
|
30
|
+
<!-- /generated -->
|
|
31
|
+
|
|
32
|
+
## Where each role lives
|
|
33
|
+
|
|
34
|
+
<!-- Healthy homes left in place: the changelog, the decision log, research, test records, the canvas. -->
|
|
35
|
+
|
|
36
|
+
## How we do things
|
|
37
|
+
|
|
38
|
+
- [`reorganize-context`](playbooks/reorganize-context/PLAYBOOK.md) - when context exists but is
|
|
39
|
+
scattered: specs out of date, nobody sure what is live.
|
|
40
|
+
- [`publish-canvas`](playbooks/publish-canvas/PLAYBOOK.md) - when the canvas should be on a URL.
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
|
|
44
|
+
- **Availability is written only in `shipped.md`,** each cell `confirmed`, `reported` or `unknown`,
|
|
45
|
+
with a citation.
|
|
46
|
+
- **A behaviour change updates its contract,** or the pull request says
|
|
47
|
+
`no-contract-change: <capability> - <why>`.
|
|
48
|
+
- **Every deploy writes the record.**
|
|
49
|
+
- **A plan lives in `plans/` while open** and folds into its contract when it lands.
|
|
50
|
+
- `npx marver context check` checks these; ci runs it.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Which code each capability lives in - routing for agents, and the input of the pull-request rule in `npx marver context check`. A file may map to several capabilities. `source` names what must be mapped or excluded; the check reports any file that is neither. Only capabilities with a contract are checked on pull requests.",
|
|
3
|
+
"source": ["src/**", "app/**", "apps/**", "packages/**", "lib/**"],
|
|
4
|
+
"capabilities": {},
|
|
5
|
+
"excluded": []
|
|
6
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: record
|
|
3
|
+
audience: team
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Delivered - what was delivered, to whom, and when
|
|
7
|
+
|
|
8
|
+
The only file that says what is delivered. Every evidence cell carries its level, and `confirmed`
|
|
9
|
+
and `reported` carry a citation:
|
|
10
|
+
|
|
11
|
+
- `confirmed` - observed in a system of record: the client's written acceptance, the sent email,
|
|
12
|
+
the signed-off document.
|
|
13
|
+
- `reported` - a written claim, cited: a meeting note, a changelog line.
|
|
14
|
+
- `unknown` - neither, with the reason. An honest unknown is the right answer.
|
|
15
|
+
|
|
16
|
+
## Projects
|
|
17
|
+
|
|
18
|
+
| Project | Delivered | Evidence | Contract |
|
|
19
|
+
|---|---|---|---|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: record
|
|
3
|
+
audience: team
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Shipped - what runs where, and what is available to whom
|
|
7
|
+
|
|
8
|
+
The only file that says what is available. Every evidence cell carries its level, and `confirmed`
|
|
9
|
+
and `reported` carry a citation:
|
|
10
|
+
|
|
11
|
+
- `confirmed` - observed in a system of record: the deploy run whose deploy step succeeded for that
|
|
12
|
+
service, a ci run that executed the suite. An environment marked "deployed" is not proof on its
|
|
13
|
+
own - some pipelines mark it when the run stood down.
|
|
14
|
+
- `reported` - a written claim, cited: a changelog line, an ops note.
|
|
15
|
+
- `unknown` - neither, with the reason. An honest unknown is the right answer.
|
|
16
|
+
|
|
17
|
+
Commit ancestry says a change is inside a revision, not that the revision is running.
|
|
18
|
+
|
|
19
|
+
## Services × environments
|
|
20
|
+
|
|
21
|
+
| Service | Environment | Revision | Deployed at | How | Evidence |
|
|
22
|
+
|---|---|---|---|---|---|
|
|
23
|
+
| app | production | - | - | - | `unknown` - nothing recorded yet |
|
|
24
|
+
|
|
25
|
+
## Capabilities
|
|
26
|
+
|
|
27
|
+
| Capability | Implemented | Verified | Available | Contract |
|
|
28
|
+
|---|---|---|---|---|
|
|
@@ -158,6 +158,33 @@ you rewrite it. Published canvases show the folders of the published boards only
|
|
|
158
158
|
sub-folder's parent included; a folder with nothing published at any depth never reaches
|
|
159
159
|
the bundle.
|
|
160
160
|
|
|
161
|
+
## Types and status - what a board is for, and where it stands
|
|
162
|
+
|
|
163
|
+
Every canvas shares one sidebar vocabulary. A board's `"type"` - `start`, `feature`, `surface`,
|
|
164
|
+
`project`, `feedback`, `context`, `deck`, `archive` - draws its icon; set it on the board, or on a
|
|
165
|
+
folder's entry in `_folders.json` and every board inside wears it (its own type wins; a sub-folder's
|
|
166
|
+
boards fall back to the parent folder's type). Moving a board changes an inherited type only.
|
|
167
|
+
|
|
168
|
+
- **Make a board in its starting layout:** `npx marver boards new <name> --folder features` (the
|
|
169
|
+
folder's type), or `--type <type>`. A feature gets three phase scenes as three bands -
|
|
170
|
+
`<name>-specs`, `<name>-lofi`, `<name>` - each with a brief; a start board renders
|
|
171
|
+
`context/INDEX.md` and `context/shipped.md`; a deck starts on a title slide. It never overwrites.
|
|
172
|
+
The phase scenes start empty: pin each frame you make in one as a node, or the board never shows it.
|
|
173
|
+
- **Add a typed folder:** `npx marver folders add decks` (or `start`, `features`, `surfaces`,
|
|
174
|
+
`projects`, `feedback`, `context`, `archive`) - appended, never moving one that exists.
|
|
175
|
+
- **Feature and project boards wear a status** read from `context/` (instructions/context.md): the
|
|
176
|
+
capability is the board's name, or `"capability": "<slug>"`. You may decide only three things on
|
|
177
|
+
the board: `"status": "archived"`, `"paused"`, or `"blocked"` with `"reason": "<one sentence>"`.
|
|
178
|
+
**Never write `"status": "done"`** - Done comes from `context/shipped.md`, and the check fails it.
|
|
179
|
+
**No `context/`? Your reply ends with the context line** from design/AGENTS.md ("Before the method")
|
|
180
|
+
- every feature board reads Backlog until there is one. Only when the human declines may a board
|
|
181
|
+
say `"todo"`, `"backlog"` or `"in-progress"` by hand.
|
|
182
|
+
A person sets the same from the sidebar (right-click, Change status…) - it rewrites only `status`
|
|
183
|
+
and `reason`, so re-read a board before you edit it.
|
|
184
|
+
- In progress fills by phase: the phase scenes above, or `phase: spec | lofi | hifi` in a scene's
|
|
185
|
+
`_brief.md` front matter - never by where a row sits.
|
|
186
|
+
- `npx marver boards` prints every board's type and status with what decided it.
|
|
187
|
+
|
|
161
188
|
## The default composition: one horizontal band
|
|
162
189
|
|
|
163
190
|
A board reads like a page: left to right first, down only for a reason. The
|
|
@@ -249,6 +276,12 @@ be ON a published board - unlisted frames are excluded from the bundle at build
|
|
|
249
276
|
time. Deploying the built canvas - gate password, the collaboration volume,
|
|
250
277
|
accounts and invites - is its own phase: **instructions/publish.md**.
|
|
251
278
|
|
|
279
|
+
When you add a board to `publish.json`, write the `type` its board type suggests - `slides` for a
|
|
280
|
+
deck, `refs` for a context board, `doc` for a project (`marver build` notes any row that names none).
|
|
281
|
+
A board's status, reason and capability never ship - unless the row says `"showStatus": true`, and
|
|
282
|
+
then only a status drawn from publishable evidence (`audience: publishable` under `context/`; a scene
|
|
283
|
+
brief counts unless it says otherwise), never Blocked, never a reason.
|
|
284
|
+
|
|
252
285
|
The published gate page shows the app's identity: `design/logo.svg` + the host
|
|
253
286
|
package name (overridable via config `share`). If the app has no logo asset yet,
|
|
254
287
|
create a simple `design/logo.svg`. Leave `share.branding` ON unless the human
|