clearotron 0.3.0-beta.1 → 0.3.0-beta.2

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 (63) hide show
  1. package/.env.example +13 -2
  2. package/CONTRIBUTING.md +1 -1
  3. package/INSTALL.md +12 -10
  4. package/README.md +3 -2
  5. package/bin/brandowner.mjs +94 -1
  6. package/bin/onboard.mjs +156 -55
  7. package/bin/passphrase.mjs +23 -4
  8. package/bin/start.mjs +177 -38
  9. package/build-info.json +2 -2
  10. package/docs/architecture/04-configuration-reference.md +5 -2
  11. package/docs/architecture/05-config-governance.md +3 -2
  12. package/driver/CHANGELOG.md +95 -0
  13. package/driver/demo-posture.mjs +59 -8
  14. package/driver/driver.config.mjs +36 -7
  15. package/driver/engine/child-record.mjs +93 -0
  16. package/driver/findings-model.mjs +6 -1
  17. package/driver/knockout-assess-record.mjs +6 -3
  18. package/driver/package.json +1 -1
  19. package/driver/portal-local-auth.mjs +98 -3
  20. package/driver/portal-service.mjs +65 -29
  21. package/driver/publish/knockout.mjs +12 -1
  22. package/driver/publish/office-record-links.mjs +61 -10
  23. package/driver/publish/render-knockout.mjs +31 -11
  24. package/driver/publish/xlsx.mjs +33 -1
  25. package/driver/register-records.mjs +6 -0
  26. package/driver/run-requirements.mjs +24 -1
  27. package/driver/runner.mjs +10 -5
  28. package/driver/skills/knockout-assess/SKILL.md +1 -1
  29. package/driver/suite-census.json +174 -30
  30. package/driver/unit-inventory.mjs +109 -5
  31. package/driver/updater-identity.mjs +178 -0
  32. package/driver/usage-ledger.mjs +5 -5
  33. package/mcp-server/CHANGELOG.md +6 -0
  34. package/mcp-server/http-server.mjs +16 -13
  35. package/mcp-server/lib/driver.mjs +7 -0
  36. package/mcp-server/lib/knockout.mjs +14 -2
  37. package/mcp-server/lib/ops.mjs +38 -16
  38. package/mcp-server/package.json +2 -2
  39. package/mcp-server/server.mjs +1 -1
  40. package/package.json +1 -1
  41. package/portal-ui/dist/assets/{index-CWTHP0sH.js → index-CcFjgM78.js} +32 -17
  42. package/portal-ui/dist/assets/{index-KpytsmNH.css → index-CsCuPshD.css} +7 -2
  43. package/portal-ui/dist/index.html +2 -2
  44. package/portal-ui/package.json +3 -3
  45. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  46. package/providers/oauth-mcp-bridge/package.json +2 -2
  47. package/scripts/changelog-plain-language.mjs +7 -30
  48. package/scripts/e2e.mjs +13 -2
  49. package/scripts/env-audit.mjs +10 -3
  50. package/scripts/env-classify.mjs +205 -12
  51. package/scripts/live-surface-check.mjs +74 -2
  52. package/scripts/plain-language-rules.mjs +103 -0
  53. package/scripts/release-note-required.mjs +118 -24
  54. package/scripts/release-notes-lint.mjs +22 -43
  55. package/scripts/release-version.mjs +59 -0
  56. package/scripts/revisit-render-check.mjs +6 -2
  57. package/scripts/text-difference.mjs +22 -0
  58. package/shared/env-local.mjs +25 -2
  59. package/shared/names-in-force.mjs +1 -0
  60. package/shared/reap-on-exit.mjs +27 -14
  61. package/shared/store-in-repo.mjs +147 -0
  62. package/shared/withheld-paths-access.mjs +6 -6
  63. package/shared/yes-no-echo.mjs +35 -0
