sign-off-axi 0.6.5 → 0.6.7
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/README.md +15 -3
- package/dist/cli.js +101 -23
- package/dist/contract.js +157 -21
- package/dist/pagestyle.js +73 -0
- package/dist/store.js +37 -3
- package/fixtures/contract/report.v2/page/index.html +14 -12
- package/fixtures/contract/report.v2/valid-full.json +1 -1
- package/fixtures/contract/report.v2/valid-minimal.json +2 -0
- package/fixtures/contract/report.v3/valid-full.json +2 -0
- package/fixtures/contract/report.v3/valid-minimal.json +2 -0
- package/fixtures/contract/report.v4/valid-full.json +2 -0
- package/fixtures/contract/report.v4/valid-minimal.json +2 -0
- package/fixtures/contract/report.v5/valid-full.json +1 -0
- package/fixtures/contract/report.v5/valid-minimal.json +1 -0
- package/package.json +1 -1
- package/skills/sign-off-axi/SKILL.md +29 -4
- package/web/index.html +162 -30
- package/web/report-template.json +1 -0
- package/web/report.html +13 -13
package/README.md
CHANGED
|
@@ -78,7 +78,18 @@ What you send on a card (an answer, a note, a new round, a comment) shows its tr
|
|
|
78
78
|
|
|
79
79
|
### Plans and reports, on their own pages
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
An agent writes a report as data and the board draws it: `sign-off-axi report init <folder> --purpose <kind>` writes `report.json`, and each section takes `blocks` to show a point rather than tell it.
|
|
82
|
+
|
|
83
|
+
| Block | What it draws |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `callout` | a note, warning or question set apart |
|
|
86
|
+
| `beats` | a stepper of pictures, each with a time, title and words |
|
|
87
|
+
| `video` | a short film with its poster |
|
|
88
|
+
| `table` | the same measures for each option |
|
|
89
|
+
| `compare` | before and after |
|
|
90
|
+
| `evidence` | captures, each marked Fixed, In progress or Waiting on you |
|
|
91
|
+
|
|
92
|
+
Design output (screens a model made) goes in a `gallery` on the section: `report init --gallery`. Every picture and film is a file beside `report.json`, and post pins it. A page an agent writes itself (`report init --page`) is refused when it brings its own colours, three or more, or a card inside a card; reports already on the board stay as filed.
|
|
82
93
|
|
|
83
94
|
<table>
|
|
84
95
|
<tr>
|
|
@@ -150,8 +161,9 @@ One line on what the agent runs per kind; the full reference is [docs/question-k
|
|
|
150
161
|
| Proof update | `post --type evidence --media shot.png` | the capture, its result, Pass / Fail / Needs work |
|
|
151
162
|
| Proof with a file to install | `post --type evidence --media shot.png --attach app.apk` | the capture, and the file with its size and a Download button (any type, up to 1 GB, `SIGN_OFF_ATTACH_MAX_MB` to change) |
|
|
152
163
|
| Explainer video | `post --type explainer --media clip.mp4 --chapter 0:00=Start` | a player with chapter buttons, then Yes or No |
|
|
153
|
-
| Report | `report init <folder
|
|
154
|
-
| Design output report | `report init <folder> --gallery`, put the screenshots under the board's folder, fill the gallery (label, model, run time, a caption and device per frame), then `post --kind report` | a swipeable strip of options on a phone, a row on a desk: each option's phone and desktop frame side by side, the caption with the model and run time under each, a tap to open full size with Previous and Next |
|
|
164
|
+
| Report | `report init <folder> --purpose <kind>` (always: research, plan, decision, audit, incident, status or other), fill `report.json`, then `post --kind report --from <folder>/report.json` | one scroll: the answer and a picture, what I understood, what I found, the next step; numbers under their part, one bar (Agree, Disagree, Comment, Ask) that acts on the part in view, and one Activity of every note, ask, reply and read |
|
|
165
|
+
| Design output report | `report init <folder> --purpose <kind> --gallery`, put the screenshots under the board's folder, fill the gallery (label, model, run time, a caption and device per frame), then `post --kind report` | a swipeable strip of options on a phone, a row on a desk: each option's phone and desktop frame side by side, the caption with the model and run time under each, a tap to open full size with Previous and Next |
|
|
166
|
+
| Report kit blocks | add `blocks` to a section of `report.json`: `callout`, `beats` (a stepper), `video`, `table`, `compare` (before and after), `evidence` rows, with files under the board's folder | the board draws each in its own type and colours, light and dark: full width, one gutter, no card inside a card; a gallery and a video may run wider than the reading column on a desk |
|
|
155
167
|
| Diagram, interactive page | `post --type diagram --view flow.svg` | the picture on the card; interactive pages run sandboxed, and open full screen at phone or desktop width |
|
|
156
168
|
| Plan | `report init <folder> --plan`, fill `plan.json`, then `post --kind plan --from plan.json` | the same scroll, plus each decision with my pick and a Go ahead |
|
|
157
169
|
| Sign-in link | `post --type signin --service npm --link <url> --unlocks ...` | an Open button and the time left |
|
package/dist/cli.js
CHANGED
|
@@ -14,8 +14,8 @@ import { parseArgs } from 'node:util';
|
|
|
14
14
|
import { encode } from '@toon-format/toon';
|
|
15
15
|
import { acknowledgeOwner, checkOwner, deliveries, dispatchOwners, registerOwner } from './owner.js';
|
|
16
16
|
import { AxiError, runAxiCli } from 'axi-sdk-js';
|
|
17
|
-
import { ATTACH_DEFAULT_MB, ATTACH_MAX, ATTACH_NAME, guardOf, plainAsk, CHAPTERS_MAX, SIGNED_IN, CLEARED, InputError, MEDIA_MAX, NOTE_MAX, REPLY, answersPath, appendAnnotation, appendPartComment, annotationsPath, markCommentsRead, readPartComments, replyComment, asks, appendAnswer, appendDismissed, DISCARD_REASONS, readDiscards, UNDO_MS, appendPromoted, appendSendBack, appendTicket, readTickets, rollUp, appendTicketComment, ticketCommentFault, ticketFault, appendProject, readProjects, projectFault, iconPathFault, iconContentFault, ICON_BYTES, ICON_TYPES, PRIORITIES, REPORT_PARTS, REPORT_PURPOSES, appendDesignAnswer, answerLines, appendMark, readTrail, trailPath, appendMessage, appendRound, awaitsVerdict, lapsed, askEngagement, checkEngagement, markEngagementSeen, appendRead, appendOpened, readOpened, answerDeeper, askDeeper, deeperPath, guidePart, markDeeperRead, readDeeper, readReads, screenViews, seenAt, isKey, loadDecisions, markDelivered, markSeen, messagesPath, readAnnotations, readAnswers, reading, readDismissed, readItems, readPromoted, sameDecision, readMessages, readRounds, roundsPath, sameRound, seenBy, seenPath, sentIds, unreadable as reasonOf, writeItems } from './store.js';
|
|
18
|
-
import { ContractError, KINDS, MEDIA_BYTES, REPORT_INIT, checkMessage, diagramPages, diagramViews, galleryFrames, guided, mediaFiles, parseMessage, reportPage } from './contract.js';
|
|
17
|
+
import { ATTACH_DEFAULT_MB, ATTACH_MAX, ATTACH_NAME, guardOf, plainAsk, CHAPTERS_MAX, SIGNED_IN, CLEARED, InputError, MEDIA_MAX, NOTE_MAX, REPLY, answersPath, appendAnnotation, appendPartComment, annotationsPath, markCommentsRead, readPartComments, replyComment, asks, appendAnswer, appendDismissed, DISCARD_REASONS, readDiscards, UNDO_MS, appendPromoted, appendKind, readKinds, appendSendBack, appendTicket, readTickets, rollUp, appendTicketComment, ticketCommentFault, ticketFault, appendProject, readProjects, projectFault, iconPathFault, iconContentFault, ICON_BYTES, ICON_TYPES, PRIORITIES, REPORT_PARTS, REPORT_PURPOSES, appendDesignAnswer, answerLines, appendMark, readTrail, trailPath, appendMessage, appendRound, awaitsVerdict, lapsed, askEngagement, checkEngagement, markEngagementSeen, appendRead, appendOpened, readOpened, answerDeeper, askDeeper, deeperPath, guidePart, markDeeperRead, readDeeper, readReads, screenViews, seenAt, isKey, loadDecisions, markDelivered, markSeen, messagesPath, readAnnotations, readAnswers, reading, readDismissed, readItems, readPromoted, sameDecision, readMessages, readRounds, roundsPath, sameRound, seenBy, seenPath, sentIds, unreadable as reasonOf, writeItems } from './store.js';
|
|
18
|
+
import { ContractError, KINDS, MEDIA_BYTES, REPORT_INIT, checkMessage, diagramPages, diagramViews, galleryFrames, guided, kitWarnings, mediaFiles, parseMessage, reportPage } from './contract.js';
|
|
19
19
|
import { faults, isStill } from './evidence-text.js';
|
|
20
20
|
import { copiesOf, differs } from './images.js';
|
|
21
21
|
import { deliverVault, errors, readVaultExpiry, sweepVault, takeVaultExpiries, VaultError } from './vault.js';
|
|
@@ -124,7 +124,7 @@ examples:
|
|
|
124
124
|
validate: `usage: sign-off-axi validate <message.json>... [--kind <kind>] [--expect-invalid]
|
|
125
125
|
Checks message files against the communication contract without posting them: ${CONTRACT}
|
|
126
126
|
A message is one JSON object with "contract": 1, its "kind" and its "schema". This build checks kinds ask, evidence, gate and finding at schema 1, design at schema 2 (schema 1 still passes), report at schema 5 (schemas 2 to 4 still pass) and plan at schema 3 (schemas 1 and 2 still pass).
|
|
127
|
-
A report 3 to 5 and a plan 2 or 3 are guided pages: fields the board draws as one scroll, no page of your own (see report init).
|
|
127
|
+
A report 3 to 5 and a plan 2 or 3 are guided pages: fields the board draws as one scroll, no page of your own (see report init). Every report names what it is for in "purpose", required on a report 5 and on any new report, and so is its "project".
|
|
128
128
|
An evidence message's media paths read from the message file's folder: each file must be there, inside it, and 50 MB or less.
|
|
129
129
|
A report is a page to tap through: its "view" (.html) and "preview" (its picture, beside it) are checked for their shape here; the files themselves are checked at post, from the board file's folder, filled with no FILL: lines left (see report init).
|
|
130
130
|
A plan's "view" (.html) is checked for its shape here; the file itself is checked at post, from the board file's folder.
|
|
@@ -380,8 +380,10 @@ examples:
|
|
|
380
380
|
sign-off-axi feedback fleet-dashboard-full --reply "The colours now come from the new tokens." --link https://github.com/owner/repo/pull/245
|
|
381
381
|
sign-off-axi feedback fleet-dashboard-full --acted --link https://github.com/owner/repo/pull/245
|
|
382
382
|
`,
|
|
383
|
-
report: `usage: sign-off-axi report init <folder>
|
|
384
|
-
|
|
383
|
+
report: `usage: sign-off-axi report init <folder> --purpose research|plan|decision|audit|incident|status|other [--page] [--gallery]
|
|
384
|
+
sign-off-axi report init <folder> --plan
|
|
385
|
+
sign-off-axi report kind <key> <kind> --project <name> --by <who> [--board board.json] (or --from <file> of key<TAB>kind<TAB>project lines)
|
|
386
|
+
Writes the guided template to fill: <folder>/report.json (--purpose is required), or <folder>/plan.json with --plan. A file already there is kept.
|
|
385
387
|
Every report names what it is for in "purpose", its kind: research (research and discovery), plan (implementation plan), decision (decision and recommendation),
|
|
386
388
|
audit, incident (incident and retro), status, or other. --purpose fills it in and writes that kind's parts to fill, one section each, which post then requires
|
|
387
389
|
(research: question, scope, method, sources with links and a user report, options, findings with their confidence, not-checked, recommendation, open-questions;
|
|
@@ -392,7 +394,7 @@ Numbers sit behind a Details tap; every part has a Go deeper button that asks yo
|
|
|
392
394
|
Replace every FILL: line, then post it: post --kind report --from <folder>/report.json, or post --kind plan --from <folder>/plan.json --filer <your id>.
|
|
393
395
|
A report is for an explanation the user asked for, or a concept, design or trade-off they must understand. Proof that something works is evidence.
|
|
394
396
|
--gallery adds a gallery to the first section: a design output's options, each with its model, run time and a phone and a desktop frame (an image or page, as a path from the board file's folder), which the board draws as a swipeable strip with each frame opening full size.
|
|
395
|
-
--page writes the older scaffold page instead, for a
|
|
397
|
+
--page (with --purpose) writes the older scaffold page instead, for a demo to tap that no block of the kit can show (report schema 2); post it with --type report --purpose <kind>. The board owns the colours: post refuses a page that writes out three or more colours of its own, or draws a card inside a card, on its first post. It is:
|
|
396
398
|
Writes a report page to fill: <folder>/index.html and its card picture <folder>/preview.svg. A file already there is kept.
|
|
397
399
|
The page is one self-contained file, phone first, light and dark, in four parts, each one idea:
|
|
398
400
|
1. the answer in one plain sentence and a picture of it 2. a diagram of how it works
|
|
@@ -400,8 +402,10 @@ The page is one self-contained file, phone first, light and dark, in four parts,
|
|
|
400
402
|
Replace every FILL: line, open the page in a browser 390 px wide and look: mostly text means it is not ready.
|
|
401
403
|
Then post it with the command it prints; the paths are from the board file's folder, so make the folder under it, such as .sign-off/mocks/<key>.
|
|
402
404
|
A report is for an explanation the user asked for, or a concept, design or trade-off they must understand. Proof that something works is evidence.
|
|
405
|
+
report kind sets the kind and the project of a report posted before every report had them, and records --by, who set it; the board itself is not changed, only <board>.kinds.jsonl. All the lines of --from are checked before the first is written.
|
|
403
406
|
examples:
|
|
404
|
-
sign-off-axi report init .sign-off/
|
|
407
|
+
sign-off-axi report init .sign-off/reports/sign-in-flow --purpose research
|
|
408
|
+
sign-off-axi report kind sign-in-flow research --project Beacon --by agent-41
|
|
405
409
|
`,
|
|
406
410
|
init: `usage: sign-off-axi init [dir] [--agent auto|<name>[,<name>]...]
|
|
407
411
|
Writes a repo's sign-off setup under <dir>/.sign-off/ (config, an empty board, mocks/ and evidence/ folders, and a .gitignore keeping media and answer files out of git). A file that is already there is kept, never changed, except that a given --agent replaces the config's agent: line; a re-run adds what is missing.
|
|
@@ -511,6 +515,18 @@ const CONTRACT_TYPES = ['decision', 'ask', 'evidence', 'report', 'plan', 'design
|
|
|
511
515
|
const asSent = ({ filed: _filed, announced: _announced, ...rest }) => rest;
|
|
512
516
|
/** A report names its own `filed` time, so only the notice record goes. */
|
|
513
517
|
const asReport = ({ announced: _announced, ...rest }) => rest;
|
|
518
|
+
/** The projects the board knows, for a refusal that names them. */
|
|
519
|
+
const knownProjects = (file) => { const all = existsSync(file) ? readProjects(file).map((p) => p.name) : []; return all.length ? `known projects: ${all.join(', ')}` : 'no project is registered on this board yet (sign-off-axi project --name <project> --icon <picture>), so name the one the report is about'; };
|
|
520
|
+
/** A new report names its kind and its project, whatever its schema; one already on the board stays a re-post of its own key (an older one is named with report kind). */
|
|
521
|
+
function newReportFields(m, file) {
|
|
522
|
+
if (m?.kind !== 'report')
|
|
523
|
+
return;
|
|
524
|
+
if (!Object.hasOwn(m, 'purpose')) {
|
|
525
|
+
throw new ContractError(`report ${m.key} has no kind: "purpose" is required, what the report is for`, `Set "purpose" to one of ${REPORT_PURPOSES.join(', ')}; ${REPORT_INIT} --purpose <kind> writes a report with it filled in`);
|
|
526
|
+
}
|
|
527
|
+
if (!Object.hasOwn(m, 'project'))
|
|
528
|
+
throw new ContractError(`report ${m.key} has no project: "project" is required, so its row shows the project's icon`, `Set "project" to the project the report is about; ${knownProjects(file)}`);
|
|
529
|
+
}
|
|
514
530
|
/** Items of a --from file that carry contract or kind are contract messages: each is checked against its kind's rules before anything is stored.
|
|
515
531
|
* A new item of a contract kind must carry them (contract section 2). A --replace feed replaces the stored item, so an item under an existing key
|
|
516
532
|
* that changed is checked again; an unchanged item, a board-format one and every item outside a --replace feed are not checked again. */
|
|
@@ -551,6 +567,8 @@ function contractItems(items, from, file, replace = false) {
|
|
|
551
567
|
}
|
|
552
568
|
try {
|
|
553
569
|
checkMessage(check);
|
|
570
|
+
if (!held)
|
|
571
|
+
newReportFields(check, file);
|
|
554
572
|
if ((check.kind === 'report' || check.kind === 'plan') && !guided(check))
|
|
555
573
|
reportPage(check, dirname(file));
|
|
556
574
|
if (guided(check))
|
|
@@ -1139,12 +1157,12 @@ async function locked(lock, fn, busy = 'another raise is still writing the board
|
|
|
1139
1157
|
const mockFile = (path) => path.split('?')[0];
|
|
1140
1158
|
/** Posts an item. `raise` is the alias for `post --type decision`: same flags as before, same output. */
|
|
1141
1159
|
function post(args, cmd = 'post') {
|
|
1142
|
-
const typed = cmd === 'post' ? { replace: { type: 'boolean' }, kind: { type: 'string' }, type: { type: 'string' }, tradeoff: { type: 'string' }, mock: { type: 'string', multiple: true },
|
|
1160
|
+
const typed = cmd === 'post' ? { replace: { type: 'boolean' }, kind: { type: 'string' }, purpose: { type: 'string' }, project: { type: 'string' }, type: { type: 'string' }, tradeoff: { type: 'string' }, mock: { type: 'string', multiple: true },
|
|
1143
1161
|
media: { type: 'string', multiple: true }, attach: { type: 'string', multiple: true }, link: { type: 'string', multiple: true }, view: { type: 'string' }, preview: { type: 'string' }, chapter: { type: 'string', multiple: true },
|
|
1144
1162
|
...Object.fromEntries(SIGNIN_FIELDS.map((f) => [f, { type: 'string' }])) } : {};
|
|
1145
1163
|
const { file, named, values, on } = parse(cmd, args, { from: { type: 'string' }, filer: { type: 'string' }, 'ask-ref': { type: 'string' }, ticket: { type: 'string' }, priority: { type: 'string' }, ...typed,
|
|
1146
1164
|
...Object.fromEntries(RAISE_FIELDS.map((f) => [f, { type: 'string', multiple: MANY.includes(f) }])) });
|
|
1147
|
-
const given = [...RAISE_FIELDS, 'type', 'tradeoff', 'mock', 'media', 'attach', 'link', 'view', 'preview', 'chapter', ...SIGNIN_FIELDS].filter((f) => values[f] !== undefined);
|
|
1165
|
+
const given = [...RAISE_FIELDS, 'type', 'purpose', 'project', 'tradeoff', 'mock', 'media', 'attach', 'link', 'view', 'preview', 'chapter', ...SIGNIN_FIELDS].filter((f) => values[f] !== undefined);
|
|
1148
1166
|
// Only a design takes --link more than once (one per option); any other type keeps its one item link, the last given.
|
|
1149
1167
|
const optionLinks = values.link ?? [];
|
|
1150
1168
|
if (values.type !== 'design' && optionLinks.length)
|
|
@@ -1850,15 +1868,21 @@ function postReport(file, named, on, values) {
|
|
|
1850
1868
|
const odd = ['option', 'recommend', 'default', 'close', 'detail', 'mock', 'tradeoff', 'media', 'chapter'].find((f) => values[f] !== undefined);
|
|
1851
1869
|
if (odd)
|
|
1852
1870
|
throw fail(`--${odd} does not apply to --type report: a report takes no answer`, 'VALIDATION_ERROR', 'Run `sign-off-axi post --help`');
|
|
1871
|
+
if (values.purpose === undefined)
|
|
1872
|
+
throw fail(`a report needs --purpose <kind>, what it is for: ${REPORT_PURPOSES.join(', ')}`, 'VALIDATION_ERROR', 'sign-off-axi post --type report --purpose research --title "<the answer>" --about "<why it matters>" --view <page>.html --preview <picture>.svg');
|
|
1873
|
+
if (!REPORT_PURPOSES.includes(values.purpose))
|
|
1874
|
+
throw fail(`--purpose ${values.purpose} must be one of ${REPORT_PURPOSES.join(', ')}`, 'VALIDATION_ERROR', 'sign-off-axi post --type report --purpose research --title "<the answer>" ...');
|
|
1875
|
+
if (values.project === undefined)
|
|
1876
|
+
throw fail(`a report needs --project <name>, the project it is about; ${knownProjects(file)}`, 'VALIDATION_ERROR', 'sign-off-axi post --type report --purpose research --project Beacon --title "<the answer>" ...');
|
|
1853
1877
|
const missing = ['title', 'about', 'view', 'preview'].filter((f) => values[f] === undefined).map((f) => `--${f}`);
|
|
1854
1878
|
if (missing.length) {
|
|
1855
|
-
throw fail(`a report is a page to tap through, not text: ${missing.join(' and ').replace(/ and (?=.* and )/g, ', ')} ${missing.length > 1 ? 'are' : 'is'} missing`, 'VALIDATION_ERROR', `Run \`${REPORT_INIT} --page\` to make the page and its picture, fill them, then post --type report --title "<the answer>" --about "<why it matters>" --view <folder>/index.html --preview <folder>/preview.svg`);
|
|
1879
|
+
throw fail(`a report is a page to tap through, not text: ${missing.join(' and ').replace(/ and (?=.* and )/g, ', ')} ${missing.length > 1 ? 'are' : 'is'} missing`, 'VALIDATION_ERROR', `Run \`${REPORT_INIT} --page\` to make the page and its picture, fill them, then post --type report --purpose <kind> --project <name> --title "<the answer>" --about "<why it matters>" --view <folder>/index.html --preview <folder>/preview.svg`);
|
|
1856
1880
|
}
|
|
1857
1881
|
const key = values.key ?? slug(values.title), had = existsSync(file) ? readItems(file).items.find((i) => i?.key === key) : undefined;
|
|
1858
1882
|
// Posting the same report again is a no-op, so it keeps the time it was first filed.
|
|
1859
1883
|
const filed = (had?.kind === 'report' && had.filed) || new Date().toISOString().replace(/\.\d+Z$/, 'Z');
|
|
1860
1884
|
const { title, about, view, preview, link } = values;
|
|
1861
|
-
return postKind(file, named, on, values, { contract: 1, kind: 'report', schema: 2, key, title, about, view, preview, ...(link !== undefined && { link }), filed });
|
|
1885
|
+
return postKind(file, named, on, values, { contract: 1, kind: 'report', schema: 2, key, title, about, view, preview, purpose: values.purpose, project: values.project, ...(link !== undefined && { link }), filed });
|
|
1862
1886
|
}
|
|
1863
1887
|
/** Posts one contract message (docs/contract.md), from a file or from stdin with --from -: checked against --kind, then stored like a raised item.
|
|
1864
1888
|
* `built` is a message post --type report made from its flags. */
|
|
@@ -1890,14 +1914,19 @@ async function postKind(file, named, on, values, built) {
|
|
|
1890
1914
|
// A new ask is held to the question standard, and so is a new design's title; a card already on the board still re-posts as it was.
|
|
1891
1915
|
const existing = !built && ['ask', 'design'].includes(kind) && existsSync(file) && readItems(file).items.some((i) => i?.key === m.key);
|
|
1892
1916
|
checkMessage(m, built ? 'report' : kind, !built && kind === 'ask' && !existing, !built && kind === 'design' && !existing);
|
|
1917
|
+
if (!(existsSync(file) && readItems(file).items.some((i) => i?.key === m.key)))
|
|
1918
|
+
newReportFields(m, file);
|
|
1893
1919
|
// Media paths read from the message file's folder, or from the current folder for stdin.
|
|
1894
1920
|
if (m.kind === 'evidence')
|
|
1895
1921
|
sources = mediaFiles(m, from === '-' ? process.cwd() : dirname(resolve(from)));
|
|
1896
1922
|
// A report's or plan's page is served from the board file's folder, as --view is.
|
|
1897
1923
|
if ((m.kind === 'report' || m.kind === 'plan') && !guided(m))
|
|
1898
|
-
reportPage(m, dirname(file));
|
|
1899
|
-
if (guided(m))
|
|
1924
|
+
reportPage(m, dirname(file), !(existsSync(file) && readItems(file).items.some((i) => i?.key === m.key)));
|
|
1925
|
+
if (guided(m)) {
|
|
1900
1926
|
diagramPages(m, dirname(file));
|
|
1927
|
+
for (const w of kitWarnings(m))
|
|
1928
|
+
process.stderr.write(`warning: ${name}: ${w}\n`);
|
|
1929
|
+
}
|
|
1901
1930
|
if (m.kind === 'design')
|
|
1902
1931
|
designFiles(m, dirname(file));
|
|
1903
1932
|
}
|
|
@@ -2581,18 +2610,27 @@ const kindSections = (purpose) => REPORT_PARTS[purpose].map(([part, name]) => {
|
|
|
2581
2610
|
function report(args) {
|
|
2582
2611
|
let parsed;
|
|
2583
2612
|
try {
|
|
2584
|
-
parsed = parseArgs({ args, allowPositionals: true, strict: true, options: { plan: { type: 'boolean' }, page: { type: 'boolean' }, purpose: { type: 'string' }, gallery: { type: 'boolean' } } });
|
|
2613
|
+
parsed = parseArgs({ args, allowPositionals: true, strict: true, options: { plan: { type: 'boolean' }, page: { type: 'boolean' }, purpose: { type: 'string' }, gallery: { type: 'boolean' }, project: { type: 'string' }, board: { type: 'string' }, by: { type: 'string' }, from: { type: 'string' } } });
|
|
2585
2614
|
}
|
|
2586
2615
|
catch (e) {
|
|
2587
2616
|
throw fail(e.message, 'VALIDATION_ERROR', 'Run `sign-off-axi report --help`');
|
|
2588
2617
|
}
|
|
2589
2618
|
const [sub, dir, extra] = parsed.positionals, { plan, page, purpose, gallery } = parsed.values;
|
|
2619
|
+
if (parsed.values.project !== undefined && sub !== 'kind')
|
|
2620
|
+
throw fail('--project belongs to report kind; a new report carries its project in report.json', 'VALIDATION_ERROR', 'sign-off-axi report kind <key> <kind> --project <name> --by <who>');
|
|
2621
|
+
if (sub === 'kind')
|
|
2622
|
+
return reportKind(parsed.positionals.slice(1), parsed.values);
|
|
2623
|
+
if (parsed.values.board !== undefined || parsed.values.by !== undefined || parsed.values.from !== undefined)
|
|
2624
|
+
throw fail('--board, --by and --from belong to report kind', 'VALIDATION_ERROR', 'sign-off-axi report kind <key> <kind> --by <who>');
|
|
2625
|
+
if (purpose !== undefined && plan)
|
|
2626
|
+
throw fail('--purpose does not go with --plan: a plan is always an implementation plan', 'VALIDATION_ERROR', 'sign-off-axi report init <folder> --plan');
|
|
2590
2627
|
if (gallery && (plan || page))
|
|
2591
|
-
throw fail(`--gallery does not go with --${plan ? 'plan' : 'page'}: a gallery is a section of a guided report`, 'VALIDATION_ERROR', 'sign-off-axi report init <folder> --gallery');
|
|
2592
|
-
if (purpose !== undefined && (plan || page))
|
|
2593
|
-
throw fail(`--purpose does not go with --${plan ? 'plan' : 'page'}: ${plan ? 'a plan is always an implementation plan' : 'a page report has no purpose field'}`, 'VALIDATION_ERROR', 'sign-off-axi report init <folder> --purpose research');
|
|
2628
|
+
throw fail(`--gallery does not go with --${plan ? 'plan' : 'page'}: a gallery is a section of a guided report`, 'VALIDATION_ERROR', 'sign-off-axi report init <folder> --purpose research --gallery');
|
|
2594
2629
|
if (purpose !== undefined && !REPORT_PURPOSES.includes(purpose))
|
|
2595
2630
|
throw fail(`--purpose ${purpose} must be one of ${REPORT_PURPOSES.join(', ')}`, 'VALIDATION_ERROR', 'sign-off-axi report init <folder> --purpose research');
|
|
2631
|
+
if (sub === 'init' && !plan && purpose === undefined) {
|
|
2632
|
+
throw fail(`report init needs --purpose <kind>, what the report is for: ${REPORT_PURPOSES.join(', ')}`, 'VALIDATION_ERROR', 'sign-off-axi report init <folder> --purpose research');
|
|
2633
|
+
}
|
|
2596
2634
|
if (sub !== 'init' || dir === undefined || extra !== undefined || (plan && page)) {
|
|
2597
2635
|
throw fail(sub !== 'init' ? `unknown report command ${sub ?? '(none)'}` : extra !== undefined ? `unexpected argument ${extra}: report init takes one folder` : plan && page ? '--plan and --page do not go together' : 'report init needs the folder to write the template in', 'VALIDATION_ERROR', 'sign-off-axi report init <folder> [--plan]');
|
|
2598
2636
|
}
|
|
@@ -2619,7 +2657,6 @@ function report(args) {
|
|
|
2619
2657
|
created.push(shellPath(path));
|
|
2620
2658
|
}
|
|
2621
2659
|
return toon({ folder: shellPath(out), created: created.length ? created : 'nothing new (re-run is a no-op)', kept }, [
|
|
2622
|
-
...(plan || purpose ? [] : [`Set "purpose" to what the report is for: ${REPORT_PURPOSES.join(', ')} (or run report init again with --purpose <kind> in a new folder)`]),
|
|
2623
2660
|
`Replace every FILL: line in ${kind}.json with your own words: the answer first, one picture (bars, steps or tiles), what you understood, then the sections${plan ? ' and the decisions' : ''}`,
|
|
2624
2661
|
`${plan ? 'A plan' : 'A design or architecture report'} needs one real diagram: a flow or architecture drawing as inline SVG, in light and dark, as "diagram" on the visual (see the skill)`,
|
|
2625
2662
|
...(gallery ? ['Save each option\'s screenshots (or pages) under the board file\'s folder, a phone frame 390 wide and a desktop frame 1280 wide, and fill the gallery: its label, model, run time (UTC), and per frame its path from that folder and a caption'] : []),
|
|
@@ -2639,10 +2676,51 @@ function report(args) {
|
|
|
2639
2676
|
return toon({ folder: shellPath(out), created: created.length ? created : 'nothing new (re-run is a no-op)', kept }, [
|
|
2640
2677
|
'Replace every FILL: line: the answer and its picture first, then the diagram, the steps to tap, and the folded detail',
|
|
2641
2678
|
'Open index.html in a browser 390 px wide and look before posting: mostly text means it is not ready',
|
|
2642
|
-
served ? `Run \`sign-off-axi post ${shellPath(board)} --type report --title "<the answer>" --about "<why it matters>" --view ${rel}/index.html --preview ${rel}/preview.svg\``
|
|
2643
|
-
: `Run \`sign-off-axi post <board.json> --type report --title "<the answer>" --about "<why it matters>" --view <path>/index.html --preview <path>/preview.svg\`, both paths from the board file's folder, which must hold this folder`,
|
|
2679
|
+
served ? `Run \`sign-off-axi post ${shellPath(board)} --type report --purpose ${purpose} --project <name> --title "<the answer>" --about "<why it matters>" --view ${rel}/index.html --preview ${rel}/preview.svg\``
|
|
2680
|
+
: `Run \`sign-off-axi post <board.json> --type report --purpose ${purpose} --project <name> --title "<the answer>" --about "<why it matters>" --view <path>/index.html --preview <path>/preview.svg\`, both paths from the board file's folder, which must hold this folder`,
|
|
2644
2681
|
]);
|
|
2645
2682
|
}
|
|
2683
|
+
/** report kind <key> <kind> --by <who>, or --from <file>: one `key<TAB>kind<TAB>project` line each, with a note after if you like. Sets the kind and the project of reports posted before every report had one, and records who set it.
|
|
2684
|
+
* Every key is checked before the first is written, so a bad line changes nothing. */
|
|
2685
|
+
function reportKind(given, values) {
|
|
2686
|
+
const example = 'sign-off-axi report kind sign-in-flow research --project Beacon --by agent-41', { by, from, project } = values;
|
|
2687
|
+
if (by === undefined || !by.trim() || by.length > 80)
|
|
2688
|
+
throw fail('report kind needs --by <who set it>, in 80 characters or fewer', 'VALIDATION_ERROR', example);
|
|
2689
|
+
if (from !== undefined === given.length > 0)
|
|
2690
|
+
throw fail(from !== undefined ? '--from takes the place of <key> <kind>' : 'report kind needs <key> <kind> --project <name>, or --from <file> with one `key<TAB>kind<TAB>project` line each', 'VALIDATION_ERROR', example);
|
|
2691
|
+
let pairs = [[...given, project]];
|
|
2692
|
+
if (from !== undefined) {
|
|
2693
|
+
let text;
|
|
2694
|
+
try {
|
|
2695
|
+
text = readFileSync(from, 'utf8');
|
|
2696
|
+
}
|
|
2697
|
+
catch (e) {
|
|
2698
|
+
throw fail(`${from} ${reasonOf(e.code)}`, 'NOT_FOUND', 'Check the path names a readable file');
|
|
2699
|
+
}
|
|
2700
|
+
pairs = text.split('\n').filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.split('\t').map((x) => x.trim()));
|
|
2701
|
+
}
|
|
2702
|
+
const file = values.board === undefined ? boardHere() : resolve(values.board), items = readItems(file).items, reports = new Map(items.filter((i) => i?.kind === 'report').map((i) => [i.key, i]));
|
|
2703
|
+
const set = readKinds(file, warn), rows = pairs.map(([key, kind, name, _why, ...rest], n) => {
|
|
2704
|
+
const at = from === undefined ? '' : `${from} line ${n + 1}: `;
|
|
2705
|
+
if (key === undefined || kind === undefined || !name || rest.length)
|
|
2706
|
+
throw fail(`${at}expected <key> <kind> <project>`, 'VALIDATION_ERROR', example);
|
|
2707
|
+
if (!REPORT_PURPOSES.includes(kind))
|
|
2708
|
+
throw fail(`${at}${kind} is not a report kind: use one of ${REPORT_PURPOSES.join(', ')}`, 'VALIDATION_ERROR', example);
|
|
2709
|
+
if (name.length > 40)
|
|
2710
|
+
throw fail(`${at}project ${name} is over 40 characters`, 'VALIDATION_ERROR', example);
|
|
2711
|
+
const d = reports.get(key);
|
|
2712
|
+
if (!d)
|
|
2713
|
+
throw fail(`${at}${key} is not a report on ${tilde(file)}`, 'NOT_FOUND', 'Run `sign-off-axi list` for the keys; a plan is always an implementation plan and needs no kind');
|
|
2714
|
+
for (const [what, has, now] of [['kind', d.purpose, kind], ['project', d.project, name]])
|
|
2715
|
+
if (has !== undefined && has !== now)
|
|
2716
|
+
throw fail(`${at}${key} already has its ${what} ${has} in the report itself`, 'VALIDATION_ERROR', 'Only a report posted without one is named with report kind');
|
|
2717
|
+
return { key, kind, project: name, same: (d.purpose ?? set.get(key)?.purpose) === kind && (d.project ?? set.get(key)?.project) === name };
|
|
2718
|
+
});
|
|
2719
|
+
const changed = rows.filter((r) => !r.same);
|
|
2720
|
+
for (const r of changed)
|
|
2721
|
+
appendKind(file, r.key, r.kind, r.project, by);
|
|
2722
|
+
return toon({ board: tilde(file), by, changed: changed.length, unchanged: rows.length - changed.length, reports: changed.map(({ key, kind, project }) => ({ key, kind, project })) }, [changed.length ? 'The board shows each kind and project on its row and head now' : 'Every report named already had that kind and project']);
|
|
2723
|
+
}
|
|
2646
2724
|
/** `notify check|test`: whether device notifications can work from this setup, and one real test notice. Never prints the token. */
|
|
2647
2725
|
async function notify(args) {
|
|
2648
2726
|
const mode = ['check', 'test'].includes(args[0]) ? args.shift() : 'check';
|
|
@@ -3173,13 +3251,13 @@ const state = () => {
|
|
|
3173
3251
|
const rounds = readRounds(file, warn).map(({ comments: _, ...r }) => r);
|
|
3174
3252
|
// The board file is read once for the whole state: it can hold thousands of items.
|
|
3175
3253
|
const { extra } = readItems(file), fed = extra.readonly === true || extra.answers_only === true, items = loadDecisions(file);
|
|
3176
|
-
const lines = items.some((d) => d.type === 'design') ? answerLines(file) : [];
|
|
3254
|
+
const lines = items.some((d) => d.type === 'design') ? answerLines(file) : [], kinds = readKinds(file, warn);
|
|
3177
3255
|
return {
|
|
3178
3256
|
file,
|
|
3179
3257
|
mockBase: `/mock/${loopback(bind) ? 'local' : mockPass(tokenOf(file))}/`,
|
|
3180
3258
|
// Who filed an item stays in the file, except as `by`, the name a card's thread says posted it and took its answer: never a bare session id, which names nobody.
|
|
3181
3259
|
// A sign-in's start command stays on this host too: the page only learns that a tap makes a new link, and how the last run went.
|
|
3182
|
-
decisions: items.filter((d) => !fakeEvidence(d)).map(({ filer, start, ...d }) => ({ ...d, ...(filer && !SESSION_ID.test(filer) && { by: filer }), ...(plainAsk(d) && guardOf(d) && { guard: guardOf(d) }), ...(d.type === 'design' && answers[d.key]?.answer === 'approve' && { status: 'approved' }), ...(d.type === 'design' && designState(d, answers[d.key], lines)), ...(start && { start: true, run: signinState(d.key) }), evidenceCard: d.evidence ? evidenceCard(d.evidence) : null,
|
|
3260
|
+
decisions: items.filter((d) => !fakeEvidence(d)).map(({ filer, start, ...d }) => ({ ...d, ...(d.kind === 'report' && kinds.has(d.key) && { purpose: d.purpose ?? kinds.get(d.key).purpose, project: d.project ?? kinds.get(d.key).project, kind_by: kinds.get(d.key).by }), ...(filer && !SESSION_ID.test(filer) && { by: filer }), ...(plainAsk(d) && guardOf(d) && { guard: guardOf(d) }), ...(d.type === 'design' && answers[d.key]?.answer === 'approve' && { status: 'approved' }), ...(d.type === 'design' && designState(d, answers[d.key], lines)), ...(start && { start: true, run: signinState(d.key) }), evidenceCard: d.evidence ? evidenceCard(d.evidence) : null,
|
|
3183
3261
|
...(d.mocks && { mocks: d.mocks.map((m) => ({ ...m, hash: mockHash(m.path) })) }) })),
|
|
3184
3262
|
readonly: extra.readonly === true,
|
|
3185
3263
|
answers_only: extra.answers_only === true,
|
|
@@ -3369,7 +3447,7 @@ async function route(req) {
|
|
|
3369
3447
|
if (!mock || !type)
|
|
3370
3448
|
return [404, { error: 'Mock is unavailable' }];
|
|
3371
3449
|
const actual = under(dirname(under(dirname(file), mockFile(mock))), path);
|
|
3372
|
-
if (statSync(actual).size > MOCK_BYTES)
|
|
3450
|
+
if (statSync(actual).size > (type.startsWith('video/') && frame ? MEDIA_BYTES : MOCK_BYTES))
|
|
3373
3451
|
return [404, { error: 'Mock is unavailable' }];
|
|
3374
3452
|
const bytes = readFileSync(actual);
|
|
3375
3453
|
// A diagram is its one pinned page: the file the plan posted, or nothing. A plan posted before the pin carries none and is served as it was.
|