@davesheffer/hunch 1.31.0 → 1.32.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 (228) hide show
  1. package/README.md +3 -6
  2. package/dist/cli/automaticReviewMemory.d.ts +13 -0
  3. package/dist/cli/dna.d.ts +2 -0
  4. package/dist/cli/index.d.ts +16 -0
  5. package/dist/cli/index.js +602 -43
  6. package/dist/cli/integrations.d.ts +2 -0
  7. package/dist/cli/integrations.js +5 -2
  8. package/dist/cli/invocation.d.ts +35 -0
  9. package/dist/cli/preflight.d.ts +1 -0
  10. package/dist/cli/reviewMemory.d.ts +4 -0
  11. package/dist/cli/reviewMemoryProvider.d.ts +14 -0
  12. package/dist/cli/serve.d.ts +2 -0
  13. package/dist/cli/taskReport.d.ts +6 -0
  14. package/dist/cli/taskReport.js +124 -0
  15. package/dist/cli/update.d.ts +14 -0
  16. package/dist/client/state.d.ts +248 -0
  17. package/dist/client/state.js +2 -0
  18. package/dist/constitution/adapters.d.ts +29 -0
  19. package/dist/constitution/behaviorAttestationBinding.d.ts +3 -0
  20. package/dist/constitution/behaviorEvaluator.d.ts +8 -0
  21. package/dist/constitution/behaviorProof.d.ts +9 -0
  22. package/dist/constitution/behaviorWorkspace.d.ts +25 -0
  23. package/dist/constitution/bootstrap.d.ts +30 -0
  24. package/dist/constitution/canonical.d.ts +15 -0
  25. package/dist/constitution/card.d.ts +58 -0
  26. package/dist/constitution/compiler.d.ts +52 -0
  27. package/dist/constitution/composition.d.ts +10 -0
  28. package/dist/constitution/corpus.d.ts +7 -0
  29. package/dist/constitution/correctionPolicyMaterializer.d.ts +57 -0
  30. package/dist/constitution/delta.d.ts +4 -0
  31. package/dist/constitution/disposition.d.ts +20 -0
  32. package/dist/constitution/evaluator.d.ts +47 -0
  33. package/dist/constitution/experiment.d.ts +466 -0
  34. package/dist/constitution/experimentRunner.d.ts +7 -0
  35. package/dist/constitution/g2.d.ts +209 -0
  36. package/dist/constitution/g2BehaviorAttestation.d.ts +45 -0
  37. package/dist/constitution/g2BehaviorCandidates.d.ts +123 -0
  38. package/dist/constitution/g2BehaviorDependencies.d.ts +83 -0
  39. package/dist/constitution/g2BehaviorMaterialization.d.ts +77 -0
  40. package/dist/constitution/g2BehaviorPolicyMaterializer.d.ts +48 -0
  41. package/dist/constitution/g2CandidateAttestation.d.ts +43 -0
  42. package/dist/constitution/g2Candidates.d.ts +80 -0
  43. package/dist/constitution/g2Drills.d.ts +33 -0
  44. package/dist/constitution/g3.d.ts +311 -0
  45. package/dist/constitution/g3Conformance.d.ts +33 -0
  46. package/dist/constitution/lifecycle.d.ts +25 -0
  47. package/dist/constitution/mutation.d.ts +15 -0
  48. package/dist/constitution/nodeTestEvidence.d.ts +13 -0
  49. package/dist/constitution/plan.d.ts +18 -0
  50. package/dist/constitution/policyRuntime.d.ts +5 -0
  51. package/dist/constitution/proof.d.ts +11 -0
  52. package/dist/constitution/repairPolicies.d.ts +34 -0
  53. package/dist/constitution/replacementFreeGit.d.ts +14 -0
  54. package/dist/constitution/replay.d.ts +40 -0
  55. package/dist/constitution/replayCache.d.ts +14 -0
  56. package/dist/constitution/replayWorker.d.ts +1 -0
  57. package/dist/constitution/repository.d.ts +129 -0
  58. package/dist/constitution/safeCheckout.d.ts +9 -0
  59. package/dist/constitution/schema.d.ts +1763 -0
  60. package/dist/constitution/scorecard.d.ts +219 -0
  61. package/dist/constitution/service.d.ts +398 -0
  62. package/dist/constitution/shadow.d.ts +54 -0
  63. package/dist/constitution/sourceMutation.d.ts +15 -0
  64. package/dist/constitution/staticGraphBaseline.d.ts +13 -0
  65. package/dist/constitution/structural.d.ts +64 -0
  66. package/dist/core/agenthook.d.ts +63 -0
  67. package/dist/core/agenthook.js +1 -0
  68. package/dist/core/automaticReviewMemory.d.ts +60 -0
  69. package/dist/core/autoreview.d.ts +66 -0
  70. package/dist/core/canonicalOrder.d.ts +3 -0
  71. package/dist/core/capturetoken.d.ts +19 -0
  72. package/dist/core/changeIdentity.d.ts +22 -0
  73. package/dist/core/changeProof.d.ts +14 -0
  74. package/dist/core/checkreport.d.ts +140 -0
  75. package/dist/core/commitrepair.d.ts +171 -0
  76. package/dist/core/commitrepair.js +238 -0
  77. package/dist/core/compare.d.ts +21 -0
  78. package/dist/core/config.d.ts +20 -0
  79. package/dist/core/conformance.d.ts +35 -0
  80. package/dist/core/constraintmatch.d.ts +54 -0
  81. package/dist/core/correction.d.ts +42 -0
  82. package/dist/core/correctionStage.d.ts +224 -0
  83. package/dist/core/declarationClusters.d.ts +149 -0
  84. package/dist/core/delivery.d.ts +110 -0
  85. package/dist/core/docanchors.d.ts +32 -0
  86. package/dist/core/docscan.d.ts +26 -0
  87. package/dist/core/drift.d.ts +13 -0
  88. package/dist/core/drift.js +31 -1
  89. package/dist/core/dupdetect.d.ts +43 -0
  90. package/dist/core/escalations.d.ts +129 -0
  91. package/dist/core/escalations.js +165 -0
  92. package/dist/core/events.d.ts +29 -0
  93. package/dist/core/evidenceMap.d.ts +164 -0
  94. package/dist/core/externalImports.d.ts +8 -0
  95. package/dist/core/format.d.ts +13 -0
  96. package/dist/core/glob.d.ts +9 -0
  97. package/dist/core/groundingLag.d.ts +84 -0
  98. package/dist/core/groundingLag.js +16 -2
  99. package/dist/core/groundingMerge.d.ts +15 -0
  100. package/dist/core/groundingMerge.js +95 -0
  101. package/dist/core/hookcache.d.ts +10 -0
  102. package/dist/core/hookpolicy.d.ts +47 -0
  103. package/dist/core/ids.d.ts +36 -0
  104. package/dist/core/importReview.d.ts +30 -0
  105. package/dist/core/io.d.ts +23 -0
  106. package/dist/core/jsonc.d.ts +4 -0
  107. package/dist/core/landscapeAdoption.d.ts +66 -0
  108. package/dist/core/landscapeDelivery.d.ts +76 -0
  109. package/dist/core/memorylog.d.ts +37 -0
  110. package/dist/core/migrate.d.ts +28 -0
  111. package/dist/core/outcomeExperience.d.ts +89 -0
  112. package/dist/core/overlaySafety.d.ts +24 -0
  113. package/dist/core/overlaySafety.js +7 -1
  114. package/dist/core/paths.d.ts +31 -0
  115. package/dist/core/pipeline.d.ts +289 -0
  116. package/dist/core/premises.d.ts +43 -0
  117. package/dist/core/provenance.d.ts +21 -0
  118. package/dist/core/publication.d.ts +54 -0
  119. package/dist/core/refrepair.d.ts +24 -0
  120. package/dist/core/refrepair.js +1 -1
  121. package/dist/core/relativeImports.d.ts +11 -0
  122. package/dist/core/repair.d.ts +44 -0
  123. package/dist/core/repairqueue.d.ts +40 -0
  124. package/dist/core/repairqueue.js +133 -0
  125. package/dist/core/reviewMemory.d.ts +40 -0
  126. package/dist/core/reviewqueue.d.ts +54 -0
  127. package/dist/core/safeRepoFile.d.ts +20 -0
  128. package/dist/core/served.d.ts +57 -0
  129. package/dist/core/served.js +14 -0
  130. package/dist/core/stateContract.d.ts +979 -0
  131. package/dist/core/stateContract.js +87 -4
  132. package/dist/core/stateDelivery.d.ts +78 -0
  133. package/dist/core/stateRecords.d.ts +383 -0
  134. package/dist/core/stateRecords.js +16 -1
  135. package/dist/core/stats.d.ts +88 -0
  136. package/dist/core/strictgate.d.ts +33 -0
  137. package/dist/core/taskReport.d.ts +226 -0
  138. package/dist/core/taskReport.js +514 -0
  139. package/dist/core/taskReportCapture.d.ts +8 -0
  140. package/dist/core/taskReportCapture.js +36 -0
  141. package/dist/core/taskReportEvidence.d.ts +26 -0
  142. package/dist/core/taskReportEvidence.js +268 -0
  143. package/dist/core/taskReportHook.d.ts +20 -0
  144. package/dist/core/taskReportHook.js +76 -0
  145. package/dist/core/taskReportPaths.d.ts +3 -0
  146. package/dist/core/taskReportPaths.js +23 -0
  147. package/dist/core/taskReportPublic.d.ts +31 -0
  148. package/dist/core/taskReportPublic.js +45 -0
  149. package/dist/core/taskReportRender.d.ts +6 -0
  150. package/dist/core/taskReportRender.js +97 -0
  151. package/dist/core/topics.d.ts +73 -0
  152. package/dist/core/types.d.ts +1239 -0
  153. package/dist/core/version.d.ts +10 -0
  154. package/dist/eval/guards.d.ts +45 -0
  155. package/dist/eval/harness.d.ts +58 -0
  156. package/dist/extractors/adrImport.d.ts +59 -0
  157. package/dist/extractors/comments.d.ts +7 -0
  158. package/dist/extractors/correctionSources.d.ts +11 -0
  159. package/dist/extractors/diff.d.ts +40 -0
  160. package/dist/extractors/git.d.ts +304 -0
  161. package/dist/extractors/git.js +208 -20
  162. package/dist/extractors/helm.d.ts +31 -0
  163. package/dist/extractors/indexer.d.ts +55 -0
  164. package/dist/extractors/landscapeDiscovery.d.ts +41 -0
  165. package/dist/extractors/languages.d.ts +77 -0
  166. package/dist/extractors/nativeTreeSitter.d.ts +16 -0
  167. package/dist/extractors/parse.d.ts +54 -0
  168. package/dist/extractors/php.d.ts +26 -0
  169. package/dist/extractors/repoSource.d.ts +53 -0
  170. package/dist/extractors/testreport.d.ts +36 -0
  171. package/dist/integrations/ciAction.d.ts +8 -0
  172. package/dist/integrations/claudeConfig.d.ts +44 -0
  173. package/dist/integrations/claudemd.d.ts +13 -0
  174. package/dist/integrations/claudemd.js +11 -2
  175. package/dist/integrations/gitignore.d.ts +16 -0
  176. package/dist/integrations/gitignore.js +7 -0
  177. package/dist/integrations/health.d.ts +74 -0
  178. package/dist/integrations/hooks.d.ts +41 -0
  179. package/dist/integrations/hooks.js +92 -66
  180. package/dist/integrations/madrExport.d.ts +39 -0
  181. package/dist/integrations/madrManifest.d.ts +69 -0
  182. package/dist/integrations/mergeDriver.d.ts +3 -0
  183. package/dist/integrations/mergeDriver.js +25 -8
  184. package/dist/integrations/probe.d.ts +4 -0
  185. package/dist/integrations/providers.d.ts +103 -0
  186. package/dist/integrations/scaffold.d.ts +29 -0
  187. package/dist/integrations/sync.d.ts +30 -0
  188. package/dist/integrations/sync.js +3 -2
  189. package/dist/integrations/team.d.ts +70 -0
  190. package/dist/integrations/worktree.d.ts +8 -0
  191. package/dist/mcp/roots.d.ts +16 -0
  192. package/dist/mcp/server.d.ts +36 -0
  193. package/dist/mcp/server.js +114 -25
  194. package/dist/mcp/taskReportTools.d.ts +3 -0
  195. package/dist/mcp/taskReportTools.js +106 -0
  196. package/dist/serve/app.d.ts +36 -0
  197. package/dist/serve/app.js +20 -0
  198. package/dist/serve/config.d.ts +100 -0
  199. package/dist/serve/writelock.d.ts +22 -0
  200. package/dist/store/changeLedger.d.ts +132 -0
  201. package/dist/store/changeLedger.js +40 -9
  202. package/dist/store/compact.d.ts +33 -0
  203. package/dist/store/db.d.ts +9 -0
  204. package/dist/store/embedder.d.ts +32 -0
  205. package/dist/store/hunchStore.d.ts +624 -0
  206. package/dist/store/hunchStore.js +13 -5
  207. package/dist/store/jsonStore.d.ts +125 -0
  208. package/dist/store/jsonStore.js +15 -0
  209. package/dist/store/merge.d.ts +39 -0
  210. package/dist/store/privateMigrate.d.ts +24 -0
  211. package/dist/store/replay.d.ts +56 -0
  212. package/dist/store/schema.d.ts +17 -0
  213. package/dist/store/stateBinding.d.ts +145 -0
  214. package/dist/store/stateBinding.js +110 -15
  215. package/dist/store/stateCapture.d.ts +15 -0
  216. package/dist/store/stateCapture.js +145 -0
  217. package/dist/synthesis/cliAdapter.d.ts +22 -0
  218. package/dist/synthesis/initiator.d.ts +13 -0
  219. package/dist/synthesis/provider.d.ts +307 -0
  220. package/dist/synthesis/synthesize.d.ts +117 -0
  221. package/dist/synthesis/tripwires.d.ts +9 -0
  222. package/dist/taskReports.d.ts +76 -0
  223. package/dist/taskReports.js +67 -0
  224. package/dist/wiki/adopt.d.ts +22 -0
  225. package/dist/wiki/graph.d.ts +87 -0
  226. package/dist/wiki/wiki.d.ts +242 -0
  227. package/package.json +6 -5
  228. package/server.json +2 -2