@@ -362,3 +362,150 @@ export function resolveStoreRepoRoot({ names, fallback = null, env = process.env
362
362
  if (fallback) return { root: fallback, from: "module-relative fallback", tried };
363
363
  return { root: null, from: null, tried };
364
364
  }
365
+
366
+ // ── WHERE A SAVE GOES AFTER IT IS COMMITTED ──────────────────────────────────────────────────────────
367
+ //
368
+ // Everything above ends at a commit. Nothing in this file pushes, and nothing should: what happens to a
369
+ // commit next is a property of the repository the store sits in, not of the save. When that repository's
370
+ // branch tracks a remote one, the next person who syncs or pushes the checkout publishes every save made
371
+ // there, and that person is usually not the one who made them.
372
+ //
373
+ // Measured on a test instance, 2026-09-10: its store sat inside a checkout that people also synced for
374
+ // unrelated work. A company created in the portal was committed there as designed, waited eleven hours
375
+ // as a local commit, and went out with an ordinary pull-then-push. It had happened the evening before,
376
+ // and the only thing that stopped that one was somebody noticing.
377
+ //
378
+ // THIS IS A REPORT, NEVER A REFUSAL. Keeping a store in a repository you push on purpose is a
379
+ // reasonable deployment, and the harm needs a second person doing ordinary git work in the same checkout,
380
+ // which no service can see. So the answer is for whoever reads it (doctor), and no save waits on it.
381
+ //
382
+ // READ FROM THE BRANCH'S TRACKING CONFIGURATION, NOT FROM `@{u}`. The clone of an empty remote has its
383
+ // tracking configured and nothing to resolve `@{u}` to until the first commit, so `@{u}` answers with a
384
+ // usage error for a store whose first save a bare push WOULD publish. The configuration answers the
385
+ // question actually being asked: will a push or pull with no arguments carry this branch anywhere?
386
+ //
387
+ // NO FETCH. A doctor that reaches the network hangs on a box without a route, and the count of commits
388
+ // waiting is measured against the last fetch, which is enough: a commit made here is ahead of any
389
+ // version of the remote branch until somebody pushes it.
390
+
391
+ /**
392
+ * Where does a save committed in `dir`'s repository go next? Read-only.
393
+ *
394
+ * @param {string} dir the store directory — any directory inside the repository; git finds the root
395
+ * @param {{ env?: object, gitVersion?: string }} [opts] the environment git runs in — a caller can bound the
396
+ * search upwards — and, for a test, the `git version` answer to reason from instead of asking git
397
+ * @returns one of:
398
+ * `{ state: "publishes", root, branch, upstream, ahead, via }` — a push with no arguments carries the store's
399
+ * commits out: the branch tracks a remote branch (`via: "tracking"`), or it tracks none and git's push
400
+ * settings send it anyway (`via` names the setting). `ahead` counts the commits waiting, or is null when
401
+ * git cannot count them (nothing to count against yet).
402
+ * `{ state: "stays-here", root, branch, remotes }` — no tracking branch (or no branch at all), and no push
403
+ * setting that sends it anyway: nothing leaves unless someone pushes it by name. `remotes` lists the remotes such a push could name.
404
+ * `{ state: "not-a-repository", why }` — a save through a door is refused until the store is in one.
405
+ * `{ state: "could-not-look", why }` — git would not answer: a missing directory, a repository owned
406
+ * by another account, no git on the path. Never read as either answer above.
407
+ */
408
+ export function whereSavesGo(dir, { env = process.env, gitVersion = null } = {}) {
409
+ const ask = (...args) => {
410
+ try {
411
+ return { ok: true, out: execFileSync("git", ["-C", String(dir ?? ""), ...args],
412
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env }).trim() };
413
+ } catch (e) {
414
+ return { ok: false, status: e?.status ?? null,
415
+ why: String(e?.stderr || e?.message || e).trim().split("\n")[0].slice(0, 200) };
416
+ }
417
+ };
418
+ const top = ask("rev-parse", "--show-toplevel");
419
+ if (!top.ok) {
420
+ return /not a git repository/i.test(top.why)
421
+ ? { state: "not-a-repository", why: top.why }
422
+ : { state: "could-not-look", why: top.why };
423
+ }
424
+ const root = top.out;
425
+ const listed = ask("remote");
426
+ if (!listed.ok) return { state: "could-not-look", why: listed.why };
427
+ const remotes = listed.out.split("\n").filter(Boolean);
428
+ // `-q` makes a detached HEAD a quiet exit 1, which is an answer (no branch, so nothing tracks). Any
429
+ // other failure is git refusing to say, and is reported as that.
430
+ const head = ask("symbolic-ref", "--short", "-q", "HEAD");
431
+ if (!head.ok) return head.status === 1 ? { state: "stays-here", root, branch: null, remotes } : { state: "could-not-look", why: head.why };
432
+ const branch = head.out;
433
+ // `config --get` exits 1 for a key that is not set, which is again an answer; anything else is not.
434
+ const setting = (key) => {
435
+ const r = ask("config", "--get", key);
436
+ if (r.ok) return { value: r.out };
437
+ return r.status === 1 ? { value: null } : { why: r.why };
438
+ };
439
+ const remote = setting(`branch.${branch}.remote`);
440
+ if (remote.why) return { state: "could-not-look", why: remote.why };
441
+ // A branch tracking another LOCAL branch names the remote `.`: a pull merges within this repository,
442
+ // and unless a push remote is named below, a push goes nowhere else.
443
+ if (!remote.value || remote.value === ".") {
444
+ // ── NO TRACKING IS NOT THE SAME AS NOTHING LEAVES ────────────────────────────────────────────────
445
+ //
446
+ // A bare `git push` on a branch with no tracking still publishes it in three configurations. Measured
447
+ // with git 2.43 against real remotes, 2026-09-10, after a review drove the first one:
448
+ // `push.autoSetupRemote=true` (the push sets up tracking as it goes), `push.default=current`, and
449
+ // `push.default=matching` once the remote has a branch of the same name. `upstream` and `nothing` refuse
450
+ // with no upstream. Git's default, `simple`, refuses too, UNLESS the push is triangular: a push remote
451
+ // (the branch's `pushRemote`, or `remote.pushDefault`) that is not the remote the branch fetches from.
452
+ // Then `simple` pushes as `current` does. A review drove that case with git 2.43 after this comment had
453
+ // said `simple` refuses whenever only those keys are set; that is true only while they name the fetch
454
+ // remote. Where a push goes is git's order: the branch's push remote, then `remote.pushDefault`, then
455
+ // `origin`, then the only remote there is. The remote a branch with no upstream fetches from is found
456
+ // the same way without the first two. The test beside this pushes for real in each configuration and
457
+ // holds this function to what git did.
458
+ const pushRemote = setting(`branch.${branch}.pushRemote`), pushDefault = setting("remote.pushDefault"), mode = setting("push.default");
459
+ for (const x of [pushRemote, pushDefault, mode]) if (x.why) return { state: "could-not-look", why: x.why };
460
+ const auto = ask("config", "--type=bool", "--get", "push.autoSetupRemote");
461
+ if (!auto.ok && auto.status !== 1) return { state: "could-not-look", why: auto.why };
462
+ // ── WHICH GIT IS ANSWERING ─────────────────────────────────────────────────────────────────────────
463
+ //
464
+ // Two of these rules arrived in git 2.37.0: `push.autoSetupRemote`, and the fallback that makes the only
465
+ // remote the default when it is not named `origin`. Before 2.37 the default remote is `origin` whether
466
+ // or not one exists, so a lone remote with another name is never the default. A push remote naming it
467
+ // is then triangular against a missing `origin`, and `simple` pushes. A review found that for the git on
468
+ // Ubuntu 22.04 (2.34), where the 2.37 reading here said the store stayed: a missed publish, the one
469
+ // direction this must not err in. A version git will not state is read both ways, below.
470
+ const ver = /(\d+)\.(\d+)/.exec(String(gitVersion ?? ask("version").out ?? ""));
471
+ const pushing = mode.value || "simple";
472
+ const reading = (modern) => {
473
+ const fallback = remotes.includes("origin") ? "origin" : modern && remotes.length === 1 ? remotes[0] : null;
474
+ const fetchesFrom = remote.value || (modern ? fallback : "origin");
475
+ const dest = pushRemote.value || pushDefault.value || (remote.value === "." ? null : fallback);
476
+ const triangular = dest !== fetchesFrom;
477
+ const autoOn = modern && auto.ok && auto.out === "true";
478
+ const via = !dest || !remotes.includes(dest) ? null
479
+ : pushing === "current" ? "push.default is current"
480
+ // `upstream` refuses a push to a remote the branch does not fetch from, tracking set up or not:
481
+ // "You are pushing to remote 'x', which is not the upstream of your current branch".
482
+ : autoOn && (pushing === "simple" || (pushing === "upstream" && !triangular)) ? "push.autoSetupRemote is on"
483
+ : pushing === "simple" && triangular ? `push.default is simple, and it pushes to ${dest}, not the remote this branch fetches from`
484
+ : pushing === "matching" && ask("rev-parse", "--verify", "-q", `refs/remotes/${dest}/${branch}`).ok
485
+ ? "push.default is matching, and the remote has this branch"
486
+ : null;
487
+ return { via, dest };
488
+ };
489
+ // A GIT THAT WILL NOT STATE ITS VERSION IS READ BOTH WAYS, and publishing wins. Reading it as current
490
+ // was the exception to "never err toward staying": in the lone-remote case the older reading is the one
491
+ // that publishes. A review of that exception named it; every real git states its version, so this
492
+ // costs nothing today and removes the one case where the promise above did not hold.
493
+ const modernNow = ver && (Number(ver[1]) > 2 || (Number(ver[1]) === 2 && Number(ver[2]) >= 37));
494
+ const now = ver ? reading(modernNow) : reading(true);
495
+ const older = ver ? null : reading(false);
496
+ const r = now.via ? now
497
+ : older?.via ? { ...older, via: `${older.via}, as a git older than 2.37 would push it; this git did not state its version` }
498
+ : now;
499
+ const { via, dest } = r;
500
+ if (!via) return { state: "stays-here", root, branch, remotes };
501
+ const known = ask("rev-list", "--count", `refs/remotes/${dest}/${branch}..HEAD`);
502
+ return { state: "publishes", root, branch, upstream: `${dest}/${branch}`, via,
503
+ ahead: known.ok && /^\d+$/.test(known.out) ? Number(known.out) : null };
504
+ }
505
+ const merge = setting(`branch.${branch}.merge`);
506
+ if (merge.why) return { state: "could-not-look", why: merge.why };
507
+ const upstream = `${remote.value}/${String(merge.value ?? branch).replace(/^refs\/heads\//, "")}`;
508
+ const count = ask("rev-list", "--count", "@{u}..HEAD");
509
+ const ahead = count.ok && /^\d+$/.test(count.out) ? Number(count.out) : null;
510
+ return { state: "publishes", root, branch, upstream, ahead, via: "tracking" };
511
+ }
@@ -48,17 +48,17 @@
48
48
  // present, and cannot see whether it ought to be. So it states the mode and what that means for the
