@troylar/flightdeck 2.12.2 → 2.13.0
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 +12 -0
- package/README.md +1 -1
- package/bin/flightdeck.js +122 -32
- package/dist/flightdeck.html +182 -36
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Generated by build.py from src/releases.json. Edit that file, not this one. -->
|
|
4
4
|
|
|
5
|
+
## 2.13.0 · Nov 2, 2026
|
|
6
|
+
|
|
7
|
+
Your AI assistant checks a draft exactly as Review & publish does.
|
|
8
|
+
|
|
9
|
+
### New
|
|
10
|
+
|
|
11
|
+
- **flightdeck draft check** (AI assistants). flightdeck draft check gives an assistant the verdict Review & publish gives a draft: what stops publishing, the tips, and what Publish would refuse, screenshots and version: pins included. It only reads. A part of the folder it couldn't read is named, and never becomes a finding. *Where:* docs/apps.md › draft check
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **An assistant's draft checks are complete** (AI assistants). draft read, save and propose load what Review & publish loads and give the same words. AGENTS.md and the review, new-checklist and edit-checklist skills say so and use draft check. Update with flightdeck install or Settings; Team notes stay as they are. *Where:* Settings › AI assistant files
|
|
16
|
+
|
|
5
17
|
## 2.12.2 · Nov 2, 2026
|
|
6
18
|
|
|
7
19
|
AGENTS.md says what an assistant's draft checks really cover.
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
27
|
1. `npx @troylar/flightdeck open` opens the app in your browser (needs Node 18 or later).
|
|
28
|
-
2. No Node? Save [flightdeck.html](https://unpkg.com/@troylar/flightdeck@2.
|
|
28
|
+
2. No Node? Save [flightdeck.html](https://unpkg.com/@troylar/flightdeck@2.13.0/dist/flightdeck.html) from the npm package as a file (right-click, **Save link as…**) and open that file in Edge or Chrome.
|
|
29
29
|
3. Choose **Look around the examples** to try it, or **Start your team** to set up your team's folder.
|
|
30
30
|
|
|
31
31
|
<p align="center"><a href="https://github.com/troylar/flightdeck/blob/main/docs/images/run-light.png" title="Open in GitHub's image viewer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/troylar/flightdeck/main/docs/images/run-dark.png"><img src="https://raw.githubusercontent.com/troylar/flightdeck/main/docs/images/run-light.png" alt="A release in progress on the run screen: 7/15 answered in the header with Details, Screen share, Receipt and More; a strip saying you're recording, with Pause and Hand off and the run's details; Phases with each gate's state; the checklist with Next up pinned at its top; and the selected step, whose text shows the run's tag (Start the production deploy job v2026.10.16), with the values to copy, Record the result, Done, Done differently and Blocked" width="1000"></picture></a></p>
|
package/bin/flightdeck.js
CHANGED
|
@@ -500,6 +500,9 @@ function loadEngine() {
|
|
|
500
500
|
read, and named in skipped. */
|
|
501
501
|
function readAdapter(root, opts) {
|
|
502
502
|
const NOFOLLOW = fs.constants.O_NOFOLLOW || 0, links = [], skipped = [], max = (opts && opts.maxBytes) || 0;
|
|
503
|
+
/* vanished: a file or folder that was there when its path was walked and gone when it was opened (a sync or another program
|
|
504
|
+
moved it in between). The read gives nothing, as for a file that isn't there, and the path is named. */
|
|
505
|
+
const vanished = [], gone = (rel) => { if (vanished.indexOf(rel) < 0) vanished.push(rel); };
|
|
503
506
|
function linkErr(rel) { const e = new Refused('refusing to read through a link: ' + rel); e.link = true; if (links.indexOf(rel) < 0) links.push(rel); return e; }
|
|
504
507
|
function skipErr(rel, bytes, cap, more) {
|
|
505
508
|
const e = new Refused(rel + ' is ' + (more ? 'more than ' : '') + size(bytes) + ', over the ' + size(cap) + ' this command reads');
|
|
@@ -562,7 +565,7 @@ function readAdapter(root, opts) {
|
|
|
562
565
|
const f = fileAt(p);
|
|
563
566
|
if (!f) return null;
|
|
564
567
|
let fd;
|
|
565
|
-
try { fd = fs.openSync(f.full, fs.constants.O_RDONLY | NOFOLLOW); } catch (e) { if (e.code === 'ELOOP') throw linkErr(p); if (e.code === 'ENOENT') return null; throw e; }
|
|
568
|
+
try { fd = fs.openSync(f.full, fs.constants.O_RDONLY | NOFOLLOW); } catch (e) { if (e.code === 'ELOOP') throw linkErr(p); if (e.code === 'ENOENT') { gone(p); return null; } throw e; }
|
|
566
569
|
try {
|
|
567
570
|
const st = fs.fstatSync(fd), now = lstatOrNull(path.join(sameWalk(f.a, f.ids, p), f.name));
|
|
568
571
|
if (!st.isFile() || st.ino !== f.st.ino || st.dev !== f.st.dev || !now || now.isSymbolicLink() || now.ino !== st.ino || now.dev !== st.dev) throw linkErr(p);
|
|
@@ -570,13 +573,13 @@ function readAdapter(root, opts) {
|
|
|
570
573
|
} catch (e) { fs.closeSync(fd); throw e; }
|
|
571
574
|
}
|
|
572
575
|
return {
|
|
573
|
-
links, skipped,
|
|
576
|
+
links, skipped, vanished,
|
|
574
577
|
list: (p) => new Promise((res) => {
|
|
575
578
|
const a = partsOf(p), dir = dirAt(a);
|
|
576
579
|
/* No such folder: empty, and marked missing, so a reader that saw it listed can tell gone from empty. */
|
|
577
580
|
if (!dir) { const none = []; none.missing = true; return res(none); }
|
|
578
581
|
let entries;
|
|
579
|
-
try { entries = fs.readdirSync(dir.cur, { withFileTypes: true }); } catch (e) { if (e.code !== 'ENOENT') throw e; const none = []; none.missing = true; return res(none); }
|
|
582
|
+
try { entries = fs.readdirSync(dir.cur, { withFileTypes: true }); } catch (e) { if (e.code !== 'ENOENT') throw e; gone(p || '.'); const none = []; none.missing = true; return res(none); }
|
|
580
583
|
sameWalk(a, dir.ids, p || '.');
|
|
581
584
|
res(entries.map((e) => {
|
|
582
585
|
const rel = (p ? p + '/' : '') + e.name;
|
|
@@ -807,7 +810,8 @@ function folderProblems(PF, d) {
|
|
|
807
810
|
}
|
|
808
811
|
function adapterProblems(adapter) {
|
|
809
812
|
return (adapter.links || []).map((p) => ({ path: p, kind: 'link', why: 'A link. Flightdeck never reads through links.' }))
|
|
810
|
-
.concat((adapter.skipped || []).map((x) => ({ path: x.path, kind: 'skipped', why: 'Too large to read here (' + (x.more ? 'more than ' : '') + size(x.size) + '; the limit is ' + size(x.cap) + ').' })))
|
|
813
|
+
.concat((adapter.skipped || []).map((x) => ({ path: x.path, kind: 'skipped', why: 'Too large to read here (' + (x.more ? 'more than ' : '') + size(x.size) + '; the limit is ' + size(x.cap) + ').' })))
|
|
814
|
+
.concat((adapter.vanished || []).map((p) => ({ path: p, kind: 'vanished', why: 'It was there a moment ago and was gone when it was read. Try again.' })));
|
|
811
815
|
}
|
|
812
816
|
|
|
813
817
|
/* What a whole-folder command's output depends on, as folders and files: its data, the publications that say whether a
|
|
@@ -1514,6 +1518,8 @@ async function draftState(eng, dir, proc, session) {
|
|
|
1514
1518
|
try {
|
|
1515
1519
|
const f = await rfs.read(dpath, { maxBytes: PF.util.DRAFT_BYTES });
|
|
1516
1520
|
if (f) Object.assign(draft, { exists: true, rev: sha(f.text), text: f.text });
|
|
1521
|
+
/* Gone between seeing it and opening it: there, but unread (named), so nothing takes it as absent. */
|
|
1522
|
+
else if ((ra.vanished || []).indexOf(dpath) >= 0) { draft.exists = true; L.note(dpath, 'vanished', 'It was there a moment ago and was gone when it was read. Try again.'); }
|
|
1517
1523
|
} catch (e) { draft.exists = true; }
|
|
1518
1524
|
const copies = [];
|
|
1519
1525
|
try {
|
|
@@ -1566,17 +1572,116 @@ function writeNew(dir, parts, text) {
|
|
|
1566
1572
|
if (!fs.readFileSync(target).equals(data)) fail(target + ' didn\'t read back as written');
|
|
1567
1573
|
return parts.join('/');
|
|
1568
1574
|
}
|
|
1575
|
+
/* Everything Review & publish checks a draft's text against, read now through one problem ledger over a read-only view of
|
|
1576
|
+
the folder, with the app's own readers: people.json, the team's step types, jira/workflows.json, every process's published
|
|
1577
|
+
versions (which are published, the children linked steps start, the parents that start this one), the team values a
|
|
1578
|
+
linked step names and the screenshots the text shows. Then PF.publish.checkContext and publishCheck, as the app's checks.
|
|
1579
|
+
What couldn't be read (or a newer Flightdeck wrote) is a problem, which makes the answer partial, and that part is left
|
|
1580
|
+
unchecked: it never becomes a blocker, a missing person or an unpublished process. Returns { verdict, problems }. */
|
|
1581
|
+
async function draftInputs(eng, dir, text) {
|
|
1582
|
+
const PF = eng.PF, ra = readAdapter(dir, { maxBytes: READ_MAX }), L = PF.repo.ledger(PF.repo.readOnly(ra, 'Checking a draft'));
|
|
1583
|
+
/* The ledger's problems and the adapter's (links, files too big, files that vanished), each once. */
|
|
1584
|
+
const everything = () => { const out = L.problems(), seen = {}; out.forEach((x) => { seen[x.path + '\u0000' + x.kind] = true; }); adapterProblems(ra).forEach((x) => { if (!seen[x.path + '\u0000' + x.kind]) out.push(x); }); return out; };
|
|
1585
|
+
/* A failure that stops the read at any stage (the team folder can't be listed) still names every problem met before it. */
|
|
1586
|
+
try { return await readInputs(eng, text, ra, L, everything); } catch (e) { if (e && typeof e === 'object') e.problems = everything(); throw e; }
|
|
1587
|
+
}
|
|
1588
|
+
async function readInputs(eng, text, ra, L, everything) {
|
|
1589
|
+
const PF = eng.PF, repo = PF.repo, M = PF.map, rfs = L.fs;
|
|
1590
|
+
const map = M.parse(text), failedAt = (p) => L.problems().some((x) => x.path === p || x.path.indexOf(p + '/') === 0);
|
|
1591
|
+
/* The app's own loader, limited to these parts: what each part's reader gives, or the part named in failed. */
|
|
1592
|
+
const deps = [PF.people.FILE, 'step-types', 'jira/workflows.json', 'publications'];
|
|
1593
|
+
const d = await repo.loadAll(dependsOn(rfs, deps), { readOnly: true, explain: true, deep: true });
|
|
1594
|
+
const failed = d.failed || {};
|
|
1595
|
+
/* First every read, with what each loader refused noted in the ledger; then the adapter's own problems (links, files too
|
|
1596
|
+
big); then PF.publish.unknownOf decides what's unknown from that whole list, and nothing else does. */
|
|
1597
|
+
const PARTS = { people: PF.people.FILE, 'step types': 'step-types', 'Jira workflows': 'jira/workflows.json', publications: 'publications' };
|
|
1598
|
+
Object.keys(PARTS).forEach((k) => { if (k in failed && !failedAt(PARTS[k])) L.note(PARTS[k], /newer/.test(failed[k]) ? 'newer' : 'unreadable', failed[k]); });
|
|
1599
|
+
const people = 'people' in failed ? null : d.people;
|
|
1600
|
+
if (people && people.text != null && people.doc == null) L.note(PF.people.FILE, 'malformed', PF.people.FILE + ' isn\'t valid JSON.');
|
|
1601
|
+
const types = 'step types' in failed ? {} : d.stepTypes.types;
|
|
1602
|
+
if (!('step types' in failed)) {
|
|
1603
|
+
(d.stepTypes.refused || []).forEach((r) => L.note(r.path, 'newer', r.why));
|
|
1604
|
+
}
|
|
1605
|
+
/* What a loader left out over its limit, from its own read (PF.publish.excludedOf): each named, and unknown below. */
|
|
1606
|
+
PF.publish.excludedOf({ stepTypes: 'step types' in failed ? null : d.stepTypes, jira: 'Jira workflows' in failed ? null : d.jira }).forEach((x) => L.note(x.path, x.kind, x.why));
|
|
1607
|
+
const processes = 'publications' in failed ? [] : d.processes.map((p) => {
|
|
1608
|
+
/* A version that read but can't be used is named as READ names one; a failed read the ledger already has. */
|
|
1609
|
+
p.pubs.filter((x) => !x.ok && !x._readFailed).forEach((x) => { const k = PF.appjson.pubProblem(x); L.note('publications/' + p.process + '/' + x.folder + '/', k.kind, k.why); });
|
|
1610
|
+
return { id: p.process, latest: p.index.latest, all: p.index.all, clashes: p.index.clashes, broken: p.pubs.filter((x) => !x.ok) };
|
|
1611
|
+
});
|
|
1612
|
+
/* Team values (2.9), read only for a text that passes one, as publishing reads them. */
|
|
1613
|
+
let teamValues;
|
|
1614
|
+
if (M.steps(map).some((x) => x.step.type === 'run.process' && PF.links.teams(x.step).length)) {
|
|
1615
|
+
teamValues = await repo.readTeamValues(rfs);
|
|
1616
|
+
if (teamValues.state !== 'ok' && teamValues.state !== 'missing' && !failedAt(PF.teamValues.FILE)) L.note(PF.teamValues.FILE, teamValues.state, teamValues.why || PF.teamValues.FILE + ' couldn\'t be read.');
|
|
1617
|
+
}
|
|
1618
|
+
/* Screenshots, read to the last byte as publishing reads them: one that isn't there is a blocker. */
|
|
1619
|
+
const shots = M.shotSources(map), missingShots = [];
|
|
1620
|
+
for (const src of shots) {
|
|
1621
|
+
try { const b = await rfs.blob(src); if (!b) missingShots.push(src); else await b.arrayBuffer(); } catch (e) { if (!failedAt(src)) L.note(src, 'unreadable', src + ' couldn\'t be read: ' + (e && e.message || String(e))); }
|
|
1622
|
+
}
|
|
1623
|
+
/* Only what the text uses counts: a problem with an input it doesn't use (a team-values.json link and no team: value)
|
|
1624
|
+
leaves the answer complete. */
|
|
1625
|
+
const used = PF.publish.usesOf(map), problems = everything().filter((x) => PF.publish.uses(used, x, shots));
|
|
1626
|
+
const ctx = PF.publish.checkContext({ people, types, workflows: d.jira ? d.jira.workflows : null, processes, teamValues, missingShots, unknown: PF.publish.unknownOf(problems, shots) });
|
|
1627
|
+
return { verdict: PF.publish.publishCheck(text, ctx), problems };
|
|
1628
|
+
}
|
|
1629
|
+
/* flightdeck draft check <team folder> <process> [--from <file | ->] --json (2.13): the verdict Review & publish gives the
|
|
1630
|
+
draft in maps/drafts/ (or the text given with --from), from the folder as it is now (draftInputs). Read-only; prints
|
|
1631
|
+
flightdeck-read/2 with SCHEMA2.draftCheck: exit 0, 4 when part of the folder couldn't be read, 5 when there's no draft,
|
|
1632
|
+
1 for a usage problem or a draft that can't be read. */
|
|
1633
|
+
async function draftCheck(argv) {
|
|
1634
|
+
const VAL = ['--from'], args = argv.filter((a, i) => !a.startsWith('--') && !(i > 0 && VAL.indexOf(argv[i - 1]) >= 0));
|
|
1635
|
+
let eng = null;
|
|
1636
|
+
const C2 = 2, appOf = () => (eng ? eng.version : versionOf(app()) || '');
|
|
1637
|
+
const failOut = (kind, why, problems, code) => { const F = eng ? eng.PF.appjson : null, json = F ? F.failure(appOf(), kind, why, problems || [], C2) : { contract: 'flightdeck-read/2', app: appOf(), error: { kind, why }, problems: [] }; return { out: JSON.stringify(json, null, 2), code: code || EXIT.error }; };
|
|
1638
|
+
const usage = (why) => failOut('usage', why);
|
|
1639
|
+
const fi = argv.indexOf('--from'), from = fi >= 0 ? argv[fi + 1] : null;
|
|
1640
|
+
if (fi >= 0 && (from == null || (from.startsWith('--') && from !== '-'))) return usage('say what goes after --from.');
|
|
1641
|
+
const unknown = argv.filter((a) => a.startsWith('--') && ['--json', '--from'].indexOf(a) < 0);
|
|
1642
|
+
if (unknown.length) return usage('draft check doesn\'t take ' + unknown.join(', ') + '.');
|
|
1643
|
+
if (!argv.includes('--json')) return usage('flightdeck draft prints JSON. Add --json.');
|
|
1644
|
+
const proc = args[3];
|
|
1645
|
+
if (!args[2] || !proc || args[4] || !PROC_ID.test(proc)) return usage('say the team folder and a process ID (lowercase letters, numbers and dashes), for example: flightdeck draft check <team folder> release --json');
|
|
1646
|
+
let dir;
|
|
1647
|
+
try { eng = loadEngine(); dir = folderArg(args[2], 'draft check'); } catch (e) { return failOut('failed', e.message); }
|
|
1648
|
+
const PF = eng.PF, A = PF.appjson;
|
|
1649
|
+
let text, said;
|
|
1650
|
+
if (from != null) {
|
|
1651
|
+
const input = inputText(PF, from, 'the text');
|
|
1652
|
+
if (input.why) return usage(input.why);
|
|
1653
|
+
if (PF.proposals.textProcess(input.text) !== proc) return usage('the text\'s process: line must say ' + proc + '.');
|
|
1654
|
+
text = input.text; said = { kind: 'file', ref: null };
|
|
1655
|
+
} else {
|
|
1656
|
+
const ra = readAdapter(dir, { maxBytes: READ_MAX }), L = PF.repo.ledger(PF.repo.readOnly(ra, 'Checking a draft')), dpath = 'maps/drafts/' + proc + '.md';
|
|
1657
|
+
let f = null;
|
|
1658
|
+
try { f = await L.fs.read(dpath, { maxBytes: PF.util.DRAFT_BYTES }); } catch (e) { return failOut('failed', dpath + ' couldn\'t be read, so nothing was checked. Try again.', L.problems().concat(adapterProblems(ra))); }
|
|
1659
|
+
/* A draft that was there a moment ago and gone when opened is unknown, never "no draft". */
|
|
1660
|
+
if (!f && (ra.vanished || []).indexOf(dpath) >= 0) return failOut('failed', dpath + ' was gone when it was read, so nothing was checked. Try again.', L.problems().concat(adapterProblems(ra)));
|
|
1661
|
+
if (!f) return failOut('not-found', 'There\'s no draft for "' + proc + '" in this folder (' + dpath + '). Pass the text to check with --from.', [], EXIT.notFound);
|
|
1662
|
+
text = f.text; said = { kind: 'draft', ref: dpath };
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
const got = await draftInputs(eng, dir, text);
|
|
1666
|
+
const data = { process: proc, from: { kind: said.kind, ref: said.ref, rev: PF.proposals.sha(text) }, checks: got.verdict.checks, publish: got.verdict.publish };
|
|
1667
|
+
const e = A.envelope(eng.version, new Date().toISOString(), got.problems, data, C2);
|
|
1668
|
+
return { out: JSON.stringify(e, null, 2), code: e.complete ? 0 : EXIT.partial };
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
return failOut('failed', e instanceof Refused ? e.message : 'The team folder couldn\'t be read: ' + (e && e.message || String(e)), e && e.problems);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1569
1673
|
async function draftMain(argv) {
|
|
1570
1674
|
const VAL = ['--session', '--expect', '--from', '--base', '--label', '--by', '--note'], val = {};
|
|
1571
1675
|
const args = argv.filter((a, i) => !a.startsWith('--') && !(i > 0 && VAL.indexOf(argv[i - 1]) >= 0));
|
|
1572
1676
|
const sub = args[1], usage = (why) => draftFail(null, 'usage', why);
|
|
1677
|
+
if (sub === 'check') return draftCheck(argv);
|
|
1573
1678
|
for (const f of VAL) {
|
|
1574
1679
|
const i = argv.indexOf(f);
|
|
1575
1680
|
if (i < 0) continue;
|
|
1576
1681
|
if (argv[i + 1] == null || (argv[i + 1].startsWith('--') && argv[i + 1] !== '-')) return usage('say what goes after ' + f + '.');
|
|
1577
1682
|
val[f] = argv[i + 1];
|
|
1578
1683
|
}
|
|
1579
|
-
if (['read', 'save', 'propose'].indexOf(sub) < 0) return usage('say read, save or
|
|
1684
|
+
if (['read', 'save', 'propose'].indexOf(sub) < 0) return usage('say read, save, propose or check, for example: flightdeck draft read <team folder> <process> --json');
|
|
1580
1685
|
if (!argv.includes('--json')) return usage('flightdeck draft prints JSON. Add --json.');
|
|
1581
1686
|
const proc = args[3];
|
|
1582
1687
|
if (!proc || !PROC_ID.test(proc)) return usage('a process ID is lowercase letters, numbers and dashes, for example: flightdeck draft ' + sub + ' <team folder> release --json');
|
|
@@ -1585,30 +1690,8 @@ async function draftMain(argv) {
|
|
|
1585
1690
|
try { eng = loadEngine(); dir = folderArg(args[2], 'draft ' + sub); } catch (e) { return draftFail(eng, 'failed', e.message); }
|
|
1586
1691
|
const PF = eng.PF, PP = PF.proposals, U = PF.util, M = PF.map, now = Date.now();
|
|
1587
1692
|
const session = val['--session'] || 's-' + U.randomId(12);
|
|
1588
|
-
/* The same checks the app shows,
|
|
1589
|
-
|
|
1590
|
-
const checksOf = async (t) => {
|
|
1591
|
-
const map = M.parse(t), procs = {}, names = [];
|
|
1592
|
-
M.steps(map).forEach((x) => { const f = x.step.fields || {}; if (x.step.type === 'run.process' && f.process && names.indexOf(f.process) < 0) names.push(f.process); });
|
|
1593
|
-
/* The children are read through a problem ledger of their own, so a child that couldn't be read makes the answer partial. */
|
|
1594
|
-
const L = PF.repo.ledger(readAdapter(dir, { maxBytes: READ_MAX })), rfs = L.fs;
|
|
1595
|
-
for (const n of names) {
|
|
1596
|
-
try {
|
|
1597
|
-
const p = await PF.repo.readProcess(rfs, n);
|
|
1598
|
-
if (p) procs[n] = p;
|
|
1599
|
-
/* A version that read but can't be used is named as READ names one; a failed read the ledger already has. */
|
|
1600
|
-
(p ? p.broken : []).forEach((x) => { if (!x._readFailed) { const k = PF.appjson.pubProblem(x); L.note('publications/' + n + '/' + x.folder + '/', k.kind, k.why); } });
|
|
1601
|
-
} catch (e) { procs[n] = { latest: null, all: [], clashes: [], broken: [], unlisted: 'publications/' + n + '/ couldn\'t be read' }; }
|
|
1602
|
-
}
|
|
1603
|
-
/* Team values (2.9), through the same ledger: a failed read is noted by the ledger; damaged or newer content here. */
|
|
1604
|
-
let tv;
|
|
1605
|
-
if (M.steps(map).some((x) => x.step.type === 'run.process' && PF.links.teams(x.step).length)) {
|
|
1606
|
-
tv = await PF.repo.readTeamValues(rfs);
|
|
1607
|
-
if (tv.state === 'malformed' || tv.state === 'newer') L.note(PF.teamValues.FILE, tv.state, tv.why);
|
|
1608
|
-
}
|
|
1609
|
-
const c = M.check(map, { people: [], types: {}, workflows: {}, processes: [], children: PF.links.childrenFrom(procs), teamValues: tv });
|
|
1610
|
-
return { checks: { blockers: c.blockers, suggestions: c.suggestions }, problems: L.problems() };
|
|
1611
|
-
};
|
|
1693
|
+
/* The same checks the app shows, on the folder as it is now (draftInputs), through the same functions. */
|
|
1694
|
+
const checksOf = async (t) => { const got = await draftInputs(eng, dir, t); return { checks: got.verdict.checks, publish: got.verdict.publish, problems: got.problems }; };
|
|
1612
1695
|
/* Every problem once: the draft's reads, then the children's. */
|
|
1613
1696
|
const merged = (a, b) => { const seen = {}, out = []; a.concat(b).forEach((p) => { const k = p.path + '\u0000' + p.kind; if (!seen[k]) { seen[k] = true; out.push(p); } }); return out; };
|
|
1614
1697
|
try {
|
|
@@ -1620,7 +1703,9 @@ async function draftMain(argv) {
|
|
|
1620
1703
|
const cf = pending ? { rev: pending.rev, kind: 'proposal', ref: pending.id, text: pending.text }
|
|
1621
1704
|
: s.draft.exists && s.draft.text != null ? { rev: s.draft.rev, kind: 'draft', ref: s.draft.path, text: s.draft.text }
|
|
1622
1705
|
: latest ? { rev: latest.rev, kind: 'version', ref: latest.folder, text: latest.text } : { rev: PP.sha(''), kind: 'none', ref: null, text: '' };
|
|
1623
|
-
|
|
1706
|
+
/* The draft is there but couldn't be read, and nothing else continues it: its text is unknown, so nothing is checked. */
|
|
1707
|
+
const unread = !pending && s.draft.exists && s.draft.text == null;
|
|
1708
|
+
const ck = unread ? { checks: { blockers: [], suggestions: [] }, problems: [] } : await checksOf(cf.text), problems = merged(s.problems, ck.problems);
|
|
1624
1709
|
const out = draftJson(eng, { process: proc, session, draft: s.draft, openIn: s.presence.openIn, mine: s.mine.map((m) => ({ id: m.id, rev: m.rev, status: m.status, text: m.text })),
|
|
1625
1710
|
continueFrom: cf, checks: ck.checks, copies: s.copies, readAt: new Date(now).toISOString(), complete: problems.length === 0, problems });
|
|
1626
1711
|
return { out: JSON.stringify(out, null, 2), code: out.complete ? 0 : EXIT.partial };
|
|
@@ -1641,7 +1726,9 @@ async function draftMain(argv) {
|
|
|
1641
1726
|
const base = PP.baseFor({ expect: val['--expect'], draft: s.draft, mine: s.mine, versions: s.versions, baseText, afterEarlier: argv.includes('--after-earlier') });
|
|
1642
1727
|
if (base.missing) return draftFail(eng, 'missing-base', 'The text you started from isn\'t the draft, one of this session\'s proposals or a published version any more. Pass the text you read with --base, or read the draft again.');
|
|
1643
1728
|
const operation = PP.newId(now, U.randomId(10)), at = new Date(now).toISOString();
|
|
1644
|
-
const result = { process: proc, session, operation, outcome: null, why: null, link: base.link, rev: PP.sha(text), file: null, ids: PP.ids(base.text, text), checks:
|
|
1729
|
+
const result = { process: proc, session, operation, outcome: null, why: null, link: base.link, rev: PP.sha(text), file: null, ids: PP.ids(base.text, text), checks: null, warnings: [], history: null, complete: true, problems: [] };
|
|
1730
|
+
const ck = await checksOf(text), problems = merged(s.problems, ck.problems);
|
|
1731
|
+
Object.assign(result, { checks: ck.checks, complete: problems.length === 0, problems });
|
|
1645
1732
|
if (result.ids.removed.length) result.warnings.push('This removes step or phase IDs: ' + result.ids.removed.join(', ') + '. Runs and skills that name them will need a look.');
|
|
1646
1733
|
let why = sub === 'propose' ? 'asked' : s.presence.open ? 'open' : null;
|
|
1647
1734
|
const draftNow = s.draft.exists ? s.draft.text : null, expectText = base.kind === 'none' || base.kind === 'version' ? null : base.text;
|
|
@@ -1684,7 +1771,7 @@ async function draftMain(argv) {
|
|
|
1684
1771
|
Object.assign(result, { outcome: 'proposed', why });
|
|
1685
1772
|
return { out: JSON.stringify(draftJson(eng, result), null, 2), code: 0 };
|
|
1686
1773
|
} catch (e) {
|
|
1687
|
-
return draftFail(eng, 'failed', e instanceof Refused ? e.message : 'The team folder couldn\'t be changed: ' + (e && e.message || String(e)));
|
|
1774
|
+
return draftFail(eng, 'failed', e instanceof Refused ? e.message : 'The team folder couldn\'t be changed: ' + (e && e.message || String(e)), EXIT.error, e && e.problems);
|
|
1688
1775
|
}
|
|
1689
1776
|
}
|
|
1690
1777
|
|
|
@@ -1735,6 +1822,9 @@ const HELP = `flightdeck: team process checklists, in one HTML file
|
|
|
1735
1822
|
read; otherwise write a proposal a person applies in Flightdeck
|
|
1736
1823
|
flightdeck draft propose <same as save>
|
|
1737
1824
|
always write a proposal
|
|
1825
|
+
flightdeck draft check <team folder> <process> [--from <file|->] --json
|
|
1826
|
+
the verdict Review & publish gives the draft (or the text in the file): what
|
|
1827
|
+
stops publishing and the tips, from the folder as it is now. Only reads
|
|
1738
1828
|
flightdeck skill register <team folder> --from <file|-> [--json]
|
|
1739
1829
|
for AI assistants: register a skill against the exact version it was built
|
|
1740
1830
|
from (Flightdeck 2.0). The only writer of skill-registrations/
|