rcf-lite 0.32.4 → 0.33.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/fixtures/canary-manifest.json +9 -9
  3. package/guidance/discovery/README.md +22 -0
  4. package/guidance/discovery/discovery-viewer-command-page-1-layout.html +191 -0
  5. package/guidance/discovery/discovery-viewer-command-page-2-coverage.html +193 -0
  6. package/guidance/discovery/discovery-viewer-command-page-3-trace-matrix.html +195 -0
  7. package/guidance/discovery/discovery-viewer-command-page-4-tables.html +196 -0
  8. package/guidance/discovery/discovery-viewer-command-page-5-subtabs.html +224 -0
  9. package/guidance/discovery/discovery-viewer-command-page-6-one-page.html +280 -0
  10. package/guidance/discovery/discovery-viewer-command-page.md +301 -0
  11. package/package.json +1 -1
  12. package/rcf/adrs/adr-4139-readiness-tables-read-only-no-commands.json +30 -0
  13. package/rcf/adrs/adr-4140-view-query-routes-per-pivot-matrix-no-mermaid.json +30 -0
  14. package/rcf/adrs/adr-4141-verify-modes.json +25 -0
  15. package/rcf/code-nodes/cn-771.json +3 -2
  16. package/rcf/code-nodes/cn-779.json +19 -0
  17. package/rcf/code-nodes/cn-780.json +14 -0
  18. package/rcf/code-nodes/cn-781.json +14 -0
  19. package/rcf/code-nodes/cn-782.json +15 -0
  20. package/rcf/code-nodes/cn-783.json +17 -0
  21. package/rcf/code-nodes/cn-784.json +20 -0
  22. package/rcf/code-nodes/cn-785.json +14 -0
  23. package/rcf/code-nodes/cn-786.json +14 -0
  24. package/rcf/code-nodes/cn-787.json +18 -0
  25. package/rcf/code-nodes/cn-788.json +14 -0
  26. package/rcf/code-nodes/cn-789.json +14 -0
  27. package/rcf/code-nodes/cn-790.json +15 -0
  28. package/rcf/code-nodes/cn-791.json +14 -0
  29. package/rcf/code-nodes/cn-792.json +17 -0
  30. package/rcf/code-nodes/cn-793.json +17 -0
  31. package/rcf/code-nodes/cn-794.json +18 -0
  32. package/rcf/code-nodes/cn-795.json +15 -0
  33. package/rcf/code-nodes/cn-796.json +14 -0
  34. package/rcf/code-nodes/cn-797.json +14 -0
  35. package/rcf/code-nodes/cn-798.json +14 -0
  36. package/rcf/define/brief-ledger.json +47 -0
  37. package/rcf/define/decisions-ledger.json +113 -0
  38. package/rcf/fbs/fbs-204.json +46 -0
  39. package/rcf/fbs/fbs-205.json +46 -0
  40. package/rcf/fbs/fbs-206.json +49 -0
  41. package/rcf/fbs/fbs-207.json +43 -0
  42. package/rcf/fbs/fbs-208.json +47 -0
  43. package/rcf/fbs/fbs-209.json +22 -0
  44. package/rcf/requirements/req-002.json +5 -2
  45. package/rcf/requirements/req-180.json +6 -3
  46. package/rcf/tacs/tac-4127-readiness-view.json +2 -2
  47. package/rcf/tacs/tac-4133-readiness-po-layer.json +2 -2
  48. package/rcf/tacs/tac-4135-readiness-command-page.json +94 -0
  49. package/rcf/tacs/tac-4136-view-query-routes.json +68 -0
  50. package/rcf/tacs/tac-4141-verify-modes.json +25 -0
  51. package/rcf/test-suites/ts-223.json +7 -7
  52. package/rcf/test-suites/ts-237.json +78 -0
  53. package/rcf/test-suites/ts-238.json +70 -0
  54. package/rcf/test-suites/ts-239.json +78 -0
  55. package/rcf/test-suites/ts-240.json +70 -0
  56. package/rcf/test-suites/ts-241.json +70 -0
  57. package/rcf/test-suites/ts-242.json +46 -0
  58. package/rcf/user-stories/us-16503.json +51 -0
  59. package/rcf/user-stories/us-18001.json +4 -4
  60. package/rcf/user-stories/us-18003.json +79 -0
  61. package/rcf/user-stories/us-18004.json +70 -0
  62. package/rcf/user-stories/us-18005.json +79 -0
  63. package/rcf/user-stories/us-208.json +74 -0
  64. package/rcf/user-stories/us-209.json +70 -0
  65. package/releases/releases.yaml +18 -1
  66. package/src/cli/finalise.js +141 -2
  67. package/src/cli/view.js +13 -1
  68. package/src/define/ledgers.js +9 -1
  69. package/src/finalise/spawn.js +2 -0
  70. package/src/server/index.js +104 -39
  71. package/src/server/routes.js +169 -0
  72. package/src/verify/cli/run.js +40 -0
  73. package/src/verify/engine/deterministic.js +150 -0
  74. package/src/verify/engine/index.js +27 -4
  75. package/src/view/doc-renderers/req.js +11 -9
  76. package/src/view/html-page.js +15 -11
  77. package/src/view/index.js +7 -1
  78. package/src/view/live-client.js +4 -0
  79. package/src/view/page-init.js +842 -1
  80. package/src/view/readiness/composite-id.js +35 -0
  81. package/src/view/readiness/coverage-summary.js +293 -0
  82. package/src/view/readiness/delta-counts.js +156 -0
  83. package/src/view/readiness/grid.js +175 -0
  84. package/src/view/readiness/po-layer.js +110 -220
  85. package/src/view/readiness/tables.js +500 -0
  86. package/src/view/readiness/thin-reqs.js +237 -0
  87. package/src/view/readiness/trace-matrix.js +392 -0
  88. package/src/view/readiness.js +359 -370
  89. package/src/view/style.css +594 -0
  90. package/src/watch/index.js +75 -17
