@augurworks/augur 0.15.2 → 0.15.4

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.
@@ -1,107 +0,0 @@
1
- // Working marks, client side — one definition of the path spelling and one of the phrasing.
2
- //
3
- // `F-presence-marks`. Three commands surface marks (`mark`, `status`, `pull`) and a fourth
4
- // will. If each spelled a path its own way, two agents naming the same folder would write
5
- // two rows and read past each other — which is the exact failure the feature exists to
6
- // prevent, arriving through the tool that was supposed to prevent it. So the normalization
7
- // here MIRRORS `normalizeMarkPath` in src/_worker.js on purpose, and the server's answer is
8
- // always the one printed back: the client never assumes its own spelling won.
9
- //
10
- // ⚠️ A MARK REFUSES NOTHING. Nothing in this file returns a verdict, sets an exit code, or
11
- // gives a caller something to branch on that would let it block. It reads, and it prints.
12
-
13
- /** Leading and trailing slash. Same rule as the worker, for the same containment reason. */
14
- export function normalizeMarkPath(p) {
15
- const s = String(p == null ? "" : p).trim().slice(0, 300);
16
- if (!s) return "";
17
- const t = s.replace(/^\.\//, "").replace(/\/{2,}/g, "/");
18
- if (!t || t === "/") return "/";
19
- return `/${t.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
20
- }
21
-
22
- /**
23
- * A REPO folder, as the URL it publishes to.
24
- *
25
- * `<project>/prototypes/<name>` is the nesting `discoverSpaces()` looks in, and it is
26
- * served at `/<project>/<name>/`. An agent has just been editing the folder, so it is the
27
- * folder it will type; taking it without translation would mark a path no card and no
28
- * published unit will ever match.
29
- */
30
- export function markPathFor(input) {
31
- return normalizeMarkPath(String(input == null ? "" : input).replace(/\/prototypes\//g, "/"));
32
- }
33
-
34
- /** Does either path contain the other? The whole overlap test. */
35
- export function marksOverlap(a, b) {
36
- const x = normalizeMarkPath(a), y = normalizeMarkPath(b);
37
- if (!x || !y) return false;
38
- return x === y || x.startsWith(y) || y.startsWith(x);
39
- }
40
-
41
- /**
42
- * Of the marks that were there before you wrote yours, whose are worth telling you about.
43
- *
44
- * ⚠️ "SOMEBODY ELSE" IS DECIDED BY WHO, NEVER BY WHERE, and this function exists so that
45
- * decision has somewhere to be tested. The obvious way to stop your own renewal warning at
46
- * you is to drop the exact path from the list — and that silently drops the ONE case the
47
- * whole feature exists to surface: two agents on the same prototype. It shipped that way
48
- * once and printed nothing at all for an exact collision, which is worse than not having
49
- * the warning, because it reads as an all-clear.
50
- *
51
- * `mine` is the id the INSTANCE resolved from the credential and handed back, never one the
52
- * client worked out for itself — the same rule the row's authorship follows.
53
- */
54
- export function othersOverlapping(before, path, mine) {
55
- return (before || []).filter((m) => m && m.personId !== mine && marksOverlap(m.path, path));
56
- }
57
-
58
- /**
59
- * Every live mark at an instance. NEVER THROWS: a `status` or a `pull` that died because
60
- * the coordination note could not be fetched would make the note the most fragile thing in
61
- * the toolchain. An older instance answers 404 and gets an empty list, which reads exactly
62
- * like "nobody is working on anything" — and is the right answer there, because on that
63
- * instance nobody can be.
64
- */
65
- export async function fetchMarks(req) {
66
- try {
67
- const r = await req("_marks/list");
68
- const body = await r.json();
69
- return Array.isArray(body.marks) ? body.marks : [];
70
- } catch (e) { return []; }
71
- }
72
-
73
- const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
74
-
75
- /** "4 minutes ago" / "just now", from a millisecond age. */
76
- export function since(ms) {
77
- const s = Math.max(0, Math.round(ms / 1000));
78
- if (s < 45) return "just now";
79
- if (s < 5400) return `${plural(Math.round(s / 60), "minute")} ago`;
80
- return `${plural(Math.round(s / 3600), "hour")} ago`;
81
- }
82
-
83
- /**
84
- * "for another 6 minutes", from a millisecond remainder.
85
- *
86
- * Switches to hours at exactly 3600s rather than at the 90 minutes `since` uses, because
87
- * a mark's ceiling IS an hour: at the other threshold the longest mark anybody can ask
88
- * for would read "for another 60 minutes", and the hours branch could never fire at all.
89
- */
90
- export function forAnother(ms) {
91
- const s = Math.max(0, Math.round(ms / 1000));
92
- if (s < 60) return `for another ${plural(s, "second")}`;
93
- if (s < 3600) return `for another ${plural(Math.round(s / 60), "minute")}`;
94
- return `for another ${plural(Math.round(s / 3600), "hour")}`;
95
- }
96
-
97
- /**
98
- * One line per mark. `by` is null when the id behind the mark resolves to nobody on the
99
- * roster — a token an admin labelled by hand, or somebody who has since left — and
100
- * "Someone" is the honest rendering of that, never a guess.
101
- */
102
- export function markLine(m) {
103
- const who = m.by || "Someone";
104
- const started = Date.parse(m.startedAt);
105
- const age = Number.isFinite(started) ? since(Date.now() - started) : "";
106
- return `${m.path} ${who}${age ? ` · started ${age}` : ""} · ${forAnother(m.expiresIn)}`;
107
- }
package/scripts/mark.mjs DELETED
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * augur mark — say what you are about to work on, and read what everyone else is.
4
- *
5
- * augur mark what is being worked on right now
6
- * augur mark <path> [--ttl <s>] leave a mark on it, then start
7
- * augur mark <path> --clear take yours down early
8
- * augur mark … --json the same answer, for a tool to read
9
- *
10
- * `F-presence-marks`. Nothing anywhere said what was already being worked on. Two agents
11
- * on two machines, both told to improve the checkout flow, would each open the folder,
12
- * each edit it, and find out at publish time — where the answer is a fork and a conflict
13
- * file nobody asked for.
14
- *
15
- * ⚠️ THIS IS NOT A LOCK AND IT MUST NEVER BECOME ONE. Marking a path grants nothing and
16
- * refuses nothing: a marked path can still be edited, published and shipped by anybody, and
17
- * this command exits 0 whatever it finds. The protocol is social and it is one sentence —
18
- * READ THE MARKS BEFORE YOU START, LEAVE ONE WHEN YOU DO. What happens when coordination
19
- * fails anyway is the composed publish's problem, and it settles it on evidence rather than
20
- * on a claim.
21
- *
22
- * ⚠️ AND IT EXPIRES BY ITSELF. A mark carries how long it is good for — ten minutes by
23
- * default, an hour at the most — and the instance stops reporting it the moment that
24
- * passes, whether or not anything ever clears it. That is the point rather than a detail:
25
- * the thing leaving marks is a process that can be killed, and a claim that outlives the
26
- * claimant is worse than no claim at all. `--clear` is a courtesy, never the guarantee.
27
- *
28
- * A path is a URL path (`/checkout/flow/`). A repo folder is accepted and translated, so
29
- * `checkout/prototypes/flow` — the folder you were just editing — marks the URL it
30
- * publishes to. The line printed back is always the instance's own spelling.
31
- */
32
- import { target, apiClient } from "./lib/store.mjs";
33
- import { fetchMarks, markPathFor, othersOverlapping, markLine, forAnother } from "./lib/marks.mjs";
34
-
35
- const C = { dim: "\x1b[2m", ok: "\x1b[32m", warn: "\x1b[33m", off: "\x1b[0m" };
36
- const log = (m) => console.log(`\x1b[35m[mark]\x1b[0m ${m}`);
37
- const die = (m) => { console.error(`\x1b[31m[mark] ${m}\x1b[0m`); process.exit(1); };
38
-
39
- const argv = process.argv.slice(2);
40
- const flag = (n) => argv.includes(n);
41
- const opt = (n, d = null) => { const i = argv.indexOf(n); return i > -1 && argv[i + 1] ? argv[i + 1] : d; };
42
- const JSON_OUT = flag("--json");
43
- const CLEAR = flag("--clear");
44
- const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1] === "--ttl"));
45
-
46
- async function main() {
47
- const { origin, token } = target({ needToken: true });
48
- const req = apiClient(origin, token);
49
-
50
- const raw = positional[0] || "";
51
- const path = raw ? markPathFor(raw) : "";
52
- if (raw && !path) die(`"${raw}" is not a path anything could be working on.`);
53
-
54
- // ── list ──────────────────────────────────────────────────────────────────
55
- if (!path) {
56
- const marks = await fetchMarks(req);
57
- if (JSON_OUT) { console.log(JSON.stringify({ origin, marks }, null, 2)); return; }
58
- if (!marks.length) { log(`${C.dim}nobody is working on anything at ${origin} right now${C.off}`); return; }
59
- log(`being worked on at ${origin}:`);
60
- for (const m of marks) console.log(` ${markLine(m)}`);
61
- console.log(`\n${C.dim}Nothing here stops you. Pick a different path, or go ahead and expect to merge.${C.off}`);
62
- return;
63
- }
64
-
65
- // ── clear ─────────────────────────────────────────────────────────────────
66
- if (CLEAR) {
67
- const r = await req("_marks/clear", {
68
- method: "POST",
69
- headers: { "content-type": "application/json" },
70
- body: JSON.stringify({ path }),
71
- });
72
- const body = await r.json();
73
- if (JSON_OUT) { console.log(JSON.stringify({ origin, path, ...body }, null, 2)); return; }
74
- if (body.cleared) log(`${C.ok}${path} released${C.off}`);
75
- else if (body.reason === "not-yours") log(`${C.dim}${path} is somebody else's mark — left alone. It expires on its own.${C.off}`);
76
- else log(`${C.dim}no mark of yours on ${path}${C.off}`);
77
- return;
78
- }
79
-
80
- // ── set ───────────────────────────────────────────────────────────────────
81
- const ttlArg = opt("--ttl");
82
- const ttl = ttlArg ? Math.round(Number(ttlArg) * 1000) : undefined;
83
- if (ttlArg && !Number.isFinite(ttl)) die(`--ttl takes seconds, not "${ttlArg}".`);
84
-
85
- // WHO ELSE IS ALREADY HERE — read BEFORE writing, because that is the whole protocol and
86
- // an agent that only ever writes is an agent that has learned nothing. Taken before the
87
- // write and not after: the write replaces the row for this path, so a read afterwards
88
- // could no longer see the person this is worth telling you about.
89
- const before = await fetchMarks(req);
90
-
91
- const r = await req("_marks/set", {
92
- method: "POST",
93
- headers: { "content-type": "application/json" },
94
- body: JSON.stringify({ path, ...(ttl ? { ttl } : {}) }),
95
- });
96
- const body = await r.json();
97
- if (body.error) die(`${origin} refused the mark: ${body.error}`);
98
-
99
- // Who was already here that is not you. The rule, and the trap inside it, are on
100
- // `othersOverlapping` — it is a pure function precisely so the trap has a test.
101
- const overlapping = othersOverlapping(before, body.mark.path, body.mark.personId);
102
-
103
- if (JSON_OUT) { console.log(JSON.stringify({ origin, overlapping, ...body }, null, 2)); return; }
104
- log(`${C.ok}${body.mark.path}${C.off} marked ${forAnother(body.mark.expiresIn)}`);
105
- if (overlapping.length) {
106
- console.log(`\n ${C.warn}somebody is already working here${C.off}`);
107
- for (const m of overlapping) console.log(` ${markLine(m)}`);
108
- console.log(`\n${C.dim}Your mark went down anyway — marks never refuse. Pick a different path, wait it out, or carry on knowing you will be merging.${C.off}`);
109
- }
110
- }
111
-
112
- main().catch((e) => die(e && e.stack ? e.stack : String(e)));