@sentropic/h2a 0.98.5 → 0.98.7-rc.1

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 (225) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/dist/cli-command-map.d.ts.map +1 -1
  4. package/dist/cli-command-map.js +1 -0
  5. package/dist/cli-command-map.js.map +1 -1
  6. package/dist/cli-contract.js +2 -2
  7. package/dist/cli-contract.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +224 -16
  10. package/dist/cli.js.map +1 -1
  11. package/dist/hosts/agy.js +1 -1
  12. package/dist/hosts/agy.js.map +1 -1
  13. package/dist/hosts/installation-doctor.d.ts +1 -0
  14. package/dist/hosts/installation-doctor.d.ts.map +1 -1
  15. package/dist/hosts/installation-doctor.js +1 -1
  16. package/dist/hosts/installation-doctor.js.map +1 -1
  17. package/dist/hosts/muse.d.ts.map +1 -1
  18. package/dist/hosts/muse.js +2 -4
  19. package/dist/hosts/muse.js.map +1 -1
  20. package/dist/hosts/native-artifacts.d.ts +33 -0
  21. package/dist/hosts/native-artifacts.d.ts.map +1 -0
  22. package/dist/hosts/native-artifacts.js +104 -0
  23. package/dist/hosts/native-artifacts.js.map +1 -0
  24. package/dist/hosts/native-catalog.d.ts +19 -0
  25. package/dist/hosts/native-catalog.d.ts.map +1 -0
  26. package/dist/hosts/native-catalog.js +14 -0
  27. package/dist/hosts/native-catalog.js.map +1 -0
  28. package/dist/hosts/native-convergence.d.ts +132 -0
  29. package/dist/hosts/native-convergence.d.ts.map +1 -0
  30. package/dist/hosts/native-convergence.js +86 -0
  31. package/dist/hosts/native-convergence.js.map +1 -0
  32. package/dist/hosts/native-files.d.ts +40 -0
  33. package/dist/hosts/native-files.d.ts.map +1 -0
  34. package/dist/hosts/native-files.js +131 -0
  35. package/dist/hosts/native-files.js.map +1 -0
  36. package/dist/hosts/native-inspection.d.ts +31 -0
  37. package/dist/hosts/native-inspection.d.ts.map +1 -0
  38. package/dist/hosts/native-inspection.js +79 -0
  39. package/dist/hosts/native-inspection.js.map +1 -0
  40. package/dist/hosts/native-paths.d.ts +11 -0
  41. package/dist/hosts/native-paths.d.ts.map +1 -0
  42. package/dist/hosts/native-paths.js +21 -0
  43. package/dist/hosts/native-paths.js.map +1 -0
  44. package/dist/hosts/native-proof.d.ts +32 -0
  45. package/dist/hosts/native-proof.d.ts.map +1 -0
  46. package/dist/hosts/native-proof.js +31 -0
  47. package/dist/hosts/native-proof.js.map +1 -0
  48. package/dist/hosts/native-runtime-record.d.ts +3 -0
  49. package/dist/hosts/native-runtime-record.d.ts.map +1 -0
  50. package/dist/hosts/native-runtime-record.js +60 -0
  51. package/dist/hosts/native-runtime-record.js.map +1 -0
  52. package/dist/hosts/native-skills.d.ts +24 -0
  53. package/dist/hosts/native-skills.d.ts.map +1 -0
  54. package/dist/hosts/native-skills.js +56 -0
  55. package/dist/hosts/native-skills.js.map +1 -0
  56. package/dist/hosts/native-toml.d.ts +18 -0
  57. package/dist/hosts/native-toml.d.ts.map +1 -0
  58. package/dist/hosts/native-toml.js +91 -0
  59. package/dist/hosts/native-toml.js.map +1 -0
  60. package/dist/hosts/operator-notices.d.ts +3 -0
  61. package/dist/hosts/operator-notices.d.ts.map +1 -0
  62. package/dist/hosts/operator-notices.js +31 -0
  63. package/dist/hosts/operator-notices.js.map +1 -0
  64. package/dist/hosts/plugin.d.ts.map +1 -1
  65. package/dist/hosts/plugin.js +3 -2
  66. package/dist/hosts/plugin.js.map +1 -1
  67. package/dist/hosts/upgrade-equipment.d.ts +42 -0
  68. package/dist/hosts/upgrade-equipment.d.ts.map +1 -0
  69. package/dist/hosts/upgrade-equipment.js +26 -0
  70. package/dist/hosts/upgrade-equipment.js.map +1 -0
  71. package/dist/hosts/vibe-discovery.d.ts +12 -0
  72. package/dist/hosts/vibe-discovery.d.ts.map +1 -0
  73. package/dist/hosts/vibe-discovery.js +33 -0
  74. package/dist/hosts/vibe-discovery.js.map +1 -0
  75. package/dist/hosts/vibe-equipment.d.ts +71 -0
  76. package/dist/hosts/vibe-equipment.d.ts.map +1 -0
  77. package/dist/hosts/vibe-equipment.js +162 -0
  78. package/dist/hosts/vibe-equipment.js.map +1 -0
  79. package/dist/hosts/vibe-mcp.d.ts +32 -0
  80. package/dist/hosts/vibe-mcp.d.ts.map +1 -0
  81. package/dist/hosts/vibe-mcp.js +210 -0
  82. package/dist/hosts/vibe-mcp.js.map +1 -0
  83. package/dist/hosts/vibe.d.ts +12 -3
  84. package/dist/hosts/vibe.d.ts.map +1 -1
  85. package/dist/hosts/vibe.js +15 -7
  86. package/dist/hosts/vibe.js.map +1 -1
  87. package/dist/index.d.ts +2 -1
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/runtime/drive/consent.d.ts +1 -1
  92. package/dist/runtime/drive/consent.d.ts.map +1 -1
  93. package/dist/runtime/drive/consent.js +2 -2
  94. package/dist/runtime/drive/consent.js.map +1 -1
  95. package/dist/runtime/drive/index.d.ts +1 -1
  96. package/dist/runtime/drive/index.d.ts.map +1 -1
  97. package/dist/runtime/drive/index.js +3 -1
  98. package/dist/runtime/drive/index.js.map +1 -1
  99. package/dist/runtime/drumbeat/relaunchers.d.ts +1 -1
  100. package/dist/runtime/drumbeat/relaunchers.d.ts.map +1 -1
  101. package/dist/runtime/drumbeat/relaunchers.js +2 -1
  102. package/dist/runtime/drumbeat/relaunchers.js.map +1 -1
  103. package/dist/runtime/identity/binding-commit.d.ts +31 -0
  104. package/dist/runtime/identity/binding-commit.d.ts.map +1 -0
  105. package/dist/runtime/identity/binding-commit.js +211 -0
  106. package/dist/runtime/identity/binding-commit.js.map +1 -0
  107. package/dist/runtime/identity/bindings.d.ts +6 -30
  108. package/dist/runtime/identity/bindings.d.ts.map +1 -1
  109. package/dist/runtime/identity/bindings.js +85 -96
  110. package/dist/runtime/identity/bindings.js.map +1 -1
  111. package/dist/runtime/identity/durable.d.ts +7 -0
  112. package/dist/runtime/identity/durable.d.ts.map +1 -0
  113. package/dist/runtime/identity/durable.js +68 -0
  114. package/dist/runtime/identity/durable.js.map +1 -0
  115. package/dist/runtime/identity/live.d.ts +4 -1
  116. package/dist/runtime/identity/live.d.ts.map +1 -1
  117. package/dist/runtime/identity/live.js +59 -6
  118. package/dist/runtime/identity/live.js.map +1 -1
  119. package/dist/runtime/identity/possession.d.ts +34 -0
  120. package/dist/runtime/identity/possession.d.ts.map +1 -0
  121. package/dist/runtime/identity/possession.js +162 -0
  122. package/dist/runtime/identity/possession.js.map +1 -0
  123. package/dist/runtime/identity/worker.js +16 -5
  124. package/dist/runtime/identity/worker.js.map +1 -1
  125. package/dist/runtime/live/contracts.d.ts +55 -0
  126. package/dist/runtime/live/contracts.d.ts.map +1 -0
  127. package/dist/runtime/live/contracts.js +59 -0
  128. package/dist/runtime/live/contracts.js.map +1 -0
  129. package/dist/runtime/local-files/identity-fence.d.ts +164 -0
  130. package/dist/runtime/local-files/identity-fence.d.ts.map +1 -0
  131. package/dist/runtime/local-files/identity-fence.js +1338 -0
  132. package/dist/runtime/local-files/identity-fence.js.map +1 -0
  133. package/dist/runtime/local-files/index.d.ts +1 -0
  134. package/dist/runtime/local-files/index.d.ts.map +1 -1
  135. package/dist/runtime/local-files/index.js +1 -0
  136. package/dist/runtime/local-files/index.js.map +1 -1
  137. package/dist/runtime/local-files/legacy-identity-evidence.d.ts +26 -0
  138. package/dist/runtime/local-files/legacy-identity-evidence.d.ts.map +1 -0
  139. package/dist/runtime/local-files/legacy-identity-evidence.js +234 -0
  140. package/dist/runtime/local-files/legacy-identity-evidence.js.map +1 -0
  141. package/dist/runtime/local-files/locks.d.ts +3 -0
  142. package/dist/runtime/local-files/locks.d.ts.map +1 -1
  143. package/dist/runtime/local-files/locks.js +6 -5
  144. package/dist/runtime/local-files/locks.js.map +1 -1
  145. package/dist/runtime/local-files/orphan-gc.d.ts +3 -0
  146. package/dist/runtime/local-files/orphan-gc.d.ts.map +1 -0
  147. package/dist/runtime/local-files/orphan-gc.js +69 -0
  148. package/dist/runtime/local-files/orphan-gc.js.map +1 -0
  149. package/dist/runtime/local-files/presence.d.ts +5 -2
  150. package/dist/runtime/local-files/presence.d.ts.map +1 -1
  151. package/dist/runtime/local-files/presence.js +32 -29
  152. package/dist/runtime/local-files/presence.js.map +1 -1
  153. package/dist/runtime/local-files/recipient-hints.d.ts +10 -0
  154. package/dist/runtime/local-files/recipient-hints.d.ts.map +1 -0
  155. package/dist/runtime/local-files/recipient-hints.js +156 -0
  156. package/dist/runtime/local-files/recipient-hints.js.map +1 -0
  157. package/dist/runtime/local-files/store.d.ts +1 -1
  158. package/dist/runtime/local-files/store.d.ts.map +1 -1
  159. package/dist/runtime/local-files/store.js +12 -1
  160. package/dist/runtime/local-files/store.js.map +1 -1
  161. package/dist/runtime/local-files/succession-lock.d.ts +71 -6
  162. package/dist/runtime/local-files/succession-lock.d.ts.map +1 -1
  163. package/dist/runtime/local-files/succession-lock.js +226 -31
  164. package/dist/runtime/local-files/succession-lock.js.map +1 -1
  165. package/dist/runtime/mcp/handlers.d.ts +1 -0
  166. package/dist/runtime/mcp/handlers.d.ts.map +1 -1
  167. package/dist/runtime/mcp/handlers.js +7 -4
  168. package/dist/runtime/mcp/handlers.js.map +1 -1
  169. package/dist/runtime/mcp/identity-blocker-snapshot.d.ts +2 -0
  170. package/dist/runtime/mcp/identity-blocker-snapshot.d.ts.map +1 -0
  171. package/dist/runtime/mcp/identity-blocker-snapshot.js +98 -0
  172. package/dist/runtime/mcp/identity-blocker-snapshot.js.map +1 -0
  173. package/dist/runtime/mcp/identity-state.d.ts +20 -3
  174. package/dist/runtime/mcp/identity-state.d.ts.map +1 -1
  175. package/dist/runtime/mcp/identity-state.js +98 -13
  176. package/dist/runtime/mcp/identity-state.js.map +1 -1
  177. package/dist/runtime/mcp/notifications.d.ts +2 -1
  178. package/dist/runtime/mcp/notifications.d.ts.map +1 -1
  179. package/dist/runtime/mcp/notifications.js +16 -2
  180. package/dist/runtime/mcp/notifications.js.map +1 -1
  181. package/dist/runtime/mcp/server.d.ts +3 -0
  182. package/dist/runtime/mcp/server.d.ts.map +1 -1
  183. package/dist/runtime/mcp/server.js +31 -15
  184. package/dist/runtime/mcp/server.js.map +1 -1
  185. package/dist/runtime/mcp/sessions.d.ts +6 -6
  186. package/dist/runtime/mcp/sessions.d.ts.map +1 -1
  187. package/dist/runtime/mcp/sessions.js +18 -6
  188. package/dist/runtime/mcp/sessions.js.map +1 -1
  189. package/dist/runtime/mcp/stdio.d.ts.map +1 -1
  190. package/dist/runtime/mcp/stdio.js +11 -0
  191. package/dist/runtime/mcp/stdio.js.map +1 -1
  192. package/dist/runtime/mcp/tools.js +1 -1
  193. package/dist/runtime/mcp/tools.js.map +1 -1
  194. package/dist/runtime/remote/client.d.ts +1 -1
  195. package/dist/runtime/remote/client.d.ts.map +1 -1
  196. package/dist/runtime/remote/client.js +1 -1
  197. package/dist/runtime/remote/client.js.map +1 -1
  198. package/dist/runtime/send.d.ts +10 -0
  199. package/dist/runtime/send.d.ts.map +1 -1
  200. package/dist/runtime/send.js +80 -5
  201. package/dist/runtime/send.js.map +1 -1
  202. package/dist/runtime/upgrade/index.d.ts +1 -0
  203. package/dist/runtime/upgrade/index.d.ts.map +1 -1
  204. package/dist/runtime/upgrade/index.js +1 -0
  205. package/dist/runtime/upgrade/index.js.map +1 -1
  206. package/dist/vendor/engram-live/contracts.d.ts +208 -0
  207. package/dist/vendor/engram-live/contracts.js +558 -0
  208. package/fixtures/native-host-catalog.json +1 -0
  209. package/fixtures/native-host-history.json +1261 -0
  210. package/fixtures/native-host-renderers.json +1 -0
  211. package/hooks/deny-manual-h2a-cli.mjs +38 -0
  212. package/hooks/hooks.json +17 -0
  213. package/hooks/vibe-post-agent.mjs +29 -0
  214. package/hooks/vibe-pre-tool.mjs +40 -0
  215. package/package.json +9 -6
  216. package/scripts/qualification-budget.mjs +56 -0
  217. package/scripts/qualification-cleanup.mjs +15 -0
  218. package/scripts/qualification-oracles.mjs +44 -0
  219. package/scripts/qualification-patch.mjs +21 -0
  220. package/scripts/qualification-poll-hosts.mjs +19 -0
  221. package/scripts/qualification-probe.mjs +63 -0
  222. package/scripts/qualification-rpc.mjs +59 -0
  223. package/scripts/qualification-vibe.mjs +159 -0
  224. package/scripts/qualify-host.mjs +143 -0
  225. package/skills/h2a/SKILL.md +6 -2