@@ -8,8 +8,17 @@
8
8
  // D8: Node built-in `fs.watch` recursive - no `chokidar` dep. If dogfood
9
9
  // surfaces a failure mode the built-in cannot handle, the swap-in is
10
10
  // banked (same module interface, no caller changes).
11
+ //
12
+ // Issue #336 (reporter Dex, 2026-10-08): on Linux the recursive fs.watch
13
+ // reports the first rename over a file, then goes permanently blind to
14
+ // that path (next rename or write never fires). Editors that save by
15
+ // write-temp-then-rename (vim backupcopy=no, JetBrains safe-write) hit
16
+ // this on every save. Fix: on any `rename` event under a watched base,
17
+ // re-arm that base (close the stale handle, open a new one). The re-arm
18
+ // coalesces onto the debounce window, so a burst of renames yields one
19
+ // re-arm per flush. No dependency added.
11
20
 
12
- import { watch as fsWatch } from 'node:fs';
21
+ import { watch as defaultFsWatch } from 'node:fs';
13
22
  import { access, constants } from 'node:fs/promises';
14
23
  import { isAbsolute, join } from 'node:path';
15
24
 
@@ -31,6 +40,10 @@ const DEFAULT_DEBOUNCE_MS = 50;
31
40
  * globals. Exists as a determinism seam for tests: a manual timer lets a
32
41
  * test decide exactly when the debounce window closes instead of racing
33
42
  * real fs-event delivery latency. Production callers never pass this.
43
+ * @param {typeof defaultFsWatch} [args.fsWatchImpl] - injectable fs.watch
44
+ * implementation. Issue #336 seam: a test substitutes a fake that
45
+ * drives the Linux rename-blind delivery pattern deterministically
46
+ * on any host. Production callers never pass this.
34
47
  * @returns {{ close: () => void }}
35
48
  */
