@intentius/chant 0.100.0 → 0.101.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 (276) hide show
  1. package/dist/build.d.ts +6 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/build-options.d.ts +2 -0
  4. package/dist/cli/build-options.d.ts.map +1 -1
  5. package/dist/cli/commands/build.d.ts.map +1 -1
  6. package/dist/cli/commands/import.d.ts.map +1 -1
  7. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  8. package/dist/cli/main.d.ts.map +1 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts +8 -0
  10. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +22 -0
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/config.d.ts +11 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/lexicon.d.ts +24 -1
  16. package/dist/lexicon.d.ts.map +1 -1
  17. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  18. package/dist/lifecycle/plan-digest.d.ts +26 -5
  19. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  20. package/dist/lint/config.d.ts +4 -4
  21. package/dist/op/activities/activity-contracts.d.ts +1 -0
  22. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  23. package/dist/op/activities/propose-upgrade.d.ts +2 -0
  24. package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
  25. package/dist/op/index.d.ts +1 -1
  26. package/dist/op/index.d.ts.map +1 -1
  27. package/dist/serializer.d.ts +8 -0
  28. package/dist/serializer.d.ts.map +1 -1
  29. package/dist/telemetry-attribution.d.ts +77 -0
  30. package/dist/telemetry-attribution.d.ts.map +1 -0
  31. package/dist/workspace/agent-cli.d.ts +83 -0
  32. package/dist/workspace/agent-cli.d.ts.map +1 -0
  33. package/dist/workspace/changes-cli.d.ts.map +1 -1
  34. package/dist/workspace/changes.d.ts +8 -1
  35. package/dist/workspace/changes.d.ts.map +1 -1
  36. package/dist/workspace/checks/links.d.ts +1 -0
  37. package/dist/workspace/checks/links.d.ts.map +1 -1
  38. package/dist/workspace/checks/live.d.ts +40 -0
  39. package/dist/workspace/checks/live.d.ts.map +1 -0
  40. package/dist/workspace/checks.d.ts +21 -2
  41. package/dist/workspace/checks.d.ts.map +1 -1
  42. package/dist/workspace/compose-graph.d.ts +63 -0
  43. package/dist/workspace/compose-graph.d.ts.map +1 -1
  44. package/dist/workspace/decide.d.ts +1 -1
  45. package/dist/workspace/decide.d.ts.map +1 -1
  46. package/dist/workspace/declaration.d.ts +32 -0
  47. package/dist/workspace/declaration.d.ts.map +1 -1
  48. package/dist/workspace/declaration.schema.json +138 -3
  49. package/dist/workspace/export-cli.d.ts +12 -0
  50. package/dist/workspace/export-cli.d.ts.map +1 -0
  51. package/dist/workspace/export.d.ts +145 -0
  52. package/dist/workspace/export.d.ts.map +1 -0
  53. package/dist/workspace/graph-cli.d.ts.map +1 -1
  54. package/dist/workspace/import.d.ts +73 -0
  55. package/dist/workspace/import.d.ts.map +1 -0
  56. package/dist/workspace/kinds.d.ts +6 -2
  57. package/dist/workspace/kinds.d.ts.map +1 -1
  58. package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
  59. package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
  60. package/dist/workspace/lineage-adopt.d.ts +106 -0
  61. package/dist/workspace/lineage-adopt.d.ts.map +1 -0
  62. package/dist/workspace/lineage-check.d.ts +9 -2
  63. package/dist/workspace/lineage-check.d.ts.map +1 -1
  64. package/dist/workspace/lineage-cli.d.ts +6 -1
  65. package/dist/workspace/lineage-cli.d.ts.map +1 -1
  66. package/dist/workspace/lineage-hash-index.d.ts +110 -0
  67. package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
  68. package/dist/workspace/lineage-init.d.ts +10 -0
  69. package/dist/workspace/lineage-init.d.ts.map +1 -1
  70. package/dist/workspace/lineage-lock.d.ts +147 -0
  71. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  72. package/dist/workspace/lineage-migrations.d.ts +15 -3
  73. package/dist/workspace/lineage-migrations.d.ts.map +1 -1
  74. package/dist/workspace/lineage-provenance.d.ts +18 -0
  75. package/dist/workspace/lineage-provenance.d.ts.map +1 -0
  76. package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
  77. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
  78. package/dist/workspace/lineage-upgrade.d.ts +30 -1
  79. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  80. package/dist/workspace/lineage-versions.d.ts +116 -0
  81. package/dist/workspace/lineage-versions.d.ts.map +1 -0
  82. package/dist/workspace/links.d.ts +31 -5
  83. package/dist/workspace/links.d.ts.map +1 -1
  84. package/dist/workspace/ls-generated.d.ts +37 -0
  85. package/dist/workspace/ls-generated.d.ts.map +1 -0
  86. package/dist/workspace/ls.d.ts +4 -0
  87. package/dist/workspace/ls.d.ts.map +1 -1
  88. package/dist/workspace/member-commands.d.ts.map +1 -1
  89. package/dist/workspace/nested-graph.d.ts +56 -0
  90. package/dist/workspace/nested-graph.d.ts.map +1 -0
  91. package/dist/workspace/nesting.d.ts +21 -0
  92. package/dist/workspace/nesting.d.ts.map +1 -0
  93. package/dist/workspace/pin-cli.d.ts +10 -0
  94. package/dist/workspace/pin-cli.d.ts.map +1 -0
  95. package/dist/workspace/pin-integrity.d.ts +51 -0
  96. package/dist/workspace/pin-integrity.d.ts.map +1 -0
  97. package/dist/workspace/reason-codes.d.ts +26 -2
  98. package/dist/workspace/reason-codes.d.ts.map +1 -1
  99. package/dist/workspace/record-sessions.d.ts +7 -11
  100. package/dist/workspace/record-sessions.d.ts.map +1 -1
  101. package/dist/workspace/records-cli.d.ts +30 -1
  102. package/dist/workspace/records-cli.d.ts.map +1 -1
  103. package/dist/workspace/records-close.d.ts +5 -2
  104. package/dist/workspace/records-close.d.ts.map +1 -1
  105. package/dist/workspace/records-write.d.ts +22 -4
  106. package/dist/workspace/records-write.d.ts.map +1 -1
  107. package/dist/workspace/records.d.ts +43 -5
  108. package/dist/workspace/records.d.ts.map +1 -1
  109. package/dist/workspace/returns.d.ts +129 -0
  110. package/dist/workspace/returns.d.ts.map +1 -0
  111. package/dist/workspace/status-gates.d.ts.map +1 -1
  112. package/dist/workspace/template-manifest.d.ts +11 -3
  113. package/dist/workspace/template-manifest.d.ts.map +1 -1
  114. package/dist/workspace/trust/attestor.d.ts +8 -0
  115. package/dist/workspace/trust/attestor.d.ts.map +1 -1
  116. package/dist/workspace/trust/dsse.d.ts +58 -0
  117. package/dist/workspace/trust/dsse.d.ts.map +1 -0
  118. package/dist/workspace/trust/evidence-cli.d.ts +66 -0
  119. package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
  120. package/dist/workspace/trust/evidence.d.ts +93 -0
  121. package/dist/workspace/trust/evidence.d.ts.map +1 -0
  122. package/dist/workspace/trust/policy.d.ts +54 -1
  123. package/dist/workspace/trust/policy.d.ts.map +1 -1
  124. package/dist/workspace/trust/provenance.d.ts +21 -1
  125. package/dist/workspace/trust/provenance.d.ts.map +1 -1
  126. package/dist/workspace/trust/rotation.d.ts +132 -0
  127. package/dist/workspace/trust/rotation.d.ts.map +1 -0
  128. package/dist/workspace/trust/seal.d.ts.map +1 -1
  129. package/dist/workspace/trust/signers-cli.d.ts +49 -0
  130. package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
  131. package/dist/workspace/trust/ssh-commit.d.ts +12 -0
  132. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  133. package/dist/workspace/trust/test-repo.d.ts +13 -0
  134. package/dist/workspace/trust/test-repo.d.ts.map +1 -1
  135. package/dist/workspace/trust/verify.d.ts +15 -0
  136. package/dist/workspace/trust/verify.d.ts.map +1 -1
  137. package/dist/workspace/work-evidence.d.ts +1 -1
  138. package/dist/workspace/work-evidence.d.ts.map +1 -1
  139. package/dist/workspace/write-scope.d.ts +199 -0
  140. package/dist/workspace/write-scope.d.ts.map +1 -0
  141. package/package.json +1 -1
  142. package/src/build.ts +8 -0
  143. package/src/cli/build-options.ts +4 -1
  144. package/src/cli/commands/build.ts +2 -0
  145. package/src/cli/commands/import-live.test.ts +69 -1
  146. package/src/cli/commands/import.ts +48 -22
  147. package/src/cli/handlers/fan-out.test.ts +6 -6
  148. package/src/cli/handlers/fan-out.ts +2 -1
  149. package/src/cli/handlers/graph.test.ts +42 -0
  150. package/src/cli/handlers/graph.ts +22 -0
  151. package/src/cli/handlers/operator.ts +1 -1
  152. package/src/cli/main.test.ts +32 -0
  153. package/src/cli/main.ts +103 -6
  154. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  155. package/src/cli/mcp/workspace-tools.ts +33 -2
  156. package/src/cli/registry.ts +22 -0
  157. package/src/cli/serve-mcp-workspace.test.ts +1 -1
  158. package/src/codegen/release-wiring.test.ts +5 -1
  159. package/src/components/fan-out-output.test.ts +1 -1
  160. package/src/components/fan-out.test.ts +1 -1
  161. package/src/components/promote.test.ts +1 -1
  162. package/src/config.ts +12 -0
  163. package/src/content-digest.test.ts +2 -2
  164. package/src/lexicon.ts +25 -1
  165. package/src/lifecycle/gate-ledger.test.ts +14 -0
  166. package/src/lifecycle/gate-ledger.ts +2 -1
  167. package/src/lifecycle/plan-digest.test.ts +54 -3
  168. package/src/lifecycle/plan-digest.ts +38 -8
  169. package/src/op/activities/activity-contracts.ts +1 -0
  170. package/src/op/activities/propose-upgrade.ts +8 -5
  171. package/src/op/gate-approval.test.ts +17 -0
  172. package/src/op/gate.ts +3 -3
  173. package/src/op/index.ts +1 -1
  174. package/src/serializer.ts +9 -0
  175. package/src/telemetry-attribution.test.ts +91 -0
  176. package/src/telemetry-attribution.ts +145 -0
  177. package/src/workspace/agent-cli.ts +134 -0
  178. package/src/workspace/agent.schema.json +356 -0
  179. package/src/workspace/behold-kinds.test.ts +1 -1
  180. package/src/workspace/changes-cli.ts +5 -0
  181. package/src/workspace/changes.schema.json +179 -1
  182. package/src/workspace/changes.ts +58 -4
  183. package/src/workspace/check-live.test.ts +192 -0
  184. package/src/workspace/check.schema.json +64 -0
  185. package/src/workspace/checks/links.ts +23 -2
  186. package/src/workspace/checks/live.ts +113 -0
  187. package/src/workspace/checks.test.ts +2 -2
  188. package/src/workspace/checks.ts +20 -3
  189. package/src/workspace/compose-graph.test.ts +47 -0
  190. package/src/workspace/compose-graph.ts +120 -3
  191. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
  192. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
  193. package/src/workspace/declaration.schema.json +138 -3
  194. package/src/workspace/declaration.ts +106 -1
  195. package/src/workspace/declared-kinds.test.ts +33 -0
  196. package/src/workspace/evidence.schema.json +279 -0
  197. package/src/workspace/export-cli.ts +180 -0
  198. package/src/workspace/export-import.test.ts +282 -0
  199. package/src/workspace/export.ts +486 -0
  200. package/src/workspace/graph-cli.ts +47 -1
  201. package/src/workspace/graph-contract.test.ts +89 -4
  202. package/src/workspace/graph.schema.json +206 -1
  203. package/src/workspace/import.ts +325 -0
  204. package/src/workspace/kinds.test.ts +5 -5
  205. package/src/workspace/kinds.ts +25 -3
  206. package/src/workspace/lineage-adopt-cli.ts +103 -0
  207. package/src/workspace/lineage-adopt.test.ts +552 -0
  208. package/src/workspace/lineage-adopt.ts +452 -0
  209. package/src/workspace/lineage-check.ts +26 -5
  210. package/src/workspace/lineage-cli.ts +12 -2
  211. package/src/workspace/lineage-hash-index.ts +305 -0
  212. package/src/workspace/lineage-init.test.ts +9 -0
  213. package/src/workspace/lineage-init.ts +29 -9
  214. package/src/workspace/lineage-lock.ts +54 -0
  215. package/src/workspace/lineage-migrations.test.ts +27 -0
  216. package/src/workspace/lineage-migrations.ts +44 -14
  217. package/src/workspace/lineage-provenance.ts +40 -0
  218. package/src/workspace/lineage-upgrade-cli.ts +9 -5
  219. package/src/workspace/lineage-upgrade.test.ts +92 -1
  220. package/src/workspace/lineage-upgrade.ts +111 -16
  221. package/src/workspace/lineage-versions.ts +348 -0
  222. package/src/workspace/links.test.ts +121 -2
  223. package/src/workspace/links.ts +97 -6
  224. package/src/workspace/ls-contract.test.ts +84 -1
  225. package/src/workspace/ls-generated.ts +111 -0
  226. package/src/workspace/ls.schema.json +19 -0
  227. package/src/workspace/ls.ts +8 -1
  228. package/src/workspace/member-commands.ts +3 -1
  229. package/src/workspace/nested-graph.test.ts +176 -0
  230. package/src/workspace/nested-graph.ts +169 -0
  231. package/src/workspace/nesting.ts +37 -0
  232. package/src/workspace/pin-cli.test.ts +71 -0
  233. package/src/workspace/pin-cli.ts +57 -0
  234. package/src/workspace/pin-integrity.test.ts +121 -0
  235. package/src/workspace/pin-integrity.ts +104 -0
  236. package/src/workspace/points-write.schema.json +1 -0
  237. package/src/workspace/read-contract.test.ts +24 -0
  238. package/src/workspace/reason-codes.test.ts +10 -0
  239. package/src/workspace/reason-codes.ts +29 -2
  240. package/src/workspace/record-sessions.ts +12 -14
  241. package/src/workspace/records-amend.schema.json +4 -0
  242. package/src/workspace/records-cli.ts +93 -12
  243. package/src/workspace/records-close.schema.json +6 -2
  244. package/src/workspace/records-close.ts +10 -2
  245. package/src/workspace/records-formats.test.ts +13 -5
  246. package/src/workspace/records-new.schema.json +4 -0
  247. package/src/workspace/records-review.schema.json +4 -0
  248. package/src/workspace/records-sessions.test.ts +23 -6
  249. package/src/workspace/records-write.test.ts +65 -8
  250. package/src/workspace/records-write.ts +70 -6
  251. package/src/workspace/records.schema.json +55 -3
  252. package/src/workspace/records.ts +96 -5
  253. package/src/workspace/returns.ts +328 -0
  254. package/src/workspace/signers.schema.json +206 -0
  255. package/src/workspace/status-gates.ts +2 -1
  256. package/src/workspace/template-manifest.ts +22 -4
  257. package/src/workspace/trust/attestor.ts +15 -0
  258. package/src/workspace/trust/dsse.ts +134 -0
  259. package/src/workspace/trust/evidence-cli.ts +195 -0
  260. package/src/workspace/trust/evidence.test.ts +241 -0
  261. package/src/workspace/trust/evidence.ts +207 -0
  262. package/src/workspace/trust/policy.ts +110 -3
  263. package/src/workspace/trust/provenance.ts +41 -4
  264. package/src/workspace/trust/record-seal.test.ts +1 -1
  265. package/src/workspace/trust/rotation.test.ts +258 -0
  266. package/src/workspace/trust/rotation.ts +336 -0
  267. package/src/workspace/trust/seal.ts +11 -0
  268. package/src/workspace/trust/signers-cli.ts +178 -0
  269. package/src/workspace/trust/ssh-commit.ts +53 -4
  270. package/src/workspace/trust/test-repo.ts +18 -0
  271. package/src/workspace/trust/trust.test.ts +8 -2
  272. package/src/workspace/trust/verify-cli.ts +1 -0
  273. package/src/workspace/trust/verify.ts +22 -0
  274. package/src/workspace/work-evidence.schema.json +4 -0
  275. package/src/workspace/write-scope.test.ts +340 -0
  276. package/src/workspace/write-scope.ts +448 -0
