@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
@@ -107,6 +107,35 @@ export interface ChangesPolicy {
107
107
  ignore: string[];
108
108
  }
109
109
 
110
+ /** The core principal classes (#2524 D5). */
111
+ export const PRINCIPAL_CLASSES = ["human", "agent", "runner", "service"] as const;
112
+ export type PrincipalClass = (typeof PRINCIPAL_CLASSES)[number];
113
+
114
+ /** How a record is written, as the records commands name it (#2548). */
115
+ export const WRITE_VERBS = ["new", "amend", "review", "close"] as const;
116
+ export type WriteVerb = (typeof WRITE_VERBS)[number];
117
+
118
+ /** One principal class's entry in the declaration's `writeScope` (#2548, ws-067). */
119
+ export interface ClassScope {
120
+ /** The members whose files the class may write, or null for every path. Always null for the agent class: a session writes its own member. */
121
+ members: string[] | null;
122
+ /** Kind name to the verbs the class may write it with, or null for every kind in reach with every verb. */
123
+ records: Record<string, WriteVerb[]> | null;
124
+ pointer: string;
125
+ }
126
+
127
+ /** The declaration's `writeScope` block: an entry per restricted class. A class with no entry is not restricted. */
128
+ export type WriteScope = Partial<Record<PrincipalClass, ClassScope>>;
129
+
130
+ /** An agent session the declaration names (#2524 D20, #2548): bound to one member. */
131
+ export interface AgentDeclaration {
132
+ name: string;
133
+ member: string;
134
+ /** Principals that write only as this session. */
135
+ principals: string[];
136
+ pointer: string;
137
+ }
138
+
110
139
  export interface MemberRole {
111
140
  name: string;
112
141
  /** Relative to the member's directory, or null for the whole member. */
@@ -135,6 +164,8 @@ export interface LinkDeclaration {
135
164
  output: string;
136
165
  /** The link kind as written, or null for the default (`output`). */
137
166
  kind: string | null;
167
+ /** A telemetry link's OTLP protocol as written (#2558), or null. */
168
+ protocol: string | null;
138
169
  /** The link's JSON Pointer in the file, for messages. */
139
170
  pointer: string;
140
171
  }
@@ -292,6 +323,8 @@ export interface Member {
292
323
  box: BoxDeclaration | null;
293
324
  upstream: string | null;
294
325
  because: string | null;
326
+ /** Whether `chant workspace export` takes the member (#2552, D10). False when the entry does not say. */
327
+ travel: boolean;
295
328
  suppress: Suppression[];
296
329
  /** The entry's JSON Pointer in the file, for messages. */
297
330
  pointer: string;
@@ -338,6 +371,10 @@ export interface Declaration {
338
371
  hosts: Host[];
339
372
  /** The forward coverage check's policy (#2773), or null when the declaration has no `changes` block. */
340
373
  changes: ChangesPolicy | null;
374
+ /** Who may write what (#2548), or null when the declaration has no `writeScope` block. */
375
+ writeScope: WriteScope | null;
376
+ /** The agent sessions, in file order (#2548). */
377
+ agents: AgentDeclaration[];
341
378
  /** The file, relative to the workspace root's tree (`chant.workspace.json` or `.jsonc`). */
342
379
  file: string;
343
380
  }
@@ -543,10 +580,11 @@ export function parseDeclaration(text: string, file: string, reader: string = re
543
580
  handWritten: g.handWritten ? { because: (g.handWritten as { because: string }).because } : null,
544
581
  pointer: `${pointer}/generated/${j}`,
545
582
  }));