36
49
  export function watch({
@@ -41,6 +54,7 @@ export function watch({
41
54
  onError,
42
55
  filter,
43
56
  timers,
57
+ fsWatchImpl,
44
58
  } = {}) {
45
59
  if (!Array.isArray(paths) || paths.length === 0) {
46
60
  throw new TypeError('watch: paths must be a non-empty array of absolute paths');
@@ -50,7 +64,9 @@ export function watch({
50
64
  }
51
65
  const accept = typeof filter === 'function' ? filter : defaultFilter;
52
66
  const timerHost = { setTimeout, clearTimeout, ...timers };
53
- const watchers = [];
67
+ const fsWatchFn = typeof fsWatchImpl === 'function' ? fsWatchImpl : defaultFsWatch;
68
+ /** @type {{ base: string, handle: ReturnType<typeof defaultFsWatch> | null, needsRearm: boolean }[]} */
69
+ const baseWatchers = [];
54
70
  const pending = new Map();
55
71
  let timer = null;
56
72
  let closed = false;
@@ -61,6 +77,13 @@ export function watch({
61
77
  }
62
78
  }
63
79
 
80
+ function ensureFlushScheduled() {
81
+ if (closed) return;
82
+ if (timer) return;
83
+ timer = timerHost.setTimeout(flush, debounceMs);
84
+ if (timer && typeof timer.unref === 'function') timer.unref();
85
+ }
86
+
64
87
  function flush() {
65
88
  timer = null;
66
89
  if (closed) return;
@@ -73,6 +96,18 @@ export function watch({
73
96
  reportError(err);
74
97
  }
75
98
  }
99
+ // Issue #336: re-arm any base that saw a rename in this window.
100
+ // Closing and reopening the fs.watch handle re-registers inotify
101
+ // watches against current inodes, so a path that was replaced by
102
+ // rename starts firing again. One re-arm per flush collapses a
103
+ // burst of renames onto a single swap.
104
+ for (const rec of baseWatchers) {
105
+ if (!rec.needsRearm) continue;
106
+ rec.needsRearm = false;
107
+ if (closed) continue;
108
+ try { rec.handle?.close(); } catch { /* swallow */ }
109
+ rec.handle = openBaseHandle(rec.base);
110
+ }
76
111
  }
77
112
 
78
113
  function schedule(path, type) {
@@ -83,6 +118,16 @@ export function watch({
83
118
  if (timer && typeof timer.unref === 'function') timer.unref();
84
119
  }
85
120
 
121
+ function markRearm(base) {
122
+ const rec = baseWatchers.find((r) => r.base === base);
123
+ if (!rec) return;
124
+ rec.needsRearm = true;
125
+ // Even if no path survived the filter, a rename event means the
126
+ // handle may be about to go blind: make sure flush fires so the
127
+ // re-arm actually happens.
128
+ ensureFlushScheduled();
129
+ }
130
+
86
131
  async function classify(fullPath, eventType) {
87
132
  if (eventType === 'change') return 'change';
88
133
  try {
@@ -93,14 +138,16 @@ export function watch({
93
138
  }
94
139
  }
95
140
 
96
- for (const base of paths) {
97
- if (typeof base !== 'string' || !isAbsolute(base)) {
98
- throw new TypeError(`watch: paths must be absolute, got ${String(base)}`);
99
- }
141
+ function openBaseHandle(base) {
100
142
  let w;
101
143
  try {
102
- w = fsWatch(base, { recursive: true }, (eventType, filename) => {
144
+ w = fsWatchFn(base, { recursive: true }, (eventType, filename) => {
103
145
  if (closed) return;
146
+ // Issue #336: a rename anywhere under `base` can invalidate
147
+ // the handle's descriptors for the renamed path. Mark the base
148
+ // for re-arm regardless of whether the specific filename is
149
+ // one we would otherwise report (filtered paths still count).
150
+ if (eventType === 'rename') markRearm(base);
104
151
  if (!filename) return;
105
152
  const full = join(base, filename);
106
153
  if (!accept(full)) return;
@@ -112,14 +159,24 @@ export function watch({
112
159
  });
113
160
  } catch (err) {
114
161
  reportError(err);
115
- continue;
162
+ return null;
116
163
  }
117
- w.on('error', (err) => {
118
- // Silently drop ENOENT (watched path deleted mid-run); surface the rest.
119
- if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return;
120
- reportError(err);
121
- });
122
- watchers.push(w);
164
+ if (typeof w.on === 'function') {
165
+ w.on('error', (err) => {
166
+ // Silently drop ENOENT (watched path deleted mid-run); surface the rest.
167
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return;
168
+ reportError(err);
169
+ });
170
+ }
171
+ return w;
172
+ }
173
+
174
+ for (const base of paths) {
175
+ if (typeof base !== 'string' || !isAbsolute(base)) {
176
+ throw new TypeError(`watch: paths must be absolute, got ${String(base)}`);
177
+ }
178
+ const handle = openBaseHandle(base);
179
+ if (handle) baseWatchers.push({ base, handle, needsRearm: false });
123
180
  }
124
181
 
125
182
  function close() {
@@ -130,10 +187,11 @@ export function watch({
130
187
  timer = null;
131
188
  }
132
189
  pending.clear();
133
- for (const w of watchers) {
134
- try { w.close(); } catch { /* swallow */ }
190
+ for (const rec of baseWatchers) {
191
+ try { rec.handle?.close(); } catch { /* swallow */ }
192
+ rec.handle = null;
135
193
  }
136
- watchers.length = 0;
194
+ baseWatchers.length = 0;
137
195
  if (signal && typeof signal.removeEventListener === 'function') {
138
196
  try { signal.removeEventListener('abort', close); } catch { /* swallow */ }
139
197
  }