@troylar/flightdeck 1.20.0 → 1.21.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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  <!-- Generated by build.py from src/releases.json. Edit that file, not this one. -->
4
4
 
5
+ ## 1.21.0 · Oct 20, 2026
6
+
7
+ Ready for teammates on Flightdeck 2.0.
8
+
9
+ ### Improved
10
+
11
+ - **Files made with Flightdeck 2.0 say so** (Versions and runs). Versions and runs made with Flightdeck 2.0 say "Needs Flightdeck 2.0". A test run from Flightdeck 2.0 says so too. This copy still shows them by name and never changes them. *Where:* Process pages, Runs and the run itself
12
+ - **See what a newer Flightdeck wrote, and who is on an older one** (Folder health). Folder health lists each file a newer Flightdeck wrote, by kind, with the version it needs. It also names older copies of Flightdeck that answered connection checks or edited drafts here in the last 7 days, and says to move every computer to this version before anyone uses Flightdeck 2.0 features. *Where:* Folder health
13
+
14
+ ### Fixed
15
+
16
+ - **Decisions from a newer Flightdeck aren't read as older ones** (Follow-ups and corrections). A decision from a newer Flightdeck is no longer read as if it were an older one. It's listed as needing a newer Flightdeck, and the follow-up or correction stays as it was.
17
+ - **A "format:" line no longer passes for a requirement** (Checklist editor). A checklist with a "format:" or "features:" line can't be published until the line is removed, and the editor says why. Nothing in a checklist needs one.
18
+ - **Plans from a newer Flightdeck are named, never started on the wrong version** (Plans). A planned run saved by Flightdeck 2.0 for one exact version isn't shown as a plan here, so it can't start on the latest by mistake. Folder health lists it as needing Flightdeck 2.0.
19
+
5
20
  ## 1.20.0 · Oct 20, 2026
6
21
 
7
22
  Let another app ask for a run.
package/bin/flightdeck.js CHANGED
@@ -219,6 +219,14 @@ function merged(item) {
219
219
  const old = region(item.now), fresh = region(item.text);
220
220
  return old.marked ? norm(item.now).slice(0, old.start) + fresh.text + norm(item.now).slice(old.end) : item.text;
221
221
  }
222
+ /* A record written by a newer Flightdeck (flightdeck-installed/2 on, or a must-understand feature): never read for meaning and
223
+ never rewritten. The same verdict as PF.contract.cleanInstalled (test/cli.test.js checks they agree). */
224
+ function installedNewer(r) {
225
+ if (!r || typeof r !== 'object' || Array.isArray(r)) return '';
226
+ const m = typeof r.format === 'string' ? /^([a-z][a-z0-9-]{1,59})\/([1-9]\d{0,3})$/.exec(r.format) : null;
227
+ if ((m && m[1] === 'flightdeck-installed' && m[2] !== '1') || (m && m[1] === 'flightdeck-installed' && (Array.isArray(r.features) ? r.features.length > 0 : r.features != null))) return 'was written by a newer Flightdeck' + (typeof r.version === 'string' && /^\d+\.\d+\.\d+$/.test(r.version) ? ' (' + r.version + ')' : '') + '.';
228
+ return '';
229
+ }
222
230
  /* The folder's record, checked field by field. Anyone can edit it, so a record that isn't exactly what this command writes is
223
231
  ignored (and said so), and only entries for files this version installs, with a well-formed hash, are read. */