@@ -29,17 +29,22 @@ import { z } from "zod";
29
29
  import { compareCodeUnits } from "./canonicalOrder.js";
30
30
  import { DELIVERY_PROFILES } from "./delivery.js";
31
31
  import { isHumanConfirmed as sourceIsHumanConfirmed } from "./strictgate.js";
32
- import { ScopeSchema, scopePath, DependencyRefSchema, ExternalRefSchema, RECEIPT_SCHEMA_VERSION, COMMITMENT_SCHEMA_VERSION, DERIVED_SCHEMA_VERSION, ENTITY_SCHEMA_VERSION, RELATIONSHIP_SCHEMA_VERSION, } from "./stateRecords.js";
32
+ import { ScopeSchema, scopePath, externalKey, DependencyRefSchema, ExternalRefSchema, RECEIPT_SCHEMA_VERSION, COMMITMENT_SCHEMA_VERSION, DERIVED_SCHEMA_VERSION, ENTITY_SCHEMA_VERSION, RELATIONSHIP_SCHEMA_VERSION, } from "./stateRecords.js";
33
33
  export * from "./stateRecords.js";
34
34
  export const STATE_CONTRACT_VERSION = "nuryel.state/1";
35
35
  export const STATE_READ_VERSION = "nuryel.state.read/1";
