@supersuit/suggest-a-change 0.1.4 → 0.1.6
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/CHANGELOG.md +8 -0
- package/dist/client/detail.d.ts +1 -1
- package/dist/client/detail.js +1 -1
- package/dist/client/outbox.d.ts +2 -0
- package/dist/client/outbox.js +5 -2
- package/dist/client/words.js +2 -2
- package/dist/contract.mjs +9 -9
- package/package.json +1 -1
- package/src/client/detail.tsx +1 -1
- package/src/client/outbox.ts +7 -2
- package/src/client/words.ts +2 -2
- package/src/contract.mjs +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
- **Writes made in the same millisecond leave the phone in the order they were made.** `enqueue` stamps each op with `seq`, and the queue sorts by `at`, then `seq`, then id. Before, ties fell to the id, so a screen that saved a draft and then asked for it to be prepared in one tick sent the prepare first about half the time, and the receiver prepared the draft as it had been.
|
|
6
|
+
|
|
7
|
+
## 0.1.5 (2026-10-14)
|
|
8
|
+
|
|
9
|
+
- **The agent is Pluma.** Every word a person reads now names Pluma, the agent formerly called Freeda: "With Pluma", "Goes to <owner> and Pluma", "Pluma picks it up". The status key `with-freeda` is unchanged, so stored reports and older clients still read. A copy of the contract vendored elsewhere must be taken again: these are new bytes.
|
|
10
|
+
|
|
3
11
|
## 0.1.4 (2026-10-14)
|
|
4
12
|
|
|
5
13
|
- **The owner may decline a report Freeda has not planned.** `FLOW["with-freeda"].next` and `FLOW["planned"].next` now include `"not-now"`, so a report the reading worker cannot plan has an exit instead of showing "With Freeda" for ever. `agentStatus` accepts both moves (they take the approval, as any end does). A copy of the contract vendored elsewhere must be taken again: these are new bytes.
|
package/dist/client/detail.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Adapters } from './adapters.ts';
|
|
|
2
2
|
export declare function statusOf(r: Record<string, unknown>): string;
|
|
3
3
|
/** The step shown under "Being made". The timeline entry is the truth (that is where the receiving side puts it); a top-level `step` is read only when there is no timeline. */
|
|
4
4
|
export declare function stepOf(r: Record<string, unknown>): string | undefined;
|
|
5
|
-
/** The strip is made from the contract's own statuses, one step per distinct word ("planned" reads as "With
|
|
5
|
+
/** The strip is made from the contract's own statuses, one step per distinct word ("planned" reads as "With Pluma"),
|
|
6
6
|
* and a step is marked from the report's timeline: reached, skipped (the report went past it), or still ahead. */
|
|
7
7
|
export declare function StatusStrip({ status, owner, step, timeline, label }: {
|
|
8
8
|
status: string;
|
package/dist/client/detail.js
CHANGED
|
@@ -34,7 +34,7 @@ export function stepOf(r) {
|
|
|
34
34
|
return typeof r.step === 'string' ? r.step : undefined;
|
|
35
35
|
}
|
|
36
36
|
const timelineOf = (r) => Array.isArray(r?.timeline) ? r.timeline.map((t) => String(t?.status)) : undefined;
|
|
37
|
-
/** The strip is made from the contract's own statuses, one step per distinct word ("planned" reads as "With
|
|
37
|
+
/** The strip is made from the contract's own statuses, one step per distinct word ("planned" reads as "With Pluma"),
|
|
38
38
|
* and a step is marked from the report's timeline: reached, skipped (the report went past it), or still ahead. */
|
|
39
39
|
export function StatusStrip({ status, owner, step, timeline, label }) {
|
|
40
40
|
const nodes = useMemo(() => {
|
package/dist/client/outbox.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface Op {
|
|
|
9
9
|
/** The id of the write this one belongs to (a part's report or follow-up). While that write is refused, this one is held, not sent. */
|
|
10
10
|
parent?: string;
|
|
11
11
|
at: number;
|
|
12
|
+
/** The order this phone made it in, among writes with the same `at`. Written by enqueue. */
|
|
13
|
+
seq?: number;
|
|
12
14
|
error?: string;
|
|
13
15
|
}
|
|
14
16
|
export type Sent = 'done' | {
|
package/dist/client/outbox.js
CHANGED
|
@@ -5,8 +5,11 @@ export function makeOutbox({ store, send, reach }) {
|
|
|
5
5
|
// the new write existed. Measured on the phone: without this, a second answer sat on the phone
|
|
6
6
|
// until the 15 s tick.
|
|
7
7
|
let again = false;
|
|
8
|
+
// Two writes in one millisecond (a save, then the step that reads it) keep the order they were made
|
|
9
|
+
// in. Ordering them by id instead sent the second first about half the time.
|
|
10
|
+
let made = 0;
|
|
8
11
|
async function pending() {
|
|
9
|
-
return (await store.all('outbox')).map((r) => r.value).sort((a, b) => a.at - b.at || a.id.localeCompare(b.id));
|
|
12
|
+
return (await store.all('outbox')).map((r) => r.value).sort((a, b) => a.at - b.at || (a.seq ?? 0) - (b.seq ?? 0) || a.id.localeCompare(b.id));
|
|
10
13
|
}
|
|
11
14
|
async function tell(landed) {
|
|
12
15
|
const all = await pending();
|
|
@@ -56,7 +59,7 @@ export function makeOutbox({ store, send, reach }) {
|
|
|
56
59
|
}
|
|
57
60
|
return {
|
|
58
61
|
pending,
|
|
59
|
-
async enqueue(op) { await store.put('outbox', op.id, op); await tell(); },
|
|
62
|
+
async enqueue(op) { await store.put('outbox', op.id, { ...op, seq: op.seq ?? ++made }); await tell(); },
|
|
60
63
|
drain() {
|
|
61
64
|
if (running) {
|
|
62
65
|
again = true;
|
package/dist/client/words.js
CHANGED
|
@@ -27,7 +27,7 @@ export const DEFAULT_STRINGS = {
|
|
|
27
27
|
takeScreenshot: 'Take a screenshot of this screen',
|
|
28
28
|
remove: 'Remove',
|
|
29
29
|
withLegend: 'What goes with it',
|
|
30
|
-
goesTo: (owner) => `Goes to ${owner} and
|
|
30
|
+
goesTo: (owner) => `Goes to ${owner} and Pluma.`,
|
|
31
31
|
includes: (parts) => `Includes ${list(parts)}.`,
|
|
32
32
|
screenName: "this screen's name",
|
|
33
33
|
alwaysSent: (screen, route) => `Always sent: the screen you are on (${screen}) and its address (${route}).`,
|
|
@@ -113,7 +113,7 @@ export function statusMeaning(status, owner) {
|
|
|
113
113
|
/** The receipt's one line (design section 2). `received` is only ever asked for after the adapter said `done`. */
|
|
114
114
|
export function receiptWords(state, words) {
|
|
115
115
|
if (state === 'received')
|
|
116
|
-
return `Got it.
|
|
116
|
+
return `Got it. Pluma picks it up next time ${words.owner}'s Mac is on.`;
|
|
117
117
|
if (state === 'on-mac')
|
|
118
118
|
return 'On your Mac.';
|
|
119
119
|
return 'On this phone, waiting to send.';
|
package/dist/contract.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/contract.mjs (@supersuit/suggest-a-change)
|
|
2
2
|
// THE REPORT CONTRACT AND THE FLOW MAP for Suggest a change (design sections 4, 5 and 7). ZERO
|
|
3
3
|
// imports on purpose, so it can be vendored whole where an install step does not exist: the phone, the
|
|
4
|
-
// hosted apps and
|
|
4
|
+
// hosted apps and Pluma's inbox all validate against this one copy. The flow lives here, with who acts
|
|
5
5
|
// at each step, so what a person sees cannot drift from what happened.
|
|
6
6
|
export const KINDS = ["change", "broken", "feedback"];
|
|
7
7
|
export const ROLES = ["operator", "member", "maintainer"];
|
|
@@ -13,9 +13,9 @@ export const CEILINGS = {
|
|
|
13
13
|
};
|
|
14
14
|
export const STATUSES = ["on-phone", "received", "with-freeda", "planned", "waiting-on-owner", "being-made", "live", "sent-to-template", "filed", "not-now"];
|
|
15
15
|
export const END = ["live", "sent-to-template", "filed", "not-now"];
|
|
16
|
-
// actor: who PERFORMS this step (design section 7): sent by the person, received by the app, with
|
|
17
|
-
// the inbox, so a report may go from the phone to
|
|
18
|
-
// ("not-now") before
|
|
16
|
+
// actor: who PERFORMS this step (design section 7): sent by the person, received by the app, with Pluma by the Mac. A Helm on the owner's Mac posts straight into
|
|
17
|
+
// the inbox, so a report may go from the phone to Pluma without the hosted app's "received". The owner may decline
|
|
18
|
+
// ("not-now") before Pluma has a plan or before she asks: a report she cannot plan would otherwise have no exit.
|
|
19
19
|
export const FLOW = {
|
|
20
20
|
"on-phone": { actor: "person", next: ["received", "with-freeda"] },
|
|
21
21
|
"received": { actor: "app", next: ["with-freeda"] },
|
|
@@ -29,14 +29,14 @@ export const FLOW = {
|
|
|
29
29
|
"not-now": { actor: "owner", next: [] },
|
|
30
30
|
};
|
|
31
31
|
export const mayMove = (from, to) => Boolean(FLOW[from]?.next.includes(to));
|
|
32
|
-
// The design's status table (section 2), word for word. "planned" is a step inside
|
|
33
|
-
// that the table does not name for a person, so it reads as "With
|
|
32
|
+
// The design's status table (section 2), word for word. "planned" is a step inside Pluma's work
|
|
33
|
+
// that the table does not name for a person, so it reads as "With Pluma".
|
|
34
34
|
const TABLE = {
|
|
35
35
|
"on-phone": ["On this phone", () => "Not sent yet; it will send when there is a connection"],
|
|
36
36
|
"received": ["Received", () => "The app has it, safely stored"],
|
|
37
|
-
"with-freeda": ["With
|
|
38
|
-
"planned": ["With
|
|
39
|
-
"waiting-on-owner": [null, (o) => `
|
|
37
|
+
"with-freeda": ["With Pluma", (o) => `${o}'s Mac has picked it up`],
|
|
38
|
+
"planned": ["With Pluma", (o) => `${o}'s Mac has picked it up`],
|
|
39
|
+
"waiting-on-owner": [null, (o) => `Pluma has a plan and is asking ${o}`],
|
|
40
40
|
"being-made": ["Being made", () => "A session is working on it; the current step shows underneath"],
|
|
41
41
|
"live": ["Done", () => "Shipped in the app, with the date it went live"],
|
|
42
42
|
"sent-to-template": ["Sent to the template", () => "It became a change to the template every copy of this app uses; it reaches this app once merged"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/suggest-a-change",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "The report format and flow behind a Suggest a change button: what a report is, who acts at each step, the screens, and the check every app runs. The contract has zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/client/detail.tsx
CHANGED
|
@@ -30,7 +30,7 @@ export function stepOf(r: Record<string, unknown>): string | undefined {
|
|
|
30
30
|
const timelineOf = (r?: Record<string, unknown>): string[] | undefined =>
|
|
31
31
|
Array.isArray(r?.timeline) ? (r!.timeline as { status?: unknown }[]).map((t) => String(t?.status)) : undefined
|
|
32
32
|
|
|
33
|
-
/** The strip is made from the contract's own statuses, one step per distinct word ("planned" reads as "With
|
|
33
|
+
/** The strip is made from the contract's own statuses, one step per distinct word ("planned" reads as "With Pluma"),
|
|
34
34
|
* and a step is marked from the report's timeline: reached, skipped (the report went past it), or still ahead. */
|
|
35
35
|
export function StatusStrip({ status, owner, step, timeline, label }: { status: string; owner: string; step?: string; timeline?: string[]; /** The list's accessible name; the screens pass the app's own (words.text.stepLabel). */ label?: string }) {
|
|
36
36
|
const nodes = useMemo(() => {
|
package/src/client/outbox.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface Op {
|
|
|
18
18
|
/** The id of the write this one belongs to (a part's report or follow-up). While that write is refused, this one is held, not sent. */
|
|
19
19
|
parent?: string
|
|
20
20
|
at: number
|
|
21
|
+
/** The order this phone made it in, among writes with the same `at`. Written by enqueue. */
|
|
22
|
+
seq?: number
|
|
21
23
|
error?: string
|
|
22
24
|
}
|
|
23
25
|
export type Sent = 'done' | { refused: string }
|
|
@@ -30,9 +32,12 @@ export function makeOutbox({ store, send, reach }: { store: Store; send(op: Op):
|
|
|
30
32
|
// the new write existed. Measured on the phone: without this, a second answer sat on the phone
|
|
31
33
|
// until the 15 s tick.
|
|
32
34
|
let again = false
|
|
35
|
+
// Two writes in one millisecond (a save, then the step that reads it) keep the order they were made
|
|
36
|
+
// in. Ordering them by id instead sent the second first about half the time.
|
|
37
|
+
let made = 0
|
|
33
38
|
|
|
34
39
|
async function pending(): Promise<Op[]> {
|
|
35
|
-
return (await store.all<Op>('outbox')).map((r) => r.value).sort((a, b) => a.at - b.at || a.id.localeCompare(b.id))
|
|
40
|
+
return (await store.all<Op>('outbox')).map((r) => r.value).sort((a, b) => a.at - b.at || (a.seq ?? 0) - (b.seq ?? 0) || a.id.localeCompare(b.id))
|
|
36
41
|
}
|
|
37
42
|
async function tell(landed?: string) {
|
|
38
43
|
const all = await pending()
|
|
@@ -69,7 +74,7 @@ export function makeOutbox({ store, send, reach }: { store: Store; send(op: Op):
|
|
|
69
74
|
}
|
|
70
75
|
return {
|
|
71
76
|
pending,
|
|
72
|
-
async enqueue(op: Op) { await store.put('outbox', op.id, op); await tell() },
|
|
77
|
+
async enqueue(op: Op) { await store.put('outbox', op.id, { ...op, seq: op.seq ?? ++made }); await tell() },
|
|
73
78
|
drain(): Promise<void> {
|
|
74
79
|
if (running) { again = true; return running }
|
|
75
80
|
return (running = run().finally(() => { running = null }))
|
package/src/client/words.ts
CHANGED
|
@@ -122,7 +122,7 @@ export const DEFAULT_STRINGS: Strings = {
|
|
|
122
122
|
takeScreenshot: 'Take a screenshot of this screen',
|
|
123
123
|
remove: 'Remove',
|
|
124
124
|
withLegend: 'What goes with it',
|
|
125
|
-
goesTo: (owner) => `Goes to ${owner} and
|
|
125
|
+
goesTo: (owner) => `Goes to ${owner} and Pluma.`,
|
|
126
126
|
includes: (parts) => `Includes ${list(parts)}.`,
|
|
127
127
|
screenName: "this screen's name",
|
|
128
128
|
alwaysSent: (screen, route) => `Always sent: the screen you are on (${screen}) and its address (${route}).`,
|
|
@@ -202,7 +202,7 @@ export type ReceiptState = 'on-phone' | 'received' | 'on-mac'
|
|
|
202
202
|
|
|
203
203
|
/** The receipt's one line (design section 2). `received` is only ever asked for after the adapter said `done`. */
|
|
204
204
|
export function receiptWords(state: ReceiptState, words: Pick<Words, 'owner'>): string {
|
|
205
|
-
if (state === 'received') return `Got it.
|
|
205
|
+
if (state === 'received') return `Got it. Pluma picks it up next time ${words.owner}'s Mac is on.`
|
|
206
206
|
if (state === 'on-mac') return 'On your Mac.'
|
|
207
207
|
return 'On this phone, waiting to send.'
|
|
208
208
|
}
|
package/src/contract.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/contract.mjs (@supersuit/suggest-a-change)
|
|
2
2
|
// THE REPORT CONTRACT AND THE FLOW MAP for Suggest a change (design sections 4, 5 and 7). ZERO
|
|
3
3
|
// imports on purpose, so it can be vendored whole where an install step does not exist: the phone, the
|
|
4
|
-
// hosted apps and
|
|
4
|
+
// hosted apps and Pluma's inbox all validate against this one copy. The flow lives here, with who acts
|
|
5
5
|
// at each step, so what a person sees cannot drift from what happened.
|
|
6
6
|
|
|
7
7
|
export const KINDS = ["change", "broken", "feedback"];
|
|
@@ -17,9 +17,9 @@ export const CEILINGS = {
|
|
|
17
17
|
export const STATUSES = ["on-phone", "received", "with-freeda", "planned", "waiting-on-owner", "being-made", "live", "sent-to-template", "filed", "not-now"];
|
|
18
18
|
export const END = ["live", "sent-to-template", "filed", "not-now"];
|
|
19
19
|
|
|
20
|
-
// actor: who PERFORMS this step (design section 7): sent by the person, received by the app, with
|
|
21
|
-
// the inbox, so a report may go from the phone to
|
|
22
|
-
// ("not-now") before
|
|
20
|
+
// actor: who PERFORMS this step (design section 7): sent by the person, received by the app, with Pluma by the Mac. A Helm on the owner's Mac posts straight into
|
|
21
|
+
// the inbox, so a report may go from the phone to Pluma without the hosted app's "received". The owner may decline
|
|
22
|
+
// ("not-now") before Pluma has a plan or before she asks: a report she cannot plan would otherwise have no exit.
|
|
23
23
|
export const FLOW = {
|
|
24
24
|
"on-phone": { actor: "person", next: ["received", "with-freeda"] },
|
|
25
25
|
"received": { actor: "app", next: ["with-freeda"] },
|
|
@@ -35,14 +35,14 @@ export const FLOW = {
|
|
|
35
35
|
|
|
36
36
|
export const mayMove = (from, to) => Boolean(FLOW[from]?.next.includes(to));
|
|
37
37
|
|
|
38
|
-
// The design's status table (section 2), word for word. "planned" is a step inside
|
|
39
|
-
// that the table does not name for a person, so it reads as "With
|
|
38
|
+
// The design's status table (section 2), word for word. "planned" is a step inside Pluma's work
|
|
39
|
+
// that the table does not name for a person, so it reads as "With Pluma".
|
|
40
40
|
const TABLE = {
|
|
41
41
|
"on-phone": ["On this phone", () => "Not sent yet; it will send when there is a connection"],
|
|
42
42
|
"received": ["Received", () => "The app has it, safely stored"],
|
|
43
|
-
"with-freeda": ["With
|
|
44
|
-
"planned": ["With
|
|
45
|
-
"waiting-on-owner": [null, (o) => `
|
|
43
|
+
"with-freeda": ["With Pluma", (o) => `${o}'s Mac has picked it up`],
|
|
44
|
+
"planned": ["With Pluma", (o) => `${o}'s Mac has picked it up`],
|
|
45
|
+
"waiting-on-owner": [null, (o) => `Pluma has a plan and is asking ${o}`],
|
|
46
46
|
"being-made": ["Being made", () => "A session is working on it; the current step shows underneath"],
|
|
47
47
|
"live": ["Done", () => "Shipped in the app, with the date it went live"],
|
|
48
48
|
"sent-to-template": ["Sent to the template", () => "It became a change to the template every copy of this app uses; it reaches this app once merged"],
|