@iyulab/lowline-form 0.11.0 → 0.12.1

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/dist/draft.d.ts CHANGED
@@ -19,8 +19,18 @@ export interface Reported {
19
19
  promise?: number;
20
20
  score?: number;
21
21
  }
22
+ /**
23
+ * Whether the promise of the answer shown was displayed beside it, and the probability it was drawn to be displayed with
24
+ * — fixed before the draw: 1 for a form that always displays it, 0 for one that never does.
25
+ */
26
+ export interface PromiseShown {
27
+ shown: boolean;
28
+ probability: number;
29
+ }
22
30
  interface Track extends Reported {
23
31
  shown?: Suggestion;
32
+ /** Whether the promise of the suggestion shown was displayed beside it. */
33
+ promise_shown?: PromiseShown;
24
34
  declined?: boolean;
25
35
  held?: number;
26
36
  /** The answer held back for a check, kept so the record can say what the person was not shown. */
@@ -36,9 +46,10 @@ interface Track extends Reported {
36
46
  *
37
47
  * Written as the engine reads it: `{ opened, fields: { field: { shown, declined, held } } }`, and — only what was
38
48
  * reported — `opened_at`, `saved_at` (milliseconds on one steady clock), `arrival` (the fields in the order their values
39
- * arrived), `by` and `batch`, and in a field `layer`, `promise`, `score`, `at` and `withheld`.
49
+ * arrived), `by` and `batch`, and in a field `layer`, `promise`, `score`, `promise_shown`, `at` and `withheld`.
40
50
  */
41
51
  export declare class Draft {
52
+ #private;
42
53
  readonly opened: Values;
43
54
  readonly fields: Record<string, Track>;
44
55
  private openedAt?;
@@ -48,7 +59,10 @@ export declare class Draft {
48
59
  private batch?;
49
60
  /** A record opened holding `values` — none for a new one. Empty values are left out. */
50
61
  constructor(values?: Values);
51
- /** A suggestion came into view for the field. A suggestion turned down earlier stays turned down only if it is this. */
62
+ /**
63
+ * A suggestion came into view for the field. It is turned down if the person turned its value down before in this
64
+ * record — even with other suggestions shown since.
65
+ */
52
66
  show(field: string, suggestion: Suggestion, reported?: Reported): void;
53
67
  /**
54
68
  * The field's answer is held back for a check, drawn with `probability`: what the person settles is checked.
@@ -57,6 +71,12 @@ export declare class Draft {
57
71
  hold(field: string, probability: number, withheld?: {
58
72
  suggestion: Suggestion;
59
73
  } & Reported): void;
74
+ /**
75
+ * Whether the promise of the suggestion shown for the field was displayed beside it, drawn with `probability` (fixed
76
+ * before the draw: 1 for always, 0 for never). It stands until another suggestion is shown or the answer is held back
77
+ * for a check. False, changing nothing, when no suggestion is shown.
78
+ */
79
+ showPromise(field: string, shown: boolean, probability: number): boolean;
60
80
  /** The record was opened at `ms` on the session's clock (a steady clock in milliseconds: only differences are read). */
61
81
  openAt(ms: number): void;
62
82
  /** The record is being saved at `ms` on the same clock. */
@@ -72,7 +92,7 @@ export declare class Draft {
72
92
  private report;
73
93
  /** The person turned down the suggestion in view; false, changing nothing, when none was shown. */
74
94
  decline(field: string): boolean;
75
- /** Whether `suggestion` is the one in view for the field and the person turned it down. */
95
+ /** Whether `suggestion` is the one in view for the field and the person turned it down — now or before, in this record. */
76
96
  declined(field: string, suggestion: Suggestion): boolean;
77
97
  shown(field: string): Suggestion | undefined;
78
98
  held(field: string): number | undefined;
package/dist/draft.js CHANGED
@@ -6,20 +6,29 @@
6
6
  *
7
7
  * Written as the engine reads it: `{ opened, fields: { field: { shown, declined, held } } }`, and — only what was
8
8
  * reported — `opened_at`, `saved_at` (milliseconds on one steady clock), `arrival` (the fields in the order their values
9
- * arrived), `by` and `batch`, and in a field `layer`, `promise`, `score`, `at` and `withheld`.
9
+ * arrived), `by` and `batch`, and in a field `layer`, `promise`, `score`, `promise_shown`, `at` and `withheld`.
10
10
  */
11
11
  export class Draft {
12
+ /** The values the person turned down, by field: for this record, each stays turned down whatever is shown between. */
13
+ #turnedDown;
12
14
  /** A record opened holding `values` — none for a new one. Empty values are left out. */
13
15
  constructor(values = {}) {
14
16
  this.fields = {};
15
17
  this.arrival = [];
18
+ /** The values the person turned down, by field: for this record, each stays turned down whatever is shown between. */
19
+ this.#turnedDown = new Map();
16
20
  this.opened = Object.fromEntries(Object.entries(values).filter(([, v]) => !isEmpty(v)));
17
21
  }
18
- /** A suggestion came into view for the field. A suggestion turned down earlier stays turned down only if it is this. */
22
+ /**
23
+ * A suggestion came into view for the field. It is turned down if the person turned its value down before in this
24
+ * record — even with other suggestions shown since.
25
+ */
19
26
  show(field, suggestion, reported = {}) {
20
27
  const track = (this.fields[field] ??= {});
21
- if (!sameSuggestion(track.shown, suggestion))
22
- track.declined = false;
28
+ if (!sameSuggestion(track.shown, suggestion)) {
29
+ track.declined = this.#turnedDown.get(field)?.has(suggestion.value) === true;
30
+ delete track.promise_shown;
31
+ }
23
32
  track.shown = { ...suggestion };
24
33
  delete track.held;
25
34
  delete track.withheld;
@@ -42,6 +51,21 @@ export class Draft {
42
51
  }
43
52
  this.fields[field] = track;
44
53
  }
54
+ /**
55
+ * Whether the promise of the suggestion shown for the field was displayed beside it, drawn with `probability` (fixed
56
+ * before the draw: 1 for always, 0 for never). It stands until another suggestion is shown or the answer is held back
57
+ * for a check. False, changing nothing, when no suggestion is shown.
58
+ */
59
+ showPromise(field, shown, probability) {
60
+ if (!(probability >= 0 && probability <= 1) || (shown ? probability === 0 : probability === 1)) {
61
+ throw new RangeError(`a promise ${shown ? 'shown' : 'not shown'} with probability ${probability} is not possible`);
62
+ }
63
+ const track = this.fields[field];
64
+ if (!track?.shown)
65
+ return false;
66
+ track.promise_shown = { shown, probability };
67
+ return true;
68
+ }
45
69
  /** The record was opened at `ms` on the session's clock (a steady clock in milliseconds: only differences are read). */
46
70
  openAt(ms) {
47
71
  this.openedAt = whole(ms);
@@ -88,9 +112,12 @@ export class Draft {
88
112
  if (!track?.shown)
89
113
  return false;
90
114
  track.declined = true;
115
+ const values = this.#turnedDown.get(field) ?? new Set();
116
+ values.add(track.shown.value);
117
+ this.#turnedDown.set(field, values);
91
118
  return true;
92
119
  }
93
- /** Whether `suggestion` is the one in view for the field and the person turned it down. */
120
+ /** Whether `suggestion` is the one in view for the field and the person turned it down — now or before, in this record. */
94
121
  declined(field, suggestion) {
95
122
  const track = this.fields[field];
96
123
  return track?.declined === true && sameSuggestion(track.shown, suggestion);
@@ -1,3 +1,3 @@
1
- export { Draft, valuesOf, type Suggestion, type Value, type Values } from './draft.js';
1
+ export { Draft, valuesOf, type PromiseShown, type Suggestion, type Value, type Values } from './draft.js';
2
2
  export { Session, englishStrings, type Abstain, type Basis, type FieldState, type Proposal, type Round, type Strings } from './session.js';
3
3
  export { LowlineClient, LowlineError, type Attachment, type FieldReport, type ThresholdChoice } from './client.js';
@@ -28,8 +28,27 @@ export declare class LowlineForm extends LitElement {
28
28
  data: Record<string, unknown>;
29
29
  /** How often a field's answer is held back to check it, from 0 to 1. */
30
30
  rate: number;
31
+ /**
32
+ * The probability the host drew this record with to be settled again by a second person — an audit — from 0 (not
33
+ * audited, the default) to 1. Audited, the form shows none of what the first settler or the decider said: the decided
34
+ * fields' answers are held back with it as checks. Open the record with only the values it does not decide, and name
35
+ * the second person with `settled-by`.
36
+ */
37
+ audit: number;
31
38
  /** The words the form shows, in the host's language. */
32
39
  strings: Strings;
40
+ /**
41
+ * Whether a suggestion the engine allows to go in unasked is put in as the field's starting value. Off by default: a
42
+ * value in a field reads as the person's own, so a suggestion stays beside the field until the person takes it. A host
43
+ * that turns it on puts values in before anyone looks, and a value put in and saved unchanged is settled as taken.
44
+ */
45
+ prefill: boolean;
46
+ /**
47
+ * How often a suggestion's promise — how often a suggestion like it has been right, as a count out of ten — is shown
48
+ * beside it, from 0 to 1. Off (0) by default. Drawn once for each suggestion shown, and the draw is sent with the save,
49
+ * so a host can measure whether showing it helps people settle rightly before turning it on for everyone.
50
+ */
51
+ promiseRate: number;
33
52
  /** Who is writing, as an opaque id the host gives — one person, one id; not a name. Sent with the save when set. */
34
53
  settledBy: string;
35
54
  /** The batch the record is written in, when several come at once from one source. Sent with the save when set. */
@@ -39,8 +39,27 @@ let LowlineForm = class LowlineForm extends LitElement {
39
39
  this.data = {};
40
40
  /** How often a field's answer is held back to check it, from 0 to 1. */
41
41
  this.rate = 0.05;
42
+ /**
43
+ * The probability the host drew this record with to be settled again by a second person — an audit — from 0 (not
44
+ * audited, the default) to 1. Audited, the form shows none of what the first settler or the decider said: the decided
45
+ * fields' answers are held back with it as checks. Open the record with only the values it does not decide, and name
46
+ * the second person with `settled-by`.
47
+ */
48
+ this.audit = 0;
42
49
  /** The words the form shows, in the host's language. */
43
50
  this.strings = englishStrings;
51
+ /**
52
+ * Whether a suggestion the engine allows to go in unasked is put in as the field's starting value. Off by default: a
53
+ * value in a field reads as the person's own, so a suggestion stays beside the field until the person takes it. A host
54
+ * that turns it on puts values in before anyone looks, and a value put in and saved unchanged is settled as taken.
55
+ */
56
+ this.prefill = false;
57
+ /**
58
+ * How often a suggestion's promise — how often a suggestion like it has been right, as a count out of ten — is shown
59
+ * beside it, from 0 to 1. Off (0) by default. Drawn once for each suggestion shown, and the draw is sent with the save,
60
+ * so a host can measure whether showing it helps people settle rightly before turning it on for everyone.
61
+ */
62
+ this.promiseRate = 0;
44
63
  /** Who is writing, as an opaque id the host gives — one person, one id; not a name. Sent with the save when set. */
45
64
  this.settledBy = '';
46
65
  /** The batch the record is written in, when several come at once from one source. Sent with the save when set. */
@@ -60,6 +79,8 @@ let LowlineForm = class LowlineForm extends LitElement {
60
79
  const formdown = this.formdown;
61
80
  if (formdown)
62
81
  formdown.updateData({ ...formdown.data, [field]: value });
82
+ // A value taken is one the records can answer from: ask again, as after a value typed.
83
+ this.changed();
63
84
  };
64
85
  this.declined = (event) => {
65
86
  const { field } = event.detail;
@@ -80,11 +101,26 @@ let LowlineForm = class LowlineForm extends LitElement {
80
101
  this.client = new LowlineClient(this.src);
81
102
  this.canJudge = true;
82
103
  }
83
- if (changed.has('record') || changed.has('form') || changed.has('data') || changed.has('content')) {
104
+ if (changed.has('record') ||
105
+ changed.has('form') ||
106
+ changed.has('data') ||
107
+ changed.has('content') ||
108
+ changed.has('audit')) {
84
109
  // The values it was opened with are taken before anything is suggested or put in.
85
110
  this.session = new Session(this.record, valuesOf(this.data), this.rate, Math.random, this.strings);
111
+ this.session.prefills = this.prefill;
112
+ this.session.promises = this.promiseRate;
113
+ this.session.audit = this.audit;
86
114
  this.fieldStates = {};
87
115
  }
116
+ else if (this.session) {
117
+ if (changed.has('prefill'))
118
+ this.session.prefills = this.prefill;
119
+ if (changed.has('promiseRate'))
120
+ this.session.promises = this.promiseRate;
121
+ if (changed.has('strings'))
122
+ this.session.strings = this.strings;
123
+ }
88
124
  }
89
125
  updated(changed) {
90
126
  if (changed.has('record') || changed.has('form') || changed.has('content'))
@@ -141,6 +177,8 @@ let LowlineForm = class LowlineForm extends LitElement {
141
177
  this.fieldStates = session.applyFill(proposals);
142
178
  if (this.formdown)
143
179
  this.formdown.fieldStates = this.fieldStates;
180
+ // The fields the writing did not fill keep what the records say of them.
181
+ void this.ask();
144
182
  }
145
183
  /** Asks again what to show for the decided fields still open. */
146
184
  async ask() {
@@ -170,7 +208,7 @@ let LowlineForm = class LowlineForm extends LitElement {
170
208
  session.took(this.values());
171
209
  }
172
210
  formdown.fieldStates = this.fieldStates;
173
- if (this.canJudge)
211
+ if (this.canJudge && !session.auditing)
174
212
  void this.judge(session, values, shown);
175
213
  }
176
214
  /**
@@ -220,9 +258,18 @@ __decorate([
220
258
  __decorate([
221
259
  property({ type: Number })
222
260
  ], LowlineForm.prototype, "rate", void 0);
261
+ __decorate([
262
+ property({ type: Number })
263
+ ], LowlineForm.prototype, "audit", void 0);
223
264
  __decorate([
224
265
  property({ attribute: false })
225
266
  ], LowlineForm.prototype, "strings", void 0);
267
+ __decorate([
268
+ property({ type: Boolean })
269
+ ], LowlineForm.prototype, "prefill", void 0);
270
+ __decorate([
271
+ property({ type: Number, attribute: 'promise-rate' })
272
+ ], LowlineForm.prototype, "promiseRate", void 0);
226
273
  __decorate([
227
274
  property({ attribute: 'settled-by' })
228
275
  ], LowlineForm.prototype, "settledBy", void 0);
package/dist/session.d.ts CHANGED
@@ -55,6 +55,18 @@ export interface Strings {
55
55
  fromJudgement: string;
56
56
  /** The note of a value filled in from what the person wrote. */
57
57
  fromWriting: string;
58
+ /**
59
+ * The note of an empty decided field shown no suggestion, by why: a field is never left looking like one that is not
60
+ * decided. An empty text shows no note for that reason.
61
+ */
62
+ abstain: Record<Abstain, string>;
63
+ /** The note of an empty decided field a judgement was asked for and gave nothing the form can show. */
64
+ judgedNothing: string;
65
+ /**
66
+ * How often a suggestion like the one shown has been right, as a count out of ten — `{right}` is filled in. Said only
67
+ * when the host turns the display on (`promises`), after where the suggestion comes from.
68
+ */
69
+ promise: string;
58
70
  }
59
71
  export declare const englishStrings: Strings;
60
72
  /** What the form does with one round of proposals: what each field shows, and the values to put in unasked. */
@@ -71,7 +83,8 @@ export declare class Session {
71
83
  readonly id: string;
72
84
  readonly rate: number;
73
85
  private readonly random;
74
- private readonly strings;
86
+ /** The words the form shows; a host may change them while a record is written, and the next round says them. */
87
+ strings: Strings;
75
88
  /** A steady clock in milliseconds (`performance.now()` by default): the record's times are read from it. */
76
89
  private readonly clock;
77
90
  draft: Draft;
@@ -80,11 +93,37 @@ export declare class Session {
80
93
  private filled;
81
94
  /** What filling from the person's writing offers, by field, until the person picks it or turns it down. */
82
95
  private written;
96
+ /** Why filling from the person's writing gave a field nothing to offer, by field, until it offers one. */
97
+ private unfilled;
83
98
  /** The judgement asked for each field, by what it reads; `null` while the host is still judging. */
84
99
  private judgements;
100
+ /**
101
+ * Whether a suggestion the engine allows to go in unasked (`prefill`) is put in as the field's starting value. Off, it
102
+ * stays beside the field until the person takes it.
103
+ */
104
+ prefills: boolean;
105
+ /**
106
+ * How often the promise of a suggestion is shown beside it, from 0 (never — the default) to 1 (always). Drawn once for
107
+ * each suggestion a field shows, and the draw goes into the draft — so the records can tell whether seeing it changed
108
+ * what people settled. A value outside 0 to 1 shows none.
109
+ */
110
+ promises: number;
111
+ /**
112
+ * The probability this record was drawn with to be settled again by a second person — an audit — or 0 (the default)
113
+ * for a record written as usual. Above 0 and at most 1, the person is shown nothing the first settler or the decider
114
+ * said: every decided field's answer is held back with this probability (a check), and no other suggestion, note or
115
+ * judgement is shown. The host draws the record and opens it with only the values it did not decide.
116
+ */
117
+ audit: number;
118
+ /** Whether the record is being audited: `audit` is a probability above 0 and at most 1. */
119
+ get auditing(): boolean;
120
+ /** The promise draw of the suggestion each field shows: whether its promise is shown. */
121
+ private promised;
85
122
  /** The values the form held when it was last looked at, to tell which fields' values arrived since. */
86
123
  private seen;
87
- constructor(id: string, opened: Values, rate?: number, random?: () => number, strings?: Strings,
124
+ constructor(id: string, opened: Values, rate?: number, random?: () => number,
125
+ /** The words the form shows; a host may change them while a record is written, and the next round says them. */
126
+ strings?: Strings,
88
127
  /** A steady clock in milliseconds (`performance.now()` by default): the record's times are read from it. */
89
128
  clock?: () => number);
90
129
  /**
@@ -133,6 +172,15 @@ export declare class Session {
133
172
  saved(values: Values): void;
134
173
  /** Whether the field's answer is held back now — drawn the first time a checkable value is proposed for it. */
135
174
  private draw;
175
+ /** How often promises are shown: `promises` when it is one, else never. */
176
+ private promiseRate;
177
+ /** Whether the promise of the field's suggestion `value` is shown — drawn the first time that suggestion is shown. */
178
+ private promiseShown;
179
+ /**
180
+ * A promise as a count out of ten, rounded down: a promise is never said higher than the records bear it out, and a
181
+ * count reads more plainly than a percentage.
182
+ */
183
+ private promiseNote;
136
184
  /** Where the suggestion comes from, in words. A field's value is shown as the record holds it now. */
137
185
  private note;
138
186
  }
package/dist/session.js CHANGED
@@ -3,8 +3,20 @@ export const englishStrings = {
3
3
  decline: 'Not this',
4
4
  fromValue: 'As before with {field} {value}',
5
5
  fromSimilar: 'As in a similar record',
6
- fromJudgement: 'Suggested from what was written',
6
+ fromJudgement: 'Judged from this record’s values — no settled record answers it',
7
7
  fromWriting: 'From what you wrote',
8
+ abstain: {
9
+ off: 'Yours to decide — this form suggests nothing here',
10
+ 'no-evidence': 'No suggestion yet — nothing settled bears on it',
11
+ unsure: 'No suggestion — the records are not sure enough',
12
+ 'not-allowed': 'No suggestion among the choices you may pick',
13
+ 'needs-judgement': 'Needs your judgement — the records cannot tell',
14
+ 'not-a-choice': 'No suggestion — the value given was not one of the choices',
15
+ malformed: 'No suggestion — the value given does not fit this field',
16
+ together: 'No suggestion until the fields decided with it are settled',
17
+ },
18
+ judgedNothing: 'Needs your judgement — neither the records nor a judgement gave a suggestion',
19
+ promise: 'right {right} times in 10 like this',
8
20
  };
9
21
  /**
10
22
  * One record being written: its draft, and what its fields show. A field's answer may be held back once, at random,
@@ -12,7 +24,13 @@ export const englishStrings = {
12
24
  * is looked at, and only for a value the decider suggests.
13
25
  */
14
26
  export class Session {
15
- constructor(id, opened, rate = 0.05, random = Math.random, strings = englishStrings,
27
+ /** Whether the record is being audited: `audit` is a probability above 0 and at most 1. */
28
+ get auditing() {
29
+ return this.audit > 0 && this.audit <= 1;
30
+ }
31
+ constructor(id, opened, rate = 0.05, random = Math.random,
32
+ /** The words the form shows; a host may change them while a record is written, and the next round says them. */
33
+ strings = englishStrings,
16
34
  /** A steady clock in milliseconds (`performance.now()` by default): the record's times are read from it. */
17
35
  clock = () => performance.now()) {
18
36
  this.id = id;
@@ -25,8 +43,30 @@ export class Session {
25
43
  this.filled = new Set();
26
44
  /** What filling from the person's writing offers, by field, until the person picks it or turns it down. */
27
45
  this.written = new Map();
46
+ /** Why filling from the person's writing gave a field nothing to offer, by field, until it offers one. */
47
+ this.unfilled = new Map();
28
48
  /** The judgement asked for each field, by what it reads; `null` while the host is still judging. */
29
49
  this.judgements = new Map();
50
+ /**
51
+ * Whether a suggestion the engine allows to go in unasked (`prefill`) is put in as the field's starting value. Off, it
52
+ * stays beside the field until the person takes it.
53
+ */
54
+ this.prefills = true;
55
+ /**
56
+ * How often the promise of a suggestion is shown beside it, from 0 (never — the default) to 1 (always). Drawn once for
57
+ * each suggestion a field shows, and the draw goes into the draft — so the records can tell whether seeing it changed
58
+ * what people settled. A value outside 0 to 1 shows none.
59
+ */
60
+ this.promises = 0;
61
+ /**
62
+ * The probability this record was drawn with to be settled again by a second person — an audit — or 0 (the default)
63
+ * for a record written as usual. Above 0 and at most 1, the person is shown nothing the first settler or the decider
64
+ * said: every decided field's answer is held back with this probability (a check), and no other suggestion, note or
65
+ * judgement is shown. The host draws the record and opens it with only the values it did not decide.
66
+ */
67
+ this.audit = 0;
68
+ /** The promise draw of the suggestion each field shows: whether its promise is shown. */
69
+ this.promised = new Map();
30
70
  this.draft = new Draft(opened);
31
71
  this.draft.openAt(this.clock());
32
72
  this.seen = { ...this.draft.opened };
@@ -63,29 +103,53 @@ export class Session {
63
103
  if (proposal.suggestion && !this.written.has(proposal.field))
64
104
  this.draw(proposal);
65
105
  }
106
+ // Audited, the record shows nothing anyone said of it: answers that can be checked are held, the rest go unshown.
107
+ if (this.auditing)
108
+ return { fieldStates: {}, prefill: {} };
66
109
  // A field held back for a check holds back the open fields of its group: the person settling it with nothing in view
67
110
  // is not shown what is suggested alongside it. Only the held field is the check.
68
111
  const held = proposals.filter((p) => this.draft.held(p.field) !== undefined);
69
112
  const hidden = (p) => held.some((h) => h.field === p.field || (h.together != null && h.together === p.together));
70
113
  for (const proposal of proposals) {
71
114
  const { field, suggestion } = proposal;
72
- if (!suggestion || this.written.has(field))
115
+ if (this.written.has(field))
73
116
  continue;
74
117
  if (hidden(proposal))
75
118
  continue;
76
- // Turned down, it stays down for this record: neither shown again nor put back in.
77
- if (this.draft.declined(field, suggestion))
119
+ if (!suggestion) {
120
+ // Why the field is empty, said beside it: silent, it would read as a field nothing is suggested for, and a
121
+ // person passes over it. A field that already holds a value needs no word.
122
+ const judged = this.judgements.get(field)?.proposal === proposal;
123
+ // A field that asks for a judgement on nothing yet has not been looked at: there is nothing to say of it.
124
+ const early = proposal.abstain === 'needs-judgement' && (proposal.asks?.length ?? 0) === 0;
125
+ const why = this.unfilled.get(field) ??
126
+ (judged ? this.strings.judgedNothing : proposal.abstain && !early ? this.strings.abstain[proposal.abstain] : '');
127
+ if (why && isEmpty(values[field]))
128
+ round.fieldStates[field] = { note: why };
78
129
  continue;
130
+ }
131
+ // Turned down, it stays down for this record — whatever was suggested between: neither shown again nor put back in.
132
+ // The draft takes it as the suggestion in view, turned down, so the record says what was last offered.
79
133
  this.draft.show(field, suggestion, proposal);
134
+ if (this.draft.declined(field, suggestion))
135
+ continue;
136
+ // The answers that carry a promise say in the record whether it was shown beside them, and with what chance.
137
+ const promised = proposal.promise !== undefined && (suggestion.origin ?? 'decider') === 'decider';
138
+ const shown = promised && this.promiseShown(field, suggestion.value);
139
+ if (promised)
140
+ this.draft.showPromise(field, shown, this.promiseRate());
80
141
  this.draft.reach(field, this.clock());
81
- if (proposal.prefill && isEmpty(values[field])) {
142
+ if (this.prefills && proposal.prefill && isEmpty(values[field])) {
82
143
  round.prefill[field] = suggestion.value;
83
144
  this.filled.add(field);
84
145
  }
146
+ const note = [this.note(proposal, values), shown ? this.promiseNote(proposal.promise) : undefined]
147
+ .filter((part) => part)
148
+ .join(' · ');
85
149
  round.fieldStates[field] = {
86
150
  suggestions: [suggestion.value],
87
151
  decline: this.strings.decline,
88
- ...(this.note(proposal, values) ? { note: this.note(proposal, values) } : {}),
152
+ ...(note ? { note } : {}),
89
153
  };
90
154
  }
91
155
  return round;
@@ -138,14 +202,21 @@ export class Session {
138
202
  * on its own, in place of what the records suggest for the field. Answers what every field shows now.
139
203
  */
140
204
  applyFill(proposals) {
141
- for (const { field, suggestion } of proposals) {
142
- if (!suggestion)
205
+ for (const { field, suggestion, abstain } of proposals) {
206
+ if (!suggestion) {
207
+ // A value was given for the field that it cannot take: said beside the field, not dropped in silence.
208
+ const why = abstain ? this.strings.abstain[abstain] : '';
209
+ if (why)
210
+ this.unfilled.set(field, why);
143
211
  continue;
212
+ }
213
+ this.unfilled.delete(field);
144
214
  this.draft.show(field, suggestion);
145
215
  this.filled.delete(field);
146
216
  this.written.set(field, { suggestions: [suggestion.value], decline: this.strings.decline, note: this.strings.fromWriting });
147
217
  }
148
- return Object.fromEntries(this.written);
218
+ const unfilled = [...this.unfilled].filter(([field]) => !this.written.has(field));
219
+ return { ...Object.fromEntries(this.written), ...Object.fromEntries(unfilled.map(([field, note]) => [field, { note }])) };
149
220
  }
150
221
  /** The person picked a field's offered value: what was offered from their writing has done its part. */
151
222
  picked(field) {
@@ -170,6 +241,7 @@ export class Session {
170
241
  this.draft.openAt(this.clock());
171
242
  this.seen = { ...this.draft.opened };
172
243
  this.drawn.clear();
244
+ this.promised.clear();
173
245
  this.filled.clear();
174
246
  this.written.clear();
175
247
  this.judgements.clear();
@@ -185,12 +257,38 @@ export class Session {
185
257
  if (!checkable)
186
258
  return false;
187
259
  this.drawn.add(field);
260
+ if (this.auditing) {
261
+ this.draft.hold(field, this.audit, { ...proposal, suggestion });
262
+ return true;
263
+ }
188
264
  if (this.rate > 0 && this.rate <= 1 && this.random() < this.rate) {
189
265
  this.draft.hold(field, this.rate, { ...proposal, suggestion });
190
266
  return true;
191
267
  }
192
268
  return false;
193
269
  }
270
+ /** How often promises are shown: `promises` when it is one, else never. */
271
+ promiseRate() {
272
+ return this.promises >= 0 && this.promises <= 1 ? this.promises : 0;
273
+ }
274
+ /** Whether the promise of the field's suggestion `value` is shown — drawn the first time that suggestion is shown. */
275
+ promiseShown(field, value) {
276
+ const drawn = this.promised.get(field);
277
+ if (drawn?.value === value)
278
+ return drawn.shown;
279
+ const rate = this.promiseRate();
280
+ const shown = rate >= 1 || (rate > 0 && this.random() < rate);
281
+ this.promised.set(field, { value, shown });
282
+ return shown;
283
+ }
284
+ /**
285
+ * A promise as a count out of ten, rounded down: a promise is never said higher than the records bear it out, and a
286
+ * count reads more plainly than a percentage.
287
+ */
288
+ promiseNote(promise) {
289
+ const right = Math.max(0, Math.min(10, Math.floor(promise * 10 + 1e-9)));
290
+ return this.strings.promise.replace('{right}', String(right));
291
+ }
194
292
  /** Where the suggestion comes from, in words. A field's value is shown as the record holds it now. */
195
293
  note(proposal, values) {
196
294
  const first = proposal.basis?.[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/lowline-form",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "The Lowline form element: a formdown form whose decided fields are suggested from the records people confirmed, talking to a Lowline host's routes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,12 +40,12 @@
40
40
  "bundle": "vite build --config vite.bundle.config.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@formdown/ui": "^0.26.0",
43
+ "@formdown/ui": "^0.26.1",
44
44
  "lit": "^3.3.3"
45
45
  },
46
46
  "devDependencies": {
47
- "typescript": "^6.0.3",
48
- "vite": "^8.3.1",
49
- "vitest": "^5.0.2"
47
+ "typescript": "^7.0.2",
48
+ "vite": "^8.3.6",
49
+ "vitest": "^5.1.0"
50
50
  }
51
51
  }