36
36
  export const STATE_WRITE_VERSION = "nuryel.state.write/1";
37
37
  export const STATE_SUBSCRIBE_VERSION = "nuryel.state.subscribe/1";
38
38
  export const STATE_RECORDS_VERSION = "nuryel.state.records/1";
39
+ export const STATE_CAPTURE_VERSION = "nuryel.state.capture/1";
40
+ export const STATE_CAPTURE_BATCH_VERSION = "nuryel.state.capture-batch/1";
41
+ export const STATE_OBSERVATION_LINKS_VERSION = "nuryel.observation-links/1";
42
+ export const STATE_OBSERVATION_REVIEW_VERSION = "nuryel.observation-review/1";
43
+ export const STATE_OBSERVATION_PAGES_VERSION = "nuryel.observation-pages/1";
39
44
  /** Capabilities a server advertises; a client that needs one the server lacks gets a typed
40
45
  * `unsupported`, never a compatible-looking degraded answer. */
41
46
  export const STATE_CAPABILITIES = [
42
- STATE_READ_VERSION, STATE_WRITE_VERSION, STATE_SUBSCRIBE_VERSION, STATE_RECORDS_VERSION,
47
+ STATE_READ_VERSION, STATE_WRITE_VERSION, STATE_SUBSCRIBE_VERSION, STATE_RECORDS_VERSION, STATE_CAPTURE_VERSION, STATE_CAPTURE_BATCH_VERSION, STATE_OBSERVATION_LINKS_VERSION, STATE_OBSERVATION_REVIEW_VERSION, STATE_OBSERVATION_PAGES_VERSION,
43
48
  RECEIPT_SCHEMA_VERSION, COMMITMENT_SCHEMA_VERSION, DERIVED_SCHEMA_VERSION, ENTITY_SCHEMA_VERSION, RELATIONSHIP_SCHEMA_VERSION,
44
49
  ];
45
50
  const SHA256 = /^sha256:[a-f0-9]{64}$/;
@@ -55,12 +60,52 @@ export const PrincipalSchema = z.object({
55
60
  display: z.string().max(256).optional(),
56
61
  grants: z.array(ScopeSchema).min(1).max(64),
57
62
  }).strict();
63
+ /** One relevant assertion, never a whole conversation. Source text is transient input:
64
+ * only its exact supporting excerpt and a hashed external pointer may reach the store. */
65
+ export const CaptureRequestSchema = z.object({
66
+ schema: z.literal(STATE_CAPTURE_VERSION),
67
+ principal: PrincipalSchema,
68
+ scope: ScopeSchema,
69
+ subject: z.string().min(1).max(512),
70
+ statement: z.string().trim().min(1).max(1200),
71
+ relevance: z.object({
72
+ use: z.enum(["decision", "constraint", "preference", "operational_fact", "ongoing_issue"]),
73
+ reason: z.string().trim().min(1).max(600),
74
+ }).strict(),
75
+ evidence: z.array(z.object({
76
+ ref: ExternalRefSchema,
77
+ source_text: z.string().min(1).max(64000),
78
+ excerpt: z.string().trim().min(1).max(1200),
79
+ }).strict()).min(1).max(8),
80
+ }).strict();
81
+ /** Sources cross the transport once; assertions name only their supporting excerpts. */
82
+ export const CaptureBatchRequestSchema = z.object({
83
+ schema: z.literal(STATE_CAPTURE_BATCH_VERSION), principal: PrincipalSchema, scope: ScopeSchema,
84
+ sources: z.array(CaptureRequestSchema.shape.evidence.element.omit({ excerpt: true })).min(1).max(8),
85
+ observations: z.array(CaptureRequestSchema.pick({ subject: true, statement: true, relevance: true }).extend({
86
+ evidence: z.array(z.object({ source: z.number().int().min(0).max(7), excerpt: z.string().trim().min(1).max(1200) }).strict()).min(1).max(8),
87
+ })).min(0).max(32),
88
+ reviews: z.array(z.object({
89
+ record_id: z.string().regex(/^nds_[a-f0-9]{24}$/), expected_hash: z.string().regex(SHA256),
90
+ reason: z.string().trim().min(1).max(600),
91
+ evidence: z.array(z.object({ source: z.number().int().min(0).max(7), excerpt: z.string().trim().min(1).max(1200) }).strict()).min(1).max(8),
92
+ }).strict()).min(1).max(32).optional(),
93
+ }).strict();
94
+ export const CAPTURE_TRANSFORM = "agent-capture/1:";
95
+ export const normalizeAssertion = (text) => text.normalize("NFC").replace(/\r\n?/g, "\n").trim();
96
+ export function captureTransform(scope, subject, statement, evidence) {
97
+ const identities = [...new Set(evidence.map(e => stateHash({ source: e.source, excerpt: normalizeAssertion(e.excerpt) })))].sort();
98
+ return CAPTURE_TRANSFORM + stateHash({ scope, subject, statement: normalizeAssertion(statement), evidence: identities }).slice(7);
99
+ }
58
100
  export const STATE_FACETS = ["decisions", "constraints", "bugs", "findings", "receipts", "commitments", "derived", "entities", "relationships"];
59
101
  // ---- verbs ------------------------------------------------------------------------------