49
49
  // three readers, and leaves the judgement to a reader who knows which tree they are looking at.
50
50
  //
51
- // NOTHING PINS THIS ANNOUNCEMENT, and this block used to claim something did. Measured 2026-09-09: no
52
- // test in either repository imports `announceWithheldMode` or asserts its line, so the wording above is
53
- // held by nothing and a silent removal of it would pass. That gap is filed rather than fixed here,
54
- // because closing it adds a test file and this change is comment text only.
51
+ // WHAT HOLDS THE ANNOUNCEMENT: `driver/test/the-public-residue-is-a-floor.test.mjs`, in a throwaway tree
52
+ // in both modes. Each mode says so, the two say different things, the present one counts its entries, the
53
+ // line is said once and returned on every call, and the stricter mode does not call itself an alarm. Those
54
+ // arms hold the property rather than the sentence, so the wording can be corrected without re-pointing them.
55
55
 
56
56
  let record = null;
57
57
  try {
58
58
  record = await import("./withheld-paths.mjs");
59
59
  } catch {
60
- // Absent is the PUBLIC tree's normal state and our tree's alarm. Which one it is, is the caller's to
61
- // decide from `CUT_RECORD_PRESENT`; this module only refuses to guess.
60
+ // Absent is the state of every tree today, as the header says. Whether that is right is the caller's
61
+ // to judge from `CUT_RECORD_PRESENT`; this module only refuses to guess.
62
62
  record = null;
63
63
  }
