@fungi.computer/bonsai 0.0.0-stage → 0.1.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 (133) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -2
  4. package/backend/background.ts +259 -0
  5. package/backend/change-copy.ts +99 -0
  6. package/backend/changes.ts +530 -0
  7. package/backend/diff.ts +228 -0
  8. package/backend/file-edits.ts +850 -0
  9. package/backend/files-copy.ts +361 -0
  10. package/backend/gills.ts +125 -0
  11. package/backend/git.ts +123 -0
  12. package/backend/graph-commit-copies.ts +321 -0
  13. package/backend/history.ts +178 -0
  14. package/backend/index.ts +144 -0
  15. package/backend/insights.ts +256 -0
  16. package/backend/merge.ts +999 -0
  17. package/backend/methods.ts +848 -0
  18. package/backend/policy.ts +160 -0
  19. package/backend/read-copy-page.ts +77 -0
  20. package/backend/repository-copy.ts +70 -0
  21. package/backend/reviews.ts +317 -0
  22. package/backend/service.ts +892 -0
  23. package/backend/state.ts +914 -0
  24. package/demo/koi-pond/README.md +17 -0
  25. package/demo/koi-pond/index.html +11 -0
  26. package/demo/koi-pond/package.json +13 -0
  27. package/demo/koi-pond/pnpm-lock.yaml +450 -0
  28. package/demo/koi-pond/src/fish.ts +32 -0
  29. package/demo/koi-pond/src/main.ts +24 -0
  30. package/demo/koi-pond/src/pond.ts +27 -0
  31. package/docs/media/bonsai-insights.png +0 -0
  32. package/docs/media/overview-phone.png +0 -0
  33. package/docs/media/overview.png +0 -0
  34. package/package.json +152 -3
  35. package/scripts/agent.sh +55 -0
  36. package/scripts/demo.sh +233 -0
  37. package/scripts/rebuild-copies.mjs +34 -0
  38. package/scripts/release.mjs +52 -0
  39. package/scripts/runtime-proof.mjs +299 -0
  40. package/scripts/seed-demo.mjs +724 -0
  41. package/scripts/seed-step.mjs +37 -0
  42. package/scripts/shed-terminal-proof.mjs +774 -0
  43. package/shared/contract.ts +404 -0
  44. package/shared/hooks.ts +208 -0
  45. package/shared/insights.ts +90 -0
  46. package/shared/picker-options.ts +23 -0
  47. package/shared/presentation.ts +21 -0
  48. package/shared/repository-deletion.ts +4 -0
  49. package/test/apps-build-install.test.ts +302 -0
  50. package/test/background-runtime.test.ts +206 -0
  51. package/test/browser/board.browser.mjs +52 -0
  52. package/test/browser/capture.browser.mjs +1264 -0
  53. package/test/browser/edit-in-inkcap.browser.mjs +83 -0
  54. package/test/browser/repository-settings-fixture.tsx +108 -0
  55. package/test/browser/repository-settings.browser.mjs +136 -0
  56. package/test/browser/repository-settings.html +10 -0
  57. package/test/browser/visual-fixture.tsx +667 -0
  58. package/test/browser/visual.html +10 -0
  59. package/test/browser/vite.config.mjs +9 -0
  60. package/test/change-page.test.ts +916 -0
  61. package/test/changes.test.ts +1063 -0
  62. package/test/checks-workflow-template.test.mjs +234 -0
  63. package/test/diff.test.ts +70 -0
  64. package/test/edit-in-inkcap.test.ts +227 -0
  65. package/test/file-edits.test.ts +2714 -0
  66. package/test/files-read-copy-calls.test.ts +374 -0
  67. package/test/fork-adoption.test.ts +111 -0
  68. package/test/gills.test.ts +125 -0
  69. package/test/git-binding.fixture.ts +30 -0
  70. package/test/git-push.config.jsonc +9 -0
  71. package/test/git-push.native.mjs +413 -0
  72. package/test/git-push.worker.ts +280 -0
  73. package/test/graph-commit-read-copy-calls.test.ts +401 -0
  74. package/test/home-page.test.ts +170 -0
  75. package/test/hooks.test.ts +250 -0
  76. package/test/insight-data.test.ts +51 -0
  77. package/test/insight-layout.test.mjs +230 -0
  78. package/test/insights.test.ts +154 -0
  79. package/test/merge-background.test.ts +756 -0
  80. package/test/merge-conflict.test.ts +337 -0
  81. package/test/merge-declaration.test.ts +46 -0
  82. package/test/merge-repro.test.ts +561 -0
  83. package/test/merge-template-build.test.mjs +234 -0
  84. package/test/network-layout.test.ts +200 -0
  85. package/test/outbox.test.ts +424 -0
  86. package/test/parts.test.ts +86 -0
  87. package/test/pkt-line.test.ts +54 -0
  88. package/test/policy.test.ts +172 -0
  89. package/test/present.test.ts +27 -0
  90. package/test/read-copy-calls.test.ts +691 -0
  91. package/test/read-copy-composer.test.ts +388 -0
  92. package/test/read-copy-lifetimes.test.ts +214 -0
  93. package/test/read-copy-rpc.config.jsonc +12 -0
  94. package/test/read-copy-rpc.native.mjs +47 -0
  95. package/test/read-copy-rpc.worker.ts +92 -0
  96. package/test/recent-activity.test.ts +62 -0
  97. package/test/repository-deletion.test.ts +500 -0
  98. package/test/seed-resume.test.mjs +91 -0
  99. package/test/storyboard-save-https-git.fixture.mjs +119 -0
  100. package/test/storyboard-save-template.test.mjs +545 -0
  101. package/test/ui-errors.test.ts +976 -0
  102. package/test/workflow-store.test.ts +115 -0
  103. package/tsconfig.json +36 -0
  104. package/turbo.jsonc +25 -0
  105. package/vite.config.ts +19 -0
  106. package/vitest.config.ts +47 -0
  107. package/web/api.ts +224 -0
  108. package/web/app-build-panel.tsx +171 -0
  109. package/web/board.tsx +259 -0
  110. package/web/bonsai.css +642 -0
  111. package/web/change-page.tsx +719 -0
  112. package/web/change-rows.tsx +135 -0
  113. package/web/code-tab.tsx +267 -0
  114. package/web/commit-tree.tsx +152 -0
  115. package/web/format.ts +54 -0
  116. package/web/home-page.tsx +274 -0
  117. package/web/index.html +17 -0
  118. package/web/insight-data.ts +76 -0
  119. package/web/insight-tiles.tsx +534 -0
  120. package/web/insights.tsx +250 -0
  121. package/web/issues-panel.tsx +305 -0
  122. package/web/main.tsx +72 -0
  123. package/web/network-graph.tsx +647 -0
  124. package/web/overview.tsx +339 -0
  125. package/web/pages.tsx +469 -0
  126. package/web/parts.tsx +381 -0
  127. package/web/public/favicon.svg +1 -0
  128. package/web/repo-navigation.tsx +256 -0
  129. package/web/repo-settings.tsx +145 -0
  130. package/web/router.tsx +151 -0
  131. package/web/transport.ts +91 -0
  132. package/web/views.tsx +151 -0
  133. package/web/working-now.tsx +98 -0
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ approvalKind,
4
+ decide,
5
+ review,
6
+ type PolicyFacts,
7
+ } from "../backend/policy.js";
8
+
9
+ const head = "a".repeat(40);
10
+ const target = "b".repeat(40);
11
+ function facts(): PolicyFacts {
12
+ return {
13
+ actor: { name: "Levi", kind: "human" },
14
+ change: { state: "open", head, target, openedBy: "morel", conflict: null },
15
+ head,
16
+ approvals: [],
17
+ checks: [],
18
+ headAuthors: ["morel"],
19
+ isMaintainer: true,
20
+ mayPush: true,
21
+ sourceOwner: "morel",
22
+ soleMember: false,
23
+ };
24
+ }
25
+
26
+ describe("Change policy from host identity and pinned review facts", () => {
27
+ it("lets a person maintainer merge an Agent's work with an implicit approval", () => {
28
+ expect(decide("merge", facts())).toEqual({ allowed: true });
29
+ expect(review(facts())).toEqual({
30
+ status: "ready",
31
+ decision: { allowed: true },
32
+ });
33
+ });
34
+ it("requires another eligible approval when the maintainer wrote the work", () => {
35
+ const own: PolicyFacts = { ...facts(), headAuthors: ["Levi"] };
36
+ expect(decide("merge", own)).toEqual({
37
+ allowed: false,
38
+ reason: "needs-approval",
39
+ });
40
+ expect(
41
+ decide("merge", {
42
+ ...own,
43
+ approvals: [{ actor: "Ada", head, kind: "human" }],
44
+ }),
45
+ ).toEqual({ allowed: true });
46
+ });
47
+ it("does not count approvals or checks from a previous head", () => {
48
+ const old: PolicyFacts = {
49
+ ...facts(),
50
+ headAuthors: ["Levi"],
51
+ approvals: [{ actor: "Ada", head: target, kind: "human" }],
52
+ checks: [{ head: target, status: "failed" }],
53
+ };
54
+ expect(decide("merge", old)).toEqual({
55
+ allowed: false,
56
+ reason: "needs-approval",
57
+ });
58
+ });
59
+ it.each(["queued", "running"] as const)(
60
+ "shows %s checks as waiting",
61
+ (status) => {
62
+ expect(
63
+ decide("merge", { ...facts(), checks: [{ head, status }] }),
64
+ ).toEqual({ allowed: false, reason: "checks-running" });
65
+ },
66
+ );
67
+ it("blocks failed checks even with an eligible approver", () => {
68
+ expect(
69
+ decide("merge", { ...facts(), checks: [{ head, status: "failed" }] }),
70
+ ).toEqual({ allowed: false, reason: "checks-failed" });
71
+ });
72
+ it("refuses the caller's stale head and a second merge while leased", () => {
73
+ expect(decide("merge", { ...facts(), head: target })).toEqual({
74
+ allowed: false,
75
+ reason: "moved",
76
+ });
77
+ expect(
78
+ decide("merge", {
79
+ ...facts(),
80
+ change: { ...facts().change, state: "merging" },
81
+ }),
82
+ ).toEqual({ allowed: false, reason: "merging" });
83
+ });
84
+ it("uses conflicts only for the current source and target pair", () => {
85
+ const conflict = { head, target, paths: ["food.txt"] };
86
+ expect(
87
+ decide("merge", { ...facts(), change: { ...facts().change, conflict } }),
88
+ ).toEqual({ allowed: false, reason: "conflict" });
89
+ expect(
90
+ decide("merge", {
91
+ ...facts(),
92
+ change: {
93
+ ...facts().change,
94
+ conflict: { ...conflict, target: "c".repeat(40) },
95
+ },
96
+ }),
97
+ ).toEqual({ allowed: true });
98
+ });
99
+ it("does not let Agents approve or merge", () => {
100
+ const agent: PolicyFacts = {
101
+ ...facts(),
102
+ actor: { name: "morel", kind: "agent" },
103
+ isMaintainer: false,
104
+ };
105
+ expect(decide("merge", agent)).toEqual({
106
+ allowed: false,
107
+ reason: "agents-cannot-merge",
108
+ });
109
+ expect(decide("approve", agent).allowed).toBe(false);
110
+ });
111
+ it("refuses approval by the opener or an author", () => {
112
+ expect(
113
+ decide("approve", {
114
+ ...facts(),
115
+ actor: { name: "morel", kind: "human" },
116
+ }),
117
+ ).toEqual({ allowed: false, reason: "self-approval" });
118
+ expect(decide("approve", { ...facts(), headAuthors: ["Levi"] })).toEqual({
119
+ allowed: false,
120
+ reason: "self-approval",
121
+ });
122
+ });
123
+ it("permits opening only for a source writer", () => {
124
+ expect(decide("open", { ...facts(), mayPush: false }).allowed).toBe(false);
125
+ expect(decide("open", facts())).toEqual({ allowed: true });
126
+ });
127
+ it.each(["merged", "closed"] as const)(
128
+ "preserves %s lifecycle status",
129
+ (state) => {
130
+ expect(
131
+ review({ ...facts(), change: { ...facts().change, state } }).status,
132
+ ).toBe(state);
133
+ expect(
134
+ decide("merge", { ...facts(), change: { ...facts().change, state } })
135
+ .allowed,
136
+ ).toBe(false);
137
+ },
138
+ );
139
+ it("lets only a host-attested sole member approve and merge their own work", () => {
140
+ const own: PolicyFacts = { ...facts(), headAuthors: ["Levi"] };
141
+ expect(decide("approve", own)).toEqual({
142
+ allowed: false,
143
+ reason: "self-approval",
144
+ });
145
+ const alone: PolicyFacts = { ...own, soleMember: true };
146
+ expect(decide("approve", alone)).toEqual({ allowed: true });
147
+ expect(approvalKind(alone)).toBe("self");
148
+ expect(decide("merge", alone)).toEqual({ allowed: true });
149
+ expect(approvalKind({ ...facts(), soleMember: true })).toBe("human");
150
+ // A stored self-approval never stands in for another reviewer.
151
+ expect(
152
+ decide("merge", {
153
+ ...own,
154
+ actor: { name: "Ada", kind: "human" },
155
+ headAuthors: ["Levi", "Ada"],
156
+ approvals: [{ actor: "Levi", head, kind: "self" }],
157
+ }),
158
+ ).toEqual({ allowed: false, reason: "needs-approval" });
159
+ });
160
+ it("never lets an Agent self-approve, even with a sole-member fact", () => {
161
+ const agent: PolicyFacts = {
162
+ ...facts(),
163
+ actor: { name: "morel", kind: "agent" },
164
+ isMaintainer: false,
165
+ soleMember: true,
166
+ };
167
+ expect(decide("approve", agent)).toEqual({
168
+ allowed: false,
169
+ reason: "agents-cannot-approve",
170
+ });
171
+ });
172
+ });
@@ -0,0 +1,27 @@
1
+ import { expect, it } from "vitest";
2
+ import { appBackend } from "../backend/methods.js";
3
+
4
+ const repo = (name: string) => ({
5
+ name,
6
+ description: null,
7
+ parent: null,
8
+ owner: null,
9
+ createdAt: 1,
10
+ lastPushAt: null,
11
+ forks: [],
12
+ openChanges: 0,
13
+ activity: [],
14
+ });
15
+
16
+ it("reads a repository listing as a count and the first few names", () => {
17
+ const present = appBackend.methods["repos.list"]?.present;
18
+ expect(present?.({}, { repos: [repo("storyboard")] })).toEqual({
19
+ id: "repos.list",
20
+ title: "Bonsai listed 1 repository",
21
+ detail: "storyboard",
22
+ });
23
+ expect(present?.({}, { repos: [repo("a"), repo("b")] })).toMatchObject({
24
+ title: "Bonsai listed 2 repositories",
25
+ detail: "a · b",
26
+ });
27
+ });