224
232
  function readRecord(t, keys) {
@@ -226,6 +234,8 @@ function readRecord(t, keys) {
226
234
  if (text === null) return none;
227
235
  let r = null;
228
236
  try { r = JSON.parse(text); } catch (e) { /* reported below */ }
237
+ const newer = installedNewer(r);
238
+ if (newer) fail(path.join(t.dir, ...t.recordAt) + ' ' + newer + ' Nothing was changed. Use the newer Flightdeck package.');
229
239
  const ok = r && typeof r === 'object' && !Array.isArray(r) && r.format === 'flightdeck-installed/1' && typeof r.version === 'string' && /^(\d+\.\d+\.\d+|dev)$/.test(r.version)
230
240
  && typeof r.at === 'string' && (r.by === 'cli' || r.by === 'app') && r.files && typeof r.files === 'object' && !Array.isArray(r.files)
231
241
  && Object.keys(r.files).every((k) => typeof r.files[k] === 'string' && /^[0-9a-f]{64}$/.test(r.files[k]));
@@ -699,8 +709,8 @@ function folderProblems(PF, d) {
699
709
  (d.unreadable || []).forEach((u) => out.push({ path: u.path, kind: u.kind || 'unreadable', why: fileWhy(u.why) }));
700
710
  (d.nofile || []).forEach((n) => out.push({ path: n.path, kind: 'nofile', why: 'The run folder has no run.json.' }));
701
711
  (d.malformed || []).forEach((m) => out.push({ path: m.path, kind: m.kind || 'malformed', why: fileWhy(m.why) }));
702
- (d.fuUnread || []).forEach((u) => out.push({ path: 'followups/' + u.process + '/' + u._file, kind: 'malformed', why: 'This file couldn\'t be read, or isn\'t a follow-up decision this copy can read.' }));
703
- (d.corrUnread || []).forEach((u) => out.push({ path: u.dir + '/' + u.process + '/' + u._file, kind: 'malformed', why: 'This file couldn\'t be read, or isn\'t a decision this copy can read.' }));
712
+ (d.fuUnread || []).forEach((u) => out.push({ path: 'followups/' + u.process + '/' + u._file, kind: u.kind === 'newer' ? 'newer' : 'malformed', why: u.kind === 'newer' ? u.why : 'This file couldn\'t be read, or isn\'t a follow-up decision this copy can read.' }));
713
+ (d.corrUnread || []).forEach((u) => out.push({ path: u.dir + '/' + u.process + '/' + u._file, kind: u.kind === 'newer' ? 'newer' : 'malformed', why: u.kind === 'newer' ? u.why : 'This file couldn\'t be read, or isn\'t a decision this copy can read.' }));
704
714
  (d.processes || []).forEach((proc) => (proc.pubs || []).forEach((p) => { if (!p.ok && !p._readFailed) { const x = A.pubProblem(p); out.push({ path: 'publications/' + proc.process + '/' + p.folder + '/', kind: x.kind, why: x.why }); } }));
705
715
  (d.fuDecisions || []).forEach((x) => { const p = decisionDrift(x); if (p) out.push(p); });
706
716
  const pw = peopleWhy(d.people);
@@ -768,7 +778,7 @@ async function readData(eng, adapter, req) {
768
778
  const r = one.run, key = m[1] + '/' + m[2];
769
779
  if (one.nofile) L.note('runs/' + key + '/run.json', 'nofile', 'The run folder has no run.json.');
770
780
  take(one.bad.map((b) => ({ path: b.path, kind: b.kind || 'unreadable', why: fileWhy(b.why) })));
771
- take(one.fuUnread.map((u) => ({ path: u.path, kind: u.kind || 'malformed', why: fileWhy(u.why) })));
781
+ take(one.fuUnread.map((u) => ({ path: u.path, kind: u.kind || 'malformed', why: u.kind === 'newer' ? u.why : fileWhy(u.why) })));
772
782
  one.fuDecisions.forEach((x) => { const p = decisionDrift(x); if (p) take([p]); });
773
783
  if (one.peopleWhy) L.note('people.json', one.peopleKind || 'unreadable', fileWhy(one.peopleWhy));
774
784
  const pw = peopleWhy(one.people);
@@ -1465,4 +1475,4 @@ if (require.main === module) {
1465
1475
  if (r && typeof r.then === 'function') r.then(done, failed); else done(r);
1466
1476
  } catch (e) { failed(e); }
1467
1477
  }
1468
- module.exports = { _hooks, main, versionOf, cmp, skillFiles, norm, region, managed, merged, hash, baselineKey, examples, readAdapter, loadEngine, readData, requestData, writeRequest, EXIT };
1478
+ module.exports = { _hooks, main, installedNewer, versionOf, cmp, skillFiles, norm, region, managed, merged, hash, baselineKey, examples, readAdapter, loadEngine, readData, requestData, writeRequest, EXIT };