@davesheffer/hunch 1.32.8 → 1.33.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 (56) hide show
  1. package/README.md +9 -3
  2. package/dist/cli/index.js +2 -0
  3. package/dist/cli/serve.js +28 -2
  4. package/dist/cli/state.d.ts +3 -0
  5. package/dist/cli/state.js +150 -0
  6. package/dist/client/state.d.ts +82 -14
  7. package/dist/client/state.js +16 -2
  8. package/dist/client/stateProof.d.ts +4 -0
  9. package/dist/client/stateProof.js +17 -0
  10. package/dist/constitution/behaviorEvaluator.js +1 -1
  11. package/dist/constitution/schema.d.ts +2 -2
  12. package/dist/core/automaticReviewMemory.d.ts +5 -0
  13. package/dist/core/conventionDelivery.d.ts +8 -0
  14. package/dist/core/conventionDelivery.js +52 -0
  15. package/dist/core/fieldProvenance.d.ts +8 -0
  16. package/dist/core/fieldProvenance.js +72 -0
  17. package/dist/core/recordVisibility.d.ts +9 -0
  18. package/dist/core/recordVisibility.js +25 -0
  19. package/dist/core/stateCanonical.d.ts +3 -0
  20. package/dist/core/stateCanonical.js +34 -0
  21. package/dist/core/stateContract.d.ts +122 -7
  22. package/dist/core/stateContract.js +26 -31
  23. package/dist/core/stateDelivery.d.ts +3 -3
  24. package/dist/core/stateDelivery.js +10 -1
  25. package/dist/core/stateHttp.d.ts +280 -0
  26. package/dist/core/stateHttp.js +17 -0
  27. package/dist/core/stateProof.d.ts +13 -0
  28. package/dist/core/stateProof.js +34 -0
  29. package/dist/core/stateRecords.d.ts +127 -0
  30. package/dist/core/stateRecords.js +48 -0
  31. package/dist/core/types.d.ts +146 -4
  32. package/dist/core/types.js +8 -2
  33. package/dist/extractors/git.js +3 -10
  34. package/dist/mcp/server.js +10 -4
  35. package/dist/serve/app.d.ts +2 -0
  36. package/dist/serve/app.js +71 -30
  37. package/dist/serve/config.d.ts +16 -0
  38. package/dist/serve/config.js +27 -7
  39. package/dist/serve/operator.d.ts +4 -0
  40. package/dist/serve/operator.js +223 -0
  41. package/dist/serve/stateProof.d.ts +15 -0
  42. package/dist/serve/stateProof.js +105 -0
  43. package/dist/store/changeLedger.d.ts +6 -0
  44. package/dist/store/hunchStore.d.ts +4 -2
  45. package/dist/store/hunchStore.js +18 -19
  46. package/dist/store/stateAccess.d.ts +13 -0
  47. package/dist/store/stateAccess.js +85 -0
  48. package/dist/store/stateBinding.d.ts +13 -18
  49. package/dist/store/stateBinding.js +161 -52
  50. package/dist/store/stateCapture.js +10 -2
  51. package/dist/store/stateError.d.ts +12 -0
  52. package/dist/store/stateError.js +12 -0
  53. package/dist/store/statePartition.d.ts +9 -0
  54. package/dist/store/statePartition.js +30 -0
  55. package/package.json +5 -1
  56. package/server.json +2 -2
