bareloop 0.41.0 → 0.43.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 (75) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +13 -21
  3. package/bareloop.context.md +97 -34
  4. package/package.json +4 -3
  5. package/src/authorflow.js +337 -29
  6. package/src/authoring.js +92 -7
  7. package/src/authorjob.js +84 -16
  8. package/src/authorreadout.js +89 -5
  9. package/src/authorrun.js +14 -5
  10. package/src/authorscout.js +108 -0
  11. package/src/declaredclose.js +25 -14
  12. package/src/facts.js +317 -0
  13. package/src/factwords.js +129 -0
  14. package/src/inputlook.js +301 -0
  15. package/src/interviewrun.js +0 -1
  16. package/src/job.js +40 -12
  17. package/src/jobblock.js +7 -2
  18. package/src/kinds.js +84 -3
  19. package/src/labelscan.js +170 -0
  20. package/src/pagefetch.js +125 -0
  21. package/src/panel/authorroutes.js +121 -34
  22. package/src/panel/authorsession.js +154 -45
  23. package/src/panel/index.html +845 -490
  24. package/src/panel/jobplan.js +34 -11
  25. package/src/panel/runroutes.js +163 -5
  26. package/src/panel/scheduler.js +156 -0
  27. package/src/panel/schedules.js +667 -0
  28. package/src/panel/schedulestart.js +243 -0
  29. package/src/panel/server.js +467 -48
  30. package/src/planrun.js +42 -9
  31. package/src/providerrows.js +3 -3
  32. package/src/reviewdoor.js +37 -0
  33. package/src/rubricstep.js +804 -0
  34. package/src/runlist.js +35 -0
  35. package/src/schedulewords.js +296 -0
  36. package/src/source.js +166 -1
  37. package/src/trigger.js +48 -0
  38. package/src/triggerclock.js +127 -0
  39. package/src/triggerfiles.js +215 -0
  40. package/src/triggerledger.js +79 -0
  41. package/src/u-readout.js +1 -1
  42. package/src/userrun.js +20 -9
  43. package/types/authorflow.d.ts +267 -13
  44. package/types/authoring.d.ts +29 -1
  45. package/types/authorjob.d.ts +7 -5
  46. package/types/authorreadout.d.ts +27 -2
  47. package/types/authorscout.d.ts +70 -0
  48. package/types/declaredclose.d.ts +5 -3
  49. package/types/facts.d.ts +71 -0
  50. package/types/factwords.d.ts +42 -0
  51. package/types/inputlook.d.ts +238 -0
  52. package/types/job.d.ts +8 -1
  53. package/types/jobblock.d.ts +8 -1
  54. package/types/labelscan.d.ts +38 -0
  55. package/types/pagefetch.d.ts +71 -0
  56. package/types/panel/authorroutes.d.ts +34 -1
  57. package/types/panel/authorsession.d.ts +55 -14
  58. package/types/panel/jobplan.d.ts +12 -4
  59. package/types/panel/runroutes.d.ts +32 -1
  60. package/types/panel/scheduler.d.ts +17 -0
  61. package/types/panel/schedules.d.ts +412 -0
  62. package/types/panel/schedulestart.d.ts +106 -0
  63. package/types/panel/server.d.ts +145 -14
  64. package/types/planrun.d.ts +2 -0
  65. package/types/providerrows.d.ts +2 -2
  66. package/types/reviewdoor.d.ts +27 -0
  67. package/types/rubricstep.d.ts +286 -0
  68. package/types/runlist.d.ts +20 -0
  69. package/types/schedulewords.d.ts +135 -0
  70. package/types/source.d.ts +94 -0
  71. package/types/trigger.d.ts +37 -0
  72. package/types/triggerclock.d.ts +52 -0
  73. package/types/triggerfiles.d.ts +93 -0
  74. package/types/triggerledger.d.ts +35 -0
  75. package/types/userrun.d.ts +11 -0
