sign-off-axi 0.6.7 → 0.6.8
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/dist/cli.js +5 -1
- package/dist/contract.js +51 -11
- package/package.json +1 -1
- package/skills/sign-off-axi/SKILL.md +4 -1
- package/web/index.html +19 -6
package/dist/cli.js
CHANGED
|
@@ -3443,7 +3443,9 @@ async function route(req) {
|
|
|
3443
3443
|
const frame = value?.startsWith('gallery.') && d?.guided ? galleryFrames(d)[Number(value.slice(8))] : undefined;
|
|
3444
3444
|
const mock = diagram || frame ? (diagram ?? frame).src : value ? d?.mocks?.find((m) => m.value === value)?.path : value === '' && d?.view;
|
|
3445
3445
|
try {
|
|
3446
|
-
const path = rest.map(decodeURIComponent).join('/')
|
|
3446
|
+
const path = rest.map(decodeURIComponent).join('/');
|
|
3447
|
+
const download = frame && d?.sections?.some((s) => s.blocks?.some((b) => b.type === 'file' && b.src === frame.src));
|
|
3448
|
+
const type = download ? 'application/octet-stream' : assetTypes[extname(path).toLowerCase()];
|
|
3447
3449
|
if (!mock || !type)
|
|
3448
3450
|
return [404, { error: 'Mock is unavailable' }];
|
|
3449
3451
|
const actual = under(dirname(under(dirname(file), mockFile(mock))), path);
|
|
@@ -3454,6 +3456,8 @@ async function route(req) {
|
|
|
3454
3456
|
const pinned = diagram ?? frame;
|
|
3455
3457
|
if (pinned?.sha256 && (actual !== under(dirname(file), mockFile(mock)) || bytes.length !== pinned.bytes || sha256(bytes) !== pinned.sha256))
|
|
3456
3458
|
return [404, { error: 'Diagram file changed since it was posted' }];
|
|
3459
|
+
if (download)
|
|
3460
|
+
return [200, { attachment: { path: actual, name: basename(path).replace(/[^a-z0-9._ -]/gi, '_'), bytes: bytes.length, type } }];
|
|
3457
3461
|
if (frame && new URL(req.url, 'http://localhost').searchParams.get('v') === 'light' && type.startsWith('image/')) {
|
|
3458
3462
|
const light = galleryLight(pinned.sha256 ?? sha256(bytes), bytes, extname(path));
|
|
3459
3463
|
if (light)
|
package/dist/contract.js
CHANGED
|
@@ -51,14 +51,14 @@ const ACTIONS = {
|
|
|
51
51
|
export const GALLERY_DEVICES = ['phone', 'desktop'];
|
|
52
52
|
const GALLERY_FILE = /\.(png|jpe?g|webp|html?)$/i;
|
|
53
53
|
/** The report kit's blocks, the files a block may name, and the words a status may take. */
|
|
54
|
-
export const KIT_BLOCKS = ['callout', 'beats', 'video', 'table', 'compare', 'evidence'];
|
|
54
|
+
export const KIT_BLOCKS = ['callout', 'beats', 'video', 'table', 'compare', 'evidence', 'image', 'file'];
|
|
55
55
|
const KIT_IMAGE = /\.(png|jpe?g|webp)$/i, KIT_VIDEO = /\.(mp4|webm)$/i;
|
|
56
56
|
export const KIT_STATUS = { fixed: 'Fixed', progress: 'In progress', waiting: 'Waiting on you' };
|
|
57
57
|
const KIT_TONES = ['note', 'warn', 'ask'];
|
|
58
58
|
export const MEDIA_BYTES = 50_000_000;
|
|
59
59
|
/** The command that makes a report's page, named in every refusal of a report without one. */
|
|
60
60
|
export const REPORT_INIT = 'sign-off-axi report init <folder>';
|
|
61
|
-
const KIT_HELP = `Draw the report with the board's own blocks instead: run \`${REPORT_INIT} --purpose <kind>\` and add \`blocks\` to a section (callout, beats, video, table, compare, evidence). A page of your own may use the system colours (Canvas, CanvasText, GrayText) and no cards`;
|
|
61
|
+
const KIT_HELP = `Draw the report with the board's own blocks instead: run \`${REPORT_INIT} --purpose <kind>\` and add \`blocks\` to a section (callout, beats, video, table, compare, evidence, image, file). A page of your own may use the system colours (Canvas, CanvasText, GrayText) and no cards`;
|
|
62
62
|
const PAGE_HELP = `Run \`${REPORT_INIT} --page\` to make the page, fill it, then post it with "view" and "preview"`;
|
|
63
63
|
const GUIDE_HELP = `Run \`${REPORT_INIT}\` to write report.json, fill every FILL: line, then post it with \`post --kind report --from <folder>/report.json\``;
|
|
64
64
|
/** Parses one message; a field written twice in any object refuses it, since plain JSON.parse keeps the last one quietly. */
|
|
@@ -308,17 +308,57 @@ export function checkMessage(m, want, flags = false, fresh = false) {
|
|
|
308
308
|
text(`${at}.length`, o.length, 12, 'the film\'s length, such as "0:10", in 12 characters or fewer');
|
|
309
309
|
text(`${at}.caption`, o.caption, 280, 'what the film shows, in 280 characters or fewer');
|
|
310
310
|
}
|
|
311
|
+
else if (o.type === 'image' || o.type === 'file') {
|
|
312
|
+
only(['src', 'caption', ...(o.type === 'image' ? ['alt'] : [])], '{"type":"image","src":"frame.png","caption":"The reference"}');
|
|
313
|
+
kitFile(`${at}.src`, o.src, o.type === 'image' ? KIT_IMAGE : /./, o.type === 'image' ? 'a .png, .jpg, .jpeg or .webp' : 'file', 'report.md');
|
|
314
|
+
text(`${at}.caption`, o.caption, 280, 'the original caption or link text, in 280 characters or fewer');
|
|
315
|
+
if (Object.hasOwn(o, 'alt'))
|
|
316
|
+
text(`${at}.alt`, o.alt, 280, 'what the image shows, in 280 characters or fewer');
|
|
317
|
+
}
|
|
311
318
|
else if (o.type === 'table') {
|
|
312
|
-
only(['columns', 'rows'], '{"type":"table","columns":["Measured","App promo"
|
|
313
|
-
if (!Array.isArray(o.columns) || o.columns.length <
|
|
314
|
-
throw bad(`${at}.columns`, 'must list
|
|
315
|
-
o.columns.forEach((c, k) => text(`${at}.columns[${k}]`, c, 40, 'the column\'s name, in 40 characters or fewer'));
|
|
319
|
+
only(['columns', 'rows'], '{"type":"table","columns":["Measured","App promo"],"rows":[["AI calls","3"]]}');
|
|
320
|
+
if (!Array.isArray(o.columns) || o.columns.length < 1 || o.columns.length > 20)
|
|
321
|
+
throw bad(`${at}.columns`, 'must list 1 to 20 column names', 'Set "columns" to the original headings');
|
|
316
322
|
if (!Array.isArray(o.rows) || o.rows.length < 1 || o.rows.length > 30)
|
|
317
|
-
throw bad(`${at}.rows`, 'must list 1 to 30 rows', 'Set "rows" to arrays of
|
|
323
|
+
throw bad(`${at}.rows`, 'must list 1 to 30 rows', 'Set "rows" to arrays of cells, preserving ragged rows');
|
|
324
|
+
const occupied = o.rows.map(() => []);
|
|
325
|
+
const cell = (c, field, remaining) => {
|
|
326
|
+
if (typeof c === 'string') {
|
|
327
|
+
if (c !== '')
|
|
328
|
+
text(field, c, 120, 'the original cell text');
|
|
329
|
+
return [1, 1];
|
|
330
|
+
}
|
|
331
|
+
one(field, c, ['text', 'colspan', 'rowspan'], '{"text":"Calls","colspan":2}');
|
|
332
|
+
if (c.text !== '')
|
|
333
|
+
text(`${field}.text`, c.text, 120, 'the original cell text');
|
|
334
|
+
for (const [f, max] of [['colspan', 20], ['rowspan', remaining]]) {
|
|
335
|
+
if (Object.hasOwn(c, f) && (!Number.isInteger(c[f]) || c[f] < 1 || c[f] > max))
|
|
336
|
+
throw bad(`${field}.${f}`, `must be an integer from 1 to ${max}`, 'Keep the span inside the table');
|
|
337
|
+
}
|
|
338
|
+
return [c.colspan ?? 1, c.rowspan ?? 1];
|
|
339
|
+
};
|
|
340
|
+
let headWidth = 0;
|
|
341
|
+
o.columns.forEach((c, k) => { headWidth += cell(c, `${at}.columns[${k}]`, 1)[0]; });
|
|
342
|
+
if (headWidth > 20)
|
|
343
|
+
throw bad(`${at}.columns`, 'spans more than 20 columns', 'Keep the table within 20 columns');
|
|
318
344
|
o.rows.forEach((r, k) => {
|
|
319
|
-
if (!Array.isArray(r) || r.length
|
|
320
|
-
throw bad(`${at}.rows[${k}]`,
|
|
321
|
-
|
|
345
|
+
if (!Array.isArray(r) || r.length > 20)
|
|
346
|
+
throw bad(`${at}.rows[${k}]`, 'must be an array of at most 20 cells', 'Write the original cells without padding ragged rows');
|
|
347
|
+
let col = 0;
|
|
348
|
+
r.forEach((c, j) => {
|
|
349
|
+
while (occupied[k][col])
|
|
350
|
+
col++;
|
|
351
|
+
const [w, h] = cell(c, `${at}.rows[${k}][${j}]`, o.rows.length - k);
|
|
352
|
+
if (col + w > 20)
|
|
353
|
+
throw bad(`${at}.rows[${k}][${j}]`, 'extends beyond 20 columns', 'Keep the table within 20 columns');
|
|
354
|
+
for (let y = k; y < k + h; y++)
|
|
355
|
+
for (let x = col; x < col + w; x++) {
|
|
356
|
+
if (occupied[y][x])
|
|
357
|
+
throw bad(`${at}.rows[${k}][${j}]`, 'overlaps another span', 'Keep spans from overlapping');
|
|
358
|
+
occupied[y][x] = true;
|
|
359
|
+
}
|
|
360
|
+
col += w;
|
|
361
|
+
});
|
|
322
362
|
});
|
|
323
363
|
}
|
|
324
364
|
else if (o.type === 'compare') {
|
|
@@ -956,7 +996,7 @@ export const diagramViews = (m) => [m?.visual?.diagram, ...(m?.sections ?? []).f
|
|
|
956
996
|
/** The gallery frames of a guided report or plan, in the order the board counts them for `gallery.<n>`: each section's options, each option's frames. */
|
|
957
997
|
export const galleryFrames = (m) => [...(m?.sections ?? []).flatMap((s) => (s.gallery ?? []).flatMap((o) => o.frames)), ...(m?.sections ?? []).flatMap((s) => s.pins ?? [])];
|
|
958
998
|
/** The files a section's blocks name, each once, in the order the blocks use them. */
|
|
959
|
-
export const blockFiles = (s) => [...new Set((s?.blocks ?? []).flatMap((b) => b.type === 'beats' ? b.beats.map((x) => x.image) : b.type === 'video' ? [b.poster, b.src] : b.type === 'compare' ? [b.before.image, b.after.image] : b.type === 'evidence' ? b.rows.map((r) => r.image).filter(Boolean) : []))];
|
|
999
|
+
export const blockFiles = (s) => [...new Set((s?.blocks ?? []).flatMap((b) => ['image', 'file'].includes(b.type) ? [b.src] : b.type === 'beats' ? b.beats.map((x) => x.image) : b.type === 'video' ? [b.poster, b.src] : b.type === 'compare' ? [b.before.image, b.after.image] : b.type === 'evidence' ? b.rows.map((r) => r.image).filter(Boolean) : []))];
|
|
960
1000
|
const SAME_WORDS = 0.75;
|
|
961
1001
|
/** What the kit warns about, never refuses: a lead whose first sentence says again (most of its words are in) what the report's summary already said, so the reader reads it twice. */
|
|
962
1002
|
export function kitWarnings(m) {
|
package/package.json
CHANGED
|
@@ -158,13 +158,16 @@ Pick the block by what the reader must see:
|
|
|
158
158
|
| `callout` | one thing set apart: a caveat, a warning, a question | `tone` (`note`, `warn` or `ask`), `title` (60), `text` (280) |
|
|
159
159
|
| `beats` | a sequence the reader steps through: a film's scenes, a flow | `beats`, 2 to 12, each `image`, `time`, `title` (80), `text` (280) |
|
|
160
160
|
| `video` | a short film | `src` (.mp4 or .webm, 50 MB), `poster`, `length`, `caption` |
|
|
161
|
-
| `
|
|
161
|
+
| `image` | a neutral reference image, without a verdict | `src`, `caption` (280), optional `alt` (280); the whole image keeps its aspect ratio |
|
|
162
|
+
| `file` | a local file to download | `src` (any file up to 5 MB), `caption` (280), pinned like media; downloaded as an attachment |
|
|
163
|
+
| `table` | original measures, including irregular tables | `columns` (1 to 20), `rows` (up to 30); cells are text (120) or `{text, colspan, rowspan}`; keep empty cells and ragged rows as authored. Wide or irregular tables scroll inside the block on a phone |
|
|
162
164
|
| `compare` | before and after | `before` and `after`, each `image` and `caption` (80) |
|
|
163
165
|
| `evidence` | captures with a state for each | `rows`, each `image`, `title`, `note`, `status` (`fixed`, `progress` or `waiting`) |
|
|
164
166
|
|
|
165
167
|
- One idea per block, one block per point; a table beats three paragraphs of numbers, and a stepper beats a numbered list of screens.
|
|
166
168
|
- Say something new in each part. Post warns, and does not refuse, when a section's first paragraph repeats `about`.
|
|
167
169
|
- Options a model made are a gallery, not a row of pictures: see the design-output rule above. `gallery` and `blocks` can share a section, the gallery first.
|
|
170
|
+
- File paths stay inside the board folder, with no `..`, `?`, `#` or backslash. Posting pins their bytes; a changed file is unavailable until re-posted.
|
|
168
171
|
- Images are .png, .jpg, .jpeg or .webp; a path has no `..`, `?`, `#` or backslash.
|
|
169
172
|
- Sentence case in every heading and label; no capitals shouted, no colour words, no markup in any field.
|
|
170
173
|
- A page of your own (`report init --page`) is held to the board's look at its first post: post refuses one that writes out three or more colours of its own or draws a card inside a card, and says to use blocks. The scaffold already draws neither; keep to the system colours (Canvas, CanvasText, GrayText). A report already on the board stays as it was filed.
|
package/web/index.html
CHANGED
|
@@ -790,17 +790,23 @@
|
|
|
790
790
|
.kb-btn.go { border-color: transparent; background: var(--tk-go); color: var(--tk-on-go); box-shadow: var(--tk-elev-go); }
|
|
791
791
|
.kb-btn:disabled { opacity: 0.6; box-shadow: none; }
|
|
792
792
|
@media (min-width: 43.75rem) { .kb-stage { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 1.5rem; align-items: start; } .kb-nav { grid-template-columns: auto auto; justify-content: start; } }
|
|
793
|
+
.kt-image { margin: 0; display: grid; gap: 0.375rem; }
|
|
794
|
+
.kt-image img { display: block; width: 100%; height: auto; }
|
|
795
|
+
.kt-file { display: block; padding: 0.75rem 0; color: var(--tk-ink); text-decoration: underline; text-underline-offset: 0.2em; }
|
|
796
|
+
.kt-scroll { overflow-x: auto; min-width: 0; }
|
|
797
|
+
.kt-table.kt-wide { min-width: 48rem; }
|
|
798
|
+
.kt-wide :is(th, td) { min-width: 7rem; max-width: 20rem; overflow-wrap: anywhere; }
|
|
793
799
|
.kt-table { width: 100%; border-collapse: collapse; font-size: var(--tk-fs-row); line-height: var(--tk-lh-row); }
|
|
794
800
|
.kt-table th { padding: 0 0.75rem 0.5rem 0; border-bottom: 1px solid var(--tk-line); color: var(--tk-text-tertiary); font-size: var(--tk-fs-meta); font-weight: 500; text-align: left; }
|
|
795
801
|
.kt-table td { padding: 0.75rem 0.75rem 0.75rem 0; border-bottom: 1px solid var(--tk-line); vertical-align: top; font-variant-numeric: tabular-nums; }
|
|
796
802
|
.kt-table td:first-child { color: var(--tk-ink); font-weight: 600; }
|
|
797
803
|
.kt-table :is(th, td):last-child { padding-right: 0; }
|
|
798
804
|
@media (max-width: 30rem) {
|
|
799
|
-
.kt-table thead { display: none; }
|
|
800
|
-
.kt-table tr { display: grid; grid-template-columns: 1fr 1fr; gap: 0.125rem 1rem; padding: 0.75rem 0; border-bottom: 1px solid var(--tk-line); }
|
|
801
|
-
.kt-table td { padding: 0; border: 0; }
|
|
802
|
-
.kt-table td:first-child { grid-column: 1 / -1; margin-bottom: 0.125rem; }
|
|
803
|
-
.kt-table td[data-h]::before { content: attr(data-h); display: block; color: var(--tk-text-tertiary); font-size: var(--tk-fs-small); line-height: var(--tk-lh-small); font-weight: 500; }
|
|
805
|
+
.kt-table:not(.kt-wide) thead { display: none; }
|
|
806
|
+
.kt-table:not(.kt-wide) tr { display: grid; grid-template-columns: 1fr 1fr; gap: 0.125rem 1rem; padding: 0.75rem 0; border-bottom: 1px solid var(--tk-line); }
|
|
807
|
+
.kt-table:not(.kt-wide) td { padding: 0; border: 0; }
|
|
808
|
+
.kt-table:not(.kt-wide) td:first-child { grid-column: 1 / -1; margin-bottom: 0.125rem; }
|
|
809
|
+
.kt-table:not(.kt-wide) td[data-h]::before { content: attr(data-h); display: block; color: var(--tk-text-tertiary); font-size: var(--tk-fs-small); line-height: var(--tk-lh-small); font-weight: 500; }
|
|
804
810
|
}
|
|
805
811
|
.kt-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
|
806
812
|
.kt-compare figure { display: grid; gap: 0.375rem; align-content: start; margin: 0; }
|
|
@@ -4578,7 +4584,14 @@
|
|
|
4578
4584
|
if (b.type === 'callout') return E('div', `kt kt-callout ${b.tone ?? 'note'}`, E('span', 'tile', gic(b.tone === 'warn' ? 'warn' : b.tone === 'ask' ? 'deeper' : 'info')), E('div', '', mdSpan('b', '', b.title, NOLINK), mdSpan('span', '', b.text)));
|
|
4579
4585
|
if (b.type === 'beats') return beatsFig(s, b);
|
|
4580
4586
|
if (b.type === 'video') return videoFig(s, b);
|
|
4581
|
-
if (b.type === '
|
|
4587
|
+
if (b.type === 'image') return E('figure', 'kt kt-image', el('img', { src: kitUrl(s, b.src, true), alt: b.alt ?? b.caption, loading: 'lazy', decoding: 'async' }), E('figcaption', 'kt-cap', b.caption));
|
|
4588
|
+
if (b.type === 'file') return el('a', { class: 'kt kt-file', href: kitUrl(s, b.src), download: '' }, b.caption);
|
|
4589
|
+
if (b.type === 'table') {
|
|
4590
|
+
const wide = b.columns.length > 5 || [...b.columns, ...b.rows.flat()].some((c) => typeof c === 'object') || b.rows.some((r) => r.length !== b.columns.length);
|
|
4591
|
+
const cell = (c, tag, i) => el(tag, { ...(tag === 'th' ? { scope: 'col' } : !wide && i ? { 'data-h': b.columns[i] } : {}), ...(typeof c === 'object' ? { colspan: c.colspan ?? 1, rowspan: c.rowspan ?? 1 } : {}) }, typeof c === 'object' ? c.text : c);
|
|
4592
|
+
const table = el('table', { class: `kt-table${wide ? ' kt-wide' : ''}` }, el('thead', {}, el('tr', {}, ...b.columns.map((c, i) => cell(c, 'th', i)))), el('tbody', {}, ...b.rows.map((r) => el('tr', {}, ...r.map((c, i) => cell(c, 'td', i))))));
|
|
4593
|
+
return el('div', { class: `kt${wide ? ' kt-scroll' : ''}`, ...(wide ? { tabindex: '0', role: 'region', 'aria-label': 'Table' } : {}) }, table);
|
|
4594
|
+
}
|
|
4582
4595
|
if (b.type === 'compare') return E('div', 'kt kt-compare', ...[['Before', b.before], ['After', b.after]].map(([w, x]) => E('figure', '', el('img', { src: kitUrl(s, x.image, true), alt: `${w}: ${x.caption}`, loading: 'lazy', decoding: 'async' }), E('figcaption', '', E('b', '', w), x.caption))));
|
|
4583
4596
|
return el('ul', { class: 'kt kt-evid' }, ...b.rows.map((r) => E('li', '', ...(r.image ? [el('img', { src: kitUrl(s, r.image, true), alt: '', loading: 'lazy', decoding: 'async' })] : []), E('div', '', E('b', '', r.title), ...(r.note ? [E('small', '', r.note)] : [])), E('span', `kt-status ${r.status}`, KT_STATUS[r.status]))));
|
|
4584
4597
|
}
|