@davesheffer/hunch 1.32.8 → 1.35.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 (79) hide show
  1. package/README.md +18 -4
  2. package/dist/cli/index.js +52 -6
  3. package/dist/cli/invocation.d.ts +8 -0
  4. package/dist/cli/invocation.js +17 -10
  5. package/dist/cli/serve.js +28 -2
  6. package/dist/cli/state.d.ts +3 -0
  7. package/dist/cli/state.js +150 -0
  8. package/dist/cli/taskReport.js +52 -4
  9. package/dist/cli/update.js +5 -5
  10. package/dist/client/state.d.ts +86 -18
  11. package/dist/client/state.js +16 -2
  12. package/dist/client/stateProof.d.ts +4 -0
  13. package/dist/client/stateProof.js +17 -0
  14. package/dist/constitution/behaviorEvaluator.js +1 -1
  15. package/dist/constitution/schema.d.ts +14 -14
  16. package/dist/core/automaticReviewMemory.d.ts +5 -0
  17. package/dist/core/conventionDelivery.d.ts +8 -0
  18. package/dist/core/conventionDelivery.js +52 -0
  19. package/dist/core/fieldProvenance.d.ts +8 -0
  20. package/dist/core/fieldProvenance.js +72 -0
  21. package/dist/core/recordVisibility.d.ts +9 -0
  22. package/dist/core/recordVisibility.js +25 -0
  23. package/dist/core/stateCanonical.d.ts +3 -0
  24. package/dist/core/stateCanonical.js +34 -0
  25. package/dist/core/stateContract.d.ts +125 -10
  26. package/dist/core/stateContract.js +26 -31
  27. package/dist/core/stateDelivery.d.ts +3 -3
  28. package/dist/core/stateDelivery.js +10 -1
  29. package/dist/core/stateHttp.d.ts +280 -0
  30. package/dist/core/stateHttp.js +17 -0
  31. package/dist/core/stateProof.d.ts +13 -0
  32. package/dist/core/stateProof.js +34 -0
  33. package/dist/core/stateRecords.d.ts +127 -0
  34. package/dist/core/stateRecords.js +48 -0
  35. package/dist/core/taskRecord.d.ts +39 -0
  36. package/dist/core/taskRecord.js +185 -0
  37. package/dist/core/taskReport.d.ts +8 -1
  38. package/dist/core/taskReport.js +28 -14
  39. package/dist/core/taskReportEvidence.js +2 -1
  40. package/dist/core/taskReportHook.d.ts +18 -3
  41. package/dist/core/taskReportHook.js +77 -8
  42. package/dist/core/taskReportPaths.d.ts +6 -0
  43. package/dist/core/taskReportPaths.js +13 -0
  44. package/dist/core/types.d.ts +321 -4
  45. package/dist/core/types.js +47 -2
  46. package/dist/core/updatecheck.d.ts +51 -0
  47. package/dist/core/updatecheck.js +266 -0
  48. package/dist/core/version.d.ts +2 -0
  49. package/dist/core/version.js +3 -1
  50. package/dist/extractors/git.js +3 -10
  51. package/dist/integrations/gitignore.js +1 -0
  52. package/dist/integrations/health.js +27 -2
  53. package/dist/mcp/server.js +15 -5
  54. package/dist/mcp/taskReportTools.d.ts +4 -4
  55. package/dist/mcp/taskReportTools.js +32 -3
  56. package/dist/serve/app.d.ts +2 -0
  57. package/dist/serve/app.js +71 -30
  58. package/dist/serve/config.d.ts +16 -0
  59. package/dist/serve/config.js +27 -7
  60. package/dist/serve/operator.d.ts +4 -0
  61. package/dist/serve/operator.js +223 -0
  62. package/dist/serve/stateProof.d.ts +15 -0
  63. package/dist/serve/stateProof.js +105 -0
  64. package/dist/store/changeLedger.d.ts +6 -0
  65. package/dist/store/hunchStore.d.ts +9 -3
  66. package/dist/store/hunchStore.js +36 -19
  67. package/dist/store/stateAccess.d.ts +13 -0
  68. package/dist/store/stateAccess.js +85 -0
  69. package/dist/store/stateBinding.d.ts +13 -18
  70. package/dist/store/stateBinding.js +161 -52
  71. package/dist/store/stateCapture.js +10 -2
  72. package/dist/store/stateError.d.ts +12 -0
  73. package/dist/store/stateError.js +12 -0
  74. package/dist/store/statePartition.d.ts +9 -0
  75. package/dist/store/statePartition.js +30 -0
  76. package/dist/taskReports.d.ts +1 -1
  77. package/dist/taskReports.js +16 -4
  78. package/package.json +5 -1
  79. package/server.json +2 -2
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { findRoot } from "../core/paths.js";
5
- const PACKAGE = "@davesheffer/hunch";
5
+ import { HUNCH_PACKAGE_NAME } from "../core/version.js";
6
6
  /** Arguments come only from fixed commands and a validated registry version.
7
7
  * Windows needs the shell to resolve npm.cmd; cwd is never interpolated. */