@@ -0,0 +1,72 @@
1
+ import { stateHash } from "./stateCanonical.js";
2
+ /** Resolve exactly one scalar JSON field or one Unicode-code-point text range.
3
+ * No normalization, inherited properties, array aliases or substring guessing. */
4
+ export function fieldCitationValue(content, selector) {
5
+ if (selector.kind === "text") {
6
+ const points = Array.from(content);
7
+ if (!Number.isInteger(selector.start) || !Number.isInteger(selector.end) || selector.start < 0 || selector.end <= selector.start || selector.end > points.length)
8
+ throw new Error("text citation range must be nonempty and within the content's Unicode code points");
9
+ return points.slice(selector.start, selector.end).join("");
10
+ }
11
+ if (!/^(?:\/(?:[^~/]|~[01])*)*$/.test(selector.path))
12
+ throw new Error("citation path must be an escaped JSON Pointer");
13
+ let value;
14
+ try {
15
+ value = JSON.parse(content);
16
+ }
17
+ catch {
18
+ throw new Error("JSON field citations require JSON content");
19
+ }
20
+ for (const encoded of selector.path === "" ? [] : selector.path.slice(1).split("/")) {
21
+ const key = encoded.replace(/~1/g, "/").replace(/~0/g, "~");
22
+ if (value === null || typeof value !== "object" || (Array.isArray(value) && !/^(0|[1-9][0-9]*)$/.test(key)) || !Object.hasOwn(value, key))
23
+ throw new Error(`citation path ${selector.path} does not name an existing field`);
24
+ value = value[key];
25
+ }
26
+ if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value)))
27
+ return value;
28
+ throw new Error("JSON field citations must name a scalar value, not an object or array");
29
+ }
30
+ /** This is structural traceability, not a check that a source supports a claim. */
31
+ export function assertFieldProvenance(record) {
32
+ if (!record.field_provenance)
33
+ return;
34
+ const dependencies = new Set(record.dependencies.map(stateHash));
35
+ const selectors = new Set();
36
+ for (const [index, citation] of record.field_provenance.entries()) {
37
+ try {
38
+ const key = stateHash(citation.selector);
39
+ if (selectors.has(key))
40
+ throw new Error("duplicate citation selector; combine its dependency hashes in one entry");
41
+ selectors.add(key);
42
+ const value = fieldCitationValue(record.content, citation.selector);
43
+ if (stateHash(value) !== citation.value_hash)
44
+ throw new Error("citation value_hash does not match the selected content; rebuild the citation after editing");
45
+ if (new Set(citation.dependency_hashes).size !== citation.dependency_hashes.length)
46
+ throw new Error("duplicate citation dependency hash");
47
+ if (!citation.dependency_hashes.length || citation.dependency_hashes.some(hash => !dependencies.has(hash)))
48
+ throw new Error("every citation dependency_hash must name an existing summary dependency");
49
+ }
50
+ catch (error) {
51
+ throw new Error(`field_provenance[${index}]: ${error.message}`);
52
+ }
53
+ }
54
+ }
55
+ /** Bounded, plain-text source map for assistant clients; structured records remain complete. */
56
+ export function fieldCitationText(record) {
57
+ if (!record.field_provenance?.length)
58
+ return "";
59
+ const dependencies = new Map(record.dependencies.map(dep => [stateHash(dep), dep]));
60
+ const source = (dep) => !dep ? "unavailable source" : dep.kind === "external"
61
+ ? `${dep.ref.system} ${dep.ref.object_type}:${dep.ref.object_key}` : dep.kind === "record"
62
+ ? `record ${dep.id}${dep.scope ? ` in ${dep.scope.kind}/${dep.scope.id}` : ""}` : `schema ${dep.name}`;
63
+ const lines = record.field_provenance.slice(0, 8).map(citation => {
64
+ const selector = citation.selector;
65
+ const target = selector.kind === "text" ? `text ${selector.start}–${selector.end} (Unicode code points)` : `field ${selector.path || "(root)"}`;
66
+ const value = JSON.stringify(fieldCitationValue(record.content, selector));
67
+ const sources = citation.dependency_hashes.slice(0, 4).map(hash => source(dependencies.get(hash)).slice(0, 200)).join("; ");
68
+ return ` ${target}: ${value.slice(0, 200)}${value.length > 200 ? "…" : ""} ← ${sources}${citation.dependency_hashes.length > 4 ? "; more sources in structured record" : ""}`;
69
+ });
70
+ return `\n Writer-supplied field citations (traceability, not verified support or freshness):\n${lines.join("\n")}${record.field_provenance.length > 8 ? "\n More citations in structured record." : ""}`;
71
+ }
72
+ //# sourceMappingURL=fieldProvenance.js.map
@@ -0,0 +1,9 @@
1
+ /** Inline policy stays bound to the record revision and its atomic JSON write. */
2
+ import { z } from 'zod';
3
+ export declare const RecordVisibilitySchema: z.ZodObject<{
4
+ owner: z.ZodString;
5
+ readers: z.ZodArray<z.ZodString>;
6
+ writers: z.ZodArray<z.ZodString>;
7
+ }, z.core.$strict>;
8
+ export type RecordVisibility = z.infer<typeof RecordVisibilitySchema>;
9
+ export declare function visibilityAllows(record: unknown, id: string, mode?: 'read' | 'write'): boolean;
@@ -0,0 +1,25 @@
1
+ /** Inline policy stays bound to the record revision and its atomic JSON write. */
2
+ import { z } from 'zod';
3
+ const principalId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,199}$/);
4
+ export const RecordVisibilitySchema = z.object({
5
+ owner: principalId,
6
+ readers: z.array(principalId).max(256),
7
+ writers: z.array(principalId).max(256),
8
+ }).strict().superRefine((value, ctx) => {
9
+ if (new Set(value.readers).size !== value.readers.length || new Set(value.writers).size !== value.writers.length)
10
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'visibility lists must contain distinct principal IDs' });
11
+ if (value.writers.some(id => id !== value.owner && !value.readers.includes(id)))
12
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'every writer must also be a reader' });
13
+ });
14
+ export function visibilityAllows(record, id, mode = 'read') {
15
+ if (!record || typeof record !== 'object')
16
+ return false;
17
+ const raw = record.visibility;
18
+ if (raw === undefined)
19
+ return true;
20
+ const parsed = RecordVisibilitySchema.safeParse(raw);
21
+ if (!parsed.success)
22
+ return false;
23
+ return parsed.data.owner === id || (mode === 'read' ? parsed.data.readers : parsed.data.writers).includes(id);
24
+ }
25
+ //# sourceMappingURL=recordVisibility.js.map
@@ -0,0 +1,3 @@
1
+ /** Keep this encoding stable: existing record identities and dependency hashes use it. */
2
+ export declare function canonicalize(value: unknown): unknown;
3
+ export declare function stateHash(value: unknown): string;
@@ -0,0 +1,34 @@
1
+ /** Shared canonical form for state identity, revisions and citation validation. */
2
+ import { createHash } from "node:crypto";
3
+ import { compareCodeUnits } from "./canonicalOrder.js";
4
+ /** Keep this encoding stable: existing record identities and dependency hashes use it. */
5
+ export function canonicalize(value) {
6
+ if (value === null || typeof value === "string" || typeof value === "boolean")
7
+ return value;
8
+ if (typeof value === "number") {
9
+ if (!Number.isFinite(value))
10
+ throw new Error("canonical form rejects non-finite numbers");
11
+ return value;
12
+ }
13
+ if (Array.isArray(value))
14
+ return value.map(canonicalize);
15
+ if (typeof value === "object") {
16
+ const out = {};
17
+ for (const key of Object.keys(value).sort(compareCodeUnits)) {
18
+ // The historical encoding assigns into an ordinary object. This key would
19
+ // invoke its prototype setter and disappear from JSON. Refuse ambiguous
20
+ // input rather than silently collide or change existing valid identities.
21
+ if (key === "__proto__")
22
+ throw new Error("canonical form rejects reserved key __proto__");
23
+ const v = value[key];
24
+ if (v !== undefined)
25
+ out[key] = canonicalize(v);
26
+ }
27
+ return out;
28
+ }
29
+ throw new Error(`canonical form rejects ${typeof value}`);
30
+ }
31
+ export function stateHash(value) {
32
+ return `sha256:${createHash("sha256").update(JSON.stringify(canonicalize(value))).digest("hex")}`;
33
+ }
34
+ //# sourceMappingURL=stateCanonical.js.map
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
+ export { canonicalize, stateHash } from "./stateCanonical.js";
3
+ export { fieldCitationValue, assertFieldProvenance } from "./fieldProvenance.js";
2
4
  import { type DeliveryEnvelope } from "./delivery.js";
