mikser-io 10.0.4 → 10.1.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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { default as runtime } from './src/runtime.js'
2
2
  export * as constants from './src/constants.js'
3
3
  export * from './src/utils.js'
4
+ export * from './src/invalidation.js'
4
5
  export * from './src/auth.js'
5
6
  export * from './src/roles.js'
6
7
  export * from './src/inventory.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "10.0.4",
3
+ "version": "10.1.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/explain.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // Follows --audit-output's shape: report and exit, no build phases run.
11
11
  import { existsSync } from 'node:fs'
12
12
  import { inputHashOf, inputPartsOf, diffInputParts, lookupKeys, checksum as fileChecksum } from './utils.js'
13
- import { resolveOutputPath } from './manifest.js'
13
+ import { outputMissing } from './invalidation.js'
14
14
  import { filterKey } from './track.js'
15
15
  import { findEntity, findEntities, findById } from './catalog.js'
16
16
  import runtime from './runtime.js'
@@ -141,7 +141,7 @@ export async function explain(reference) {
141
141
  // attempt — if the last render threw, that is the more useful sentence,
142
142
  // and it explains the absence too.
143
143
  const missingDestinations = snapshots
144
- .filter(snap => snap.destination && !existsSync(resolveOutputPath(snap.destination)))
144
+ .filter(snap => outputMissing(snap.destination))
145
145
  .map(snap => snap.destination)
146
146
 
147
147
  // The catalog is as of the LAST BUILD. If the file has been edited since,
@@ -244,9 +244,7 @@ export async function explain(reference) {
244
244
  // re-renders. --explain contradicting the build is worse than
245
245
  // --explain being incomplete, because it is the tool someone
246
246
  // reaches for when the build has already surprised them.
247
- missing: snap.destination
248
- ? !existsSync(resolveOutputPath(snap.destination))
249
- : false,
247
+ missing: outputMissing(snap.destination),
250
248
  outputHash: snap.outputHash ?? null,
251
249
  parent: snap.parent ?? null,
252
250
  // Which keys of its OWN meta the render read, and which keys it
@@ -0,0 +1,158 @@
1
+ // Should this work be redone?
2
+ //
3
+ // One question, asked at four layers with different evidence in hand:
4
+ //
5
+ // source.js a file's checksum against the catalog's
6
+ // manifest a recorded hash, for the layouts dispatcher's seeding
7
+ // manifest a snapshot plus its refClosure, at the render gate
8
+ // assets a `.md5` marker at the current preset revision
9
+ //
10
+ // The evidence differs and belongs to each layer. What does NOT differ is
11
+ // what OVERRIDES the evidence — and that is what this module owns, because
12
+ // it was written four times and drifted the moment a fifth override was
13
+ // added to one of them.
14
+ //
15
+ // The drift, concretely: `rm -rf out` changes no input, so every layer
16
+ // answered "unchanged" and a build rendered nothing, printed no `Rendered:`
17
+ // line and exited 0 over an empty output folder. Adding an output-existence
18
+ // check to the render gate fixed nothing, because the source gate had
19
+ // already dropped the entity; adding it there fixed the site but not image
20
+ // derivatives, because files.js held a private copy of that gate; and fixing
21
+ // that still left the assets marker, which had never asked about the file at
22
+ // all. Five gates, one question, four implementations of the override rules.
23
+ //
24
+ // So the rule is: a layer may own what it KNOWS. It may not own what
25
+ // overrides it. Anything that makes a gate not apply — --force, a wiped
26
+ // cache, a reload event, an output that is gone — is declared here once, and
27
+ // a new one added here reaches every gate that ever asks.
28
+ //
29
+ // Deliberately NOT moved here: the dependency-graph walk in
30
+ // manifest.skipDecision. That reasons over snapshots, refClosures and sift
31
+ // filters the manifest owns, and hauling it out would trade a real coupling
32
+ // for a fake one. It asks this module what overrides its answer, which is the
33
+ // part that was duplicated.
34
+ //
35
+ // A leaf module: runtime and node builtins only. It reads `runtime.manifest`
36
+ // lazily rather than importing it, the way every other consumer of engine
37
+ // state does, so the manifest can import this without a cycle.
38
+
39
+ import path from 'node:path'
40
+ import { existsSync } from 'node:fs'
41
+ import runtime from './runtime.js'
42
+
43
+ // The vocabulary, in one place because it is asserted against.
44
+ //
45
+ // `--json` carries these strings out to callers and the scenario suite
46
+ // matches on them, so they are API. Defined here rather than in the gate
47
+ // that happens to emit each one, so the set can be read as a set.
48
+ export const REASON = Object.freeze({
49
+ // Overrides — this module's business.
50
+ FORCE: 'force',
51
+ CACHE_INVALIDATED: 'cache-invalidated',
52
+ RELOAD: 'reload',
53
+ OUTPUT_MISSING: 'output-missing',
54
+ // Evidence — each layer's business, named here so the vocabulary is
55
+ // legible as a whole.
56
+ UNCHANGED: 'unchanged',
57
+ NEVER_RENDERED: 'never-rendered',
58
+ INPUTS_CHANGED: 'inputs-changed',
59
+ REF_CHANGED: 'ref-changed',
60
+ QUERY_MATCHED: 'query-matched',
61
+ CACHE_DISABLED: 'cache-disabled',
62
+ RETRY_FAILED: 'retry-failed',
63
+ })
64
+
65
+ // Where an output lives, given what a snapshot recorded.
66
+ //
67
+ // Destinations come in two shapes and both are legitimate: output-relative
68
+ // for a rendered page (`/index.html`), absolute for a derivative the assets
69
+ // plugin places itself. Resolving relative-first and falling back to the
70
+ // literal path covers both without the caller having to know which it holds.
71
+ //
72
+ // Returns the joined path even when nothing exists there, so a caller can
73
+ // report WHICH path it looked for — "missing" without a path is a fact
74
+ // nobody can act on.
75
+ export function resolveOutputPath(destination, outputFolder = runtime.options?.outputFolder) {
76
+ if (!destination) return undefined
77
+ const joined = path.join(outputFolder ?? '', destination)
78
+ if (existsSync(joined)) return joined
79
+ if (path.isAbsolute(destination) && existsSync(destination)) return destination
80
+ return joined
81
+ }
82
+
83
+ // Is the file this destination names gone?
84
+ export function outputMissing(destination) {
85
+ if (!destination) return false
86
+ return !existsSync(resolveOutputPath(destination))
87
+ }
88
+
89
+ // Memoized for the cycle.
90
+ //
91
+ // Every gate asks, and they all ask before anything has rendered, so one walk
92
+ // answers all of them. Dropped at the end of onFinalize, where this cycle's
93
+ // renders are recorded and the answer stops being true — not at the start of
94
+ // the next cycle, because in watch mode there may not be one for hours and a
95
+ // stale set held that long would re-dispatch entities it saw as missing.
96
+ let cachedMissingOutputs = null
97
+
98
+ // The entity ids whose last render wrote a file that is no longer on disk.
99
+ //
100
+ // One stat per recorded snapshot. The syscalls are ~2.2ms per 1842 paths and
101
+ // cost the same whether the files are there or not; at the build level it does
102
+ // not register — warm rebuilds of the 10k perf corpus ran 3.36s median with
103
+ // this against 3.56s without, i.e. nominally faster, which is only to say the
104
+ // difference is inside the run-to-run spread. Re-measure with
105
+ // `npm run test:perf` before trusting a claim that it got slower.
106
+ export function missingOutputIds() {
107
+ if (cachedMissingOutputs) return cachedMissingOutputs
108
+ const missing = new Set()
109
+ for (const snapshot of runtime.manifest?.all?.() ?? []) {
110
+ if (!snapshot.destination) continue
111
+ if (outputMissing(snapshot.destination)) missing.add(snapshot.id)
112
+ }
113
+ cachedMissingOutputs = missing
114
+ return missing
115
+ }
116
+
117
+ export function forgetMissingOutputs() {
118
+ cachedMissingOutputs = null
119
+ }
120
+
121
+ // What overrides a gate's own evidence, or null when nothing does.
122
+ //
123
+ // Callers pass the evidence they hold: `reload` if they are a watch event,
124
+ // `id` if they are gating an entity whose outputs are recorded. Passing
125
+ // neither asks only the universal question, which is what the render gate
126
+ // wants — it checks the output separately and later, so that an entity whose
127
+ // inputs ALSO moved reports `inputs-changed` rather than losing that detail
128
+ // to a broader answer.
129
+ export function bypassReason({ reload = false, id } = {}) {
130
+ if (reload) return REASON.RELOAD
131
+ if (runtime.options?.force) return REASON.FORCE
132
+ if (runtime.catalog?.cacheInvalidated) return REASON.CACHE_INVALIDATED
133
+ if (id !== undefined && missingOutputIds().has(id)) return REASON.OUTPUT_MISSING
134
+ return null
135
+ }
136
+
137
+ // Is every entity being presented for evaluation this cycle?
138
+ //
139
+ // The same fact as bypassReason, asked for a different purpose: a "declared X
140
+ // matched nothing" warning is only worth printing when everything was looked
141
+ // at. On an incremental cycle a pattern legitimately matches nothing in a run
142
+ // of two, and a warning that always fires is one people filter out — taking
143
+ // the real instance with it.
144
+ //
145
+ // `firstRun` counts here and not in bypassReason because there is nothing to
146
+ // bypass on a first run: no prior checksum, no snapshot, so every gate opens
147
+ // on its own evidence.
148
+ //
149
+ // Takes the runtime rather than reading the singleton, so a plugin passes the
150
+ // one it was injected with — the singleton in a real build, the harness's
151
+ // stand-in under test.
152
+ export function isFullCycle(engineRuntime = runtime) {
153
+ return !!(
154
+ engineRuntime?.options?.force
155
+ || engineRuntime?.options?.firstRun
156
+ || engineRuntime?.catalog?.cacheInvalidated
157
+ )
158
+ }
package/src/manifest.js CHANGED
@@ -60,6 +60,9 @@ import { extractRefs, inputHashOf, inputPartsOf, diffInputParts, lookupKeys } fr
60
60
  import { filterKey } from './track.js'
61
61
  import { findById, findEntities } from './catalog.js'
62
62
  import { useDatabase, registerSchema } from './database/index.js'
63
+ import {
64
+ REASON, bypassReason, resolveOutputPath, outputMissing, missingOutputIds, forgetMissingOutputs,
65
+ } from './invalidation.js'
63
66
 
64
67
  // Schema registration. Applied at db.open(). PRIMARY KEY (id,
65
68
  // destination) — leading id column means `WHERE id = ?` queries use the
@@ -284,13 +287,6 @@ function snapToRow(snap) {
284
287
  // becomes two symptoms — one loud (every asset reported missing, at its
285
288
  // real and present path) and one silent (no outputHash recorded, leaving
286
289
  // most snapshots presence-checked only).
287
- export function resolveOutputPath(destination, outputFolder = runtime.options?.outputFolder) {
288
- if (!destination) return undefined
289
- const joined = path.join(outputFolder ?? '', destination)
290
- if (existsSync(joined)) return joined
291
- if (path.isAbsolute(destination) && existsSync(destination)) return destination
292
- return joined
293
- }
294
290
 
295
291
  async function hashOutputFile(destination) {
296
292
  const filePath = resolveOutputPath(destination)
@@ -372,11 +368,6 @@ export async function sourcesOf(destination) {
372
368
  export function createManifest(db) {
373
369
  if (!db) throw new Error('createManifest: db is required')
374
370
 
375
- // Per-instance, not module-level: tests build their own manifest via
376
- // createManifest(db), and a shared memo would leak one test's output
377
- // folder into the next.
378
- let cachedMissingOutputs = null
379
-
380
371
  const stmtCollisions = db.prepare(`
381
372
  SELECT destination, count(*) AS n, group_concat(id) AS ids
382
373
  FROM mikser_snapshots
@@ -682,8 +673,12 @@ export function createManifest(db) {
682
673
  // That is the situation --force exists for — the invalidation
683
674
  // graph being under suspicion — including where the preset
684
675
  // no-match warning tells the operator to use it.
685
- if (runtime.options?.force) return { skip: false, reason: 'force' }
686
- if (entity?.meta?.cache === false) return { skip: false, reason: 'cache-disabled' }
676
+ // --force and a wiped cache both mean "ignore what you think you
677
+ // know", and both are declared in invalidation.js so a third one
678
+ // added there reaches this gate without being remembered.
679
+ const override = bypassReason()
680
+ if (override) return { skip: false, reason: override }
681
+ if (entity?.meta?.cache === false) return { skip: false, reason: REASON.CACHE_DISABLED }
687
682
  // A render whose last attempt threw must be retried, and checked
688
683
  // before anything else: every other branch reasons about hashes,
689
684
  // and the hashes are consistent — the entity did not change, the
@@ -737,15 +732,13 @@ export function createManifest(db) {
737
732
  // rather than in the render loop because this is the function that
738
733
  // owns the word: anything asking shouldSkip() gets the same answer.
739
734
  //
740
- // Checked unconditionally rather than behind a flag. It is one stat
741
- // per entity that reached this point 2.2ms for 1842 entities,
742
- // the same whether the files are there or not against a failure
743
- // mode whose whole character is that nobody thinks to look for it.
744
- // auditOutput() resolves destinations exactly this way, and it has
745
- // to stay that way: the two disagreeing is the bug, since `--audit-output`
746
- // finding what the build just called clean is what sent someone here.
747
- if (snapshot.destination && !existsSync(resolveOutputPath(snapshot.destination))) {
748
- return { skip: false, reason: 'output-missing', destination: snapshot.destination }
735
+ // Asked of invalidation.js rather than answered here, so that
736
+ // auditOutput, the source gate and the assets marker resolve a
737
+ // destination the same way. The two disagreeing is the bug
738
+ // `--audit-output` finding what the build just called clean is
739
+ // what sends people to this function in the first place.
740
+ if (outputMissing(snapshot.destination)) {
741
+ return { skip: false, reason: REASON.OUTPUT_MISSING, destination: snapshot.destination }
749
742
  }
750
743
  if (!snapshot.refClosure?.length) return { skip: true, reason: 'unchanged' }
751
744
  const sourceLang = entity?.meta?.lang ?? null
@@ -1031,7 +1024,7 @@ export function createManifest(db) {
1031
1024
  // rediscover the same caveat. missingOutputIds() is memoized for the
1032
1025
  // cycle, so this shares the walk the source gate already paid for.
1033
1026
  recordedHashes() {
1034
- const missingOutputs = this.missingOutputIds()
1027
+ const missingOutputs = missingOutputIds()
1035
1028
  const map = new Map()
1036
1029
  for (const row of stmtEntityInputHashes.iterate()) {
1037
1030
  if (missingOutputs.has(row.id)) continue
@@ -1106,56 +1099,6 @@ export function createManifest(db) {
1106
1099
  return edges
1107
1100
  },
1108
1101
 
1109
- // The entity ids whose last render wrote a file that is no longer
1110
- // on disk. A dispatch hint, the same shape as queryAffected: the
1111
- // source checksum gate consults it so those entities are re-emitted
1112
- // instead of being short-circuited as unchanged.
1113
- //
1114
- // This exists because the gate that stops `rm -rf out` from being
1115
- // noticed is the FIRST one, not the last. An unchanged file never
1116
- // gets a CREATE, so it never reaches the journal, so the render loop
1117
- // never sees it and manifest.skipDecision — which does check for a
1118
- // missing output — is never asked. Fixing only the render gate makes
1119
- // the build correct for entities that get that far and changes
1120
- // nothing for the case in the report, where none of them do.
1121
- //
1122
- // One stat per recorded snapshot, once per cycle, hoisted out of the
1123
- // per-file path exactly like checksumsByCollection. The syscalls are
1124
- // ~2.2ms per 1842 paths and cost the same whether the files are there
1125
- // or not. At the build level it does not register: warm rebuilds of
1126
- // the 10k perf corpus ran 3.36s median with this against 3.56s
1127
- // without, i.e. nominally faster, which is only to say the difference
1128
- // is well inside the run-to-run spread (3.0-4.1 vs 3.5-4.7 over five
1129
- // runs each). Re-measure with `npm run test:perf` before trusting a
1130
- // claim that it got slower.
1131
- //
1132
- // Resolves destinations through resolveOutputPath, which is what
1133
- // auditOutput uses. The two must agree — `--audit-output` reporting
1134
- // files that the build just called unchanged is the symptom, not a
1135
- // separate diagnostic.
1136
- // Memoized for the cycle. Two callers ask — the source checksum gate
1137
- // once per scan, recordedHashes() once per layouts dispatch — and both
1138
- // ask before anything has rendered, so one walk answers both. Cleared
1139
- // at the end of onFinalize, which is where this cycle's renders are
1140
- // recorded and the answer stops being true.
1141
- missingOutputIds() {
1142
- if (cachedMissingOutputs) return cachedMissingOutputs
1143
- const missing = new Set()
1144
- for (const row of stmtSelectAll.iterate()) {
1145
- const snap = rowToSnap(row)
1146
- if (!snap.destination) continue
1147
- if (!existsSync(resolveOutputPath(snap.destination))) missing.add(snap.id)
1148
- }
1149
- cachedMissingOutputs = missing
1150
- return missing
1151
- },
1152
-
1153
- // Drops the memo. Called at the end of onFinalize; exported on the
1154
- // surface so a test can force a re-read without a full cycle.
1155
- forgetMissingOutputs() {
1156
- cachedMissingOutputs = null
1157
- },
1158
-
1159
1102
  // Walk the output folder against recorded snapshots, returning
1160
1103
  // a diff describing missing / mismatched / orphaned /
1161
1104
  // unverifiable. Backs `mikser --audit-output`. Pure: no mutations.
@@ -1543,5 +1486,5 @@ onFinalize(async () => {
1543
1486
  // Dropped here rather than at the start of the next cycle because in
1544
1487
  // watch mode there may not be one for hours, and a stale set held that
1545
1488
  // long would keep re-dispatching entities it saw as missing.
1546
- m.forgetMissingOutputs()
1489
+ forgetMissingOutputs()
1547
1490
  })
@@ -6,9 +6,10 @@ import { mkdir, writeFile, unlink, rm, readFile, symlink, } from 'fs/promises'
6
6
  import { existsSync } from 'node:fs'
7
7
  import { createRequire } from 'node:module'
8
8
  import { globby } from 'globby'
9
+ import { isFullCycle, outputMissing } from '../invalidation.js'
9
10
  import _ from 'lodash'
10
11
  import map from 'p-map'
11
- import { isFullCycle } from '../utils.js'
12
+
12
13
 
13
14
  // Normalize a `options.presets[name]` value to a consistent
14
15
  // { matches, options } shape so callers don't have to inspect which form
@@ -357,8 +358,10 @@ export function assets(options = {}) {
357
358
  // the only thing that ever said a file was missing.
358
359
  //
359
360
  // Checked first and cheaply: one stat, and if the derivative is gone
360
- // no amount of marker archaeology changes the answer.
361
- if (!existsSync(entity.destination)) return false
361
+ // no amount of marker archaeology changes the answer. Asked of
362
+ // invalidation.js so a derivative and a rendered page agree on what
363
+ // "the output is gone" means.
364
+ if (outputMissing(entity.destination)) return false
362
365
  let result = false
363
366
  let revisions = []
364
367
  const assetChecksum = `${entity.destination}.${entity.preset.checksum}.md5`
@@ -207,12 +207,9 @@ export function files(options = {}) {
207
207
  // so the next one cannot be added to that place and missed here.
208
208
  //
209
209
  // priorChecksums is still bulk-prefetched per scan so the gate
210
- // reads a map instead of doing per-file SQL, and missingOutputs
211
- // alongside it for the same reason; the manifest memoizes the
212
- // latter for the cycle, so this shares the walk with useSource's
213
- // own scans rather than paying for a second one.
210
+ // reads a map instead of doing per-file SQL. What overrides the
211
+ // checksum, the gate asks invalidation.js for itself.
214
212
  const priorChecksums = checksumsByCollection(collection)
215
- const missingOutputs = runtime.manifest?.missingOutputIds() ?? new Set()
216
213
  const scanned = new Set()
217
214
  await pMap(paths, async relativePath => {
218
215
  const { source } = await ensureLink(relativePath)
@@ -230,7 +227,7 @@ export function files(options = {}) {
230
227
  // the catalog already has this file unchanged and there is
231
228
  // nothing to emit. Progress ticks either way — a gated file
232
229
  // was still looked at.
233
- const newChecksum = await gateChecksum(source, id, { priorChecksums, missingOutputs })
230
+ const newChecksum = await gateChecksum(source, id, { priorChecksums })
234
231
  updateProgress()
235
232
  if (newChecksum === null) return
236
233
  await createEntity({
package/src/source.js CHANGED
@@ -46,6 +46,7 @@ import { ACTION } from './constants.js'
46
46
  import { checksum as fileChecksum, checksumOf, junkIgnore } from './utils.js'
47
47
  import { reportGated, reportChanged } from './report.js'
48
48
  import { findById, findEntities, checksumsByCollection } from './catalog.js'
49
+ import { bypassReason } from './invalidation.js'
49
50
  import { useDatabase } from './database/index.js'
50
51
 
51
52
  // Per-source-scan registerFile concurrency. The work is dominated by
@@ -81,20 +82,12 @@ const SCAN_CONCURRENCY = 16
81
82
  // disagree, and the losing combination (empty content + a checksum correct
82
83
  // for the finished file) is permanent, because every later sync then
83
84
  // short-circuits on "unchanged".
84
- // `missingOutputs`, when given, is the set of entity ids whose last render
85
- // wrote a file that is no longer on disk (manifest.missingOutputIds()). The
86
- // gate answers "has this INPUT changed", and deleting the output folder does
87
- // not change any input — so without this, `rm -rf out` followed by a build
88
- // short-circuits every file here, emits nothing, renders nothing, and reports
89
- // success over an empty folder. An entity whose output is gone is not
90
- // unchanged in any sense the caller means.
91
- export async function gateChecksum(file, id, { reload = false, priorChecksums, bytes, missingOutputs } = {}) {
85
+ export async function gateChecksum(file, id, { reload = false, priorChecksums, bytes } = {}) {
92
86
  const compute = () => (bytes !== undefined ? checksumOf(bytes) : fileChecksum(file))
93
- const canGate = !reload
94
- && !runtime.options.force
95
- && !runtime.catalog?.cacheInvalidated
96
- && !missingOutputs?.has(id)
97
- if (canGate) {
87
+ // What overrides the checksum is not this function's to decide — see
88
+ // invalidation.js. It used to be, and the copy in files.js then missed
89
+ // the output-existence clause when that was added here.
90
+ if (!bypassReason({ reload, id })) {
98
91
  const priorChecksum = priorChecksums
99
92
  ? priorChecksums.get(id)
100
93
  : findById(id)?.checksum
@@ -430,12 +423,6 @@ export function useSource(core, options) {
430
423
  // per-file instead of doing per-file SQL lookups. ~14× faster
431
424
  // at 14k entities (column projection vs full-entity JSON.parse).
432
425
  const priorChecksums = checksumsByCollection(collection)
433
- // Prefetched once per scan, beside priorChecksums and for the same
434
- // reason: the gate needs an O(1) answer per file, and this is one
435
- // pass over the snapshots rather than a stat inside the per-file
436
- // path. Entities listed here defeat the gate — their input is
437
- // unchanged but the file they produced is gone.
438
- const missingOutputs = runtime.manifest?.missingOutputIds() ?? new Set()
439
426
  // Parallel register — file read + checksum is I/O-bound. Set
440
427
  // additions, scanStats increments, and journal addEntry calls
441
428
  // are all single-threaded-JS-atomic so no locking required;
@@ -443,7 +430,7 @@ export function useSource(core, options) {
443
430
  // and INSERTs are ~10μs vs ~ms-per-file-read, so the journal
444
431
  // is never the bottleneck.
445
432
  await pMap(files, async (file) => {
446
- await registerFile(file, { logger, scanned, stats: scanStats, priorChecksums, missingOutputs })
433
+ await registerFile(file, { logger, scanned, stats: scanStats, priorChecksums })
447
434
  if (phase === 'import') updateProgress()
448
435
  }, { concurrency: SCAN_CONCURRENCY })
449
436
 
@@ -464,7 +451,7 @@ export function useSource(core, options) {
464
451
  logger.info(scanSummary({ cap, loaded: files.length, ...scanStats }))
465
452
  })
466
453
 
467
- async function registerFile(file, { logger, action = ACTION.CREATE, scanned, priorChecksums, missingOutputs } = {}) {
454
+ async function registerFile(file, { logger, action = ACTION.CREATE, scanned, priorChecksums } = {}) {
468
455
  // stats — when called from the scanHook the outer scan provides
469
456
  // a Map to accumulate counts. For onSync (chokidar single-file
470
457
  // events) the counter is absent and per-file tally is not
@@ -497,7 +484,7 @@ export function useSource(core, options) {
497
484
  }
498
485
  }
499
486
 
500
- const chksum = await gateChecksum(file, id, { reload, priorChecksums, bytes, missingOutputs })
487
+ const chksum = await gateChecksum(file, id, { reload, priorChecksums, bytes })
501
488
  if (chksum === null) {
502
489
  if (stats) stats.skipped++
503
490
  // Never becomes a render task, so it would otherwise be invisible
package/src/utils.js CHANGED
@@ -1187,29 +1187,6 @@ export async function writeOutput(file, bytes) {
1187
1187
  return true
1188
1188
  }
1189
1189
 
1190
- // Did this cycle evaluate the whole corpus, or only what changed?
1191
- //
1192
- // The distinction is what makes a "declared X matched nothing" warning
1193
- // worth printing. On an incremental cycle only changed entities are
1194
- // re-evaluated, so a pattern or preset legitimately matches nothing in a
1195
- // run of two — printing it there means the warning fires on every healthy
1196
- // build, and a warning that always fires is one people filter out, taking
1197
- // the real instance with it.
1198
- //
1199
- // True for: --force, a first-run or wiped database, and a cache
1200
- // invalidation. Those are exactly the cases where the import gate is
1201
- // bypassed and every entity is presented for evaluation.
1202
- // Takes the runtime rather than reading the singleton, so a plugin passes
1203
- // the one it was injected with — which is the singleton in a real build and
1204
- // the harness's stand-in under test.
1205
- export function isFullCycle(rt = runtime) {
1206
- return !!(
1207
- rt?.options?.force
1208
- || rt?.options?.firstRun
1209
- || rt?.catalog?.cacheInvalidated
1210
- )
1211
- }
1212
-
1213
1190
  // ── operating-system and file-manager litter ────────────────────────────
1214
1191
  //
1215
1192
  // Exposing a source folder over a network filesystem (mikser-io-drive) or