@@ -0,0 +1,282 @@
1
+ /**
2
+ * #2552: export, import and hosted return.
3
+ *
4
+ * A host workspace exports a member that travels into its export member,
5
+ * with a host-bound value switched. The export is copied into a repository
6
+ * of its own, where someone the host never saw signs new work. Import brings
7
+ * it back with host values restored, records the return with the commits the
8
+ * work was made in, and the work reads as attested-unverifiable-here until an
9
+ * admin admits the signer, then as attested by that signer.
10
+ */
11
+
12
+ import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { afterEach, describe, expect, test } from "vitest";
15
+ import { exportWorkspace, readManifestAt } from "./export";
16
+ import { importWorkspace } from "./import";
17
+ import { fileEntries, fileHash, LOCK_FILE, readLock, renderLock, type Lineage } from "./lineage-lock";
18
+ import { queryRecords, type RecordView } from "./records-cli";
19
+ import { admitReturn, buildOrigin, parseReturn, verifyOrigin } from "./returns";
20
+ import { emptyPolicy, returnedPolicy } from "./trust/policy";
21
+ import { checkRecordSeal, sealRecord } from "./trust/seal";
22
+ import { hasSshKeygen, note, TestRepo, writeRecordKind, type Key } from "./trust/test-repo";
23
+
24
+ const repos: TestRepo[] = [];
25
+ function repo(label: string): TestRepo {
26
+ const r = new TestRepo(label);
27
+ repos.push(r);
28
+ return r;
29
+ }
30
+ afterEach(() => {
31
+ while (repos.length) repos.pop()!.cleanup();
32
+ });
33
+
34
+ const HOST_DOMAIN = "studio.example.test";
35
+
36
+ function declaration(extra: Record<string, unknown>[] = []): string {
37
+ return JSON.stringify(
38
+ {
39
+ name: "acme",
40
+ schema: 1,
41
+ records: [{ kind: "kinds/note.kind.mjs" }],
42
+ members: [
43
+ { name: "app", dir: "app", kind: "other", because: "a server", travel: true, links: [{ member: "ops", output: "url" }] },
44
+ { name: "ops", dir: "ops", kind: "other", because: "stays home", outputs: ["url"] },
45
+ { name: "out", dir: "out", kind: "workspace", roles: ["export"] },
46
+ ...extra,
47
+ ],
48
+ },
49
+ null,
50
+ 2,
51
+ );
52
+ }
53
+
54
+ /** The host: app with a member lock whose `domain` parameter is host-bound, ops, and records. */
55
+ function host(r: TestRepo, admin?: Key): void {
56
+ // The signer set comes first: a commit is judged by the set in effect before it (#2553).
57
+ if (admin) {
58
+ r.write(".chant/allowed_signers", `alice@example.test ${admin.pub}\n`);
59
+ r.commit("signers", admin);
60
+ }
61
+ r.write("chant.workspace.json", declaration());
62
+ r.write("app/server.txt", "hello\n");
63
+ r.write("app/config.txt", `domain=${HOST_DOMAIN}\n`);
64
+ r.write("ops/run.txt", "ops\n");
65
+ writeRecordKind(r);
66
+ r.write("records/n-1.md", note("n-1", "from the host"));
67
+ const files = new Map([
68
+ ["server.txt", Buffer.from("hello\n")],
69
+ ["config.txt", Buffer.from(`domain=${HOST_DOMAIN}\n`)],
70
+ ]);
71
+ const lineage: Lineage = {
72
+ kind: "template",
73
+ template: "github.com/acme/starter",
74
+ source: { type: "git", repo: "github.com/acme/starter", url: "https://github.com/acme/starter" },
75
+ ref: "v1.0.0",
76
+ address: { digest: `sha256:${"0".repeat(64)}` },
77
+ parameters: { domain: HOST_DOMAIN },
78
+ hostBound: { domain: ["config.txt"] },
79
+ migrations: [],
80
+ files: fileEntries(files),
81
+ manualSteps: [],
82
+ };
83
+ r.write(`app/${LOCK_FILE}`, renderLock({ lockVersion: 1, scopes: { ".": lineage } }));
84
+ r.commit("host", admin);
85
+ }
86
+
87
+ function changedOutside(r: TestRepo): string[] {
88
+ return r
89
+ .git(["status", "--porcelain", "--untracked-files=all"])
90
+ .split("\n")
91
+ .filter(Boolean)
92
+ .map((l) => l.slice(3));
93
+ }
94
+
95
+ describe("export (#2552)", () => {
96
+ test("writes the members that travel into the export member, with host values switched", async () => {
97
+ const r = repo("export");
98
+ host(r);
99
+ const res = await exportWorkspace({ root: r.dir, params: { domain: "copy.example.test" } });
100
+ expect(res.member).toBe("out");
101
+ expect(res.manifest.members).toEqual(["app"]);
102
+ expect(res.manifest.whole).toBe(true);
103
+ expect(res.manifest.dirs).toEqual(["kinds", "records"]);
104
+ expect(res.switched).toEqual(["app/config.txt"]);
105
+ // Only the export member was written.
106
+ expect(changedOutside(r).every((p) => p.startsWith("out/"))).toBe(true);
107
+ expect(readFileSync(join(r.dir, "out/app/config.txt"), "utf-8")).toBe("domain=copy.example.test\n");
108
+ expect(readFileSync(join(r.dir, "app/config.txt"), "utf-8")).toBe(`domain=${HOST_DOMAIN}\n`);
109
+ expect(existsSync(join(r.dir, "out/ops"))).toBe(false);
110
+ // The export is a workspace: its declaration lists app, without the link to ops.
111
+ const decl = JSON.parse(readFileSync(join(r.dir, "out/chant.workspace.json"), "utf-8"));
112
+ expect(decl.members.map((m: { name: string }) => m.name)).toEqual(["app"]);
113
+ expect(decl.members[0].links).toBeUndefined();
114
+ expect(decl.records).toEqual([{ kind: "kinds/note.kind.mjs" }]);
115
+ expect(res.manifest.dropped.links).toEqual([{ member: "app", to: "ops" }]);
116
+ // The lock records the export's value, and the unedited file keeps a matching hash.
117
+ const lock = readLock(join(r.dir, "out/app"))!;
118
+ expect(lock.scopes["."].parameters).toEqual({ domain: "copy.example.test" });
119
+ expect(lock.scopes["."].files["config.txt"].sha256).toBe(fileHash("domain=copy.example.test\n"));
120
+ expect(res.manifest.hostValues[`app/${LOCK_FILE}#.`]).toEqual({ domain: { host: HOST_DOMAIN, export: "copy.example.test" } });
121
+ expect(readManifestAt(join(r.dir, "out"))!.id).toBe(res.manifest.id);
122
+ // Records go byte for byte.
123
+ expect(readFileSync(join(r.dir, "out/records/n-1.md"), "utf-8")).toBe(readFileSync(join(r.dir, "records/n-1.md"), "utf-8"));
124
+ });
125
+
126
+ test("refuses a member that does not travel, a parameter no lineage binds, and a full export member it did not write", async () => {
127
+ const r = repo("refuse");
128
+ host(r);
129
+ await expect(exportWorkspace({ root: r.dir, members: ["ops"] })).rejects.toThrow(/does not set travel/);
130
+ await expect(exportWorkspace({ root: r.dir, params: { nope: "x" } })).rejects.toThrow(/no exported lineage has a host-bound parameter/);
131
+ r.write("out/mine.txt", "not an export\n");
132
+ await expect(exportWorkspace({ root: r.dir })).rejects.toThrow(/holds files no export wrote/);
133
+ });
134
+
135
+ test("never writes into another member", async () => {
136
+ const r = repo("nested");
137
+ r.write(
138
+ "chant.workspace.json",
139
+ JSON.stringify({ name: "acme", schema: 1, members: [{ name: "app", dir: "app", kind: "other", because: "x", travel: true }, { name: "out", dir: "app/out", kind: "workspace", roles: ["export"] }] }),
140
+ );
141
+ r.write("app/a.txt", "a\n");
142
+ r.commit("nested");
143
+ await expect(exportWorkspace({ root: r.dir })).rejects.toThrow(/inside member app/);
144
+ });
145
+ });
146
+
147
+ describe("import (#2552)", () => {
148
+ test("brings changes back, restores host values, and writes the export member again", async () => {
149
+ const r = repo("import");
150
+ host(r);
151
+ await exportWorkspace({ root: r.dir, params: { domain: "copy.example.test" } });
152
+ r.commit("export");
153
+ writeFileSync(join(r.dir, "out/app/server.txt"), "hello from the copy\n");
154
+ writeFileSync(join(r.dir, "out/app/config.txt"), "domain=copy.example.test\nport=8080\n");
155
+ const res = await importWorkspace({ root: r.dir });
156
+ expect(res.conflicts).toEqual([]);
157
+ expect(res.written.sort()).toEqual(["app/config.txt", "app/server.txt"]);
158
+ expect(res.hostValues).toEqual(["app/config.txt"]);
159
+ expect(readFileSync(join(r.dir, "app/server.txt"), "utf-8")).toBe("hello from the copy\n");
160
+ expect(readFileSync(join(r.dir, "app/config.txt"), "utf-8")).toBe(`domain=${HOST_DOMAIN}\nport=8080\n`);
161
+ expect(res.member).toMatchObject({ name: "out", action: "regenerated" });
162
+ expect(readFileSync(join(r.dir, "out/app/config.txt"), "utf-8")).toBe("domain=copy.example.test\nport=8080\n");
163
+ const ret = parseReturn(readFileSync(join(r.dir, res.returnRecord!), "utf-8"), "return");
164
+ expect(Object.keys(ret.paths).sort()).toEqual(["app/config.txt", "app/server.txt"]);
165
+ // The copy was not a repository of its own, so nothing carries an origin.
166
+ expect(ret.head).toBeNull();
167
+ });
168
+
169
+ test("refuses a conflict or a file outside the members that went, and writes nothing", async () => {
170
+ const r = repo("conflict");
171
+ host(r);
172
+ await exportWorkspace({ root: r.dir, params: { domain: "copy.example.test" } });
173
+ r.commit("export");
174
+ writeFileSync(join(r.dir, "out/app/server.txt"), "copy\n");
175
+ writeFileSync(join(r.dir, "app/server.txt"), "host\n");
176
+ const res = await importWorkspace({ root: r.dir });
177
+ expect(res.conflicts.map((c) => c.path)).toEqual(["app/server.txt"]);
178
+ expect(res.applied).toBe(false);
179
+ expect(readFileSync(join(r.dir, "app/server.txt"), "utf-8")).toBe("host\n");
180
+
181
+ r.git(["checkout", "--", "app/server.txt"]);
182
+ r.write("out/ops/sneak.txt", "into another member\n");
183
+ const out = await importWorkspace({ root: r.dir });
184
+ expect(out.outside).toEqual(["ops/sneak.txt"]);
185
+ expect(existsSync(join(r.dir, "ops/sneak.txt"))).toBe(false);
186
+ });
187
+
188
+ test("--remove removes the export member and its declaration entry", async () => {
189
+ const r = repo("remove");
190
+ host(r);
191
+ await exportWorkspace({ root: r.dir, params: { domain: "copy.example.test" } });
192
+ writeFileSync(join(r.dir, "out/app/server.txt"), "changed\n");
193
+ const res = await importWorkspace({ root: r.dir, remove: true });
194
+ expect(res.member).toMatchObject({ name: "out", action: "removed" });
195
+ expect(existsSync(join(r.dir, "out"))).toBe(false);
196
+ const decl = JSON.parse(readFileSync(join(r.dir, "chant.workspace.json"), "utf-8"));
197
+ expect(decl.members.map((m: { name: string }) => m.name)).toEqual(["app", "ops"]);
198
+ });
199
+ });
200
+
201
+ describe.runIf(hasSshKeygen)("hosted return (#2552)", () => {
202
+ async function records(r: TestRepo): Promise<RecordView[]> {
203
+ const doc = await queryRecords({ kind: "kinds/note.kind.mjs", cwd: r.dir, base: "main" });
204
+ if ("error" in doc) throw new Error(doc.error.message);
205
+ return doc.records;
206
+ }
207
+
208
+ test("returned work is unverifiable until an admin admits its signer, and keeps its original signature", async () => {
209
+ const r = repo("hosted");
210
+ const alice = r.key("alice");
211
+ host(r, alice);
212
+ await exportWorkspace({ root: r.dir, params: { domain: "copy.example.test" } });
213
+ r.commit("export", alice);
214
+
215
+ // The copy lives in a repository of its own, where bob, whom the host never saw, works.
216
+ const copy = repo("copy");
217
+ const bob = copy.key("bob");
218
+ cpSync(join(r.dir, "out"), copy.dir, { recursive: true });
219
+ const bobEnv = { GIT_COMMITTER_EMAIL: "bob@example.test", GIT_AUTHOR_EMAIL: "bob@example.test" };
220
+ copy.commit("as exported", bob, bobEnv);
221
+ copy.write("records/n-2.md", note("n-2", "made outside"));
222
+ copy.write("app/server.txt", "bob was here\n");
223
+ const made = copy.commit("outside work", bob, bobEnv);
224
+
225
+ const res = await importWorkspace({ root: r.dir, from: copy.dir });
226
+ expect(res.conflicts).toEqual([]);
227
+ expect(res.written.sort()).toEqual(["app/server.txt", "records/n-2.md"]);
228
+ const ret = res.return!;
229
+ expect(ret.head).toBe(made);
230
+ expect(ret.paths["records/n-2.md"].origin!.commit).toBe(made);
231
+ expect(ret.signers).toHaveLength(1);
232
+ expect(ret.signers[0]).toMatchObject({ key: bob.pub, principal: "bob@example.test", commits: [made] });
233
+ // The record's bytes are the copy's, and its origin ties them to bob's signed commit.
234
+ const bytes = readFileSync(join(r.dir, "records/n-2.md"));
235
+ expect(bytes.equals(readFileSync(join(copy.dir, "records/n-2.md")))).toBe(true);
236
+ expect(verifyOrigin(ret.paths["records/n-2.md"].origin!, "records/n-2.md", bytes)).toEqual({ ok: true });
237
+ expect(verifyOrigin(ret.paths["records/n-2.md"].origin!, "records/n-2.md", Buffer.from("forged\n")).ok).toBe(false);
238
+
239
+ r.commit("import", alice);
240
+ let rs = await records(r);
241
+ const n2 = rs.find((x) => x.id === "n-2")!;
242
+ expect(n2.provenance.level).toBe("attested-unverifiable-here");
243
+ expect(n2.provenance.returned).toMatchObject({ id: ret.id, commit: made });
244
+ expect(n2.provenance.reason).toContain(`chant workspace admit ${ret.id}`);
245
+ // The host's own record is still judged by the host's own commit.
246
+ expect(rs.find((x) => x.id === "n-1")!.provenance).toMatchObject({ level: "attested", principal: "alice@example.test" });
247
+
248
+ // An admin admits bob for this return; it counts once merged at base.
249
+ const admitted = admitReturn(r.dir, ret, { note: "returned from the copy" });
250
+ expect(admitted.added).toEqual([{ principal: "bob@example.test", key: bob.pub }]);
251
+ r.commit("admit", alice);
252
+ rs = await records(r);
253
+ expect(rs.find((x) => x.id === "n-2")!.provenance).toMatchObject({ level: "attested", principal: "bob@example.test", returned: { id: ret.id } });
254
+ // The admitted key never vouches for a commit made here.
255
+ r.write("records/n-3.md", note("n-3"));
256
+ r.commit("bob signs here", bob);
257
+ rs = await records(r);
258
+ expect(rs.find((x) => x.id === "n-3")!.provenance.level).toBe("unattested");
259
+ });
260
+
261
+ test("an origin is only built for bytes the commit holds", () => {
262
+ const copy = repo("origin");
263
+ copy.write("a/b.txt", "one\n");
264
+ copy.commit("one");
265
+ expect(buildOrigin(copy.dir, "a/b.txt", Buffer.from("one\n"))).toBeDefined();
266
+ copy.write("a/b.txt", "two\n");
267
+ expect(buildOrigin(copy.dir, "a/b.txt", Buffer.from("two\n"))).toBeUndefined();
268
+ });
269
+
270
+ test("a returned seal by a signer nobody admitted is unverifiable, and verifies once admitted", () => {
271
+ const r = repo("seal");
272
+ const alice = r.key("alice");
273
+ const bob = r.key("bob");
274
+ const seal = sealRecord(bob.file, { record: "n-2", digest: "d".repeat(64), author: "bob", state: "closed" });
275
+ const policy = { ...emptyPolicy("x".repeat(40)), active: true, signers: [{ principal: "alice", key: alice.pub, line: 1 }] };
276
+ const subject = { record: "n-2", digest: "d".repeat(64), author: "bob", authorField: "decided_by", state: "closed", seal };
277
+ expect(checkRecordSeal(policy, subject)).toMatchObject({ attested: false, code: "seal-signer-unlisted" });
278
+ expect(checkRecordSeal(returnedPolicy(policy, "ret-0123456789ab"), subject)).toMatchObject({ attested: null, code: "seal-unverifiable" });
279
+ const admittedPolicy = { ...policy, admitted: { "ret-0123456789ab": [{ principal: "bob", key: bob.pub, line: 0 }] } };
280
+ expect(checkRecordSeal(returnedPolicy(admittedPolicy, "ret-0123456789ab"), subject)).toMatchObject({ attested: true });
281
+ });
282
+ });