mikser-io 11.10.4 → 11.10.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "11.10.4",
3
+ "version": "11.10.5",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
@@ -52,6 +52,12 @@ export const REASON = Object.freeze({
52
52
  RELOAD: 'reload',
53
53
  REBUILD_INTERRUPTED: 'rebuild-interrupted',
54
54
  OUTPUT_MISSING: 'output-missing',
55
+ // A catalog entity with a layout that has no snapshot at all — distinct
56
+ // from NEVER_RENDERED below, which is a gate reporting what it found. This
57
+ // one is an OVERRIDE: nothing would otherwise look at such an entity,
58
+ // because every other mechanism starts from a snapshot. Claimed once per
59
+ // entity per process; see claimUnrendered.
60
+ NEVER_RECORDED: 'never-recorded',
55
61
  // Evidence — each layer's business, named here so the vocabulary is
56
62
  // legible as a whole.
57
63
  UNCHANGED: 'unchanged',
@@ -117,6 +123,49 @@ export function missingOutputIds() {
117
123
 
118
124
  export function forgetMissingOutputs() {
119
125
  cachedMissingOutputs = null
126
+ // `unrenderedOnce` is NOT dropped here. It is per PROCESS, not per cycle:
127
+ // clearing it would recompute the set every cycle and hand back the same
128
+ // un-renderable entity every time, which is the churn the claim-once rule
129
+ // exists to prevent.
130
+ }
131
+
132
+ // Entities with a layout that have never recorded a snapshot.
133
+ //
134
+ // The gap `missingOutputIds` cannot see: it walks SNAPSHOTS, so an entity
135
+ // that never got one is invisible to it, and nothing else looks either — the
136
+ // source gate sees the file unchanged and never journals it again. That is
137
+ // the state a superseded DELETE left behind, and the reason it was permanent:
138
+ // a missing page, green builds forever, and only --audit-output to say so.
139
+ //
140
+ // ONE DISPATCH PER ENTITY, per process, which is the whole safety of it.
141
+ // Membership is CLAIMED rather than read: an id comes out once and is then
142
+ // gone from the set. An entity that legitimately cannot render — a layout
143
+ // that produces no destination is the real case, and `mikser_explain` names
144
+ // it — therefore costs one wasted dispatch rather than one per cycle for the
145
+ // life of the process. Re-dispatching such an entity forever is the failure
146
+ // this net could otherwise become, and it would be quiet: slower builds and
147
+ // nothing to point at.
148
+ //
149
+ // Computed lazily and only once, then dropped alongside the missing-output
150
+ // cache at the end of onFinalize.
151
+ let unrenderedOnce = null
152
+
153
+ export function claimUnrendered(id) {
154
+ if (id === undefined) return false
155
+ if (!unrenderedOnce) {
156
+ // Nothing has ever rendered, so nothing is missing: this is a cold
157
+ // build and every entity is about to be dispatched on its own merits.
158
+ // Without this the whole corpus answers `never-recorded` on a first
159
+ // build — true, useless, and it would take the real reason off every
160
+ // page in `--json`.
161
+ const recorded = runtime.manifest?.size?.() ?? 0
162
+ unrenderedOnce = recorded > 0
163
+ ? new Set(runtime.manifest?.unrenderedIds?.() ?? [])
164
+ : new Set()
165
+ }
166
+ if (!unrenderedOnce.has(id)) return false
167
+ unrenderedOnce.delete(id)
168
+ return true
120
169
  }
121
170
 
122
171
  // What overrides a gate's own evidence, or null when nothing does.
@@ -138,6 +187,10 @@ export function bypassReason({ reload = false, id } = {}) {
138
187
  // this module exists, and this is the first override added since.
139
188
  if (runtime.options?.cacheRebuildInterrupted) return REASON.REBUILD_INTERRUPTED
140
189
  if (id !== undefined && missingOutputIds().has(id)) return REASON.OUTPUT_MISSING
190
+ // Last, and after OUTPUT_MISSING on purpose: an entity with a snapshot
191
+ // whose file went missing is the better-understood case and keeps its own
192
+ // reason, which `--json` callers and the scenario suite already match on.
193
+ if (claimUnrendered(id)) return REASON.NEVER_RECORDED
141
194
  return null
142
195
  }
143
196
 
@@ -65,6 +65,10 @@ import { sha1, describeInputChange, buildRefClosure, buildSnapshot, rowToSnap, s
65
65
 
66
66
 
67
67
  export function createManifest(db) {
68
+ // See unrenderedIds(): prepared on first use, because it is the one
69
+ // statement that reaches outside the manifest's own tables.
70
+ let stmtUnrendered
71
+
68
72
  const {
69
73
  stmtCollisions,
70
74
  stmtClaimants,
@@ -504,6 +508,49 @@ export function createManifest(db) {
504
508
  return [...new Set(stmtAllFailures.all().map(row => row.id))]
505
509
  },
506
510
 
511
+ // Catalog entities that should have rendered and never did.
512
+ //
513
+ // The state a superseded DELETE used to leave behind: a catalog row
514
+ // with a layout, an output written and then unlinked, and NO snapshot
515
+ // — which made it invisible to missingOutputIds(), because that walks
516
+ // snapshots. The source gate saw the file unchanged, so nothing
517
+ // journalled it again either. Permanently missing page, green builds
518
+ // forever, and only --audit-output to say so.
519
+ //
520
+ // Both exclusions are load-bearing:
521
+ // - a snapshot means it HAS rendered, so there is nothing to
522
+ // recover;
523
+ // - a failure means the render was attempted and threw. A failed
524
+ // render writes no snapshot ON PURPOSE, so the last good bytes
525
+ // survive, and the retry path owns that case. Re-dispatching it
526
+ // here would fight the mechanism that exists for it.
527
+ //
528
+ // `meta_layout` is the column the layouts plugin mirrors its match
529
+ // into and carries a partial index, so this is an index scan that
530
+ // finds nothing in a healthy build.
531
+ //
532
+ // PREPARED LAZILY, and that is not a style choice: this is the only
533
+ // statement here that reaches outside the manifest's own tables, and
534
+ // a manifest is legitimately built against a database that holds no
535
+ // catalog — the unit suite does exactly that. Preparing it eagerly
536
+ // threw SQLITE_ERROR at construction and took twenty-one unrelated
537
+ // tests with it.
538
+ unrenderedIds() {
539
+ if (stmtUnrendered === undefined) {
540
+ try {
541
+ stmtUnrendered = db.prepare(`
542
+ SELECT e.id FROM mikser_entities e
543
+ LEFT JOIN mikser_snapshots s ON s.id = e.id
544
+ LEFT JOIN mikser_failures f ON f.id = e.id
545
+ WHERE e.meta_layout IS NOT NULL AND s.id IS NULL AND f.id IS NULL
546
+ `)
547
+ } catch {
548
+ stmtUnrendered = null // no catalog here; nothing to recover
549
+ }
550
+ }
551
+ return stmtUnrendered ? stmtUnrendered.all().map(row => row.id) : []
552
+ },
553
+
507
554
  allFailures() {
508
555
  return stmtAllFailures.all()
509
556
  },