8
8
  export function runNpm(root, args, capture = false) {
@@ -26,24 +26,24 @@ export function updateHunch(root, opts = {}, run = (args, capture) => runNpm(roo
26
26
  const manifest = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
27
27
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
28
28
  throw new Error("package.json must contain an object");
29
- if (manifest.name === PACKAGE)
29
+ if (manifest.name === HUNCH_PACKAGE_NAME)
30
30
  throw new Error("Run hunch update in a consumer repository, not Hunch's own source checkout.");
31
31
  const sections = ["dependencies", "devDependencies", "optionalDependencies"];
32
32
  const declared = sections.filter(section => {
33
33
  const deps = manifest[section];
34
34
  if (deps !== undefined && (!deps || typeof deps !== "object" || Array.isArray(deps)))
35
35
  throw new Error(`invalid ${section} in package.json`);
36
- return deps && Object.hasOwn(deps, PACKAGE);
36
+ return deps && Object.hasOwn(deps, HUNCH_PACKAGE_NAME);
37
37
  });
38
38
  if (declared.length > 1)
39
39
  throw new Error("Hunch is declared in multiple dependency sections; resolve the duplicate before updating.");
40
40
  if (declared.length && (manifest.workspaces || (manifest.packageManager && !/^npm@/.test(manifest.packageManager)) || ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"].some(name => existsSync(join(root, name))))) {
41
41
  throw new Error("Automatic dependency updates currently support standalone npm projects. Update Hunch to an exact version with your package manager, then run hunch integrations repair-pins.");
42
42
  }
43
- const version = JSON.parse(run(["view", `${PACKAGE}@latest`, "version", "--json"], true));
43
+ const version = JSON.parse(run(["view", `${HUNCH_PACKAGE_NAME}@latest`, "version", "--json"], true));
44
44
  if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version))
45
45
  throw new Error("npm returned an invalid Hunch version");
46
- const spec = `${PACKAGE}@${version}`;
46
+ const spec = `${HUNCH_PACKAGE_NAME}@${version}`;
47
47
  const commands = [];
48
48
  if (declared.length) {
49
49
  const flag = { dependencies: "--save-prod", devDependencies: "--save-dev", optionalDependencies: "--save-optional" }[declared[0]];
@@ -3,21 +3,16 @@
3
3
  * Import from `@davesheffer/hunch/state`. The principal is the bearer token's; the request
4
4
  * shapes are the contract's minus `schema` and `principal`.
5
5
  */
6
- import type { ReadRequest, SubscribeRequest, WriteRequest, ChangeEvent, Scope } from "../core/stateContract.js";
6
+ import type { ReadRequest, SubscribeRequest, WriteRequest, Scope } from "../core/stateContract.js";
7
7
  import type { DeliveryEnvelope } from "../core/delivery.js";
8
8
  import type { CaptureRequest, CaptureBatchRequest } from "../core/stateContract.js";
9
+ import type { SubscribeResponse } from '../store/stateBinding.js';
9
10
  export type ClientReadRequest = Omit<ReadRequest, "schema" | "principal">;
10
11
  export type ClientWriteRequest = Omit<WriteRequest, "schema" | "principal" | "expected_version"> & {
11
12
  expected_version?: string | number | null;
12
13
  };
13
14
  export type ClientSubscribeRequest = Omit<SubscribeRequest, "schema" | "principal">;
14
- export interface ClientSubscribeResponse {
15
- schema: string;
16
- scope: Scope;
17
- head_seq: number;
18
- events: ChangeEvent[];
19
- filtered: boolean;
20
- }
15
+ export type ClientSubscribeResponse = SubscribeResponse;
21
16
  export interface StateProblem {
22
17
  type: string;
23
18
  title: string;
@@ -36,7 +31,14 @@ export declare class StateClientError extends Error {
36
31
  readonly problem: StateProblem;
37
32
  constructor(status: number, code: string, problem: StateProblem);
38
33
  }
34
+ export interface StateProofRequest {
35
+ method: string;
36
+ url: string;
37
+ token: string;
38
+ nonce?: string;
39
+ }
39
40
  export interface StateClientOptions {
41
+ proof?: (request: StateProofRequest) => Promise<string>;
40
42
  baseUrl: string;
41
43
  token: string;
42
44
  fetch?: typeof fetch;
@@ -64,7 +66,7 @@ export declare function createStateClient(opts: StateClientOptions): {
64
66
  state_of_record: {
65
67
  subject: string;
66
68
  current: {
67
- facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships";
69
+ facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships" | "conventions";
68
70
  id: string;
69
71
  record_hash: string;
70
72
  scope: {
@@ -73,7 +75,7 @@ export declare function createStateClient(opts: StateClientOptions): {
73
75
  };
74
76
  }[];
75
77
  in_force: {
76
- facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships";
78
+ facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships" | "conventions";
77
79
  id: string;
78
80
  record_hash: string;
79
81
  scope: {
@@ -82,7 +84,7 @@ export declare function createStateClient(opts: StateClientOptions): {
82
84
  };
83
85
  }[];
84
86
  done: {
85
- facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships";
87
+ facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships" | "conventions";
86
88
  id: string;
87
89
  record_hash: string;
88
90
  scope: {
@@ -116,7 +118,7 @@ export declare function createStateClient(opts: StateClientOptions): {
116
118
  })[];
117
119
  invalidated_by: string[];
118
120
  observed?: {
119
- facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships";
121
+ facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships" | "conventions";
120
122
  id: string;
121
123
  record_hash: string;
122
124
  scope: {
@@ -139,6 +141,24 @@ export declare function createStateClient(opts: StateClientOptions): {
139
141
  kind: "repository" | "organization" | "team" | "user";
140
142
  id: string;
141
143
  }[];
144
+ conventions?: {
145
+ advisory: true;
146
+ items: {
147
+ ref: {
148
+ facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships" | "conventions";
149
+ id: string;
150
+ record_hash: string;
151
+ scope: {
152
+ kind: "repository" | "organization" | "team" | "user";
153
+ id: string;
154
+ };
155
+ };
156
+ key: string;
157
+ conflict: boolean;
158
+ currentness: "stale" | "recorded";
159
+ }[];
160
+ truncated: boolean;
161
+ } | undefined;
142
162
  records?: Record<string, Record<string, unknown>> | undefined;
143
163
  scopes?: {
144
164
  kind: "repository" | "organization" | "team" | "user";
@@ -158,7 +178,7 @@ export declare function createStateClient(opts: StateClientOptions): {
158
178
  schema: "nuryel.state.write/1";
159
179
  record_id: string;
160
180
  record_hash: string;
161
- durability: "committed" | "pushed" | "local";
181
+ durability: "local" | "committed" | "pushed";
162
182
  outcome: "updated" | "superseded" | "created" | "replayed";
163
183
  conflict: {
164
184
  incumbent_id: string;
@@ -170,7 +190,7 @@ export declare function createStateClient(opts: StateClientOptions): {
170
190
  schema: "nuryel.state.write/1";
171
191
  record_id: string;
172
192
  record_hash: string;
173
- durability: "committed" | "pushed" | "local";
193
+ durability: "local" | "committed" | "pushed";
174
194
  outcome: "updated" | "superseded" | "created" | "replayed";
175
195
  conflict: {
176
196
  incumbent_id: string;
@@ -187,7 +207,7 @@ export declare function createStateClient(opts: StateClientOptions): {
187
207
  schema: "nuryel.state.write/1";
188
208
  record_id: string;
189
209
  record_hash: string;
190
- durability: "committed" | "pushed" | "local";
210
+ durability: "local" | "committed" | "pushed";
191
211
  outcome: "updated" | "superseded" | "created" | "replayed";
192
212
  conflict: {
193
213
  incumbent_id: string;
@@ -208,7 +228,7 @@ export declare function createStateClient(opts: StateClientOptions): {
208
228
  schema: "nuryel.state.write/1";
209
229
  record_id: string;
210
230
  record_hash: string;
211
- durability: "committed" | "pushed" | "local";
231
+ durability: "local" | "committed" | "pushed";
212
232
  outcome: "updated" | "superseded" | "created" | "replayed";
213
233
  conflict: {
214
234
  incumbent_id: string;
@@ -223,7 +243,55 @@ export declare function createStateClient(opts: StateClientOptions): {
223
243
  message: string;
224
244
  })[] | undefined;
225
245
  }>;
226
- subscribe: (request: ClientSubscribeRequest) => Promise<ClientSubscribeResponse>;
246
+ subscribe: (request: ClientSubscribeRequest) => Promise<{
247
+ schema: "nuryel.state.subscribe/1";
248
+ scope: {
249
+ kind: "repository" | "organization" | "team" | "user";
250
+ id: string;
251
+ };
252
+ head_seq: number;
253
+ events: {
254
+ schema: "nuryel.state.subscribe/1";
255
+ seq: number;
256
+ at: string;
257
+ scope: {
258
+ kind: "repository" | "organization" | "team" | "user";
259
+ id: string;
260
+ };
261
+ facet: "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships" | "conventions";
262
+ record_id: string;
263
+ record_hash: string;
264
+ change: "updated" | "retired" | "superseded" | "created" | "invalidated";
265
+ invalidates: string[];
266
+ visibility?: {
267
+ owner: string;
268
+ readers: string[];
269
+ writers: string[];
270
+ } | undefined;
271
+ subject?: string | undefined;
272
+ cause?: {
273
+ kind: "receipt";
274
+ receipt_id: string;
275
+ } | {
276
+ kind: "external";
277
+ ref: {
278
+ system: string;
279
+ object_type: string;
280
+ object_key: string;
281
+ observed_at: string;
282
+ version?: string | undefined;
283
+ content_hash?: string | undefined;
284
+ locator?: string | undefined;
285
+ };
286
+ } | {
287
+ kind: "write";
288
+ principal: string;
289
+ } | undefined;
290
+ }[];
291
+ filtered: boolean;
292
+ floor_seq: number;
293
+ resync: boolean;
294
+ }>;
227
295
  records: (request: {
228
296
  scope: Scope;
229
297
  ids: string[];
@@ -234,7 +302,7 @@ export declare function createStateClient(opts: StateClientOptions): {
234
302
  id: string;
235
303
  };
236
304
  records: Record<string, Record<string, unknown>>;
237
- facets: Record<string, "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships">;
305
+ facets: Record<string, "decisions" | "constraints" | "bugs" | "findings" | "receipts" | "commitments" | "derived" | "entities" | "relationships" | "conventions">;
238
306
  missing: string[];
239
307
  denied: string[];
240
308
  }>;
@@ -15,16 +15,30 @@ export function createStateClient(opts) {
15
15
  const base = opts.baseUrl.replace(/\/+$/, "");
16
16
  const doFetch = opts.fetch ?? fetch;
17
17
  const timeoutMs = opts.timeoutMs ?? 15_000;
18
+ let nonce;
18
19
  async function call(method, path, body) {
19
20
  const controller = new AbortController();
20
21
  const timer = setTimeout(() => controller.abort(), timeoutMs);
21
22
  try {
22
- const response = await doFetch(`${base}${path}`, {
23
+ const url = `${base}${path}`;
24
+ const request = async () => doFetch(url, {
23
25
  method,
24
- headers: { authorization: `Bearer ${opts.token}`, ...(body !== undefined ? { "content-type": "application/json" } : {}) },
26
+ headers: { authorization: `${opts.proof ? 'DPoP' : 'Bearer'} ${opts.token}`, ...(opts.proof ? { dpop: await opts.proof({ method, url, token: opts.token, nonce }) } : {}), ...(body !== undefined ? { "content-type": "application/json" } : {}) },
25
27
  body: body !== undefined ? JSON.stringify(body) : undefined,
26
28
  signal: controller.signal,
29
+ redirect: "error",
27
30
  });
31
+ let response = await request();
32
+ // This one retry is an authentication challenge before any operation runs.
33
+ // Idempotency/conflict/network failures never trigger automatic write retries.
34
+ if (opts.proof && response.status === 401 && response.headers.has('dpop-nonce')) {
35
+ const challenge = await response.clone().json().catch(() => null);
36
+ if (challenge?.title === 'use_dpop_nonce') {
37
+ nonce = response.headers.get('dpop-nonce');
38
+ await response.arrayBuffer();
39
+ response = await request();
40
+ }
41
+ }
28
42
  const text = await response.text();
29
43
  const parsed = text ? JSON.parse(text) : {};
30
44
  if (!response.ok) {
@@ -0,0 +1,4 @@
1
+ /** Optional Node signer. The fetch-only client stays usable in other runtimes. */
2
+ import { type KeyObject } from 'node:crypto';
3
+ import type { StateProofRequest } from './state.js';
4
+ export declare function createStateProofSigner(input: string | KeyObject): (request: StateProofRequest) => Promise<string>;
@@ -0,0 +1,17 @@
1
+ /** Optional Node signer. The fetch-only client stays usable in other runtimes. */
2
+ import { createPrivateKey, createPublicKey, randomUUID, sign } from 'node:crypto';
3
+ import { ProofPublicKeySchema, proofTarget, tokenProofHash } from '../core/stateProof.js';
4
+ export function createStateProofSigner(input) {
5
+ const key = typeof input === 'string' ? (input.trimStart().startsWith('{') ? createPrivateKey({ key: JSON.parse(input), format: 'jwk' }) : createPrivateKey(input)) : input;
6
+ if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519')
7
+ throw new Error('proof signing requires an Ed25519 private key');
8
+ const jwk = ProofPublicKeySchema.parse(createPublicKey(key).export({ format: 'jwk' }));
9
+ const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
10
+ const header = encode({ typ: 'dpop+jwt', alg: 'EdDSA', jwk });
11
+ return async ({ method, url, token, nonce }) => {
12
+ const claims = encode({ jti: randomUUID(), htm: method, htu: proofTarget(url), iat: Math.floor(Date.now() / 1000), ath: tokenProofHash(token), ...(nonce ? { nonce } : {}) });
13
+ const message = `${header}.${claims}`;
14
+ return `${message}.${sign(null, Buffer.from(message), key).toString('base64url')}`;
15
+ };
16
+ }
17
+ //# sourceMappingURL=stateProof.js.map
@@ -132,7 +132,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
132
132
  if (!existsSync(join(root, ".hunch-cache", "behavior-deps"))) {
133
133
  return evaluation(policy, commit, { ...baseExecution, commit, error_code: "dependency-snapshot-cache-absent" }, "error", "no dependency snapshot cache exists on this machine (.hunch-cache/behavior-deps); executable behavior is unevaluated here, not failed — provision the policy's snapshots (hunch constitution g2 --behavior-deps <candidate> --behavior-review-hash <hash>) or evaluate where they were built");
134
134
  }
135
- return evaluation(policy, commit, { ...baseExecution, commit, error_code: "dependency-snapshot-unavailable" }, "error", `no unique exact dependency snapshot matches this commit's package.json/package-lock.json among the policy's pinned ids (${assertion.dependency_snapshot_ids.join(", ")}); dependency inputs changed since compilation re-plan and re-prove the policy (rb_g2_stale_policy_01)`);
135
+ return evaluation(policy, commit, { ...baseExecution, commit, error_code: "dependency-snapshot-unavailable" }, "error", `no unique exact dependency snapshot matches this commit's package.json/package-lock.json among the policy's pinned ids (${assertion.dependency_snapshot_ids.join(", ")}); restore and validate the exact pinned cache first. If dependency inputs changed, prepare a replacement behavior candidate and replay; human selection, activation and retirement remain required. Re-proving an active policy does not refresh its pins (docs/behavior-policy-recovery.md)`);
136
136
  }
137
137
  const session = mkdtempSync(join(tmpdir(), "hunch-behavior-policy-"));
138
138
  const hooks = join(session, "hooks-disabled");
@@ -370,13 +370,13 @@ export declare const PolicyStateSchema: z.ZodEnum<{
370
370
  proposed: "proposed";
371
371
  rejected: "rejected";
372
372
  superseded: "superseded";
373
+ repaired: "repaired";
374
+ active_advisory: "active_advisory";
375
+ active_blocking: "active_blocking";
373
376
  compiled: "compiled";
377
+ validating: "validating";
374
378
  uncompilable: "uncompilable";
375
379
  drafted: "drafted";
376
- validating: "validating";
377
- active_advisory: "active_advisory";
378
- active_blocking: "active_blocking";
379
- repaired: "repaired";
380
380
  }>;
381
381
  export type PolicyState = z.infer<typeof PolicyStateSchema>;
382
382
  export declare const PolicySelectorSchema: z.ZodObject<{
@@ -468,16 +468,16 @@ export type PolicyAssertion = z.infer<typeof PolicyAssertionSchema>;
468
468
  export declare const PolicyAuditEventSchema: z.ZodObject<{
469
469
  action: z.ZodEnum<{
470
470
  retired: "retired";
471
+ withdrawn: "withdrawn";
471
472
  rejected: "rejected";
472
- compiled: "compiled";
473
473
  repaired: "repaired";
474
+ compiled: "compiled";
474
475
  enriched: "enriched";
475
476
  linked_exception: "linked_exception";
476
477
  proved: "proved";
477
478
  approved_advisory: "approved_advisory";
478
479
  approved_blocking: "approved_blocking";
479
480
  demoted: "demoted";
480
- withdrawn: "withdrawn";
481
481
  }>;
482
482
  actor_kind: z.ZodEnum<{
483
483
  system: "system";
@@ -518,13 +518,13 @@ export declare const PolicySpecSchema: z.ZodObject<{
518
518
  proposed: "proposed";
519
519
  rejected: "rejected";
520
520
  superseded: "superseded";
521
+ repaired: "repaired";
522
+ active_advisory: "active_advisory";
523
+ active_blocking: "active_blocking";
521
524
  compiled: "compiled";
525
+ validating: "validating";
522
526
  uncompilable: "uncompilable";
523
527
  drafted: "drafted";
524
- validating: "validating";
525
- active_advisory: "active_advisory";
526
- active_blocking: "active_blocking";
527
- repaired: "repaired";
528
528
  }>;
529
529
  statement: z.ZodString;
530
530
  rationale: z.ZodDefault<z.ZodString>;
@@ -610,10 +610,10 @@ export declare const PolicySpecSchema: z.ZodObject<{
610
610
  }>>;
611
611
  surfaces: z.ZodDefault<z.ZodArray<z.ZodEnum<{
612
612
  ci: "ci";
613
- pre_edit: "pre_edit";
614
- pre_commit: "pre_commit";
615
613
  mcp: "mcp";
616
614
  cli: "cli";
615
+ pre_edit: "pre_edit";
616
+ pre_commit: "pre_commit";
617
617
  }>>>;
618
618
  authority: z.ZodDefault<z.ZodNullable<z.ZodObject<{
619
619
  kind: z.ZodLiteral<"human">;
@@ -661,16 +661,16 @@ export declare const PolicySpecSchema: z.ZodObject<{
661
661
  audit: z.ZodDefault<z.ZodArray<z.ZodObject<{
662
662
  action: z.ZodEnum<{
663
663
  retired: "retired";
664
+ withdrawn: "withdrawn";
664
665
  rejected: "rejected";
665
- compiled: "compiled";
666
666
  repaired: "repaired";
667
+ compiled: "compiled";
667
668
  enriched: "enriched";
668
669
  linked_exception: "linked_exception";
669
670
  proved: "proved";
670
671
  approved_advisory: "approved_advisory";
671
672
  approved_blocking: "approved_blocking";
672
673
  demoted: "demoted";
673
- withdrawn: "withdrawn";
674
674
  }>;
675
675
  actor_kind: z.ZodEnum<{
676
676
  system: "system";
@@ -55,6 +55,11 @@ export declare function automateReviewMemory(options: {
55
55
  evidence: string[];
56
56
  last_verified?: string | undefined;
57
57
  };
58
+ visibility?: {
59
+ owner: string;
60
+ readers: string[];
61
+ writers: string[];
62
+ } | undefined;
58
63
  valid_from?: string | undefined;
59
64
  }[];
60
65
  }>;
@@ -0,0 +1,8 @@
1
+ /** Explicit conventions alongside repository DNA. No scope wins and no policy is activated. */
2
+ import type { Convention, Scope } from './stateRecords.js';
3
+ import { type ConventionDelivery } from './stateContract.js';
4
+ import type { DeliverySupplement } from './delivery.js';
5
+ export declare function conventionCurrentness(record: Convention, resolve?: (id: string, scope: Scope) => unknown, now?: number): 'recorded' | 'stale';
6
+ export declare function conventionDelivery(records: readonly Convention[], resolve?: (id: string, scope: Scope) => unknown, verified?: ReadonlyMap<string, "recorded" | "stale">): ConventionDelivery | undefined;
7
+ /** The regular brief uses its existing token budget and keeps Project DNA's separate identity. */
8
+ export declare function conventionSupplements(records: readonly Convention[], resolve?: (id: string, scope: Scope) => unknown): DeliverySupplement[];
@@ -0,0 +1,52 @@
1
+ import { scopePath, stateHash } from './stateContract.js';
2
+ import { compareCodeUnits } from './canonicalOrder.js';
3
+ export function conventionCurrentness(record, resolve, now = Date.now()) {
4
+ if (record.status === 'stale' || record.status === 'withdrawn' || record.valid_to !== null || Date.parse(record.valid_from) > now || Date.parse(record.review_by) <= now)
5
+ return 'stale';
6
+ for (const source of record.sources)
7
+ if (source.kind === 'record') {
8
+ const held = resolve?.(source.id, source.scope ?? record.scope);
9
+ if (!held || stateHash(held) !== source.record_hash || held.valid_to != null || held.state === 'stale' || held.status === 'stale' || held.status === 'withdrawn')
10
+ return 'stale';
11
+ }
12
+ return 'recorded';
13
+ }
14
+ export function conventionDelivery(records, resolve, verified) {
15
+ const currentness = (record) => verified?.get(record.id) ?? conventionCurrentness(record, resolve);
16
+ const candidates = [...new Map(records.map(r => [r.id, r])).values()].filter(r => r.valid_to === null && r.status !== 'withdrawn').sort((a, b) => compareCodeUnits(a.key, b.key) || compareCodeUnits(scopePath(a.scope), scopePath(b.scope)) || compareCodeUnits(a.id, b.id));
17
+ if (!candidates.length)
18
+ return undefined;
19
+ const values = new Map();
20
+ for (const record of candidates)
21
+ if (currentness(record) !== 'stale') {
22
+ const set = values.get(record.key) ?? new Set();
23
+ set.add(record.value);
24
+ values.set(record.key, set);
25
+ }
26
+ let bytes = 0;
27
+ const selected = [];
28
+ for (const record of candidates) {
29
+ const size = Buffer.byteLength(JSON.stringify(record));
30
+ if (selected.length >= 16 || bytes + size > 16_384)
31
+ break;
32
+ selected.push(record);
33
+ bytes += size;
34
+ }
35
+ return { advisory: true, items: selected.map(record => ({ ref: { facet: 'conventions', id: record.id, record_hash: stateHash(record), scope: record.scope }, key: record.key,
36
+ currentness: currentness(record), conflict: (values.get(record.key)?.size ?? 0) > 1 })), truncated: selected.length < candidates.length };
37
+ }
38
+ /** The regular brief uses its existing token budget and keeps Project DNA's separate identity. */
39
+ export function conventionSupplements(records, resolve) {
40
+ const delivery = conventionDelivery(records, resolve);
41
+ if (!delivery)
42
+ return [];
43
+ const byId = new Map(records.map(r => [r.id, r]));
44
+ return [{ id: 'explicit-conventions', kind: 'conventions', priority: 424,
45
+ text: `EXPLICIT CONVENTIONS — advisory; scope is context, never precedence or policy authority. Resolve conflicting values before acting.${delivery.truncated || delivery.items.length > 8 ? ' Delivery is incomplete; more records exist.' : ''}` },
46
+ ...delivery.items.slice(0, 8).map((item, i) => {
47
+ const record = byId.get(item.ref.id);
48
+ return { id: record.id, kind: 'convention', priority: 423 - i,
49
+ text: `${scopePath(record.scope)} · ${record.key} · ${record.status}/${item.currentness}${item.conflict ? ' · CONFLICT' : ''}: ${record.value.slice(0, 300)} (revision ${item.ref.record_hash}; sources ${record.sources.length}; review by ${record.review_by})` };
50
+ })];
51
+ }
52
+ //# sourceMappingURL=conventionDelivery.js.map
@@ -0,0 +1,8 @@
1
+ import type { DerivedState, FieldSelector } from "./stateRecords.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 declare function fieldCitationValue(content: string, selector: FieldSelector): string | number | boolean | null;
5
+ /** This is structural traceability, not a check that a source supports a claim. */
6
+ export declare function assertFieldProvenance(record: Pick<DerivedState, "content" | "dependencies" | "field_provenance">): void;
7
+ /** Bounded, plain-text source map for assistant clients; structured records remain complete. */
8
+ export declare function fieldCitationText(record: DerivedState): string;
@@ -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