@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.
- package/README.md +18 -4
- package/dist/cli/index.js +52 -6
- package/dist/cli/invocation.d.ts +8 -0
- package/dist/cli/invocation.js +17 -10
- package/dist/cli/serve.js +28 -2
- package/dist/cli/state.d.ts +3 -0
- package/dist/cli/state.js +150 -0
- package/dist/cli/taskReport.js +52 -4
- package/dist/cli/update.js +5 -5
- package/dist/client/state.d.ts +86 -18
- package/dist/client/state.js +16 -2
- package/dist/client/stateProof.d.ts +4 -0
- package/dist/client/stateProof.js +17 -0
- package/dist/constitution/behaviorEvaluator.js +1 -1
- package/dist/constitution/schema.d.ts +14 -14
- package/dist/core/automaticReviewMemory.d.ts +5 -0
- package/dist/core/conventionDelivery.d.ts +8 -0
- package/dist/core/conventionDelivery.js +52 -0
- package/dist/core/fieldProvenance.d.ts +8 -0
- package/dist/core/fieldProvenance.js +72 -0
- package/dist/core/recordVisibility.d.ts +9 -0
- package/dist/core/recordVisibility.js +25 -0
- package/dist/core/stateCanonical.d.ts +3 -0
- package/dist/core/stateCanonical.js +34 -0
- package/dist/core/stateContract.d.ts +125 -10
- package/dist/core/stateContract.js +26 -31
- package/dist/core/stateDelivery.d.ts +3 -3
- package/dist/core/stateDelivery.js +10 -1
- package/dist/core/stateHttp.d.ts +280 -0
- package/dist/core/stateHttp.js +17 -0
- package/dist/core/stateProof.d.ts +13 -0
- package/dist/core/stateProof.js +34 -0
- package/dist/core/stateRecords.d.ts +127 -0
- package/dist/core/stateRecords.js +48 -0
- package/dist/core/taskRecord.d.ts +39 -0
- package/dist/core/taskRecord.js +185 -0
- package/dist/core/taskReport.d.ts +8 -1
- package/dist/core/taskReport.js +28 -14
- package/dist/core/taskReportEvidence.js +2 -1
- package/dist/core/taskReportHook.d.ts +18 -3
- package/dist/core/taskReportHook.js +77 -8
- package/dist/core/taskReportPaths.d.ts +6 -0
- package/dist/core/taskReportPaths.js +13 -0
- package/dist/core/types.d.ts +321 -4
- package/dist/core/types.js +47 -2
- package/dist/core/updatecheck.d.ts +51 -0
- package/dist/core/updatecheck.js +266 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.js +3 -1
- package/dist/extractors/git.js +3 -10
- package/dist/integrations/gitignore.js +1 -0
- package/dist/integrations/health.js +27 -2
- package/dist/mcp/server.js +15 -5
- package/dist/mcp/taskReportTools.d.ts +4 -4
- package/dist/mcp/taskReportTools.js +32 -3
- package/dist/serve/app.d.ts +2 -0
- package/dist/serve/app.js +71 -30
- package/dist/serve/config.d.ts +16 -0
- package/dist/serve/config.js +27 -7
- package/dist/serve/operator.d.ts +4 -0
- package/dist/serve/operator.js +223 -0
- package/dist/serve/stateProof.d.ts +15 -0
- package/dist/serve/stateProof.js +105 -0
- package/dist/store/changeLedger.d.ts +6 -0
- package/dist/store/hunchStore.d.ts +9 -3
- package/dist/store/hunchStore.js +36 -19
- package/dist/store/stateAccess.d.ts +13 -0
- package/dist/store/stateAccess.js +85 -0
- package/dist/store/stateBinding.d.ts +13 -18
- package/dist/store/stateBinding.js +161 -52
- package/dist/store/stateCapture.js +10 -2
- package/dist/store/stateError.d.ts +12 -0
- package/dist/store/stateError.js +12 -0
- package/dist/store/statePartition.d.ts +9 -0
- package/dist/store/statePartition.js +30 -0
- package/dist/taskReports.d.ts +1 -1
- package/dist/taskReports.js +16 -4
- package/package.json +5 -1
- package/server.json +2 -2
package/dist/cli/update.js
CHANGED
|
@@ -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
|
-
|
|
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 ===
|
|
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,
|
|
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", `${
|
|
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 = `${
|
|
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]];
|
package/dist/client/state.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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<
|
|
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
|
}>;
|
package/dist/client/state.js
CHANGED
|
@@ -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
|
|
23
|
+
const url = `${base}${path}`;
|
|
24
|
+
const request = async () => doFetch(url, {
|
|
23
25
|
method,
|
|
24
|
-
headers: { authorization:
|
|
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
|
|
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,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
|