60
102
  /** Union read: the partitions a principal wants in ONE answer. `scope` stays required (it is the
61
103
  * primary partition; its envelope and receipt lead the response). An entry the principal is not
62
104
  * granted is NAMED in `denied_scopes` — it never refuses the whole call, and is never described. */
63
105
  export const ReadScopesSchema = z.array(ScopeSchema).min(1).max(64);
106
+ export const ObservationCursorSchema = z.object({
107
+ snapshot_hash: z.string().regex(SHA256), offset: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
108
+ }).strict();
64
109
  export const ReadRequestSchema = z.object({
65
110
  schema: z.literal(STATE_READ_VERSION),
66
111
  principal: PrincipalSchema,
@@ -71,6 +116,8 @@ export const ReadRequestSchema = z.object({
71
116
  profile: z.enum(DELIVERY_PROFILES).optional(),
72
117
  budget_tokens: z.number().int().min(200).max(200_000).optional(),
73
118
  facets: z.array(z.enum(STATE_FACETS)).max(STATE_FACETS.length).optional(),
119
+ /** Explicit single-partition pagination; default subject reads keep their existing bound. */
120
+ observed_page: z.object({ cursor: ObservationCursorSchema.optional() }).strict().optional(),
74
121
  }).strict();
75
122
  const StateRefSchema = z.object({
76
123
  facet: z.enum(STATE_FACETS),
@@ -85,6 +132,11 @@ export const StateOfRecordSchema = z.object({
85
132
  current: z.array(StateRefSchema).max(256),
86
133
  in_force: z.array(StateRefSchema).max(256),
87
134
  done: z.array(StateRefSchema).max(256),
135
+ /** Source-backed observations, not a claim of currentness. Additive; absent on old hosts. */
136
+ observed: z.array(StateRefSchema).max(64).optional(),
137
+ observed_truncated: z.boolean().optional(),
138
+ observed_page: z.object({ snapshot_hash: z.string().regex(SHA256), total: z.number().int().nonnegative(), next_cursor: ObservationCursorSchema.nullable() }).strict().optional(),
139
+ relationships_truncated: z.boolean().optional(),
88
140
  depends_on: z.array(DependencyRefSchema).max(1024),
89
141
  invalidated_by: z.array(z.string().max(512)).max(256),
90
142
  }).strict();
@@ -131,6 +183,17 @@ export const WriteResultSchema = z.object({
131
183
  * what landed without a second lookup. Additive. */
132
184
  record: z.record(z.string(), z.unknown()).optional(),
133
185
  }).strict();
186
+ export const CaptureBatchResultSchema = z.object({
187
+ schema: z.literal(STATE_CAPTURE_BATCH_VERSION),
188
+ results: z.array(z.discriminatedUnion("status", [
189
+ z.object({ index: z.number().int(), status: z.literal("saved"), result: WriteResultSchema }).strict(),
190
+ z.object({ index: z.number().int(), status: z.literal("refused"), code: z.string(), message: z.string() }).strict(),
191
+ ])).max(32),
192
+ reviews: z.array(z.discriminatedUnion("status", [
193
+ z.object({ index: z.number().int(), status: z.literal("saved"), result: WriteResultSchema }).strict(),
194
+ z.object({ index: z.number().int(), status: z.literal("refused"), code: z.string(), message: z.string() }).strict(),
195
+ ])).max(32).optional(),
196
+ }).strict();
134
197
  export const SubscribeRequestSchema = z.object({
135
198
  schema: z.literal(STATE_SUBSCRIBE_VERSION),
136
199
  principal: PrincipalSchema,
@@ -224,6 +287,11 @@ export function commitmentId(c) {
224
287
  return idFrom("ncm", { scope: c.scope, subject: c.subject, title: c.title.trim(), owner: c.owner, due: c.due });
225
288
  }
226
289
  export function derivedId(d) {
290
+ // Capture's reserved transform includes assertion/evidence identity, independent of
291
+ // read time and unrelated source edits. Ordinary summary identity is unchanged.
292
+ if (/^agent-capture\/1:[a-f0-9]{64}$/.test(d.transform_version)) {
293
+ return idFrom("nds", { scope: d.scope, subject: d.subject, transform_version: d.transform_version });
294
+ }
227
295
  return idFrom("nds", { scope: d.scope, subject: d.subject, transform_version: d.transform_version, dependencies: d.dependencies.map((dep) => stateHash(dep)).sort(compareCodeUnits) });
228
296
  }
229
297
  // ---- invariants --------------------------------------------------------------------------
@@ -237,7 +305,7 @@ export const STATE_INVARIANTS = [
237
305
  { id: "derived-state-carries-dependencies", statement: "A derived statement without dependencies cannot be invalidated and is therefore not state." },
238
306
  { id: "one-entity-per-external-ref", statement: "One external record is one entity in a partition: a second active entity carrying an external key an incumbent already carries is refused with the incumbent named, and a subject written as that record's external key is refused with the entity's id named. Identity is explicit refs, never similarity; merge is explicit — a retired entity names the survivor in `merged_into`, the ledger holds the `retired` event, nothing under the old id is rewritten and reads resolve to the survivor — and split is the explicit reverse; never a silent rewrite." },
239
307
  { id: "human-correction-outranks-agent-writes", statement: "A record a human confirmed is never overwritten or superseded by an agent or service principal: the agent may replay it, write derived state back stale with the external cause that moved, or close a commitment with a receipt on record. Changing what the human said takes a human." },
240
- { id: "derived-state-writer-owns-currentness", statement: "No source writes the drawer. The writer of a derived statement owns keeping its dependencies true: re-validate them on a schedule or on a source event, and write the statement back stale with the moved pointer as cause when one no longer holds. An agent that will not do this must not write derived state." },
308
+ { id: "derived-state-writer-owns-currentness", statement: "No source writes the drawer. The writer of a current derived statement owns keeping its dependencies true: re-validate them on a schedule or on a source event, and write the statement back stale with the moved pointer as cause when one no longer holds. Without this duty an agent may capture source-backed observations only as unknown; observations never assert currentness." },
241
309
  ];
242
310
  const grantKey = (scope) => scopePath(scope);
243
311
  /** The memory supply chain's top tier: a record whose provenance a human signed. Same tier rule
@@ -252,7 +320,7 @@ export function assertReadWithinGrants(principal, response) {
252
320
  const granted = new Set(principal.grants.map(grantKey));
253
321
  if (!granted.has(grantKey(response.scope)))
254
322
  throw new Error(`read response scope ${grantKey(response.scope)} is outside the principal's grants`);
255
- const refs = response.state_of_record ? [...response.state_of_record.current, ...response.state_of_record.in_force, ...response.state_of_record.done] : [];
323
+ const refs = response.state_of_record ? [...response.state_of_record.current, ...response.state_of_record.in_force, ...response.state_of_record.done, ...(response.state_of_record.observed ?? [])] : [];
256
324
  for (const ref of refs) {
257
325
  if (!granted.has(grantKey(ref.scope)))
258
326
  throw new Error(`state ref ${ref.id} in scope ${grantKey(ref.scope)} leaked outside the principal's grants`);
@@ -289,6 +357,21 @@ export function assertDerivedState(d) {
289
357
  throw new Error("derived state without dependencies is not state");
290
358
  if (stateHash(d.content) !== d.content_hash)
291
359
  throw new Error("derived state content hash does not match its content");
360
+ if (d.transform_version.startsWith(CAPTURE_TRANSFORM)) {
361
+ const content = z.object({
362
+ schema: z.literal("nuryel.observation-content/1"),
363
+ statement: CaptureRequestSchema.shape.statement, relevance: CaptureRequestSchema.shape.relevance,
364
+ evidence: z.array(z.object({ source: z.string(), excerpt: z.string().min(1).max(1200) }).strict()).min(1).max(8),
365
+ captured_by: PrincipalSchema.shape.id,
366
+ }).strict().parse(JSON.parse(d.content));
367
+ if (d.state === "current")
368
+ throw new Error("capture observations cannot assert currentness; publish a separately revalidated summary");
369
+ if (captureTransform(d.scope, d.subject, content.statement, content.evidence) !== d.transform_version)
370
+ throw new Error("capture identity does not match its assertion and evidence");
371
+ const pointers = new Set(d.dependencies.filter(dep => dep.kind === "external").map(dep => externalKey(dep.ref)));
372
+ if (content.evidence.some(e => !pointers.has(e.source)))
373
+ throw new Error("capture evidence lacks its external dependency");
374
+ }
292
375
  }
293
376
  /** Subscribe streams are strictly ordered per scope; a gap or regression means the caller must
294
377
  * resynchronize instead of trusting what it holds. */
@@ -0,0 +1,78 @@
1
+ /**
2
+ * nuryel.state/1 — search and delivery of the five state kinds (receipts, commitments,
3
+ * derived, entities, relationships). Registered in 1.25 as store kinds, they were stored and
4
+ * counted but neither indexed nor delivered: `hunch_query("customer:Site:7")` could not surface
5
+ * a current summary, an open commitment or a verified receipt, and `hunch_context` had no
6
+ * "State" section. This module is the single place that says, per kind:
7
+ *
8
+ * - what text goes into the `search` FTS row (subject + content/title + actor + status, so a
9
+ * subject id and a human phrase both hit),
10
+ * - whether a record is LIVE (current / in_force / verified…) or HISTORY (superseded, done,
11
+ * cancelled, failed, retired) — history stays indexed and findable, it only ranks below,
12
+ * - the one-line render every reader gets (`[commitment/in_force] customer:Site:7 — …`),
13
+ * - the bounded, deterministically ordered "State" supplements for a context brief.
14
+ *
15
+ * Pure functions over records; no store or SQLite dependency, so the CLI, the MCP server and
16
+ * the store's own reindex/rank paths cannot drift from each other.
17
+ */
18
+ import type { ActionReceipt, Commitment, DerivedState, ExternalEntity, StateRelationship } from "./stateRecords.js";
19
+ import type { DeliverySupplement } from "./delivery.js";
20
+ export declare const STATE_KINDS: readonly ["receipts", "commitments", "derived", "entities", "relationships"];
21
+ export type StateKind = (typeof STATE_KINDS)[number];
22
+ export declare function isStateKind(kind: string): kind is StateKind;
23
+ export type StateRecord = ActionReceipt | Commitment | DerivedState | ExternalEntity | StateRelationship;
24
+ export interface StateLiveness {
25
+ /** The contract's own word for the record's standing: current, in_force, verified, done, superseded … */
26
+ label: string;
27
+ /** True when the record is the state of record NOW; false for history (still indexed, ranked below). */
28
+ live: boolean;
29
+ }
30
+ /** Liveness per kind, mirroring readState's state_of_record predicates exactly
31
+ * (derived `current` with an open window; commitment open/waiting with an open window;
32
+ * receipt succeeded/verified; entity active; a relationship is always current). */
33
+ export declare function stateLiveness(kind: StateKind, record: StateRecord): StateLiveness;
34
+ /** The subject key a reader would type: the commitment/derived subject, the receipt's target
35
+ * object (`event:10042`), the entity id, or the relationship's `from` endpoint. */
36
+ export declare function stateSubject(kind: StateKind, record: StateRecord): string;
37
+ /** The instant that orders "latest first": computed_at, valid_from, verified_at ?? occurred_at,
38
+ * updated_at. Relationships carry no clock and sort last among equals. */
39
+ export declare function stateObservedAt(kind: StateKind, record: StateRecord): string;
40
+ /** The FTS document for a state record: title = the subject key (so an id query hits the
41
+ * title column, which the snippet and LIKE fallback both prefer), body = the human words plus
42
+ * the actor/owner, the status label and the dates. Every reader's query — a subject id, a
43
+ * phrase from a summary, an action kind, a principal — lands on one of these. */
44
+ export declare function stateSearchDoc(kind: StateKind, record: StateRecord): {
45
+ title: string;
46
+ body: string;
47
+ };
48
+ /** The one-line render shared by hunch_query, `hunch query` and the context "State" section:
49
+ * [commitment/in_force] customer:Site:7 — "send report" due 2026-09-11 (owner sofia)
50
+ * [derived/current] customer:Site:7 — <first 120 chars of the summary>
51
+ * [receipt/verified] event:10042 — events_add_actions by sofia@david 2026-09-08 */
52
+ export declare function renderStateLine(kind: StateKind, record: StateRecord): string;
53
+ /** One ranked state hit, as the store's stateSlice() returns it. `score` is the search
54
+ * score (bm25: lower is better; 0 for the LIKE fallback and exact-subject matches). */
55
+ export interface StateHit<K extends StateKind = StateKind> {
56
+ kind: K;
57
+ record: StateRecord;
58
+ score: number;
59
+ }
60
+ /** Bounded caps for the context "State" section: current derived, in-force commitments,
61
+ * latest receipts. Deliberately small — a brief, not a dump; `nuryel_read` is the full view. */
62
+ export declare const STATE_SLICE_CAPS: {
63
+ readonly derived: 3;
64
+ readonly commitments: 5;
65
+ readonly receipts: 3;
66
+ };
67
+ export interface StateSlice {
68
+ derived: StateHit<"derived">[];
69
+ commitments: StateHit<"commitments">[];
70
+ receipts: StateHit<"receipts">[];
71
+ }
72
+ /** Deterministic order for a state slice: score (best first), then observed_at DESC (latest
73
+ * first), then id ASC. Applied after liveness filtering, before the cap. */
74
+ export declare function compareStateHits(a: StateHit, b: StateHit): number;
75
+ /** Render a state slice as delivery supplements (one header + one line per record) so the
76
+ * section shares the context brief's hard budget and receipt like every other grounding.
77
+ * Empty slice → no supplements at all: a store with zero state records is byte-identical. */
78
+ export declare function stateSupplements(slice: StateSlice, target: string): DeliverySupplement[];
@@ -0,0 +1,383 @@
1
+ /**
2
+ * nuryel.state/1 — the RECORD schemas (the facets that are new record kinds in the store).
3
+ *
4
+ * Kept separate from stateContract.ts (verbs, invariants, hashing) because the store's kind
5
+ * registry in types.ts must reference these schemas, and stateContract imports types.ts —
6
+ * this module imports only zod, the id helpers and the provenance leaf, so there is no cycle.
7
+ *
8
+ * Facets here: done (ActionReceipt), committed (Commitment), current (DerivedState with
9
+ * mandatory dependencies), entity / relationship (external, Landscape-shaped), plus the
10
+ * credential-free ExternalRef ("changed") and DependencyRef they share. Scope is the one
11
+ * graph's partition: organization › team › user › repository.
12
+ */
13
+ import { z } from "zod";
14
+ import { edgeId, resourceId } from "./ids.js";
15
+ export declare const RECEIPT_SCHEMA_VERSION: "nuryel.receipt/1";
16
+ export declare const COMMITMENT_SCHEMA_VERSION: "nuryel.commitment/1";
17
+ export declare const DERIVED_SCHEMA_VERSION: "nuryel.derived/1";
18
+ export declare const ENTITY_SCHEMA_VERSION: "nuryel.entity/1";
19
+ export declare const RELATIONSHIP_SCHEMA_VERSION: "nuryel.relationship/1";
20
+ /** organization › team › user › repository — partitions of ONE graph, not separate stores. */
21
+ export declare const SCOPE_KINDS: readonly ["organization", "team", "user", "repository"];
22
+ export declare const ScopeSchema: z.ZodObject<{
23
+ kind: z.ZodEnum<{
24
+ repository: "repository";
25
+ organization: "organization";
26
+ team: "team";
27
+ user: "user";
28
+ }>;
29
+ id: z.ZodString;
30
+ }, z.core.$strict>;
31
+ export type Scope = z.infer<typeof ScopeSchema>;
32
+ export declare const scopePath: (scope: Scope) => string;
33
+ /** "What changed" and "where this came from": a credential-free pointer to an object that
34
+ * stays authoritative in its own system. Nuryel never mirrors its content. */
35
+ export declare const ExternalRefSchema: z.ZodObject<{
36
+ system: z.ZodString;
37
+ object_type: z.ZodString;
38
+ object_key: z.ZodString;
39
+ version: z.ZodOptional<z.ZodString>;
40
+ content_hash: z.ZodOptional<z.ZodString>;
41
+ observed_at: z.ZodString;
42
+ locator: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strict>;
44
+ export type ExternalRef = z.infer<typeof ExternalRefSchema>;
45
+ /** The external system's own key, canonicalized so two writers that copied it from the same
46
+ * system agree byte for byte: Unicode NFC, trimmed, internal whitespace collapsed. Case is
47
+ * preserved — the key belongs to the external system, and folding it could merge two of its
48
+ * records. No other guessing: identity here is explicit refs, never similarity. */
49
+ export declare function canonicalObjectKey(key: string): string;
50
+ /** The identity of an external record across writers: system, type and canonical key. Two
51
+ * entities in one partition that carry the same external key are the same thing. */
52
+ export declare function externalKey(ref: Pick<ExternalRef, "system" | "object_type" | "object_key">): string;
53
+ /** The subject an external record is known by, the convention the read verb already uses for
54
+ * receipts (`event:26904`): the object type and the canonical key. When an entity in the
55
+ * partition carries the ref, that entity's id is the subject and this form resolves to it. */
56
+ export declare function subjectOfRef(ref: Pick<ExternalRef, "object_type" | "object_key">): string;
57
+ /** What a derived statement rests on. Exactly what a currentness check re-validates. */
58
+ export declare const DependencyRefSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
59
+ kind: z.ZodLiteral<"record">;
60
+ id: z.ZodString;
61
+ record_hash: z.ZodString;
62
+ scope: z.ZodOptional<z.ZodObject<{
63
+ kind: z.ZodEnum<{
64
+ repository: "repository";
65
+ organization: "organization";
66
+ team: "team";
67
+ user: "user";
68
+ }>;
69
+ id: z.ZodString;
70
+ }, z.core.$strict>>;
71
+ }, z.core.$strict>, z.ZodObject<{
72
+ kind: z.ZodLiteral<"external">;
73
+ ref: z.ZodObject<{
74
+ system: z.ZodString;
75
+ object_type: z.ZodString;
76
+ object_key: z.ZodString;
77
+ version: z.ZodOptional<z.ZodString>;
78
+ content_hash: z.ZodOptional<z.ZodString>;
79
+ observed_at: z.ZodString;
80
+ locator: z.ZodOptional<z.ZodString>;
81
+ }, z.core.$strict>;
82
+ }, z.core.$strict>, z.ZodObject<{
83
+ kind: z.ZodLiteral<"schema">;
84
+ name: z.ZodString;
85
+ fingerprint: z.ZodString;
86
+ }, z.core.$strict>], "kind">;
87
+ export type DependencyRef = z.infer<typeof DependencyRefSchema>;
88
+ /** done — a side effect that happened. Never replayable as a read; idempotency is explicit. */
89
+ export declare const ActionReceiptSchema: z.ZodObject<{
90
+ schema: z.ZodLiteral<"nuryel.receipt/1">;
91
+ id: z.ZodString;
92
+ scope: z.ZodObject<{
93
+ kind: z.ZodEnum<{
94
+ repository: "repository";
95
+ organization: "organization";
96
+ team: "team";
97
+ user: "user";
98
+ }>;
99
+ id: z.ZodString;
100
+ }, z.core.$strict>;
101
+ actor: z.ZodString;
102
+ action_kind: z.ZodString;
103
+ target: z.ZodObject<{
104
+ system: z.ZodString;
105
+ object_type: z.ZodString;
106
+ object_key: z.ZodString;
107
+ version: z.ZodOptional<z.ZodString>;
108
+ content_hash: z.ZodOptional<z.ZodString>;
109
+ observed_at: z.ZodString;
110
+ locator: z.ZodOptional<z.ZodString>;
111
+ }, z.core.$strict>;
112
+ request_fingerprint: z.ZodString;
113
+ idempotency_key: z.ZodOptional<z.ZodString>;
114
+ state: z.ZodEnum<{
115
+ unknown: "unknown";
116
+ requested: "requested";
117
+ succeeded: "succeeded";
118
+ failed: "failed";
119
+ verified: "verified";
120
+ }>;
121
+ occurred_at: z.ZodString;
122
+ verified_at: z.ZodOptional<z.ZodString>;
123
+ result_fingerprint: z.ZodOptional<z.ZodString>;
124
+ invalidates: z.ZodDefault<z.ZodArray<z.ZodString>>;
125
+ rests_on: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
126
+ kind: z.ZodLiteral<"record">;
127
+ id: z.ZodString;
128
+ record_hash: z.ZodString;
129
+ scope: z.ZodOptional<z.ZodObject<{
130
+ kind: z.ZodEnum<{
131
+ repository: "repository";
132
+ organization: "organization";
133
+ team: "team";
134
+ user: "user";
135
+ }>;
136
+ id: z.ZodString;
137
+ }, z.core.$strict>>;
138
+ }, z.core.$strict>, z.ZodObject<{
139
+ kind: z.ZodLiteral<"external">;
140
+ ref: z.ZodObject<{
141
+ system: z.ZodString;
142
+ object_type: z.ZodString;
143
+ object_key: z.ZodString;
144
+ version: z.ZodOptional<z.ZodString>;
145
+ content_hash: z.ZodOptional<z.ZodString>;
146
+ observed_at: z.ZodString;
147
+ locator: z.ZodOptional<z.ZodString>;
148
+ }, z.core.$strict>;
149
+ }, z.core.$strict>, z.ZodObject<{
150
+ kind: z.ZodLiteral<"schema">;
151
+ name: z.ZodString;
152
+ fingerprint: z.ZodString;
153
+ }, z.core.$strict>], "kind">>>;
154
+ provenance: z.ZodObject<{
155
+ source: z.ZodString;
156
+ confidence: z.ZodNumber;
157
+ evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
158
+ last_verified: z.ZodOptional<z.ZodString>;
159
+ }, z.core.$strip>;
160
+ }, z.core.$strict>;
161
+ export type ActionReceipt = z.infer<typeof ActionReceiptSchema>;
162
+ /** committed — an obligation with a due date and an in-force window. */
163
+ export declare const CommitmentSchema: z.ZodObject<{
164
+ schema: z.ZodLiteral<"nuryel.commitment/1">;
165
+ id: z.ZodString;
166
+ scope: z.ZodObject<{
167
+ kind: z.ZodEnum<{
168
+ repository: "repository";
169
+ organization: "organization";
170
+ team: "team";
171
+ user: "user";
172
+ }>;
173
+ id: z.ZodString;
174
+ }, z.core.$strict>;
175
+ subject: z.ZodString;
176
+ title: z.ZodString;
177
+ owner: z.ZodString;
178
+ due: z.ZodString;
179
+ status: z.ZodEnum<{
180
+ open: "open";
181
+ waiting: "waiting";
182
+ done: "done";
183
+ cancelled: "cancelled";
184
+ }>;
185
+ source: z.ZodOptional<z.ZodObject<{
186
+ system: z.ZodString;
187
+ object_type: z.ZodString;
188
+ object_key: z.ZodString;
189
+ version: z.ZodOptional<z.ZodString>;
190
+ content_hash: z.ZodOptional<z.ZodString>;
191
+ observed_at: z.ZodString;
192
+ locator: z.ZodOptional<z.ZodString>;
193
+ }, z.core.$strict>>;
194
+ evidence_excerpt: z.ZodOptional<z.ZodString>;
195
+ closed_by: z.ZodOptional<z.ZodString>;
196
+ valid_from: z.ZodString;
197
+ valid_to: z.ZodDefault<z.ZodNullable<z.ZodString>>;
198
+ provenance: z.ZodObject<{
199
+ source: z.ZodString;
200
+ confidence: z.ZodNumber;
201
+ evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
202
+ last_verified: z.ZodOptional<z.ZodString>;
203
+ }, z.core.$strip>;
204
+ }, z.core.$strict>;
205
+ export type Commitment = z.infer<typeof CommitmentSchema>;
206
+ /** current — a statement that is true now, and on what it rests. Dependencies are mandatory:
207
+ * a derived statement without them cannot be invalidated and therefore cannot be trusted. */
208
+ export declare const DerivedStateSchema: z.ZodObject<{
209
+ schema: z.ZodLiteral<"nuryel.derived/1">;
210
+ id: z.ZodString;
211
+ scope: z.ZodObject<{
212
+ kind: z.ZodEnum<{
213
+ repository: "repository";
214
+ organization: "organization";
215
+ team: "team";
216
+ user: "user";
217
+ }>;
218
+ id: z.ZodString;
219
+ }, z.core.$strict>;
220
+ subject: z.ZodString;
221
+ content: z.ZodString;
222
+ content_hash: z.ZodString;
223
+ dependencies: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
224
+ kind: z.ZodLiteral<"record">;
225
+ id: z.ZodString;
226
+ record_hash: z.ZodString;
227
+ scope: z.ZodOptional<z.ZodObject<{
228
+ kind: z.ZodEnum<{
229
+ repository: "repository";
230
+ organization: "organization";
231
+ team: "team";
232
+ user: "user";
233
+ }>;
234
+ id: z.ZodString;
235
+ }, z.core.$strict>>;
236
+ }, z.core.$strict>, z.ZodObject<{
237
+ kind: z.ZodLiteral<"external">;
238
+ ref: z.ZodObject<{
239
+ system: z.ZodString;
240
+ object_type: z.ZodString;
241
+ object_key: z.ZodString;
242
+ version: z.ZodOptional<z.ZodString>;
243
+ content_hash: z.ZodOptional<z.ZodString>;
244
+ observed_at: z.ZodString;
245
+ locator: z.ZodOptional<z.ZodString>;
246
+ }, z.core.$strict>;
247
+ }, z.core.$strict>, z.ZodObject<{
248
+ kind: z.ZodLiteral<"schema">;
249
+ name: z.ZodString;
250
+ fingerprint: z.ZodString;
251
+ }, z.core.$strict>], "kind">>;
252
+ transform_version: z.ZodString;
253
+ computed_at: z.ZodString;
254
+ valid_to: z.ZodDefault<z.ZodNullable<z.ZodString>>;
255
+ state: z.ZodEnum<{
256
+ unknown: "unknown";
257
+ current: "current";
258
+ stale: "stale";
259
+ }>;
260
+ review: z.ZodOptional<z.ZodObject<{
261
+ by: z.ZodString;
262
+ at: z.ZodString;
263
+ previous_hash: z.ZodString;
264
+ reason: z.ZodString;
265
+ evidence: z.ZodArray<z.ZodObject<{
266
+ ref: z.ZodObject<{
267
+ system: z.ZodString;
268
+ object_type: z.ZodString;
269
+ object_key: z.ZodString;
270
+ version: z.ZodOptional<z.ZodString>;
271
+ content_hash: z.ZodOptional<z.ZodString>;
272
+ observed_at: z.ZodString;
273
+ locator: z.ZodOptional<z.ZodString>;
274
+ }, z.core.$strict>;
275
+ excerpt: z.ZodString;
276
+ }, z.core.$strict>>;
277
+ }, z.core.$strict>>;
278
+ provenance: z.ZodObject<{
279
+ source: z.ZodString;
280
+ confidence: z.ZodNumber;
281
+ evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
282
+ last_verified: z.ZodOptional<z.ZodString>;
283
+ }, z.core.$strip>;
284
+ }, z.core.$strict>;
285
+ export type DerivedState = z.infer<typeof DerivedStateSchema>;
286
+ /** entity — a customer, an incident, a thread: a non-code node, Landscape-shaped (kind-qualified
287
+ * id, lifecycle, provenance), with provenance pointers instead of mirrored content. Stored in
288
+ * an index file, like resources, because kind-qualified ids are not safe file names. */
289
+ export declare const ExternalEntitySchema: z.ZodObject<{
290
+ schema: z.ZodLiteral<"nuryel.entity/1">;
291
+ id: z.ZodString;
292
+ kind: z.ZodString;
293
+ name: z.ZodString;
294
+ scope: z.ZodObject<{
295
+ kind: z.ZodEnum<{
296
+ repository: "repository";
297
+ organization: "organization";
298
+ team: "team";
299
+ user: "user";
300
+ }>;
301
+ id: z.ZodString;
302
+ }, z.core.$strict>;
303
+ refs: z.ZodArray<z.ZodObject<{
304
+ system: z.ZodString;
305
+ object_type: z.ZodString;
306
+ object_key: z.ZodString;
307
+ version: z.ZodOptional<z.ZodString>;
308
+ content_hash: z.ZodOptional<z.ZodString>;
309
+ observed_at: z.ZodString;
310
+ locator: z.ZodOptional<z.ZodString>;
311
+ }, z.core.$strict>>;
312
+ attributes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>;
313
+ lifecycle: z.ZodDefault<z.ZodEnum<{
314
+ active: "active";
315
+ deprecated: "deprecated";
316
+ retired: "retired";
317
+ }>>;
318
+ merged_into: z.ZodOptional<z.ZodString>;
319
+ provenance: z.ZodObject<{
320
+ source: z.ZodString;
321
+ confidence: z.ZodNumber;
322
+ evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
323
+ last_verified: z.ZodOptional<z.ZodString>;
324
+ }, z.core.$strip>;
325
+ created_at: z.ZodString;
326
+ updated_at: z.ZodString;
327
+ }, z.core.$strict>;
328
+ export type ExternalEntity = z.infer<typeof ExternalEntitySchema>;
329
+ /** relationship — rides the same identity rule as the graph's edges. Index-file stored. */
330
+ export declare const StateRelationshipSchema: z.ZodObject<{
331
+ schema: z.ZodLiteral<"nuryel.relationship/1">;
332
+ id: z.ZodString;
333
+ from: z.ZodString;
334
+ to: z.ZodString;
335
+ type: z.ZodString;
336
+ scope: z.ZodObject<{
337
+ kind: z.ZodEnum<{
338
+ repository: "repository";
339
+ organization: "organization";
340
+ team: "team";
341
+ user: "user";
342
+ }>;
343
+ id: z.ZodString;
344
+ }, z.core.$strict>;
345
+ reason: z.ZodDefault<z.ZodString>;
346
+ observation_hash: z.ZodOptional<z.ZodString>;
347
+ evidence: z.ZodOptional<z.ZodObject<{
348
+ system: z.ZodString;
349
+ object_type: z.ZodString;
350
+ object_key: z.ZodString;
351
+ version: z.ZodOptional<z.ZodString>;
352
+ content_hash: z.ZodOptional<z.ZodString>;
353
+ observed_at: z.ZodString;
354
+ locator: z.ZodOptional<z.ZodString>;
355
+ }, z.core.$strict>>;
356
+ lifecycle: z.ZodOptional<z.ZodEnum<{
357
+ active: "active";
358
+ retired: "retired";
359
+ }>>;
360
+ provenance: z.ZodObject<{
361
+ source: z.ZodString;
362
+ confidence: z.ZodNumber;
363
+ evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
364
+ last_verified: z.ZodOptional<z.ZodString>;
365
+ }, z.core.$strip>;
366
+ }, z.core.$strict>;
367
+ export type StateRelationship = z.infer<typeof StateRelationshipSchema>;
368
+ /** DNA facet: existing Project DNA profiles, keyed by scope. No new schema — the reference only. */
369
+ export declare const DnaFacetRefSchema: z.ZodObject<{
370
+ schema: z.ZodLiteral<"hunch.project-dna/1">;
371
+ scope: z.ZodObject<{
372
+ kind: z.ZodEnum<{
373
+ repository: "repository";
374
+ organization: "organization";
375
+ team: "team";
376
+ user: "user";
377
+ }>;
378
+ id: z.ZodString;
379
+ }, z.core.$strict>;
380
+ profile_id: z.ZodString;
381
+ }, z.core.$strict>;
382
+ export declare const entityId: typeof resourceId;
383
+ export declare const relationshipId: typeof edgeId;
@@ -127,8 +127,16 @@ export const DerivedStateSchema = z.object({
127
127
  computed_at: z.string().regex(ISO),
128
128
  valid_to: z.string().regex(ISO).nullable().default(null),
129
129
  state: z.enum(["current", "stale", "unknown"]),
130
+ /** Per-observation withdrawal. Original assertion, author and dependencies stay intact. */
131
+ review: z.object({
132
+ by: z.string().regex(TOKEN), at: z.string().regex(ISO), previous_hash: z.string().regex(SHA256), reason: z.string().trim().min(1).max(600),
133
+ evidence: z.array(z.object({ ref: ExternalRefSchema, excerpt: z.string().trim().min(1).max(1200) }).strict()).min(1).max(8),
134
+ }).strict().optional(),
130
135
  provenance: ProvenanceSchema,
131
- }).strict();
136
+ }).strict().superRefine((record, ctx) => {
137
+ if (record.review && record.state !== 'stale')
138
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['review'], message: 'withdrawal review belongs only to a stale observation' });
139
+ });
132
140
  const AttributeValue = z.union([z.string().max(2048), z.number().finite(), z.boolean(), z.null()]);