3
- import { type Scope, type ActionReceipt, type Commitment, type DerivedState } from "./stateRecords.js";
5
+ import { type Convention, type Scope, type ActionReceipt, type Commitment, type DerivedState } from "./stateRecords.js";
4
6
  export * from "./stateRecords.js";
5
7
  export declare const STATE_CONTRACT_VERSION: "nuryel.state/1";
6
8
  export declare const STATE_READ_VERSION: "nuryel.state.read/1";
@@ -12,9 +14,21 @@ export declare const STATE_CAPTURE_BATCH_VERSION: "nuryel.state.capture-batch/1"
12
14
  export declare const STATE_OBSERVATION_LINKS_VERSION: "nuryel.observation-links/1";
13
15
  export declare const STATE_OBSERVATION_REVIEW_VERSION: "nuryel.observation-review/1";
14
16
  export declare const STATE_OBSERVATION_PAGES_VERSION: "nuryel.observation-pages/1";
17
+ export declare const STATE_RECORD_VISIBILITY_VERSION: "nuryel.record-visibility/1";
18
+ export declare const PartitionDeclarationSchema: z.ZodObject<{
19
+ kind: z.ZodEnum<{
20
+ repository: "repository";
21
+ organization: "organization";
22
+ team: "team";
23
+ user: "user";
24
+ }>;
25
+ id: z.ZodString;
26
+ required_capabilities: z.ZodOptional<z.ZodArray<z.ZodLiteral<"nuryel.record-visibility/1">>>;
27
+ }, z.core.$strict>;
28
+ export declare const STATE_FIELD_PROVENANCE_VERSION: "nuryel.field-provenance/1";
15
29
  /** Capabilities a server advertises; a client that needs one the server lacks gets a typed
16
30
  * `unsupported`, never a compatible-looking degraded answer. */
17
- export declare const STATE_CAPABILITIES: readonly ["nuryel.state.read/1", "nuryel.state.write/1", "nuryel.state.subscribe/1", "nuryel.state.records/1", "nuryel.state.capture/1", "nuryel.state.capture-batch/1", "nuryel.observation-links/1", "nuryel.observation-review/1", "nuryel.observation-pages/1", "nuryel.receipt/1", "nuryel.commitment/1", "nuryel.derived/1", "nuryel.entity/1", "nuryel.relationship/1"];
31
+ export declare const STATE_CAPABILITIES: readonly ["nuryel.state.read/1", "nuryel.state.write/1", "nuryel.state.subscribe/1", "nuryel.state.records/1", "nuryel.state.capture/1", "nuryel.state.capture-batch/1", "nuryel.observation-links/1", "nuryel.observation-review/1", "nuryel.observation-pages/1", "nuryel.field-provenance/1", "nuryel.record-visibility/1", "nuryel.convention/1", "nuryel.receipt/1", "nuryel.commitment/1", "nuryel.derived/1", "nuryel.entity/1", "nuryel.relationship/1"];
18
32
  export type StateCapability = (typeof STATE_CAPABILITIES)[number];
19
33
  /** Who is reading or writing. Grants are the scopes the principal may see; authorization is
20
34
  * decided BEFORE retrieval against these, never after ranking. */
@@ -40,6 +54,11 @@ export type Principal = z.infer<typeof PrincipalSchema>;
40
54
  /** One relevant assertion, never a whole conversation. Source text is transient input:
41
55
  * only its exact supporting excerpt and a hashed external pointer may reach the store. */