64
64
 
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // A YES/NO QUESTION ECHOES ONLY A YES/NO ANSWER.
5
+ //
6
+ // Setup asks "Enter a Perplexity API key now? [Y/n]", and a person who already holds the key pastes it
7
+ // there. The wizard answered "Please answer y or n" and asked again, which is right, but the key had
8
+ // already been echoed to the terminal, into its scrollback and into whatever gets pasted into a bug
9
+ // report. Reported from a real install, 2026-09-10.
10
+ //
11
+ // THE ECHO IS DECIDED CHARACTER BY CHARACTER, because that is how it happens. In terminal mode readline
12
+ // writes each keypress as it arrives, a paste included, so by the time the line is submitted every
13
+ // character has already been shown. Waiting for the answer and then deciding is too late. So the echo is
14
+ // allowed only while the line typed so far can still become `y`, `yes`, `n` or `no`, and a key never is.
15
+
16
+ /** Whether the line typed so far can still become a yes/no answer. The empty line can: it takes the default. */
17
+ export const couldBeYesNo = (line) => /^(y|ye|yes|n|no)?$/i.test(String(line ?? ""));
18
+
19
+ /**
20
+ * Whether an answer is plainly a credential rather than a yes/no: sixteen or more characters with no space
21
+ * in them. No yes/no answer is that long, and every key this product asks for is longer.
22
+ */
23
+ export const looksLikeAKey = (answer) => /^\S{16,}$/.test(String(answer ?? "").trim());
24
+
25
+ /**
26
+ * What a yes/no question lets readline write, given the chunk it is about to write, the line typed so far
27
+ * and the question's prompt. Cursor movement and the newline always pass, so the terminal stays in step.
28
+ * Readline redraws the prompt and the whole line as one write after an edit, so a redraw keeps the prompt
29
+ * and drops the line.
30
+ */
31
+ export function yesNoEchoOf(chunk, { line = "", prompt = "" } = {}) {
32
+ const s = String(chunk ?? "");
33
+ if (!s || s.charCodeAt(0) === 0x1b || /^[\r\n]+$/.test(s) || couldBeYesNo(line)) return s;
34
+ return prompt && s.startsWith(prompt) ? prompt : "";
35
+ }