546
- const links = ((e.links as { member: string; output: string; kind?: string }[] | undefined) ?? []).map((l, j) => ({
583
+ const links = ((e.links as { member: string; output: string; kind?: string; protocol?: string }[] | undefined) ?? []).map((l, j) => ({
547
584
  member: l.member,
548
585
  output: l.output,
549
586
  kind: l.kind ?? null,
587
+ protocol: l.protocol ?? null,
550
588
  pointer: `${pointer}/links/${j}`,
551
589
  }));
552
590
  const records = recordKindsOf(e.records, e.dir as string, e.name as string, `${pointer}/records`);
@@ -566,6 +604,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
566
604
  box,
567
605
  upstream: (e.upstream as string | undefined) ?? null,
568
606
  because: (e.because as string | undefined) ?? null,
607
+ travel: e.travel === true,
569
608
  suppress,
570
609
  pointer,
571
610
  };
@@ -673,6 +712,8 @@ export function parseDeclaration(text: string, file: string, reader: string = re
673
712
  }
674
713
 
675
714
  const hosts = hostsOf(obj, members, at);
715
+ const writeScope = writeScopeOf(obj.writeScope, members, at);
716
+ const agents = agentsOf(obj.agents, members, at);
676
717
 
677
718
  const pins = ((obj.pins as Record<string, string>[] | undefined) ?? []).map((p) => ({
678
719
  package: p.package ?? null,
@@ -695,10 +736,74 @@ export function parseDeclaration(text: string, file: string, reader: string = re
695
736
  diagrams: ownDiagrams,
696
737
  hosts,
697
738
  changes: changesOf(obj.changes as Record<string, unknown> | undefined),
739
+ writeScope,
740
+ agents,
698
741
  file,
699
742
  };
700
743
  }
701
744
 
745
+ /**
746
+ * The `writeScope` block, already validated, with the rule the schema can't
747
+ * say (#2548): a member it names is a declared member.
748
+ */
749
+ function writeScopeOf(raw: unknown, members: Member[], at: (pointer: string, key?: boolean) => ErrorLocation): WriteScope | null {
750
+ if (raw === undefined) return null;
751
+ const block = raw as Partial<Record<PrincipalClass, { members?: "*" | string[]; records?: Record<string, WriteVerb[]> }>>;
752
+ const out: WriteScope = {};
753
+ for (const cls of PRINCIPAL_CLASSES) {
754
+ const entry = block[cls];
755
+ if (entry === undefined) continue;
756
+ const pointer = `/writeScope/${cls}`;
757
+ const list = entry.members === undefined || entry.members === "*" ? null : [...entry.members];
758
+ for (const [i, name] of (list ?? []).entries()) {
759
+ if (!members.some((m) => m.name === name)) {
760
+ throw new WorkspaceReadError(
761
+ "declaration-invalid",
762
+ `writeScope.${cls} names the member ${JSON.stringify(name)}, which the declaration does not declare; declared members: ${members.map((m) => m.name).join(", ") || "none"}`,
763
+ at(`${pointer}/members/${i}`),
764
+ );
765
+ }
766
+ }
767
+ const records = entry.records === undefined ? null : Object.fromEntries(Object.entries(entry.records).map(([k, v]) => [k, [...v]]));
768
+ out[cls] = { members: list, records, pointer };
769
+ }
770
+ return out;
771
+ }
772
+
773
+ /**
774
+ * The `agents` list, already validated, with the rules the schema can't say
775
+ * (#2548): names are unique, each names a declared member, and a principal
776
+ * is listed by one session at most.
777
+ */
778
+ function agentsOf(raw: unknown, members: Member[], at: (pointer: string, key?: boolean) => ErrorLocation): AgentDeclaration[] {
779
+ const agents = ((raw as { name: string; member: string; principals?: string[] }[] | undefined) ?? []).map((a, i) => ({
780
+ name: a.name,
781
+ member: a.member,
782
+ principals: [...(a.principals ?? [])],
783
+ pointer: `/agents/${i}`,
784
+ }));
785
+ const byName = new Map<string, AgentDeclaration>();
786
+ const byPrincipal = new Map<string, AgentDeclaration>();
787
+ for (const a of agents) {
788
+ const first = byName.get(a.name);
789
+ if (first) throw new WorkspaceReadError("declaration-invalid", `the agent name ${JSON.stringify(a.name)} is already used by the agent at ${first.pointer}`, at(`${a.pointer}/name`));
790
+ byName.set(a.name, a);
791
+ if (!members.some((m) => m.name === a.member)) {
792
+ throw new WorkspaceReadError(
793
+ "declaration-invalid",
794
+ `agent ${a.name} is bound to ${JSON.stringify(a.member)}, which is not a declared member; an agent session is bound to one member, not an example group`,
795
+ at(`${a.pointer}/member`),
796
+ );
797
+ }
798
+ for (const [i, p] of a.principals.entries()) {
799
+ const other = byPrincipal.get(p);
800
+ if (other) throw new WorkspaceReadError("declaration-invalid", `the principal ${JSON.stringify(p)} is already listed by agent ${other.name}`, at(`${a.pointer}/principals/${i}`));
801
+ byPrincipal.set(p, a);
802
+ }
803
+ }
804
+ return agents;
805
+ }
806
+
702
807
  /** The `changes` block, already validated, with its defaults (#2773). */
703
808
  function changesOf(block: Record<string, unknown> | undefined): ChangesPolicy | null {
704
809
  if (block === undefined) return null;
@@ -5,6 +5,7 @@
5
5
  * `--kind` is not given, while `--kind` still overrides.
6
6
  */
7
7
 
8
+ import { createHash } from "node:crypto";
8
9
  import { readFileSync } from "node:fs";
9
10
  import { join } from "node:path";
10
11
  import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
@@ -199,6 +200,38 @@ describe("records without --kind reads every declared kind (#2680)", () => {
199
200
  expect(printed.kinds.map((k: { records: { id: string }[] }) => k.records.map((r) => r.id))).toEqual([["ref-001"], ["ref-001"]]);
200
201
  });
201
202
 
203
+ test("--current adds the spec: the current records of spec: true kinds, with the files they pin (#2546, #2524 D20)", async () => {
204
+ const server = "export const port = 8080;\n";
205
+ const sha256 = createHash("sha256").update(server).digest("hex");
206
+ // ref-004 supersedes ref-001 and pins the app's server file; the note kind is not part of the spec.
207
+ const ref004 = REF_001.replace('id: "ref-001"', 'id: "ref-004"')
208
+ .replace("supersedes: []", 'supersedes:\n - decision: "ref-001"')
209
+ .replace(/^evidence:\n/m, `evidence:\n - title: "The server"\n path: "app/server.mjs"\n sha256: "${sha256}"\n`);
210
+ const root = workspace({
211
+ "decisions/ref-004-how-the-app-is-deployed-now.md": ref004,
212
+ "design/notes/note.kind.mjs": NOTE_KIND.replace("spec: true", "spec: false"),
213
+ });
214
+ expect(await run(root, "--current", "--json")).toBe(0);
215
+ const printed = JSON.parse(out.join("\n"));
216
+ expectValid(printed);
217
+ expect(printed.kinds.map((k: { kind: { spec: boolean } }) => k.kind.spec)).toEqual([true, false]);
218
+ expect(printed.spec.kinds).toEqual([{ member: null, path: "decisions/decision.kind.mjs", name: null }]);
219
+ expect(printed.spec.records).toEqual([
220
+ expect.objectContaining({
221
+ kind: "decision",
222
+ id: "ref-004",
223
+ path: "decisions/ref-004-how-the-app-is-deployed-now.md",
224
+ state: "decided",
225
+ valid: true,
226
+ assets: [{ path: "app/server.mjs", sha256, actual: sha256, state: "pinned" }],
227
+ }),
228
+ ]);
229
+ // Without --current there is no spec: superseded records are still listed under kinds.
230
+ out.length = 0;
231
+ expect(await run(root, "--json")).toBe(0);
232
+ expect(JSON.parse(out.join("\n")).spec).toBeUndefined();
233
+ });
234
+
202
235
  test("a kind that can't be read is listed with its error, the others are read, and the exit code is 1", async () => {
203
236
  const root = workspace({}, [{ kind: "gone.kind.mjs" }]);
204
237
  expect(await run(root, "--json")).toBe(1);
@@ -0,0 +1,279 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/evidence/v1/evidence.schema.json",
4
+ "title": "chant workspace evidence output",
5
+ "description": "What `chant workspace evidence verify --json` prints (#2553), and what a refused `chant workspace evidence sign --json` prints: runner evidence, an in-toto statement over record hashes in a DSSE envelope, checked offline against the runner keys `.chant/trust.json` lists at the base revision. Version 1 of the read contract. Readers ignore fields they do not know; a field is only ever added within a version. The error codes are a closed list, each in the one closed list of `reason-codes.ts`.",
6
+ "oneOf": [
7
+ {
8
+ "$ref": "#/$defs/result"
9
+ },
10
+ {
11
+ "$ref": "#/$defs/failure"
12
+ }
13
+ ],
14
+ "$defs": {
15
+ "result": {
16
+ "type": "object",
17
+ "description": "A runner key the policy at base lists signed the envelope. `status` says whether the records still hash as signed; it exits 1 when they don't.",
18
+ "required": [
19
+ "$schema",
20
+ "contract",
21
+ "chant",
22
+ "at",
23
+ "ok",
24
+ "status",
25
+ "runner",
26
+ "class",
27
+ "keyid",
28
+ "statement",
29
+ "subjects"
30
+ ],
31
+ "properties": {
32
+ "$schema": {
33
+ "const": "https://intentius.io/chant/schemas/workspace/evidence/v1/evidence.schema.json"
34
+ },
35
+ "contract": {
36
+ "const": 1
37
+ },
38
+ "chant": {
39
+ "type": "string"
40
+ },
41
+ "at": {
42
+ "type": "string",
43
+ "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$",
44
+ "description": "The commit the subjects were compared at."
45
+ },
46
+ "ok": {
47
+ "const": true
48
+ },
49
+ "status": {
50
+ "enum": [
51
+ "current",
52
+ "stale"
53
+ ],
54
+ "description": "current when every subject hashes the same at `at`; stale otherwise. Stale evidence is never reused."
55
+ },
56
+ "runner": {
57
+ "type": "string",
58
+ "description": "The runner principal whose key signed."
59
+ },
60
+ "class": {
61
+ "enum": [
62
+ "runner",
63
+ "service"
64
+ ],
65
+ "description": "A CI runner or a service, as `.chant/trust.json` lists it."
66
+ },
67
+ "keyid": {
68
+ "type": "string",
69
+ "description": "The ssh SHA256 fingerprint of the key that signed."
70
+ },
71
+ "statement": {
72
+ "$ref": "#/$defs/statement"
73
+ },
74
+ "subjects": {
75
+ "type": "array",
76
+ "items": {
77
+ "$ref": "#/$defs/subject"
78
+ }
79
+ }
80
+ }
81
+ },
82
+ "statement": {
83
+ "type": "object",
84
+ "description": "The in-toto Statement v1 the runner signed.",
85
+ "required": [
86
+ "_type",
87
+ "subject",
88
+ "predicateType",
89
+ "predicate"
90
+ ],
91
+ "properties": {
92
+ "_type": {
93
+ "const": "https://in-toto.io/Statement/v1"
94
+ },
95
+ "subject": {
96
+ "type": "array",
97
+ "items": {
98
+ "type": "object",
99
+ "required": [
100
+ "name",
101
+ "digest"
102
+ ],
103
+ "properties": {
104
+ "name": {
105
+ "type": "string"
106
+ },
107
+ "digest": {
108
+ "type": "object",
109
+ "required": [
110
+ "sha256"
111
+ ],
112
+ "properties": {
113
+ "sha256": {
114
+ "type": "string",
115
+ "pattern": "^[0-9a-f]{64}$"
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ },
122
+ "predicateType": {
123
+ "const": "https://intentius.io/chant/runner-evidence/v1"
124
+ },
125
+ "predicate": {
126
+ "type": "object",
127
+ "required": [
128
+ "runner",
129
+ "commit",
130
+ "tree",
131
+ "check"
132
+ ],
133
+ "properties": {
134
+ "runner": {
135
+ "type": "string"
136
+ },
137
+ "commit": {
138
+ "type": "string",
139
+ "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$"
140
+ },
141
+ "tree": {
142
+ "type": "string",
143
+ "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$"
144
+ },
145
+ "check": {
146
+ "type": "string",
147
+ "description": "The check's id, as given to --check-id."
148
+ },
149
+ "claim": {
150
+ "type": "object",
151
+ "required": [
152
+ "sha256"
153
+ ],
154
+ "properties": {
155
+ "sha256": {
156
+ "type": "string",
157
+ "pattern": "^[0-9a-f]{64}$"
158
+ }
159
+ }
160
+ },
161
+ "environment": {
162
+ "type": "object",
163
+ "required": [
164
+ "sha256"
165
+ ],
166
+ "properties": {
167
+ "sha256": {
168
+ "type": "string",
169
+ "pattern": "^[0-9a-f]{64}$"
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ },
177
+ "subject": {
178
+ "type": "object",
179
+ "required": [
180
+ "name",
181
+ "signed",
182
+ "now",
183
+ "matches"
184
+ ],
185
+ "properties": {
186
+ "name": {
187
+ "type": "string",
188
+ "description": "The record's path from the repository root."
189
+ },
190
+ "signed": {
191
+ "type": "string",
192
+ "pattern": "^[0-9a-f]{64}$"
193
+ },
194
+ "now": {
195
+ "oneOf": [
196
+ {
197
+ "type": "null"
198
+ },
199
+ {
200
+ "type": "string",
201
+ "pattern": "^[0-9a-f]{64}$"
202
+ }
203
+ ],
204
+ "description": "The record's hash at `at`, or null when it is not there."
205
+ },
206
+ "matches": {
207
+ "type": "boolean"
208
+ }
209
+ }
210
+ },
211
+ "failure": {
212
+ "type": "object",
213
+ "description": "The evidence was refused, or could not be checked. Exits 1.",
214
+ "required": [
215
+ "$schema",
216
+ "contract",
217
+ "chant",
218
+ "at",
219
+ "error"
220
+ ],
221
+ "properties": {
222
+ "$schema": {
223
+ "const": "https://intentius.io/chant/schemas/workspace/evidence/v1/evidence.schema.json"
224
+ },
225
+ "contract": {
226
+ "const": 1
227
+ },
228
+ "chant": {
229
+ "type": "string"
230
+ },
231
+ "at": {
232
+ "oneOf": [
233
+ {
234
+ "type": "null"
235
+ },
236
+ {
237
+ "type": "string",
238
+ "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$"
239
+ }
240
+ ]
241
+ },
242
+ "error": {
243
+ "type": "object",
244
+ "required": [
245
+ "code",
246
+ "message"
247
+ ],
248
+ "properties": {
249
+ "code": {
250
+ "enum": [
251
+ "not-a-git-repository",
252
+ "revision-unknown",
253
+ "trust-policy-unreadable",
254
+ "envelope-unreadable",
255
+ "envelope-invalid",
256
+ "envelope-untrusted",
257
+ "evidence-payload-type",
258
+ "evidence-statement-invalid",
259
+ "evidence-runner-mismatch",
260
+ "runner-key-invalid",
261
+ "runner-key-is-signer",
262
+ "runner-key-unlisted",
263
+ "kind-unreadable",
264
+ "kind-invalid",
265
+ "schema-unreadable",
266
+ "schema-id-mismatch",
267
+ "schema-invalid",
268
+ "location-missing"
269
+ ]
270
+ },
271
+ "message": {
272
+ "type": "string"
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * `chant workspace export [<member>...] [--to <member>] [--param name=value] [--dry-run] [--json]`,
3
+ * `chant workspace import [<dir>] [--remove] [--dry-run] [--json]` and
4
+ * `chant workspace admit <return id> [--note <text>] [--dry-run] [--json]` (#2552).
5
+ *
6
+ * See ./export.ts, ./import.ts and ./returns.ts.
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { formatError, formatInfo, formatSuccess, formatWarning } from "../cli/format";
11
+ import type { CommandContext } from "../cli/registry";
12
+ import { findWorkspaceRoot } from "../project-root";
13
+ import { WorkspaceReadError } from "./declaration";
14
+ import { LockError } from "./lineage-lock";
15
+ import { parseParamArgs } from "./template-manifest";
16
+
17
+ const EXPORT_USAGE = "chant workspace export [<member>[,<member>...]] [--to <export member>] [--param name=value] [--dry-run] [--json]";
18
+ const IMPORT_USAGE = "chant workspace import [<dir>] [--remove] [--dry-run] [--json]";
19
+ const ADMIT_USAGE = "chant workspace admit <return id> [--note <text>] [--dry-run] [--json]";
20
+
21
+ function workspaceRoot(usage: string): string | undefined {
22
+ const found = findWorkspaceRoot(process.cwd());
23
+ if (!found) {
24
+ console.error(formatError({ message: "no chant.workspace.json here or above", hint: usage }));
25
+ return undefined;
26
+ }
27
+ return found.dir;
28
+ }
29
+
30
+ function fail(err: unknown, usage: string): number {
31
+ if (err instanceof WorkspaceReadError) {
32
+ console.error(formatError({ message: `${err.code}: ${err.describe()}`, hint: usage }));
33
+ return 1;
34
+ }
35
+ if (err instanceof LockError) {
36
+ console.error(formatError({ message: err.message, hint: usage }));
37
+ return 1;
38
+ }
39
+ throw err;
40
+ }
41
+
42
+ export async function runWorkspaceExport(ctx: CommandContext): Promise<number> {
43
+ const { args } = ctx;
44
+ const root = workspaceRoot(EXPORT_USAGE);
45
+ if (!root) return 1;
46
+ const members = [args.extraPositional, args.extraPositional2]
47
+ .filter((x): x is string => x !== undefined)
48
+ .flatMap((x) => x.split(","))
49
+ .map((x) => x.trim())
50
+ .filter(Boolean);
51
+ try {
52
+ const { exportWorkspace } = await import("./export");
53
+ const r = await exportWorkspace({
54
+ root,
55
+ members,
56
+ to: args.migrateTo,
57
+ params: args.param?.length ? parseParamArgs(args.param) : undefined,
58
+ dryRun: args.dryRun,
59
+ });
60
+ if (args.json) {
61
+ console.log(JSON.stringify(r, null, 2));
62
+ return 0;
63
+ }
64
+ const m = r.manifest;
65
+ console.log(`${m.id} ${m.whole ? "workspace" : "members"} ${m.members.join(", ")} -> ${r.member} (${r.dir})`);
66
+ console.log(` ${Object.keys(m.files).length} file(s), ${Object.keys(m.locks).length} lock(s)${m.dirs.length ? `, record directories ${m.dirs.join(", ")}` : ""}`);
67
+ if (r.switched.length > 0) console.log(` host values switched in: ${r.switched.join(", ")}`);
68
+ const d = m.dropped;
69
+ const left = [
70
+ ...d.links.map((l) => `link ${l.member} -> ${l.to}`),
71
+ ...d.agents.map((a) => `agent ${a}`),
72
+ ...d.pins.map((p) => `pin ${p}`),
73
+ ...d.records.map((k) => `record kind ${k}`),
74
+ ...d.diagrams.map((x) => `diagram ${x}`),
75
+ ];
76
+ if (left.length > 0) console.log(` left out of the export's declaration: ${left.join("; ")}`);
77
+ if (m.from.dirty) console.error(formatWarning({ message: "exported paths have uncommitted changes; the export records HEAD, which does not hold them" }));
78
+ if (r.written) console.error(formatSuccess(`wrote the export into ${r.dir}. It is a workspace of its own; copy it where it goes, and bring it back with chant workspace import.`));
79
+ else console.error(formatInfo("--dry-run: nothing written"));
80
+ return 0;
81
+ } catch (err) {
82
+ return fail(err, EXPORT_USAGE);
83
+ }
84
+ }
85
+
86
+ export async function runWorkspaceImport(ctx: CommandContext): Promise<number> {
87
+ const { args } = ctx;
88
+ if (args.extraPositional2) {
89
+ console.error(formatError({ message: `unexpected argument: ${args.extraPositional2}`, hint: IMPORT_USAGE }));
90
+ return 1;
91
+ }
92
+ const root = workspaceRoot(IMPORT_USAGE);
93
+ if (!root) return 1;
94
+ try {
95
+ const { importWorkspace } = await import("./import");
96
+ const r = await importWorkspace({ root, from: args.extraPositional ? join(process.cwd(), args.extraPositional) : undefined, remove: args.remove, dryRun: args.dryRun });
97
+ const refused = r.outside.length > 0 || r.conflicts.length > 0;
98
+ if (args.json) {
99
+ console.log(JSON.stringify(r, null, 2));
100
+ return refused ? 1 : 0;
101
+ }
102
+ console.log(`${r.export} from ${r.from}`);
103
+ for (const p of r.written) console.log(` write ${p}${r.hostValues.includes(p) ? " (host values back)" : ""}`);
104
+ for (const p of r.removed) console.log(` remove ${p}`);
105
+ for (const p of r.locks) console.log(` lock ${p}`);
106
+ for (const c of r.conflicts) console.log(` conflict ${c.path}: ${c.reason}`);
107
+ for (const p of r.outside) console.log(` outside ${p}`);
108
+ if (r.outside.length > 0) {
109
+ console.error(formatError({ message: `the copy holds ${r.outside.length} file(s) outside the members that went with the export; import never writes into another member, so nothing was written` }));
110
+ return 1;
111
+ }
112
+ if (r.conflicts.length > 0) {
113
+ console.error(formatError({ message: `${r.conflicts.length} conflict(s): nothing was written. Resolve them in the copy or here, then import again` }));
114
+ return 1;
115
+ }
116
+ if (r.return) {
117
+ const proved = Object.values(r.return.paths).filter((p) => p.origin).length;
118
+ console.log(` return ${r.return.id}: ${proved} file(s) carry the commit they were made in${r.return.signers.length ? `, signed by ${r.return.signers.map((s) => `${s.principal} (${s.fingerprint || "key"})`).join(", ")}` : ""}`);
119
+ }
120
+ if (!r.applied) {
121
+ console.error(formatInfo("--dry-run: nothing written"));
122
+ return 0;
123
+ }
124
+ if (!r.return) console.error(formatInfo("the copy holds no change since the export"));
125
+ if (r.member) console.log(` member ${r.member.name} ${r.member.action}${r.member.note ? `: ${r.member.note}` : ""}`);
126
+ if (r.returnRecord) {
127
+ console.error(
128
+ formatSuccess(
129
+ `imported, and recorded the return in ${r.returnRecord}. Review and commit it. Work signed by keys the policy at base does not list reads as attested-unverifiable-here until an admin runs chant workspace admit ${r.return!.id} and merges that change.`,
130
+ ),
131
+ );
132
+ }
133
+ return 0;
134
+ } catch (err) {
135
+ return fail(err, IMPORT_USAGE);
136
+ }
137
+ }
138
+
139
+ export async function runWorkspaceAdmit(ctx: CommandContext): Promise<number> {
140
+ const { args } = ctx;
141
+ const id = args.extraPositional;
142
+ if (!id || args.extraPositional2) {
143
+ console.error(formatError({ message: "admit takes one return id, such as ret-0123456789ab", hint: ADMIT_USAGE }));
144
+ return 1;
145
+ }
146
+ const root = workspaceRoot(ADMIT_USAGE);
147
+ if (!root) return 1;
148
+ try {
149
+ const { readFileSync, existsSync } = await import("node:fs");
150
+ const { RETURNS_DIR, admitReturn, parseReturn } = await import("./returns");
151
+ const { gitTopOf } = await import("./export");
152
+ const file = join(root, RETURNS_DIR, `${id}.json`);
153
+ if (!/^ret-[0-9a-f]{12}$/.test(id) || !existsSync(file)) throw new LockError(`no return ${id} in ${RETURNS_DIR}`);
154
+ const ret = parseReturn(readFileSync(file, "utf-8"), file);
155
+ const repo = gitTopOf(root);
156
+ if (!repo) throw new LockError("admit writes .chant/trust.json at the repository root, and this workspace is not in a git repository");
157
+ const r = admitReturn(repo, ret, { note: args.note, dryRun: args.dryRun });
158
+ if (args.json) {
159
+ console.log(JSON.stringify(r, null, 2));
160
+ return 0;
161
+ }
162
+ if (r.added.length === 0) {
163
+ console.error(formatInfo(`every signer of ${id} is already admitted in ${r.path}`));
164
+ return 0;
165
+ }
166
+ for (const s of r.added) console.log(` admit ${s.principal} ${s.key.slice(0, 40)}...`);
167
+ if (!r.written) {
168
+ console.error(formatInfo("--dry-run: nothing written"));
169
+ return 0;
170
+ }
171
+ console.error(
172
+ formatSuccess(
173
+ `added ${r.added.length} signer(s) for ${id} to ${r.path}. Check each principal, then commit it signed: the file is policy, so the admission counts once an admin's signed commit is merged to the base branch.`,
174
+ ),
175
+ );
176
+ return 0;
177
+ } catch (err) {
178
+ return fail(err, ADMIT_USAGE);
179
+ }
180
+ }