@@ -67,6 +67,26 @@ export function appendRun(row: RunRow, opts?: {
67
67
  export function appendRunEvent(event: RunEvent, opts?: {
68
68
  home?: string;
69
69
  }): void;
70
+ /**
71
+ * RULED 2026-10-15 ("Remove from Needs you"): the person took a stopped run's card out of the Needs-you list. ONE row
72
+ * `{runid, type:'needs-removed', at}` appended beside the run rows; it is not a run, a claim or an event ({@link readRunList}
73
+ * ignores it, so the monthly limit, the run counter and the scheduler never see it). Nothing is rewritten.
74
+ * @param {string} runid
75
+ * @param {{ home?: string, now?: () => number }} [opts]
76
+ * @returns {string} the `at` written
77
+ */
78
+ export function appendNeedsRemoved(runid: string, opts?: {
79
+ home?: string;
80
+ now?: () => number;
81
+ }): string;
82
+ /**
83
+ * The newest `needs-removed` time (ms) per run id. An absent file is an empty map; a row with no readable time is not a removal.
84
+ * @param {{ home?: string }} [opts]
85
+ * @returns {Map<string, number>}
86
+ */
87
+ export function readNeedsRemovals(opts?: {
88
+ home?: string;
89
+ }): Map<string, number>;
70
90
  /**
71
91
  * @typedef {{ type: 'leg-start', runid: string, leg: number, pid?: number, capUsd?: number, at: string }} LegStart
72
92
  * A resumed run's NEW claim (P5-R, hamr 2026-10-02: one run, one id, one row): appended beside the run's row
@@ -0,0 +1,135 @@
1
+ /**
2
+ * What starts the job, in words. `null` (by hand) has no sentence.
3
+ * @param {Trigger|null|undefined} t @param {string} [home]
4
+ * @returns {string|null}
5
+ */
6
+ export function scheduledWords(t: Trigger | null | undefined, home?: string): string | null;
7
+ /** "Mon 13 Oct, 09:00" — local time, fixed English names (never the machine's locale). @param {number} ms */
8
+ export function slotWords(ms: number): string;
9
+ /**
10
+ * The Scheduled card's first line: "today 17:00", "tomorrow 09:00", else the date the NEXT line already says
11
+ * ("Tue 20 Oct, 09:05"). Local time, by calendar day. @param {number} ms the next run @param {number} now
12
+ */
13
+ export function nextTimeWords(ms: number, now: number): string;
14
+ /** "next in 20m" / "next in 3h" / "next in 3d" (whole units, rounded down, never "0m"). @param {number} ms @param {number} now */
15
+ export function nextInWords(ms: number, now: number): string;
16
+ /**
17
+ * NEXT of a scheduled job: one sentence, first match wins.
18
+ * 1. a run of this job is waiting for the person's Accept (the next scheduled run does NOT wait for it, T7);
19
+ * 2. the latest outcome this month is `monthly-exhausted` (amounts from the row), RED;
20
+ * 3. a clock kind with a next slot; 4. a file kind that is armed; 5. not armed yet.
21
+ * A last outcome of `refused` goes in front of 3-5, so a trigger that cannot start is never silent.
22
+ * `red` is the typed flag for "the person must act": the page paints it and never guesses from the text.
23
+ * @param {{trigger: Trigger, armed: boolean, nextSlotMs: number|null, lastOutcome: any, waitingRunNo: number|null, now: number, retryInMs?: number|null, gaveUp?: any}} f
24
+ * `lastOutcome` = the newest outcome row of the CURRENT signing (`kind`, `at` in ms, `why`, `leftUsd`, `needUsd`, `atLeast`)
25
+ * @returns {{text: string, red: boolean}}
26
+ */
27
+ export function scheduledNextFor(f: {
28
+ trigger: Trigger;
29
+ armed: boolean;
30
+ nextSlotMs: number | null;
31
+ lastOutcome: any;
32
+ waitingRunNo: number | null;
33
+ now: number;
34
+ retryInMs?: number | null;
35
+ gaveUp?: any;
36
+ }): {
37
+ text: string;
38
+ red: boolean;
39
+ };
40
+ /** Is `atMs` in the same local calendar month as `nowMs`? A non-number is never. @param {unknown} atMs @param {number} nowMs */
41
+ export function sameMonthAs(atMs: unknown, nowMs: number): boolean;
42
+ /** RULED 2026-10-15: the red NEXT of a schedule that gave up. @param {unknown} why the refusal's own words */
43
+ export function gaveUpNextWords(why: unknown): string;
44
+ /** The give-up date on the Needs-you card, local `MM/DD HH:MM`. @param {number} ms */
45
+ export function gaveUpDateWords(ms: number): string;
46
+ /** RULED 2026-10-15: after an install-needed refusal, the wait that is left, in whole minutes rounded up; '' when no wait. @param {number|null|undefined} ms */
47
+ export function retryWords(ms: number | null | undefined): string;
48
+ /** The fixed catch-up sentence from a run's `trigger.json`. @param {any} rec */
49
+ export function catchUpWords(rec: any): "This run was a catch-up: it started more than 2 minutes after its set time (the panel was off)." | null;
50
+ /**
51
+ * A spawn error AFTER the consuming row: `tries` releases so far, `max` the bound. Under the bound it will be tried again;
52
+ * at the bound it stays consumed and says so. @param {string} why @param {number} tries @param {number} max
53
+ */
54
+ export function lateRefusalWords(why: string, tries: number, max: number): string;
55
+ /** "Thu 10/16 08:15" — local time, fixed English day names. @param {number} ms */
56
+ export function nextSlotShort(ms: number): string;
57
+ /**
58
+ * Job tab, "Started": how the run began, then its local start time. `rec` = the run's `trigger.json` (null on a hand run).
59
+ * @param {any} rec @param {number|null} atMs the run's start
60
+ */
61
+ export function startedWords(rec: any, atMs: number | null): string;
62
+ /**
63
+ * Audit, first row of a run a schedule started. @param {any} rec `trigger.json` @param {number|null} originNo the origin run's number
64
+ */
65
+ export function startedAuditWords(rec: any, originNo: number | null): string;
66
+ /**
67
+ * A fire that started nothing, in plain words. `short` = the SCHEDULE row's `last` reason; else the Audit group's line.
68
+ * @param {any} row a ledger row (`busy` | `monthly-exhausted` | `refused` | `gave-up`) @param {boolean} [short]
69
+ */
70
+ export function skippedWords(row: any, short?: boolean): string;
71
+ /**
72
+ * Job tab, "Schedule": the job's schedule NOW and what it is bound to. The first part IS the Run tab's line ({@link scheduledWords}),
73
+ * first letter lowered, so the two can never disagree.
74
+ * @param {{trigger: Trigger|null, originNo: number|null, model: string|null, destination: string|null, home?: string}} f
75
+ */
76
+ export function jobScheduleWords(f: {
77
+ trigger: Trigger | null;
78
+ originNo: number | null;
79
+ model: string | null;
80
+ destination: string | null;
81
+ home?: string;
82
+ }): string;
83
+ /** RULED 2026-10-15: the SCHEDULE row's `next` while an install-needed refusal is waiting to be tried again. @param {number} ms */
84
+ export function retryNextWords(ms: number): string;
85
+ /**
86
+ * RULED 2026-10-15: the sign on the head of the Audit's `schedule` section. ONE small rule (hamr may still adjust it): [▶] only while a
87
+ * run of the job is running (as everywhere in the panel), [✗] when the SCHEDULE row is red, else [·] (on, waiting).
88
+ * @param {{running: boolean, red: boolean}} f @returns {{sign: string}}
89
+ */
90
+ export function scheduleSignFor(f: {
91
+ running: boolean;
92
+ red: boolean;
93
+ }): {
94
+ sign: string;
95
+ };
96
+ /** Audit schedule section, the `this run` words of a run the schedule started (the head already says which schedule and from where). @param {any} rec `trigger.json` */
97
+ export function sectionThisRunWords(rec: any): string;
98
+ /** Audit schedule section, folded tail: `1 try started nothing` / `N tries started nothing`; '' for none. @param {number} n */
99
+ export function triesTailWords(n: number): string;
100
+ /**
101
+ * Every schedule button/word the page shows lives here, so a reword is ONE edit (hamr will reword on his walk).
102
+ * The server sends this table with the run detail and the schedule card; the page reads every label from it.
103
+ */
104
+ export const SCHEDULE_WORDS: Readonly<{
105
+ byHand: "By hand";
106
+ cancelSchedule: "Cancel schedule";
107
+ cancelWords: "Cancel schedule";
108
+ scheduleRun: "Schedule run";
109
+ cancelModify: "Cancel / modify schedule";
110
+ signSchedule: "Sign schedule";
111
+ nothingChanged: "Nothing changed yet. This card only sets or adjusts the schedule; it never runs the job.";
112
+ confirmPrefix: "Confirm: ";
113
+ whenLabel: "When";
114
+ nowPrefix: "Now: ";
115
+ scheduledLabel: "SCHEDULE";
116
+ nextLabel: "NEXT";
117
+ options: Readonly<{
118
+ hand: "By hand";
119
+ daily: "Every day at HH:MM";
120
+ weekdays: "Weekdays at HH:MM";
121
+ weekly: "Every <day> at HH:MM";
122
+ 'file-updated': "When <file> is updated";
123
+ 'new-file': "When a new file arrives in <folder>";
124
+ }>;
125
+ gaveUpPill: "Check";
126
+ problemNext: "next: fix it in Cancel / modify schedule";
127
+ monthNext: "next: raise the limit in Settings, or it starts on the 1st";
128
+ acceptWaiting: "waiting for your Accept (the next run does not wait)";
129
+ scheduleGroup: "schedule";
130
+ notGreen: "This job has not had a good run yet. Run it by hand and get it right first; then you can set when it runs. A rubric job also needs your Accept once. Nothing was changed.";
131
+ stale: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
132
+ }>;
133
+ export function dateTimeWords(ms: number): string;
134
+ export function lowerFirst(s: string): string;
135
+ export type Trigger = import("./trigger.js").Trigger;
package/types/source.d.ts CHANGED
@@ -330,6 +330,100 @@ export function proveInputFiles(inputs: {
330
330
  ok: false;
331
331
  error: string;
332
332
  }>;
333
+ /**
334
+ * Does this source manifest belong to a rubric job on local files (`proveRubricInputs` wrote it)? One spelling for both
335
+ * doors (the panel session and the CLI author run): a non-repo source that froze numbered, labelled input files.
336
+ * @param {Record<string, any>|null|undefined} manifest
337
+ * @returns {boolean}
338
+ */
339
+ export function isRubricFilesManifest(manifest: Record<string, any> | null | undefined): boolean;
340
+ /**
341
+ * PART 1a (docs/product/ITEM33-BUILD.md, "Addendum 2026-10-12"): the front door for a RUBRIC job whose inputs are the
342
+ * person's own LOCAL FILES, not a repo. Each numbered `label: /absolute/file` line is proven at $0 and frozen into the
343
+ * same hidden-git tree a plain folder gets (`prepareSource`, which also refuses a secret shape, an environment file
344
+ * and a symlink), under `input/`; the manifest then also records `inputs: [{label, rel, sha256}]` — the bytes the
345
+ * rubric's `cited` check will search a quote in, hashed once, here. A line that is a web address is fetched ONCE with
346
+ * barebrowse (PART 1c, read only) and its text frozen under `input/` like a file (the manifest row also records its `url`,
347
+ * so run start can fetch it again); a `search:` line is a named refusal (a later build); so is a relative path, a missing file, a folder, a file over
348
+ * {@link RUBRIC_INPUT_MAX_BYTES}, and text that is not valid UTF-8. Every refusal is plain words naming the line and
349
+ * ends "Nothing spent." — nothing here touches a model.
350
+ * @param {{inputs: {n: number, label: string, value: string}[], into: string, destination?: string, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>}} o `fetchUrl` - how a web address is read (default barebrowse via `fetchPageText`; a test injects a stub, never the network)
351
+ * @returns {Promise<{ok: true, prep: {stop: null, into: string, tree: string, manifestPath: string, manifest: Record<string, any>}}|{ok: false, error: string, code: string}>}
352
+ */
353
+ export function proveRubricInputs({ inputs, into, destination, fetchUrl }: {
354
+ inputs: {
355
+ n: number;
356
+ label: string;
357
+ value: string;
358
+ }[];
359
+ into: string;
360
+ destination?: string;
361
+ fetchUrl?: (url: string) => Promise<{
362
+ ok: true;
363
+ text: string;
364
+ url: string;
365
+ } | {
366
+ ok: false;
367
+ error: string;
368
+ }>;
369
+ }): Promise<{
370
+ ok: true;
371
+ prep: {
372
+ stop: null;
373
+ into: string;
374
+ tree: string;
375
+ manifestPath: string;
376
+ manifest: Record<string, any>;
377
+ };
378
+ } | {
379
+ ok: false;
380
+ error: string;
381
+ code: string;
382
+ }>;
383
+ /**
384
+ * Is this Inputs value a path to an existing regular FILE on this machine (not a repo folder, not a web address, not a
385
+ * missing path)? The $0 peek that tells a rubric job on local files from a rubric job on a repo before a session exists;
386
+ * `proveRubricInputs` makes the authoritative, line-by-line proof.
387
+ * @param {string} value
388
+ * @returns {boolean}
389
+ */
390
+ export function looksLikeLocalFile(value: string): boolean;
391
+ /**
392
+ * Is this Inputs value one a rubric job can work on without a repo: a regular FILE on this machine, or a web address (PART 1c)?
393
+ * @param {string} value
394
+ * @returns {boolean}
395
+ */
396
+ export function looksLikeRubricInput(value: string): boolean;
397
+ /**
398
+ * RUN START for a rubric job with web inputs (PART 1c, RF22): fetch every web input AGAIN and make that text the run's frozen input
399
+ * BEFORE the worker starts. The drafting copy was only for the look (prices change); the close grades, and the worker reads, THIS
400
+ * copy. It is written into the prepared tree, committed as the new seed (so the page is never read as a worker write), and
401
+ * `source.json` is rewritten (new sha256 per input, new seed, `draftSha256` keeps what the look saw). Nothing but the text of a
402
+ * recorded web address is ever refetched; a job with only file inputs returns at once and changes nothing.
403
+ * @param {{into: string, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>}} o `into` - the prepared copy (source.json + tree/)
404
+ * @returns {Promise<{ok: true, refreshed: string[], seed: string|null}|{ok: false, error: string}>}
405
+ */
406
+ export function refreezeUrlInputs({ into, fetchUrl }: {
407
+ into: string;
408
+ fetchUrl?: (url: string) => Promise<{
409
+ ok: true;
410
+ text: string;
411
+ url: string;
412
+ } | {
413
+ ok: false;
414
+ error: string;
415
+ }>;
416
+ }): Promise<{
417
+ ok: true;
418
+ refreshed: string[];
419
+ seed: string | null;
420
+ } | {
421
+ ok: false;
422
+ error: string;
423
+ }>;
424
+ /** the per-file ceiling for a rubric input: bareguard's `cited` check searches a quote in a source only up to this size
425
+ * (its `quoteSourceMaxBytes`); a bigger file could never be cited from, so the door refuses it at $0 */
426
+ export const RUBRIC_INPUT_MAX_BYTES: number;
333
427
  /**
334
428
  * a named, non-throwing stop
335
429
  */
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The only way a trigger enters the system. Returns the typed trigger, or says what to fix (and which field).
3
+ * @param {unknown} t
4
+ * @returns {{ok: true, trigger: Trigger} | {ok: false, say: string, code: 'invalid-value'|'unknown-field', path: string}}
5
+ */
6
+ export function validateTrigger(t: unknown): {
7
+ ok: true;
8
+ trigger: Trigger;
9
+ } | {
10
+ ok: false;
11
+ say: string;
12
+ code: "invalid-value" | "unknown-field";
13
+ path: string;
14
+ };
15
+ /** @typedef {import('./triggerclock.js').Schedule | {kind:'new-file', folder:string} | {kind:'file-updated', path:string}} Trigger */
16
+ export const TRIGGER_KINDS: readonly string[];
17
+ export const CLOCK_KINDS: readonly string[];
18
+ export const FILE_KINDS: readonly string[];
19
+ export function isClockTrigger(t: unknown): t is {
20
+ kind: "daily" | "weekdays" | "weekly";
21
+ hour: number;
22
+ minute: number;
23
+ };
24
+ export function isFileTrigger(t: unknown): t is {
25
+ kind: "new-file";
26
+ folder: string;
27
+ } | {
28
+ kind: "file-updated";
29
+ path: string;
30
+ };
31
+ export type Trigger = import("./triggerclock.js").Schedule | {
32
+ kind: "new-file";
33
+ folder: string;
34
+ } | {
35
+ kind: "file-updated";
36
+ path: string;
37
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The only way a clock schedule enters the system. Refuses anything else, naming the field.
3
+ * @param {unknown} s
4
+ * @returns {Schedule}
5
+ */
6
+ export function validateSchedule(s: unknown): Schedule;
7
+ /**
8
+ * Is a run due at `now`? Slots missed (panel off) collapse to ONE fire, for the latest slot, marked catch-up.
9
+ * Catch-up = more than one slot was missed, or the latest slot is more than GRACE_MS old.
10
+ * @param {Schedule} schedule
11
+ * @param {ClockRow[]} rows
12
+ * @param {number} now epoch ms
13
+ * @returns {Due}
14
+ */
15
+ export function due(schedule: Schedule, rows: ClockRow[], now: number): Due;
16
+ /**
17
+ * The next slot time strictly after `afterMs` (epoch ms). Words are the caller's (schedulewords.js).
18
+ * @param {Schedule} schedule
19
+ * @param {number} afterMs
20
+ * @returns {number}
21
+ */
22
+ export function nextSlot(schedule: Schedule, afterMs: number): number;
23
+ /** The trigger was set at `now` under signature `sig`: no slot before this ever fires. @param {string} file @param {number} now @param {string} sig */
24
+ export function armTrigger(file: string, now: number, sig: string): void;
25
+ /**
26
+ * Record that the run for this due slot is starting (the consuming row). Write-once per slot.
27
+ * @param {string} file @param {Due} d the result of `due` with fire true @param {number} now @param {string} sig
28
+ */
29
+ export function recordFire(file: string, d: Due, now: number, sig: string): void;
30
+ /** @typedef {{kind:'daily'|'weekdays', hour:number, minute:number} | {kind:'weekly', day:string, hour:number, minute:number}} Schedule */
31
+ /** @typedef {import('./triggerledger.js').LedgerRow} ClockRow */
32
+ /** @typedef {{fire:boolean, catchUp:boolean, missed:number, slot?:number}} Due */
33
+ /** A slot seen MORE than this after its set time is a catch-up. Exactly this late is on time. */
34
+ export const GRACE_MS: 120000;
35
+ export const DAYS: string[];
36
+ export type Schedule = {
37
+ kind: "daily" | "weekdays";
38
+ hour: number;
39
+ minute: number;
40
+ } | {
41
+ kind: "weekly";
42
+ day: string;
43
+ hour: number;
44
+ minute: number;
45
+ };
46
+ export type ClockRow = import("./triggerledger.js").LedgerRow;
47
+ export type Due = {
48
+ fire: boolean;
49
+ catchUp: boolean;
50
+ missed: number;
51
+ slot?: number;
52
+ };
@@ -0,0 +1,93 @@
1
+ /** The file-kind rows of the job's ledger that still count (a released `ran` does not). @param {string} file @returns {SeenRow[]} */
2
+ export function readSeen(file: string): SeenRow[];
3
+ /** Arrived rows are never overwritten: the 2nd arrival of a sha (after a drop) gets its own id. @param {string} ledger @param {ToRun & {name?:string}} item @param {number} at the scheduler's clock, never the file's mtime @param {string} sig */
4
+ export function recordArrived(ledger: string, item: ToRun & {
5
+ name?: string;
6
+ }, at: number, sig: string): boolean;
7
+ /** `of` = the arrived row it cancels (item.id when a queue row is passed). @param {string} ledger @param {{sha:string, path:string, size:number, id?:string}} item @param {string} why a plain reason: deleted, changed, schedule-changed, schedule-cancelled @param {number} at @param {string} sig */
8
+ export function recordDropped(ledger: string, item: {
9
+ sha: string;
10
+ path: string;
11
+ size: number;
12
+ id?: string;
13
+ }, why: string, at: number, sig: string): boolean;
14
+ /**
15
+ * The queue: arrived and neither ran nor dropped, oldest arrival first. Identical arrival times: by name, then by sha (deterministic).
16
+ * @param {SeenRow[]} rows @returns {(SeenRow & {path:string})[]}
17
+ */
18
+ export function queueOf(rows: SeenRow[]): (SeenRow & {
19
+ path: string;
20
+ })[];
21
+ /**
22
+ * At a queued file's turn, look at it again, at use time: lstat (no symlink, a regular file), realpath of its folder, re-hash.
23
+ * @param {{path:string, sha:string}} item
24
+ * @returns {{state:'ok', path:string, sha:string, size:number} | {state:'deleted', say:string} | {state:'changed', sha:string}}
25
+ */
26
+ export function recheck(item: {
27
+ path: string;
28
+ sha: string;
29
+ }): {
30
+ state: "ok";
31
+ path: string;
32
+ sha: string;
33
+ size: number;
34
+ } | {
35
+ state: "deleted";
36
+ say: string;
37
+ } | {
38
+ state: "changed";
39
+ sha: string;
40
+ };
41
+ /**
42
+ * The target is usable NOW, or an Error naming why: a file that is not a symlink and not something else (a file that does not exist yet
43
+ * is allowed: it may arrive), a folder that exists, resolved with realpathSync. The one rule `arm` and a door's set-time check share.
44
+ * @param {'new'|'updated'} mode @param {string} target
45
+ */
46
+ export function checkTarget(mode: "new" | "updated", target: string): void;
47
+ /**
48
+ * @param {{mode:'new'|'updated', target:string, ledger:string, sig:string, now?:() => number}} opts target = folder (new) or file (updated)
49
+ */
50
+ export function createFileTrigger({ mode, target, ledger, sig, now }: {
51
+ mode: "new" | "updated";
52
+ target: string;
53
+ ledger: string;
54
+ sig: string;
55
+ now?: () => number;
56
+ }): {
57
+ arm: () => void;
58
+ poll: () => {
59
+ arrived: ToRun[];
60
+ refused: Refused[];
61
+ };
62
+ };
63
+ /** @typedef {{id:string, kind:'baseline'|'arrived'|'ran'|'dropped', sha:string, name:string, size:number, at:number, sig?:string, path?:string, why?:string, of?:string}} SeenRow */
64
+ /** @typedef {{path:string, sha:string, size:number}} ToRun */
65
+ /** @typedef {{path:string, why:string}} Refused */
66
+ /** Consecutive polls with the same size and mtime before a file is taken. (Two matches in a row = taken on the third sighting.) */
67
+ export const STABLE_POLLS: 2;
68
+ export function recordRan(ledger: string, item: {
69
+ sha: string;
70
+ path: string;
71
+ size: number;
72
+ }, at: number, sig: string): boolean;
73
+ export type SeenRow = {
74
+ id: string;
75
+ kind: "baseline" | "arrived" | "ran" | "dropped";
76
+ sha: string;
77
+ name: string;
78
+ size: number;
79
+ at: number;
80
+ sig?: string;
81
+ path?: string;
82
+ why?: string;
83
+ of?: string;
84
+ };
85
+ export type ToRun = {
86
+ path: string;
87
+ sha: string;
88
+ size: number;
89
+ };
90
+ export type Refused = {
91
+ path: string;
92
+ why: string;
93
+ };
@@ -0,0 +1,35 @@
1
+ /** @typedef {{id?: string, kind: string, sig?: string, at?: number, [k: string]: unknown}} LedgerRow */
2
+ /**
3
+ * Every row that parses, oldest first, and how many lines did not. A missing file is no rows.
4
+ * @param {string} file
5
+ * @returns {{rows: LedgerRow[], skipped: number}}
6
+ */
7
+ export function readLedger(file: string): {
8
+ rows: LedgerRow[];
9
+ skipped: number;
10
+ };
11
+ /**
12
+ * Append one row (dir 0700, file 0600). A torn tail gets a newline first. `once` refuses a row whose `id` is
13
+ * already on disk (write-once ids), by throwing.
14
+ * @param {string} file
15
+ * @param {LedgerRow} row
16
+ * @param {{once?: boolean}} [opts]
17
+ */
18
+ export function appendRow(file: string, row: LedgerRow, opts?: {
19
+ once?: boolean;
20
+ }): void;
21
+ /**
22
+ * Walk the rows in order: a `released` row (`of` = an id) cancels the latest earlier live row with that id, and
23
+ * is itself dropped. A released `fire` or `ran` is not consumed, so its slot or file is retryable (Part 4) and
24
+ * the same id may be written again.
25
+ * @param {LedgerRow[]} rows
26
+ * @returns {LedgerRow[]}
27
+ */
28
+ export function withoutReleased(rows: LedgerRow[]): LedgerRow[];
29
+ export type LedgerRow = {
30
+ id?: string;
31
+ kind: string;
32
+ sig?: string;
33
+ at?: number;
34
+ [k: string]: unknown;
35
+ };
@@ -61,6 +61,17 @@ export type Deps = {
61
61
  */
62
62
  provider?: any;
63
63
  providerFor?: ((tier: string) => any) | undefined;
64
+ /**
65
+ * PART 1c - a test seam: how a web input is read at run start (production: barebrowse)
66
+ */
67
+ fetchUrl?: ((url: string) => Promise<{
68
+ ok: true;
69
+ text: string;
70
+ url: string;
71
+ } | {
72
+ ok: false;
73
+ error: string;
74
+ }>) | undefined;
64
75
  judgeProvider?: any;
65
76
  /**
66
77
  * F196 — a test seam for `appendRun`'s