@@ -0,0 +1,558 @@
1
+ // Vendored from @sentropic/engram-live 0.1.0 dist/contracts.js — extracted
2
+ // byte-identical from vendor/sentropic-engram-live-0.1.0.tgz (graphify@
3
+ // 70ebdfee, PR rhanka/engram#359; the tarball is removed after vendoring and
4
+ // its sha512 is recorded in the GUARD test of live-registration.test.ts).
5
+ // Only the portable entries H consumes are vendored (this module takes no
6
+ // imports at all); upstream is unpublished and os-bounded (linux), so a
7
+ // file: dependency breaks cross-platform install and packed redistribution.
8
+ // Do not hand-edit.
9
+ // @sentropic/engram-live/contracts — built, data-pure contract export
10
+ // (spec/SPEC_LIVE_GIT_INDEXING.md §3, §11; plan/09 L1b).
11
+ //
12
+ // This module is data-pure: DTOs, constants and validators only. It takes no
13
+ // imports at all — neither node: builtins, host types, model modules nor
14
+ // storage drivers. The executable factory lives in ./integration.js; hosts
15
+ // import only this contract at the adapter boundary and lazily load the
16
+ // factory when they compose live (§3).
17
+ export const CONTRACT_NAME = "@sentropic/engram-live/contracts";
18
+ export const CONTRACT_VERSION = 1;
19
+ export const PRODUCER_NAME = "engram-live";
20
+ export const POLICY_VERSION = "live-git-indexing/1";
21
+ export const CAPABILITY_IDS = [
22
+ "refs",
23
+ "history-dag",
24
+ "github-pr",
25
+ "index-snapshot",
26
+ "working-tree",
27
+ "diffs",
28
+ "code-analysis",
29
+ "retention-requests",
30
+ "reconciliation",
31
+ "publication",
32
+ ];
33
+ /** The lot that implements each capability; part of the advertised contract. */
34
+ export const CAPABILITY_PLANNED_LOTS = {
35
+ "refs": "L2a",
36
+ "history-dag": "L2b",
37
+ "github-pr": "P1",
38
+ "index-snapshot": "L4a",
39
+ "working-tree": "L4a",
40
+ "diffs": "L4b",
41
+ "code-analysis": "L5",
42
+ "retention-requests": "R1",
43
+ "reconciliation": "L6b",
44
+ "publication": "L6b",
45
+ };
46
+ export const NOT_IMPLEMENTED_REASON = "engram-live L1b supplies the package/factory seams and grammar loading only; this capability is explicitly unavailable until its lot lands";
47
+ export function describeCapabilities() {
48
+ return CAPABILITY_IDS.map((capability) => ({
49
+ capability,
50
+ state: "unavailable",
51
+ reason: NOT_IMPLEMENTED_REASON,
52
+ plannedLot: CAPABILITY_PLANNED_LOTS[capability],
53
+ }));
54
+ }
55
+ export const COVERAGE_AXES = [
56
+ "refs",
57
+ "dag",
58
+ "pr-metadata",
59
+ "object-availability",
60
+ "pr-attribution",
61
+ "ast",
62
+ "index",
63
+ "working-tree",
64
+ "remote-publication",
65
+ ];
66
+ /** Languages with a pinned, packaged tree-sitter WASM grammar (L1b matrix). */
67
+ export const ADVERTISED_LANGUAGES = [
68
+ "c",
69
+ "cpp",
70
+ "go",
71
+ "java",
72
+ "javascript",
73
+ "php",
74
+ "python",
75
+ "ruby",
76
+ "rust",
77
+ "typescript",
78
+ "tsx",
79
+ ];
80
+ export const GRAMMAR_UNAVAILABLE_REASON = "no packaged grammar is pinned for this language; AST coverage is explicitly unavailable";
81
+ function isRecord(value) {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
84
+ function isNonEmptyString(value) {
85
+ return typeof value === "string" && value.trim().length > 0;
86
+ }
87
+ function isPositiveInteger(value) {
88
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
89
+ }
90
+ function hasNul(value) {
91
+ return typeof value === "string" && value.includes("\0");
92
+ }
93
+ /** Opaque scoped id as minted by the canonical derivation (prefixed, NUL-free). */
94
+ function isOpaqueId(value, prefix) {
95
+ return isNonEmptyString(value) && !hasNul(value) && value.startsWith(prefix);
96
+ }
97
+ function isUncertaintyList(value) {
98
+ return Array.isArray(value) && value.every((state) => isIdentityUncertaintyState(state));
99
+ }
100
+ // Exact-key enforcement (L6a): every DTO validator rejects unknown keys so
101
+ // a misspelled or smuggled field can never pass as a valid declaration,
102
+ // clone, worktree, batch or revocation.
103
+ function rejectUnknownKeys(value, allowed, path, errors) {
104
+ for (const key of Object.keys(value)) {
105
+ if (!allowed.includes(key))
106
+ errors.push(`${path}.${key} is not a known key`);
107
+ }
108
+ }
109
+ const LIVE_SCOPE_KEYS = ["tenant", "clone", "worktree"];
110
+ export function validateLiveScope(scope) {
111
+ if (!isRecord(scope))
112
+ return { ok: false, errors: ["scope must be an object"] };
113
+ const errors = [];
114
+ rejectUnknownKeys(scope, LIVE_SCOPE_KEYS, "scope", errors);
115
+ if (!isNonEmptyString(scope.tenant))
116
+ errors.push("scope.tenant must be a non-empty string");
117
+ if (!isNonEmptyString(scope.clone))
118
+ errors.push("scope.clone must be a non-empty string");
119
+ if (scope.worktree !== undefined && !isNonEmptyString(scope.worktree)) {
120
+ errors.push("scope.worktree, when present, must be a non-empty string");
121
+ }
122
+ return { ok: errors.length === 0, errors };
123
+ }
124
+ const WORKSPACE_DECLARATION_KEYS = [
125
+ "tenant",
126
+ "principal",
127
+ "hostFingerprint",
128
+ "identityUncertainty",
129
+ "declarationId",
130
+ "cloneId",
131
+ "worktreeId",
132
+ "authorizedRoots",
133
+ "liveCapability",
134
+ "policyVersion",
135
+ "declarationRevision",
136
+ "serverWorkspaceId",
137
+ "serverIssuer",
138
+ "bindingRevision",
139
+ ];
140
+ export function validateWorkspaceDeclaration(declaration) {
141
+ if (!isRecord(declaration))
142
+ return { ok: false, errors: ["declaration must be an object"] };
143
+ const errors = [];
144
+ rejectUnknownKeys(declaration, WORKSPACE_DECLARATION_KEYS, "declaration", errors);
145
+ if (!isNonEmptyString(declaration.tenant) || hasNul(declaration.tenant)) {
146
+ errors.push("declaration.tenant must be a non-empty string without NUL");
147
+ }
148
+ if (!isNonEmptyString(declaration.principal) || hasNul(declaration.principal)) {
149
+ errors.push("declaration.principal must be a non-empty string without NUL");
150
+ }
151
+ if (!isUncertaintyList(declaration.identityUncertainty)) {
152
+ errors.push(`declaration.identityUncertainty must be an array of uncertainty states (${IDENTITY_UNCERTAINTY_STATES.join(", ")})`);
153
+ }
154
+ if (declaration.hostFingerprint !== undefined) {
155
+ if (!isNonEmptyString(declaration.hostFingerprint) || hasNul(declaration.hostFingerprint)) {
156
+ errors.push("declaration.hostFingerprint, when present, must be a non-empty string without NUL");
157
+ }
158
+ }
159
+ else if (!isUncertaintyList(declaration.identityUncertainty) ||
160
+ !declaration.identityUncertainty.includes("missing_history")) {
161
+ errors.push('declaration.hostFingerprint is absent: identityUncertainty must explicitly contain "missing_history" (LIVE §12)');
162
+ }
163
+ if (!isOpaqueId(declaration.declarationId, ID_PREFIXES.declaration)) {
164
+ errors.push(`declaration.declarationId must be a non-empty opaque id starting with "${ID_PREFIXES.declaration}"`);
165
+ }
166
+ if (!isOpaqueId(declaration.cloneId, ID_PREFIXES.clone)) {
167
+ errors.push(`declaration.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
168
+ }
169
+ if (!isOpaqueId(declaration.worktreeId, ID_PREFIXES.worktree)) {
170
+ errors.push(`declaration.worktreeId must be a non-empty opaque id starting with "${ID_PREFIXES.worktree}"`);
171
+ }
172
+ if (!Array.isArray(declaration.authorizedRoots) || declaration.authorizedRoots.length === 0) {
173
+ errors.push("declaration.authorizedRoots must be a non-empty array");
174
+ }
175
+ else if (declaration.authorizedRoots.some((path) => !isNonEmptyString(path) || hasNul(path))) {
176
+ errors.push("declaration.authorizedRoots entries must be non-empty strings without NUL");
177
+ }
178
+ if (declaration.liveCapability !== "live") {
179
+ errors.push('declaration.liveCapability must be the explicit string "live" (bind/upload is not activation)');
180
+ }
181
+ if (declaration.policyVersion !== POLICY_VERSION) {
182
+ errors.push(`declaration.policyVersion must be the fixed live policy version "${POLICY_VERSION}"`);
183
+ }
184
+ if (!isPositiveInteger(declaration.declarationRevision) ||
185
+ declaration.declarationRevision < INITIAL_DECLARATION_REVISION) {
186
+ errors.push(`declaration.declarationRevision must be an integer >= ${INITIAL_DECLARATION_REVISION}`);
187
+ }
188
+ const hasServerWorkspace = declaration.serverWorkspaceId !== undefined;
189
+ const hasServerIssuer = declaration.serverIssuer !== undefined;
190
+ const hasBindingRevision = declaration.bindingRevision !== undefined;
191
+ if (hasServerWorkspace && (!isNonEmptyString(declaration.serverWorkspaceId) || hasNul(declaration.serverWorkspaceId))) {
192
+ errors.push("declaration.serverWorkspaceId, when present, must be a non-empty string without NUL");
193
+ }
194
+ if (hasServerIssuer && (!isNonEmptyString(declaration.serverIssuer) || hasNul(declaration.serverIssuer))) {
195
+ errors.push("declaration.serverIssuer, when present, must be a non-empty string without NUL");
196
+ }
197
+ if (hasBindingRevision && (!isPositiveInteger(declaration.bindingRevision) ||
198
+ declaration.bindingRevision < INITIAL_BINDING_REVISION)) {
199
+ errors.push(`declaration.bindingRevision, when present, must be an integer >= ${INITIAL_BINDING_REVISION}`);
200
+ }
201
+ // Product live intent binds the complete server/issuer/revision triplet
202
+ // (spec §12): a workspace without its issuer or binding revision, an
203
+ // issuer or revision without the workspace, is not a valid product
204
+ // binding. Absence of all three is a valid local-only declaration
205
+ // requiring no enrollment.
206
+ if ((hasServerWorkspace || hasServerIssuer || hasBindingRevision) &&
207
+ !(hasServerWorkspace && hasServerIssuer && hasBindingRevision)) {
208
+ errors.push("the product binding must be the complete serverWorkspaceId + serverIssuer + bindingRevision triplet: a partial binding is not a valid product intent");
209
+ }
210
+ return { ok: errors.length === 0, errors };
211
+ }
212
+ const FACT_BATCH_KEYS = [
213
+ "scope",
214
+ "producerName",
215
+ "contractVersion",
216
+ "policyVersion",
217
+ "generationId",
218
+ "batchId",
219
+ "batchDigest",
220
+ "facts",
221
+ ];
222
+ export function validateFactBatch(batch) {
223
+ if (!isRecord(batch))
224
+ return { ok: false, errors: ["batch must be an object"] };
225
+ const errors = [];
226
+ rejectUnknownKeys(batch, FACT_BATCH_KEYS, "batch", errors);
227
+ const scope = validateLiveScope(batch.scope);
228
+ if (!scope.ok)
229
+ errors.push(...scope.errors.map((error) => `batch.scope: ${error}`));
230
+ if (batch.producerName !== PRODUCER_NAME)
231
+ errors.push(`batch.producerName must be "${PRODUCER_NAME}"`);
232
+ if (batch.contractVersion !== CONTRACT_VERSION) {
233
+ errors.push(`batch.contractVersion must be ${CONTRACT_VERSION}`);
234
+ }
235
+ if (!isNonEmptyString(batch.policyVersion))
236
+ errors.push("batch.policyVersion must be a non-empty string");
237
+ if (!isNonEmptyString(batch.generationId))
238
+ errors.push("batch.generationId must be a non-empty string");
239
+ if (!isNonEmptyString(batch.batchId))
240
+ errors.push("batch.batchId must be a non-empty string");
241
+ if (!isNonEmptyString(batch.batchDigest))
242
+ errors.push("batch.batchDigest must be a non-empty string");
243
+ if (!Array.isArray(batch.facts))
244
+ errors.push("batch.facts must be an array");
245
+ return { ok: errors.length === 0, errors };
246
+ }
247
+ // Same identity, same bytes is idempotent; same identity, different bytes is
248
+ // refused (spec §3). Pure decision over validated batches.
249
+ export function classifyDuplicateBatch(previous, next) {
250
+ if (previous.batchId !== next.batchId) {
251
+ throw new Error("classifyDuplicateBatch compares the same batch identity only");
252
+ }
253
+ return previous.batchDigest === next.batchDigest ? "idempotent" : "refused";
254
+ }
255
+ // — L6a: explicit host declarations and clone identity
256
+ // (spec/SPEC_LIVE_GIT_INDEXING.md §§2–3, 10, 12; plan/09 L6a; owner Q4 A).
257
+ //
258
+ // Consumer contract for the h2a (H) and cluster-mesh/sentropic (S) seats.
259
+ // H owns durable local declarations plus clone/worktree resolution
260
+ // (live-registration.ts, live-declarations.ts, data-pure DTOs); S owns the
261
+ // registration persistence, product declarations and migrations; engram
262
+ // owns only this data-pure contract and the pure functions in
263
+ // ./registration.js. Read this block before implementing either host side.
264
+ //
265
+ // - Explicit opt-in, once per workspace, through h2a/cluster-mesh. Only
266
+ // liveCapability "live" (EXPLICIT_LIVE_CAPABILITY) activates collection.
267
+ // Bind, upload, code-workspace creation, session opening or session
268
+ // ending never activate or remove a declaration
269
+ // (NON_ACTIVATING_LIFECYCLE_KINDS). Replay of a stored declaration after
270
+ // projection loss resumes work without renewed activation.
271
+ // - Engram neutrality: this module takes no imports of any kind (no h2a,
272
+ // no sentropic, no storage driver, no network); registration.ts adds pure
273
+ // functions with no host state, no storage and no network. Durable state
274
+ // belongs to the hosts — local h2a state outside source roots plus
275
+ // app-operated product persistence — independent of the live projection.
276
+ // - Opaque identity triangle: every declaration carries a declarationId
277
+ // (unique per declaration, stable across revisions) attached to its
278
+ // cloneId and worktreeId. A revocation names exactly one declaration;
279
+ // withdrawing a whole clone means revoking each of its declarations,
280
+ // each revocation naming its exact declarationId.
281
+ // - Workspace fingerprint vs clone identity: the public h2a `ws:`
282
+ // fingerprint stays on the declaration (hostFingerprint) and is never
283
+ // the sole clone key. The clone key is the (hostScope, common-dir
284
+ // realpath, common-dir project fingerprint) triple. Two clones with an
285
+ // equal project fingerprint remain distinct; linked worktrees sharing one
286
+ // common-dir share one clone with isolated worktree identities. The
287
+ // common-dir realpath is host-local and never published to sentropic:
288
+ // share only the opaque cloneId.
289
+ // - Identity uncertainty (LIVE §12): a repo without history (unborn HEAD)
290
+ // or with grafted (shallow) history declares explicit uncertainty
291
+ // states; a fingerprint may be absent only under "missing_history".
292
+ // Uncertainty never merges clones: the hostScope + common-dir realpath
293
+ // key components keep them distinct.
294
+ // - Revisions increment by exactly one, preserve tenant/principal/
295
+ // hostFingerprint, and preserve the product binding unless the binding
296
+ // itself changes: a revision outside the binding (roots, policy version)
297
+ // keeps it, while a changed serverWorkspaceId or serverIssuer — or an
298
+ // explicit revalidation — requires a strictly increasing
299
+ // bindingRevision (the first binding carries exactly 1). The host must
300
+ // additionally refuse to redirect in-flight collection/publication on
301
+ // rebind.
302
+ // - Revocation is a private-purge request: the host stops work, rejects
303
+ // delayed jobs, and purges private snapshots, derived indexes and
304
+ // decryptability, while preserving authorized committed history. Purging
305
+ // never deletes committed facts.
306
+ //
307
+ // FROZEN L6a CONVENTIONS — both seats align on these values:
308
+ // - POLICY_VERSION ("live-git-indexing/1") is the one policy name stamped
309
+ // on every live declaration; h2a's provisional "live-git-1" retires.
310
+ // - INITIAL_DECLARATION_REVISION (1): the explicit activation itself is
311
+ // the first revision; there is no revision-0 declaration.
312
+ // - INITIAL_BINDING_REVISION (1): the first product binding carries 1,
313
+ // and every rebind/revalidation carries a strictly greater
314
+ // bindingRevision.
315
+ // - Identity uncertainty vocabulary: "missing_history", "shallow_history"
316
+ // (the labels H already produces under LIVE §12).
317
+ // - Opaque ids are minted ONLY through the canonical derivation below
318
+ // (canonicalIdKey + the sha256 minting in ./registration.js): the same
319
+ // inputs give H, S and E byte-identical cloneId/worktreeId/
320
+ // declarationId. MIGRATION (H): identifiers already persisted by h2a
321
+ // were hashed from newline-joined "hostScope\nrealpath\nfingerprint"
322
+ // (clone), "hostScope\ngitDir" (worktree) and
323
+ // "hostScope\ncommonDirRealpath\nws:" (declaration); that scheme is
324
+ // ambiguous and diverges from this contract. Re-derive every persisted
325
+ // id with the canonical derivation and rewrite the local h2a store; no
326
+ // old id has crossed to sentropic (L6a is not consumed yet), so the
327
+ // migration is a purely local rewrite with no product-side replay.
328
+ /** The only capability value that activates live collection (Q4 A). */
329
+ export const EXPLICIT_LIVE_CAPABILITY = "live";
330
+ /**
331
+ * Lifecycle kinds that never activate or remove a live declaration:
332
+ * enrollment bind, transcript upload, code-workspace creation and agent
333
+ * session open/end are distinct capabilities (spec §12).
334
+ */
335
+ export const NON_ACTIVATING_LIFECYCLE_KINDS = [
336
+ "bind",
337
+ "upload",
338
+ "code-workspace-create",
339
+ "session-open",
340
+ "session-end",
341
+ ];
342
+ export function isNonActivatingLifecycleKind(value) {
343
+ return (typeof value === "string" &&
344
+ NON_ACTIVATING_LIFECYCLE_KINDS.includes(value));
345
+ }
346
+ export const OID_ALGORITHMS = ["sha1", "sha256"];
347
+ export const REVOCATION_REASONS = ["workspace-remove", "owner-request", "policy-withdrawn"];
348
+ export function isRevocationReason(value) {
349
+ return (typeof value === "string" &&
350
+ REVOCATION_REASONS.includes(value));
351
+ }
352
+ /**
353
+ * Explicit identity-coverage uncertainty states (LIVE §12). A repo with no
354
+ * history (unborn HEAD) declares "missing_history"; a grafted (shallow)
355
+ * history declares "shallow_history" while keeping its fingerprints, whose
356
+ * coverage stays uncertain. An absent fingerprint is valid only under
357
+ * "missing_history"; uncertainty never merges clones.
358
+ */
359
+ export const IDENTITY_UNCERTAINTY_STATES = ["missing_history", "shallow_history"];
360
+ export function isIdentityUncertaintyState(value) {
361
+ return (typeof value === "string" &&
362
+ IDENTITY_UNCERTAINTY_STATES.includes(value));
363
+ }
364
+ /** First declaration revision: the explicit activation is revision 1. */
365
+ export const INITIAL_DECLARATION_REVISION = 1;
366
+ /** First product binding revision; every rebind carries a strictly greater one. */
367
+ export const INITIAL_BINDING_REVISION = 1;
368
+ /** The three opaque id kinds minted by the canonical derivation. */
369
+ export const ID_KINDS = ["clone", "worktree", "declaration"];
370
+ /** Opaque id prefixes; a valid id of a kind starts with its prefix. */
371
+ export const ID_PREFIXES = {
372
+ clone: "clone:",
373
+ worktree: "wt:",
374
+ declaration: "decl:",
375
+ };
376
+ export function isIdKind(value) {
377
+ return (typeof value === "string" &&
378
+ ID_KINDS.includes(value));
379
+ }
380
+ /**
381
+ * THE canonical identity serialization shared by every live opaque id
382
+ * (cloneId, worktreeId, declarationId). Both seats and engram mint ids
383
+ * from this exact function, so the same identity components always give
384
+ * the same id on every side. The serialization is unambiguous: a JSON
385
+ * array of the kind plus the components in order, so no component value
386
+ * can shift a boundary. NUL is refused outright in every component, so the
387
+ * ambiguous couples ("a\0b", "c") vs ("a", "b\0c") can never be
388
+ * constructed. Pure and total over valid inputs; throws on malformed
389
+ * components so no silent normalization can produce a colliding key.
390
+ */
391
+ export function canonicalIdKey(kind, components) {
392
+ if (!isIdKind(kind)) {
393
+ throw new Error(`canonicalIdKey requires a known id kind (${ID_KINDS.join(", ")})`);
394
+ }
395
+ for (const component of components) {
396
+ if (typeof component !== "string") {
397
+ throw new Error("canonicalIdKey requires string components");
398
+ }
399
+ if (component.includes("\0")) {
400
+ throw new Error("canonicalIdKey refuses NUL in identity components");
401
+ }
402
+ }
403
+ return JSON.stringify([kind, ...components]);
404
+ }
405
+ const CLONE_IDENTITY_KEYS = [
406
+ "hostScope",
407
+ "commonDirRealpath",
408
+ "commonDirProjectFingerprint",
409
+ "cloneId",
410
+ "oidAlgorithm",
411
+ "identityUncertainty",
412
+ ];
413
+ const WORKTREE_IDENTITY_KEYS = [
414
+ "cloneId",
415
+ "worktreeId",
416
+ "worktreePath",
417
+ "gitDir",
418
+ "worktreeFingerprint",
419
+ "identityUncertainty",
420
+ ];
421
+ const PRIVATE_PURGE_REQUEST_KEYS = [
422
+ "purgePrivateSnapshots",
423
+ "purgeDerivedIndexes",
424
+ "revokeDecryptability",
425
+ "preserveCommittedHistory",
426
+ ];
427
+ const DECLARATION_REVOCATION_KEYS = [
428
+ "declarationId",
429
+ "tenant",
430
+ "cloneId",
431
+ "worktreeId",
432
+ "declarationRevision",
433
+ "reason",
434
+ "requestPrivatePurge",
435
+ ];
436
+ /**
437
+ * Lexical normalization applied to the host-resolved common-dir realpath
438
+ * before key derivation: trailing slashes are stripped (the filesystem
439
+ * root alone is kept). The host resolves symlinks to the realpath before
440
+ * calling; this function performs no I/O.
441
+ */
442
+ export function normalizeCommonDirRealpath(realpath) {
443
+ if (realpath.length > 1) {
444
+ const stripped = realpath.replace(/\/+$/, "");
445
+ return stripped.length === 0 ? "/" : stripped;
446
+ }
447
+ return realpath;
448
+ }
449
+ export function validateCloneIdentity(identity) {
450
+ if (!isRecord(identity))
451
+ return { ok: false, errors: ["cloneIdentity must be an object"] };
452
+ const errors = [];
453
+ rejectUnknownKeys(identity, CLONE_IDENTITY_KEYS, "cloneIdentity", errors);
454
+ if (!isNonEmptyString(identity.hostScope) || hasNul(identity.hostScope)) {
455
+ errors.push("cloneIdentity.hostScope must be a non-empty string without NUL");
456
+ }
457
+ if (!isNonEmptyString(identity.commonDirRealpath) || hasNul(identity.commonDirRealpath)) {
458
+ errors.push("cloneIdentity.commonDirRealpath must be a non-empty string without NUL");
459
+ }
460
+ if (identity.commonDirProjectFingerprint !== undefined &&
461
+ (!isNonEmptyString(identity.commonDirProjectFingerprint) || hasNul(identity.commonDirProjectFingerprint))) {
462
+ errors.push("cloneIdentity.commonDirProjectFingerprint, when present, must be a non-empty string without NUL");
463
+ }
464
+ else if (identity.commonDirProjectFingerprint === undefined &&
465
+ (!isUncertaintyList(identity.identityUncertainty) ||
466
+ !identity.identityUncertainty.includes("missing_history"))) {
467
+ errors.push('cloneIdentity.commonDirProjectFingerprint is absent: identityUncertainty must explicitly contain "missing_history" (LIVE §12)');
468
+ }
469
+ if (!isOpaqueId(identity.cloneId, ID_PREFIXES.clone)) {
470
+ errors.push(`cloneIdentity.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
471
+ }
472
+ if (identity.oidAlgorithm !== "sha1" && identity.oidAlgorithm !== "sha256") {
473
+ errors.push('cloneIdentity.oidAlgorithm must be "sha1" or "sha256"');
474
+ }
475
+ if (!isUncertaintyList(identity.identityUncertainty)) {
476
+ errors.push(`cloneIdentity.identityUncertainty must be an array of uncertainty states (${IDENTITY_UNCERTAINTY_STATES.join(", ")})`);
477
+ }
478
+ return { ok: errors.length === 0, errors };
479
+ }
480
+ export function validateWorktreeIdentity(identity) {
481
+ if (!isRecord(identity))
482
+ return { ok: false, errors: ["worktreeIdentity must be an object"] };
483
+ const errors = [];
484
+ rejectUnknownKeys(identity, WORKTREE_IDENTITY_KEYS, "worktreeIdentity", errors);
485
+ if (!isOpaqueId(identity.cloneId, ID_PREFIXES.clone)) {
486
+ errors.push(`worktreeIdentity.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
487
+ }
488
+ if (!isOpaqueId(identity.worktreeId, ID_PREFIXES.worktree)) {
489
+ errors.push(`worktreeIdentity.worktreeId must be a non-empty opaque id starting with "${ID_PREFIXES.worktree}"`);
490
+ }
491
+ if (!isNonEmptyString(identity.worktreePath) || hasNul(identity.worktreePath)) {
492
+ errors.push("worktreeIdentity.worktreePath must be a non-empty string without NUL");
493
+ }
494
+ if (!isNonEmptyString(identity.gitDir) || hasNul(identity.gitDir)) {
495
+ errors.push("worktreeIdentity.gitDir must be a non-empty string without NUL");
496
+ }
497
+ if (identity.worktreeFingerprint !== undefined &&
498
+ (!isNonEmptyString(identity.worktreeFingerprint) || hasNul(identity.worktreeFingerprint))) {
499
+ errors.push("worktreeIdentity.worktreeFingerprint, when present, must be a non-empty string without NUL");
500
+ }
501
+ else if (identity.worktreeFingerprint === undefined &&
502
+ (!isUncertaintyList(identity.identityUncertainty) ||
503
+ !identity.identityUncertainty.includes("missing_history"))) {
504
+ errors.push('worktreeIdentity.worktreeFingerprint is absent: identityUncertainty must explicitly contain "missing_history" (LIVE §12)');
505
+ }
506
+ if (!isUncertaintyList(identity.identityUncertainty)) {
507
+ errors.push(`worktreeIdentity.identityUncertainty must be an array of uncertainty states (${IDENTITY_UNCERTAINTY_STATES.join(", ")})`);
508
+ }
509
+ return { ok: errors.length === 0, errors };
510
+ }
511
+ export function validatePrivatePurgeRequest(request) {
512
+ if (!isRecord(request))
513
+ return { ok: false, errors: ["requestPrivatePurge must be an object"] };
514
+ const errors = [];
515
+ rejectUnknownKeys(request, PRIVATE_PURGE_REQUEST_KEYS, "requestPrivatePurge", errors);
516
+ if (request.purgePrivateSnapshots !== true) {
517
+ errors.push("requestPrivatePurge.purgePrivateSnapshots must be true (revocation always purges private snapshots)");
518
+ }
519
+ if (request.purgeDerivedIndexes !== true) {
520
+ errors.push("requestPrivatePurge.purgeDerivedIndexes must be true (revocation always purges derived indexes)");
521
+ }
522
+ if (request.revokeDecryptability !== true) {
523
+ errors.push("requestPrivatePurge.revokeDecryptability must be true (revocation always revokes decryptability)");
524
+ }
525
+ if (request.preserveCommittedHistory !== true) {
526
+ errors.push("requestPrivatePurge.preserveCommittedHistory must be true (revocation never deletes committed history)");
527
+ }
528
+ return { ok: errors.length === 0, errors };
529
+ }
530
+ export function validateDeclarationRevocation(revocation) {
531
+ if (!isRecord(revocation))
532
+ return { ok: false, errors: ["revocation must be an object"] };
533
+ const errors = [];
534
+ rejectUnknownKeys(revocation, DECLARATION_REVOCATION_KEYS, "revocation", errors);
535
+ if (!isOpaqueId(revocation.declarationId, ID_PREFIXES.declaration)) {
536
+ errors.push(`revocation.declarationId must be a non-empty opaque id starting with "${ID_PREFIXES.declaration}"`);
537
+ }
538
+ if (!isNonEmptyString(revocation.tenant) || hasNul(revocation.tenant)) {
539
+ errors.push("revocation.tenant must be a non-empty string without NUL");
540
+ }
541
+ if (!isOpaqueId(revocation.cloneId, ID_PREFIXES.clone)) {
542
+ errors.push(`revocation.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
543
+ }
544
+ if (!isOpaqueId(revocation.worktreeId, ID_PREFIXES.worktree)) {
545
+ errors.push(`revocation.worktreeId must be a non-empty opaque id starting with "${ID_PREFIXES.worktree}"`);
546
+ }
547
+ if (!isPositiveInteger(revocation.declarationRevision) ||
548
+ revocation.declarationRevision < INITIAL_DECLARATION_REVISION) {
549
+ errors.push(`revocation.declarationRevision must be an integer >= ${INITIAL_DECLARATION_REVISION}`);
550
+ }
551
+ if (!isRevocationReason(revocation.reason)) {
552
+ errors.push(`revocation.reason must be one of ${REVOCATION_REASONS.join(", ")}`);
553
+ }
554
+ const purge = validatePrivatePurgeRequest(revocation.requestPrivatePurge);
555
+ if (!purge.ok)
556
+ errors.push(...purge.errors.map((error) => `revocation.${error}`));
557
+ return { ok: errors.length === 0, errors };
558
+ }