133
141
  /** entity — a customer, an incident, a thread: a non-code node, Landscape-shaped (kind-qualified
134
142
  * id, lifecycle, provenance), with provenance pointers instead of mirrored content. Stored in
@@ -175,10 +183,17 @@ export const StateRelationshipSchema = z.object({
175
183
  type: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
176
184
  scope: ScopeSchema,
177
185
  reason: z.string().max(1024).default(""),
186
+ /** observation_about is a one-hop projection, never an entity merge or a copied fact. */
187
+ observation_hash: z.string().regex(SHA256).optional(),
188
+ evidence: ExternalRefSchema.optional(),
189
+ lifecycle: z.enum(["active", "retired"]).optional(),
178
190
  provenance: ProvenanceSchema,
179
191
  }).strict().superRefine((rel, ctx) => {
180
192
  if (rel.id !== edgeId(rel.from, rel.to, rel.type))
181
193
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["id"], message: "relationship id must derive from its endpoints and type" });
194
+ if (rel.type === "observation_about" && (!/^nds_[a-f0-9]{24}$/.test(rel.from) || !rel.observation_hash || !rel.evidence?.content_hash || !rel.reason.trim() || rel.to.length > 512 || rel.to === rel.from)) {
195
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "observation_about requires an observation id/hash, a subject, reason and hashed external evidence of the explicit association" });
196
+ }
182
197
  });
183
198
  /** DNA facet: existing Project DNA profiles, keyed by scope. No new schema — the reference only. */
184
199
  export const DnaFacetRefSchema = z.object({