42
56
  export declare const CaptureRequestSchema: z.ZodObject<{
57
+ visibility: z.ZodOptional<z.ZodObject<{
58
+ owner: z.ZodString;
59
+ readers: z.ZodArray<z.ZodString>;
60
+ writers: z.ZodArray<z.ZodString>;
61
+ }, z.core.$strict>>;
43
62
  schema: z.ZodLiteral<"nuryel.state.capture/1">;
44
63
  principal: z.ZodObject<{
45
64
  id: z.ZodString;
@@ -148,6 +167,11 @@ export declare const CaptureBatchRequestSchema: z.ZodObject<{
148
167
  }>;
149
168
  reason: z.ZodString;
150
169
  }, z.core.$strict>;
170
+ visibility: z.ZodOptional<z.ZodObject<{
171
+ owner: z.ZodString;
172
+ readers: z.ZodArray<z.ZodString>;
173
+ writers: z.ZodArray<z.ZodString>;
174
+ }, z.core.$strict>>;
151
175
  subject: z.ZodString;
152
176
  statement: z.ZodString;
153
177
  evidence: z.ZodArray<z.ZodObject<{
@@ -172,7 +196,7 @@ export declare function captureTransform(scope: Scope, subject: string, statemen
172
196
  source: string;
173
197
  excerpt: string;
174
198
  }[]): string;
175
- export declare const STATE_FACETS: readonly ["decisions", "constraints", "bugs", "findings", "receipts", "commitments", "derived", "entities", "relationships"];
199
+ export declare const STATE_FACETS: readonly ["decisions", "constraints", "bugs", "findings", "receipts", "commitments", "derived", "entities", "relationships", "conventions"];
176
200
  export type StateFacet = (typeof STATE_FACETS)[number];
177
201
  /** Union read: the partitions a principal wants in ONE answer. `scope` stays required (it is the
178
202
  * primary partition; its envelope and receipt lead the response). An entry the principal is not
@@ -246,6 +270,7 @@ export declare const ReadRequestSchema: z.ZodObject<{
246
270
  derived: "derived";
247
271
  entities: "entities";
248
272
  relationships: "relationships";
273
+ conventions: "conventions";
249
274
  }>>>;
250
275
  observed_page: z.ZodOptional<z.ZodObject<{
251
276
  cursor: z.ZodOptional<z.ZodObject<{
@@ -266,6 +291,7 @@ declare const StateRefSchema: z.ZodObject<{
266
291
  derived: "derived";
267
292
  entities: "entities";
268
293
  relationships: "relationships";
294
+ conventions: "conventions";
269
295
  }>;
270
296
  id: z.ZodString;
271
297
  record_hash: z.ZodString;
@@ -295,6 +321,7 @@ export declare const StateOfRecordSchema: z.ZodObject<{
295
321
  derived: "derived";
296
322
  entities: "entities";
297
323
  relationships: "relationships";
324
+ conventions: "conventions";
298
325
  }>;
299
326
  id: z.ZodString;
300
327
  record_hash: z.ZodString;
@@ -319,6 +346,7 @@ export declare const StateOfRecordSchema: z.ZodObject<{
319
346
  derived: "derived";
320
347
  entities: "entities";
321
348
  relationships: "relationships";
349
+ conventions: "conventions";
322
350
  }>;
323
351
  id: z.ZodString;
324
352
  record_hash: z.ZodString;
@@ -343,6 +371,7 @@ export declare const StateOfRecordSchema: z.ZodObject<{
343
371
  derived: "derived";
344
372
  entities: "entities";
345
373
  relationships: "relationships";
374
+ conventions: "conventions";
346
375
  }>;
347
376
  id: z.ZodString;
348
377
  record_hash: z.ZodString;
@@ -367,6 +396,7 @@ export declare const StateOfRecordSchema: z.ZodObject<{
367
396
  derived: "derived";
368
397
  entities: "entities";
369
398
  relationships: "relationships";
399
+ conventions: "conventions";
370
400
  }>;
371
401
  id: z.ZodString;
372
402
  record_hash: z.ZodString;
@@ -422,7 +452,82 @@ export declare const StateOfRecordSchema: z.ZodObject<{
422
452
  invalidated_by: z.ZodArray<z.ZodString>;
423
453
  }, z.core.$strict>;
424
454
  export type StateOfRecord = z.infer<typeof StateOfRecordSchema>;
455
+ export declare const ConventionDeliverySchema: z.ZodObject<{
456
+ advisory: z.ZodLiteral<true>;
457
+ items: z.ZodArray<z.ZodObject<{
458
+ ref: z.ZodObject<{
459
+ facet: z.ZodEnum<{
460
+ decisions: "decisions";
461
+ constraints: "constraints";
462
+ bugs: "bugs";
463
+ findings: "findings";
464
+ receipts: "receipts";
465
+ commitments: "commitments";
466
+ derived: "derived";
467
+ entities: "entities";
468
+ relationships: "relationships";
469
+ conventions: "conventions";
470
+ }>;
471
+ id: z.ZodString;
472
+ record_hash: z.ZodString;
473
+ scope: z.ZodObject<{
474
+ kind: z.ZodEnum<{
475
+ repository: "repository";
476
+ organization: "organization";
477
+ team: "team";
478
+ user: "user";
479
+ }>;
480
+ id: z.ZodString;
481
+ }, z.core.$strict>;
482
+ }, z.core.$strict>;
483
+ key: z.ZodString;
484
+ conflict: z.ZodBoolean;
485
+ currentness: z.ZodEnum<{
486
+ stale: "stale";
487
+ recorded: "recorded";
488
+ }>;
489
+ }, z.core.$strict>>;
490
+ truncated: z.ZodBoolean;
491
+ }, z.core.$strict>;
492
+ export type ConventionDelivery = z.infer<typeof ConventionDeliverySchema>;
425
493
  export declare const ReadResponseSchema: z.ZodObject<{
494
+ conventions: z.ZodOptional<z.ZodObject<{
495
+ advisory: z.ZodLiteral<true>;
496
+ items: z.ZodArray<z.ZodObject<{
497
+ ref: z.ZodObject<{
498
+ facet: z.ZodEnum<{
499
+ decisions: "decisions";
500
+ constraints: "constraints";
501
+ bugs: "bugs";
502
+ findings: "findings";
503
+ receipts: "receipts";
504
+ commitments: "commitments";
505
+ derived: "derived";
506
+ entities: "entities";
507
+ relationships: "relationships";
508
+ conventions: "conventions";
509
+ }>;
510
+ id: z.ZodString;
511
+ record_hash: z.ZodString;
512
+ scope: z.ZodObject<{
513
+ kind: z.ZodEnum<{
514
+ repository: "repository";
515
+ organization: "organization";
516
+ team: "team";
517
+ user: "user";
518
+ }>;
519
+ id: z.ZodString;
520
+ }, z.core.$strict>;
521
+ }, z.core.$strict>;
522
+ key: z.ZodString;
523
+ conflict: z.ZodBoolean;
524
+ currentness: z.ZodEnum<{
525
+ stale: "stale";
526
+ recorded: "recorded";
527
+ }>;
528
+ }, z.core.$strict>>;
529
+ truncated: z.ZodBoolean;
530
+ }, z.core.$strict>>;
426
531
  schema: z.ZodLiteral<"nuryel.state.read/1">;
427
532
  receipt_id: z.ZodString;
428
533
  scope: z.ZodObject<{
@@ -447,6 +552,7 @@ export declare const ReadResponseSchema: z.ZodObject<{
447
552
  derived: "derived";
448
553
  entities: "entities";
449
554
  relationships: "relationships";
555
+ conventions: "conventions";
450
556
  }>;
451
557
  id: z.ZodString;
452
558
  record_hash: z.ZodString;
@@ -471,6 +577,7 @@ export declare const ReadResponseSchema: z.ZodObject<{
471
577
  derived: "derived";
472
578
  entities: "entities";
473
579
  relationships: "relationships";
580
+ conventions: "conventions";
474
581
  }>;
475
582
  id: z.ZodString;
476
583
  record_hash: z.ZodString;
@@ -495,6 +602,7 @@ export declare const ReadResponseSchema: z.ZodObject<{
495
602
  derived: "derived";
496
603
  entities: "entities";
497
604
  relationships: "relationships";
605
+ conventions: "conventions";
498
606
  }>;
499
607
  id: z.ZodString;
500
608
  record_hash: z.ZodString;
@@ -519,6 +627,7 @@ export declare const ReadResponseSchema: z.ZodObject<{
519
627
  derived: "derived";
520
628
  entities: "entities";
521
629
  relationships: "relationships";
630
+ conventions: "conventions";
522
631
  }>;
523
632
  id: z.ZodString;
524
633
  record_hash: z.ZodString;
@@ -645,6 +754,7 @@ export declare const WriteRequestSchema: z.ZodObject<{
645
754
  derived: "derived";
646
755
  entities: "entities";
647
756
  relationships: "relationships";
757
+ conventions: "conventions";
648
758
  }>;
649
759
  record: z.ZodRecord<z.ZodString, z.ZodUnknown>;
650
760
  idempotency_key: z.ZodString;
@@ -792,10 +902,16 @@ export declare const SubscribeRequestSchema: z.ZodObject<{
792
902
  derived: "derived";
793
903
  entities: "entities";
794
904
  relationships: "relationships";
905
+ conventions: "conventions";
795
906
  }>>>;
796
907
  }, z.core.$strict>;
797
908
  export type SubscribeRequest = z.infer<typeof SubscribeRequestSchema>;
798
909
  export declare const ChangeEventSchema: z.ZodObject<{
910
+ visibility: z.ZodOptional<z.ZodObject<{
911
+ owner: z.ZodString;
912
+ readers: z.ZodArray<z.ZodString>;
913
+ writers: z.ZodArray<z.ZodString>;
914
+ }, z.core.$strict>>;
799
915
  schema: z.ZodLiteral<"nuryel.state.subscribe/1">;
800
916
  seq: z.ZodNumber;
801
917
  at: z.ZodString;
@@ -818,6 +934,7 @@ export declare const ChangeEventSchema: z.ZodObject<{
818
934
  derived: "derived";
819
935
  entities: "entities";
820
936
  relationships: "relationships";
937
+ conventions: "conventions";
821
938
  }>;
822
939
  record_id: z.ZodString;
823
940
  record_hash: z.ZodString;
@@ -907,6 +1024,7 @@ export declare const RecordsResponseSchema: z.ZodObject<{
907
1024
  derived: "derived";
908
1025
  entities: "entities";
909
1026
  relationships: "relationships";
1027
+ conventions: "conventions";
910
1028
  }>>;
911
1029
  missing: z.ZodDefault<z.ZodArray<z.ZodString>>;
912
1030
  denied: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -920,16 +1038,13 @@ export declare function negotiate(offered: readonly string[], required?: readonl
920
1038
  supported: string[];
921
1039
  unsupported: string[];
922
1040
  };
923
- /** Canonical JSON: keys sorted by code unit at every level, `undefined` dropped, non-finite
924
- * numbers rejected. Two records with the same facts hash the same regardless of who wrote them. */
925
- export declare function canonicalize(value: unknown): unknown;
926
- export declare function stateHash(value: unknown): string;
927
1041
  /** Identity = what makes two receipts the same action: who did what to which object, with which
928
1042
  * request. Re-sending the same action replays the same receipt instead of minting a second one. */
929
1043
  export declare function actionReceiptId(r: Pick<ActionReceipt, "scope" | "actor" | "action_kind" | "target" | "request_fingerprint"> & {
930
1044
  idempotency_key?: string;
931
1045
  }): string;
932
1046
  export declare function commitmentId(c: Pick<Commitment, "scope" | "subject" | "title" | "owner" | "due">): string;
1047
+ export declare function conventionId(c: Pick<Convention, "scope" | "key" | "value" | "sources">): string;
933
1048
  export declare function derivedId(d: Pick<DerivedState, "scope" | "subject" | "transform_version" | "dependencies">): string;
934
1049
  export declare const STATE_INVARIANTS: readonly [{
935
1050
  readonly id: "authorization-before-retrieval";
@@ -1,3 +1,4 @@
1
+ import { RecordVisibilitySchema } from "./recordVisibility.js";
1
2
  /**
2
3
  * nuryel.state/1 — the ONE contract every orchestrator and agent speaks to the state layer.
3
4
  *
@@ -27,9 +28,13 @@
27
28
  import { createHash } from "node:crypto";
28
29
  import { z } from "zod";
29
30
  import { compareCodeUnits } from "./canonicalOrder.js";
31
+ import { canonicalize, stateHash } from "./stateCanonical.js";
32
+ import { assertFieldProvenance } from "./fieldProvenance.js";
33
+ export { canonicalize, stateHash } from "./stateCanonical.js";
34
+ export { fieldCitationValue, assertFieldProvenance } from "./fieldProvenance.js";
30
35
  import { DELIVERY_PROFILES } from "./delivery.js";
31
36
  import { isHumanConfirmed as sourceIsHumanConfirmed } from "./strictgate.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";
37
+ import { ScopeSchema, scopePath, externalKey, DependencyRefSchema, ExternalRefSchema, CONVENTION_SCHEMA_VERSION, RECEIPT_SCHEMA_VERSION, COMMITMENT_SCHEMA_VERSION, DERIVED_SCHEMA_VERSION, ENTITY_SCHEMA_VERSION, RELATIONSHIP_SCHEMA_VERSION, } from "./stateRecords.js";
33
38
  export * from "./stateRecords.js";
34
39
  export const STATE_CONTRACT_VERSION = "nuryel.state/1";
35
40
  export const STATE_READ_VERSION = "nuryel.state.read/1";
@@ -41,11 +46,14 @@ export const STATE_CAPTURE_BATCH_VERSION = "nuryel.state.capture-batch/1";
41
46
  export const STATE_OBSERVATION_LINKS_VERSION = "nuryel.observation-links/1";
42
47
  export const STATE_OBSERVATION_REVIEW_VERSION = "nuryel.observation-review/1";
43
48
  export const STATE_OBSERVATION_PAGES_VERSION = "nuryel.observation-pages/1";
49
+ export const STATE_RECORD_VISIBILITY_VERSION = "nuryel.record-visibility/1";
50
+ export const PartitionDeclarationSchema = ScopeSchema.extend({ required_capabilities: z.array(z.literal(STATE_RECORD_VISIBILITY_VERSION)).min(1).max(1).optional() }).strict();
51
+ export const STATE_FIELD_PROVENANCE_VERSION = "nuryel.field-provenance/1";
44
52
  /** Capabilities a server advertises; a client that needs one the server lacks gets a typed
45
53
  * `unsupported`, never a compatible-looking degraded answer. */
46
54
  export const STATE_CAPABILITIES = [
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,
48
- RECEIPT_SCHEMA_VERSION, COMMITMENT_SCHEMA_VERSION, DERIVED_SCHEMA_VERSION, ENTITY_SCHEMA_VERSION, RELATIONSHIP_SCHEMA_VERSION,
55
+ 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, STATE_FIELD_PROVENANCE_VERSION, STATE_RECORD_VISIBILITY_VERSION,
56
+ CONVENTION_SCHEMA_VERSION, RECEIPT_SCHEMA_VERSION, COMMITMENT_SCHEMA_VERSION, DERIVED_SCHEMA_VERSION, ENTITY_SCHEMA_VERSION, RELATIONSHIP_SCHEMA_VERSION,
49
57
  ];
50
58
  const SHA256 = /^sha256:[a-f0-9]{64}$/;
51
59
  // Explicit classes, no `i` flag: the pattern must survive zod → JSON schema for MCP output validation.
@@ -63,6 +71,7 @@ export const PrincipalSchema = z.object({
63
71
  /** One relevant assertion, never a whole conversation. Source text is transient input:
64
72
  * only its exact supporting excerpt and a hashed external pointer may reach the store. */
65
73
  export const CaptureRequestSchema = z.object({
74
+ visibility: RecordVisibilitySchema.optional(),
66
75
  schema: z.literal(STATE_CAPTURE_VERSION),
67
76
  principal: PrincipalSchema,
68
77
  scope: ScopeSchema,
@@ -82,7 +91,7 @@ export const CaptureRequestSchema = z.object({
82
91
  export const CaptureBatchRequestSchema = z.object({
83
92
  schema: z.literal(STATE_CAPTURE_BATCH_VERSION), principal: PrincipalSchema, scope: ScopeSchema,
84
93
  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({
94
+ observations: z.array(CaptureRequestSchema.pick({ subject: true, statement: true, relevance: true, visibility: true }).extend({
86
95
  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
96
  })).min(0).max(32),
88
97
  reviews: z.array(z.object({
@@ -97,7 +106,7 @@ export function captureTransform(scope, subject, statement, evidence) {
97
106
  const identities = [...new Set(evidence.map(e => stateHash({ source: e.source, excerpt: normalizeAssertion(e.excerpt) })))].sort();
98
107
  return CAPTURE_TRANSFORM + stateHash({ scope, subject, statement: normalizeAssertion(statement), evidence: identities }).slice(7);
99
108
  }
100
- export const STATE_FACETS = ["decisions", "constraints", "bugs", "findings", "receipts", "commitments", "derived", "entities", "relationships"];
109
+ export const STATE_FACETS = ["decisions", "constraints", "bugs", "findings", "receipts", "commitments", "derived", "entities", "relationships", "conventions"];
101
110
  // ---- verbs ------------------------------------------------------------------------------
102
111
  /** Union read: the partitions a principal wants in ONE answer. `scope` stays required (it is the
103
112
  * primary partition; its envelope and receipt lead the response). An entry the principal is not
@@ -140,7 +149,13 @@ export const StateOfRecordSchema = z.object({
140
149
  depends_on: z.array(DependencyRefSchema).max(1024),
141
150
  invalidated_by: z.array(z.string().max(512)).max(256),
142
151
  }).strict();
152
+ export const ConventionDeliverySchema = z.object({
153
+ advisory: z.literal(true),
154
+ items: z.array(z.object({ ref: StateRefSchema, key: z.string(), conflict: z.boolean(), currentness: z.enum(["recorded", "stale"]) }).strict()).max(16),
155
+ truncated: z.boolean(),
156
+ }).strict();
143
157
  export const ReadResponseSchema = z.object({
158
+ conventions: ConventionDeliverySchema.optional(),
144
159
  schema: z.literal(STATE_READ_VERSION),
145
160
  receipt_id: z.string().regex(/^hdr_[a-f0-9]{24}$/).describe("the delivery envelope's receipt"),
146
161
  scope: ScopeSchema,
@@ -203,6 +218,7 @@ export const SubscribeRequestSchema = z.object({
203
218
  facets: z.array(z.enum(STATE_FACETS)).max(STATE_FACETS.length).optional(),
204
219
  }).strict();
205
220
  export const ChangeEventSchema = z.object({
221
+ visibility: RecordVisibilitySchema.optional(),
206
222
  schema: z.literal(STATE_SUBSCRIBE_VERSION),
207
223
  seq: z.number().int().positive(),
208
224
  at: z.string().regex(ISO),
@@ -251,32 +267,6 @@ export function negotiate(offered, required = STATE_CAPABILITIES) {
251
267
  return { supported, unsupported };
252
268
  }
253
269
  // ---- canonical form, hashes, ids -----------------------------------------------------------
254
- /** Canonical JSON: keys sorted by code unit at every level, `undefined` dropped, non-finite
255
- * numbers rejected. Two records with the same facts hash the same regardless of who wrote them. */
256
- export function canonicalize(value) {
257
- if (value === null || typeof value === "string" || typeof value === "boolean")
258
- return value;
259
- if (typeof value === "number") {
260
- if (!Number.isFinite(value))
261
- throw new Error("canonical form rejects non-finite numbers");
262
- return value;
263
- }
264
- if (Array.isArray(value))
265
- return value.map(canonicalize);
266
- if (typeof value === "object") {
267
- const out = {};
268
- for (const key of Object.keys(value).sort(compareCodeUnits)) {
269
- const v = value[key];
270
- if (v !== undefined)
271
- out[key] = canonicalize(v);
272
- }
273
- return out;
274
- }
275
- throw new Error(`canonical form rejects ${typeof value}`);
276
- }
277
- export function stateHash(value) {
278
- return `sha256:${createHash("sha256").update(JSON.stringify(canonicalize(value))).digest("hex")}`;
279
- }
280
270
  const idFrom = (prefix, seed) => `${prefix}_${createHash("sha256").update(JSON.stringify(canonicalize(seed))).digest("hex").slice(0, 24)}`;
281
271
  /** Identity = what makes two receipts the same action: who did what to which object, with which
282
272
  * request. Re-sending the same action replays the same receipt instead of minting a second one. */
@@ -286,6 +276,9 @@ export function actionReceiptId(r) {
286
276
  export function commitmentId(c) {
287
277
  return idFrom("ncm", { scope: c.scope, subject: c.subject, title: c.title.trim(), owner: c.owner, due: c.due });
288
278
  }
279
+ export function conventionId(c) {
280
+ return idFrom("ncv", { scope: c.scope, key: c.key, value: c.value.trim(), sources: c.sources.map(stateHash).sort(compareCodeUnits) });
281
+ }
289
282
  export function derivedId(d) {
290
283
  // Capture's reserved transform includes assertion/evidence identity, independent of
291
284
  // read time and unrelated source edits. Ordinary summary identity is unchanged.
@@ -322,6 +315,7 @@ export function assertReadWithinGrants(principal, response) {
322
315
  if (!granted.has(grantKey(response.scope)))
323
316
  throw new Error(`read response scope ${grantKey(response.scope)} is outside the principal's grants`);
324
317
  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 ?? [])] : [];
318
+ refs.push(...(response.conventions?.items.map(item => item.ref) ?? []));
325
319
  for (const ref of refs) {
326
320
  if (!granted.has(grantKey(ref.scope)))
327
321
  throw new Error(`state ref ${ref.id} in scope ${grantKey(ref.scope)} leaked outside the principal's grants`);
@@ -358,6 +352,7 @@ export function assertDerivedState(d) {
358
352
  throw new Error("derived state without dependencies is not state");
359
353
  if (stateHash(d.content) !== d.content_hash)
360
354
  throw new Error("derived state content hash does not match its content");
355
+ assertFieldProvenance(d);
361
356
  if (d.transform_version.startsWith(CAPTURE_TRANSFORM)) {
362
357
  const content = z.object({
363
358
  schema: z.literal("nuryel.observation-content/1"),
@@ -15,12 +15,12 @@
15
15
  * Pure functions over records; no store or SQLite dependency, so the CLI, the MCP server and
16
16
  * the store's own reindex/rank paths cannot drift from each other.
17
17
  */
18
- import type { ActionReceipt, Commitment, DerivedState, ExternalEntity, StateRelationship } from "./stateRecords.js";
18
+ import type { Convention, ActionReceipt, Commitment, DerivedState, ExternalEntity, StateRelationship } from "./stateRecords.js";
19
19
  import type { DeliverySupplement } from "./delivery.js";
20
- export declare const STATE_KINDS: readonly ["receipts", "commitments", "derived", "entities", "relationships"];
20
+ export declare const STATE_KINDS: readonly ["receipts", "commitments", "derived", "entities", "relationships", "conventions"];
21
21
  export type StateKind = (typeof STATE_KINDS)[number];
22
22
  export declare function isStateKind(kind: string): kind is StateKind;
23
- export type StateRecord = ActionReceipt | Commitment | DerivedState | ExternalEntity | StateRelationship;
23
+ export type StateRecord = Convention | ActionReceipt | Commitment | DerivedState | ExternalEntity | StateRelationship;
24
24
  export interface StateLiveness {
25
25
  /** The contract's own word for the record's standing: current, in_force, verified, done, superseded … */
26
26
  label: string;
@@ -1,10 +1,11 @@
1
- export const STATE_KINDS = ["receipts", "commitments", "derived", "entities", "relationships"];
1
+ export const STATE_KINDS = ["receipts", "commitments", "derived", "entities", "relationships", "conventions"];
2
2
  const STATE_KIND_SET = new Set(STATE_KINDS);
3
3
  export function isStateKind(kind) {
4
4
  return STATE_KIND_SET.has(kind);
5
5
  }
6
6
  /** Singular facet label used in renders: `[commitment/in_force]`, `[derived/current]`. */
7
7
  const FACET_LABEL = {
8
+ conventions: "convention",
8
9
  receipts: "receipt",
9
10
  commitments: "commitment",
10
11
  derived: "derived",
@@ -16,6 +17,10 @@ const FACET_LABEL = {
16
17
  * receipt succeeded/verified; entity active; a relationship is always current). */
17
18
  export function stateLiveness(kind, record) {
18
19
  switch (kind) {
20
+ case "conventions": {
21
+ const c = record;
22
+ return { label: c.valid_to != null ? 'superseded' : c.status, live: c.status === 'accepted' && c.valid_to === null && Date.parse(c.review_by) > Date.now() };
23
+ }
19
24
  case "derived": {
20
25
  const d = record;
21
26
  if (d.valid_to != null)
@@ -45,6 +50,7 @@ export function stateLiveness(kind, record) {
45
50
  * object (`event:10042`), the entity id, or the relationship's `from` endpoint. */
46
51
  export function stateSubject(kind, record) {
47
52
  switch (kind) {
53
+ case "conventions": return record.key;
48
54
  case "derived": return record.subject;
49
55
  case "commitments": return record.subject;
50
56
  case "receipts": {
@@ -59,6 +65,7 @@ export function stateSubject(kind, record) {
59
65
  * updated_at. Relationships carry no clock and sort last among equals. */
60
66
  export function stateObservedAt(kind, record) {
61
67
  switch (kind) {
68
+ case "conventions": return record.valid_from;
62
69
  case "derived": return record.computed_at;
63
70
  case "commitments": return record.valid_from;
64
71
  case "receipts": {
@@ -77,6 +84,7 @@ export function stateSearchDoc(kind, record) {
77
84
  const { label } = stateLiveness(kind, record);
78
85
  const subject = stateSubject(kind, record);
79
86
  switch (kind) {
87
+ case "conventions": return { title: subject, body: `${record.value} ${label} advisory convention` };
80
88
  case "derived": {
81
89
  const d = record;
82
90
  return { title: subject, body: `${d.content} ${label} ${d.transform_version} ${d.computed_at.slice(0, 10)}` };
@@ -116,6 +124,7 @@ export function renderStateLine(kind, record) {
116
124
  const { label } = stateLiveness(kind, record);
117
125
  const head = `[${FACET_LABEL[kind]}/${label}] ${stateSubject(kind, record)} — `;
118
126
  switch (kind) {
127
+ case "conventions": return `${head}${oneLine(record.value, 120)} (advisory)`;
119
128
  case "derived":
120
129
  return `${head}${oneLine(record.content, DERIVED_HEADLINE_CHARS)}`;
121
130
  case "commitments": {