@davesheffer/hunch 1.22.2 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/contracts/change-proof/hunch.change-proof.v1.example.json +96 -0
- package/contracts/change-proof/hunch.change-proof.v1.schema.json +215 -0
- package/dist/changeProof.d.ts +1 -0
- package/dist/changeProof.js +2 -0
- package/dist/cli/index.js +37 -0
- package/dist/core/changeProof.js +512 -0
- package/dist/core/changeProofContract.d.ts +164 -0
- package/dist/core/changeProofContract.js +253 -0
- package/dist/core/projectDnaOutcomeExperience.d.ts +105 -0
- package/dist/core/projectDnaOutcomeExperience.js +271 -0
- package/dist/mcp/server.js +28 -0
- package/dist/projectDna.d.ts +1 -0
- package/dist/projectDna.js +1 -0
- package/package.json +8 -1
- package/server.json +2 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { compareCodeUnits } from "./canonicalOrder.js";
|
|
4
|
+
export const CHANGE_PROOF_SCHEMA_VERSION = "hunch.change-proof/1";
|
|
5
|
+
export const CHANGE_PROOF_ALGORITHM = "hunch-change-proof-sha256/1";
|
|
6
|
+
export const CHANGE_PROOF_VERDICTS = ["fail", "pass", "unknown"];
|
|
7
|
+
export const CHANGE_PROOF_RELEVANCE = ["blast_radius", "changed_path", "conformance", "guard"];
|
|
8
|
+
const GIT_OBJECT = /^[a-f0-9]{40,64}$/;
|
|
9
|
+
const SHA1 = /^sha1:[a-f0-9]{40}$/;
|
|
10
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
11
|
+
const CHANGE_ID = /^hchg_[a-f0-9]{24}$/;
|
|
12
|
+
const PROOF_ID = /^hproof_[a-f0-9]{24}$/;
|
|
13
|
+
const PROFILE_ID = /^pdna_[a-f0-9]{24}$/;
|
|
14
|
+
const REPOSITORY_ID = /^pdnar_[a-f0-9]{24}$/;
|
|
15
|
+
const RepoPathSchema = z.string().min(1).max(4_096).refine((path) => {
|
|
16
|
+
if (path.includes("\0") || path.includes("\\") || path.startsWith("/") || /^[A-Za-z]:/.test(path))
|
|
17
|
+
return false;
|
|
18
|
+
return !path.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
|
|
19
|
+
}, "repository path must be canonical and relative");
|
|
20
|
+
const HashGapSchema = z.object({
|
|
21
|
+
code: z.string().regex(/^[a-z][a-z0-9_.-]{2,127}$/),
|
|
22
|
+
count: z.number().int().positive().max(1_000_000),
|
|
23
|
+
evidence_hash: z.string().regex(SHA256),
|
|
24
|
+
}).strict();
|
|
25
|
+
const ChangeIdentitySchema = z.object({
|
|
26
|
+
schema: z.literal("hunch.change-identity/1"),
|
|
27
|
+
algorithm: z.literal("git-raw-tree-delta-sha256/1"),
|
|
28
|
+
change_id: z.string().regex(CHANGE_ID),
|
|
29
|
+
base_revision: z.string().regex(GIT_OBJECT),
|
|
30
|
+
head_revision: z.string().regex(GIT_OBJECT),
|
|
31
|
+
base_tree: z.string().regex(GIT_OBJECT),
|
|
32
|
+
head_tree: z.string().regex(GIT_OBJECT),
|
|
33
|
+
delta_hash: z.string().regex(SHA256),
|
|
34
|
+
patch_id: z.string().regex(GIT_OBJECT).nullable(),
|
|
35
|
+
file_count: z.number().int().positive().max(16_384),
|
|
36
|
+
paths_hash: z.string().regex(SHA256),
|
|
37
|
+
content_hash: z.string().regex(SHA256),
|
|
38
|
+
}).strict();
|
|
39
|
+
const GraphSealSchema = z.object({
|
|
40
|
+
source: z.literal("commit"),
|
|
41
|
+
revision: z.string().regex(GIT_OBJECT),
|
|
42
|
+
source_hash: z.string().regex(SHA1),
|
|
43
|
+
topology_hash: z.string().regex(SHA256),
|
|
44
|
+
files: z.number().int().nonnegative().max(1_000_000),
|
|
45
|
+
symbols: z.number().int().nonnegative().max(10_000_000),
|
|
46
|
+
edges: z.number().int().nonnegative().max(50_000_000),
|
|
47
|
+
components: z.number().int().nonnegative().max(1_000_000),
|
|
48
|
+
issue_count: z.number().int().nonnegative().max(1_000_000),
|
|
49
|
+
}).strict();
|
|
50
|
+
const BlastEntrySchema = z.object({
|
|
51
|
+
source_path: RepoPathSchema,
|
|
52
|
+
dependent_path: RepoPathSchema,
|
|
53
|
+
depth: z.number().int().min(1).max(4),
|
|
54
|
+
graphs: z.array(z.enum(["base", "result"])).min(1).max(2),
|
|
55
|
+
}).strict();
|
|
56
|
+
const DecisionRefSchema = z.object({
|
|
57
|
+
id: z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/),
|
|
58
|
+
record_hash: z.string().regex(SHA256),
|
|
59
|
+
relevance: z.array(z.enum(CHANGE_PROOF_RELEVANCE)).min(1).max(4),
|
|
60
|
+
paths: z.array(RepoPathSchema).max(128),
|
|
61
|
+
path_count: z.number().int().nonnegative().max(1_000_000),
|
|
62
|
+
paths_hash: z.string().regex(SHA256),
|
|
63
|
+
}).strict();
|
|
64
|
+
const ConstraintRefSchema = z.object({
|
|
65
|
+
id: z.string().regex(/^con_[A-Za-z0-9_.-]{3,}$/),
|
|
66
|
+
record_hash: z.string().regex(SHA256),
|
|
67
|
+
severity: z.enum(["advisory", "warning", "blocking"]),
|
|
68
|
+
relevance: z.array(z.enum(["blast_radius", "changed_path"])).min(1).max(2),
|
|
69
|
+
paths: z.array(RepoPathSchema).max(128),
|
|
70
|
+
path_count: z.number().int().nonnegative().max(1_000_000),
|
|
71
|
+
paths_hash: z.string().regex(SHA256),
|
|
72
|
+
}).strict();
|
|
73
|
+
const ConformanceReceiptSchema = z.object({
|
|
74
|
+
decision_id: z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/),
|
|
75
|
+
predicate_index: z.number().int().nonnegative().max(1_000_000),
|
|
76
|
+
predicate_hash: z.string().regex(SHA256),
|
|
77
|
+
satisfied: z.boolean(),
|
|
78
|
+
detail_hash: z.string().regex(SHA256),
|
|
79
|
+
}).strict();
|
|
80
|
+
export const ChangeProofSchema = z.object({
|
|
81
|
+
schema: z.literal(CHANGE_PROOF_SCHEMA_VERSION),
|
|
82
|
+
algorithm: z.literal(CHANGE_PROOF_ALGORITHM),
|
|
83
|
+
proof_id: z.string().regex(PROOF_ID),
|
|
84
|
+
engine: z.object({
|
|
85
|
+
package: z.literal("@davesheffer/hunch"),
|
|
86
|
+
version: z.string().min(1).max(64),
|
|
87
|
+
}).strict(),
|
|
88
|
+
repository: z.object({
|
|
89
|
+
repository_id: z.string().regex(REPOSITORY_ID),
|
|
90
|
+
base_revision: z.string().regex(GIT_OBJECT),
|
|
91
|
+
result_revision: z.string().regex(GIT_OBJECT),
|
|
92
|
+
}).strict(),
|
|
93
|
+
change: ChangeIdentitySchema,
|
|
94
|
+
project_dna: z.object({
|
|
95
|
+
schema: z.literal("hunch.project-dna/1"),
|
|
96
|
+
profile_id: z.string().regex(PROFILE_ID),
|
|
97
|
+
repository_id: z.string().regex(REPOSITORY_ID),
|
|
98
|
+
repository_revision: z.string().regex(GIT_OBJECT),
|
|
99
|
+
content_hash: z.string().regex(SHA256),
|
|
100
|
+
trait_ids: z.array(z.string().regex(/^pdnat_[a-f0-9]{20}$/)).max(64),
|
|
101
|
+
}).strict(),
|
|
102
|
+
graph: z.object({
|
|
103
|
+
base: GraphSealSchema,
|
|
104
|
+
result: GraphSealSchema,
|
|
105
|
+
}).strict(),
|
|
106
|
+
changed_files: z.array(RepoPathSchema).max(2_048),
|
|
107
|
+
changed_file_count: z.number().int().positive().max(16_384),
|
|
108
|
+
blast_radius: z.array(BlastEntrySchema).max(4_096),
|
|
109
|
+
blast_radius_count: z.number().int().nonnegative().max(10_000_000),
|
|
110
|
+
decisions: z.array(DecisionRefSchema).max(1_024),
|
|
111
|
+
decision_count: z.number().int().nonnegative().max(1_000_000),
|
|
112
|
+
constraints: z.array(ConstraintRefSchema).max(1_024),
|
|
113
|
+
constraint_count: z.number().int().nonnegative().max(1_000_000),
|
|
114
|
+
conformance: z.array(ConformanceReceiptSchema).max(1_024),
|
|
115
|
+
conformance_count: z.number().int().nonnegative().max(1_000_000),
|
|
116
|
+
guard: z.object({
|
|
117
|
+
verdict: z.enum(["pass", "fail"]),
|
|
118
|
+
strict_blocker_ids: z.array(z.string().min(1).max(256)).max(2_048),
|
|
119
|
+
regression_decision_ids: z.array(z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/)).max(1_024),
|
|
120
|
+
veto_decision_ids: z.array(z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/)).max(1_024),
|
|
121
|
+
report_hash: z.string().regex(SHA256),
|
|
122
|
+
}).strict(),
|
|
123
|
+
memory: z.object({
|
|
124
|
+
scope: z.enum(["public", "union"]),
|
|
125
|
+
records_hash: z.string().regex(SHA256),
|
|
126
|
+
}).strict(),
|
|
127
|
+
omissions: z.array(HashGapSchema).max(64),
|
|
128
|
+
unknowns: z.array(HashGapSchema).max(64),
|
|
129
|
+
verdict: z.enum(CHANGE_PROOF_VERDICTS),
|
|
130
|
+
authority: z.object({
|
|
131
|
+
execution: z.literal(false),
|
|
132
|
+
ci: z.literal(false),
|
|
133
|
+
deployment: z.literal(false),
|
|
134
|
+
merge: z.literal(false),
|
|
135
|
+
ranking: z.literal(false),
|
|
136
|
+
promotion: z.literal(false),
|
|
137
|
+
policy: z.literal(false),
|
|
138
|
+
}).strict(),
|
|
139
|
+
content_hash: z.string().regex(SHA256),
|
|
140
|
+
}).strict();
|
|
141
|
+
export function canonicalChangeProofJson(value) {
|
|
142
|
+
if (Array.isArray(value))
|
|
143
|
+
return `[${value.map(canonicalChangeProofJson).join(",")}]`;
|
|
144
|
+
if (value && typeof value === "object") {
|
|
145
|
+
return `{${Object.entries(value)
|
|
146
|
+
.filter(([, child]) => child !== undefined)
|
|
147
|
+
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
148
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalChangeProofJson(child)}`)
|
|
149
|
+
.join(",")}}`;
|
|
150
|
+
}
|
|
151
|
+
return JSON.stringify(value) ?? "null";
|
|
152
|
+
}
|
|
153
|
+
export function changeProofHash(value) {
|
|
154
|
+
return `sha256:${createHash("sha256").update(canonicalChangeProofJson(value)).digest("hex")}`;
|
|
155
|
+
}
|
|
156
|
+
function canonicalArray(values) {
|
|
157
|
+
const rendered = values.map(canonicalChangeProofJson);
|
|
158
|
+
return rendered.every((value, index) => index === 0 || compareCodeUnits(rendered[index - 1], value) < 0);
|
|
159
|
+
}
|
|
160
|
+
function expectedChangeId(change) {
|
|
161
|
+
return `hchg_${changeProofHash({ algorithm: change.algorithm, delta_hash: change.delta_hash })
|
|
162
|
+
.slice("sha256:".length, "sha256:".length + 24)}`;
|
|
163
|
+
}
|
|
164
|
+
export function sealChangeProof(unsigned) {
|
|
165
|
+
const proofId = `hproof_${changeProofHash(unsigned).slice("sha256:".length, "sha256:".length + 24)}`;
|
|
166
|
+
const sealed = { ...unsigned, proof_id: proofId };
|
|
167
|
+
const proof = { ...sealed, content_hash: changeProofHash(sealed) };
|
|
168
|
+
assertChangeProof(proof);
|
|
169
|
+
return proof;
|
|
170
|
+
}
|
|
171
|
+
export function assertChangeProof(value) {
|
|
172
|
+
const proof = ChangeProofSchema.parse(value);
|
|
173
|
+
const sortedArrays = [
|
|
174
|
+
proof.changed_files,
|
|
175
|
+
proof.blast_radius,
|
|
176
|
+
proof.decisions,
|
|
177
|
+
proof.constraints,
|
|
178
|
+
proof.conformance,
|
|
179
|
+
proof.guard.strict_blocker_ids,
|
|
180
|
+
proof.guard.regression_decision_ids,
|
|
181
|
+
proof.guard.veto_decision_ids,
|
|
182
|
+
proof.omissions,
|
|
183
|
+
proof.unknowns,
|
|
184
|
+
proof.project_dna.trait_ids,
|
|
185
|
+
];
|
|
186
|
+
if (sortedArrays.some((items) => !canonicalArray(items)))
|
|
187
|
+
throw new Error("change proof collections must be unique and canonically ordered");
|
|
188
|
+
for (const decision of proof.decisions) {
|
|
189
|
+
if (!canonicalArray(decision.relevance) || !canonicalArray(decision.paths)
|
|
190
|
+
|| decision.path_count < decision.paths.length
|
|
191
|
+
|| (decision.path_count === decision.paths.length && decision.paths_hash !== changeProofHash(decision.paths))) {
|
|
192
|
+
throw new Error("change proof decision reference is non-canonical");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const constraint of proof.constraints) {
|
|
196
|
+
if (!canonicalArray(constraint.relevance) || !canonicalArray(constraint.paths)
|
|
197
|
+
|| constraint.path_count < constraint.paths.length
|
|
198
|
+
|| (constraint.path_count === constraint.paths.length && constraint.paths_hash !== changeProofHash(constraint.paths))) {
|
|
199
|
+
throw new Error("change proof constraint reference is non-canonical");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const blast of proof.blast_radius) {
|
|
203
|
+
if (!canonicalArray(blast.graphs) || blast.source_path === blast.dependent_path) {
|
|
204
|
+
throw new Error("change proof blast radius is non-canonical");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const conformanceKeys = new Set();
|
|
208
|
+
for (const receipt of proof.conformance) {
|
|
209
|
+
const key = `${receipt.decision_id}\0${receipt.predicate_index}`;
|
|
210
|
+
if (conformanceKeys.has(key))
|
|
211
|
+
throw new Error("change proof conformance predicate identity is duplicated");
|
|
212
|
+
conformanceKeys.add(key);
|
|
213
|
+
}
|
|
214
|
+
const changeUnsigned = (({ content_hash: _contentHash, ...rest }) => rest)(proof.change);
|
|
215
|
+
if (proof.change.change_id !== expectedChangeId(proof.change)
|
|
216
|
+
|| proof.change.content_hash !== changeProofHash(changeUnsigned)) {
|
|
217
|
+
throw new Error("change proof change identity seal is invalid");
|
|
218
|
+
}
|
|
219
|
+
if (proof.repository.base_revision !== proof.change.base_revision
|
|
220
|
+
|| proof.repository.result_revision !== proof.change.head_revision
|
|
221
|
+
|| proof.project_dna.repository_id !== proof.repository.repository_id
|
|
222
|
+
|| proof.project_dna.repository_revision !== proof.repository.result_revision
|
|
223
|
+
|| proof.graph.base.revision !== proof.repository.base_revision
|
|
224
|
+
|| proof.graph.result.revision !== proof.repository.result_revision
|
|
225
|
+
|| proof.changed_file_count !== proof.change.file_count
|
|
226
|
+
|| proof.changed_file_count < proof.changed_files.length
|
|
227
|
+
|| proof.blast_radius_count < proof.blast_radius.length
|
|
228
|
+
|| proof.decision_count < proof.decisions.length
|
|
229
|
+
|| proof.constraint_count < proof.constraints.length
|
|
230
|
+
|| proof.conformance_count < proof.conformance.length) {
|
|
231
|
+
throw new Error("change proof exact-revision or count binding is invalid");
|
|
232
|
+
}
|
|
233
|
+
const recordsHash = changeProofHash({ decisions: proof.decisions, constraints: proof.constraints });
|
|
234
|
+
if (proof.memory.records_hash !== recordsHash)
|
|
235
|
+
throw new Error("change proof memory seal is invalid");
|
|
236
|
+
const guardFails = proof.guard.strict_blocker_ids.length > 0;
|
|
237
|
+
if (proof.guard.verdict !== (guardFails ? "fail" : "pass"))
|
|
238
|
+
throw new Error("change proof guard verdict is invalid");
|
|
239
|
+
const expectedVerdict = guardFails || proof.conformance.some((receipt) => !receipt.satisfied)
|
|
240
|
+
? "fail"
|
|
241
|
+
: proof.omissions.length || proof.unknowns.length
|
|
242
|
+
? "unknown"
|
|
243
|
+
: "pass";
|
|
244
|
+
if (proof.verdict !== expectedVerdict)
|
|
245
|
+
throw new Error("change proof verdict is invalid");
|
|
246
|
+
const { proof_id: _proofId, content_hash: _proofHash, ...unsigned } = proof;
|
|
247
|
+
const expectedProofId = `hproof_${changeProofHash(unsigned).slice("sha256:".length, "sha256:".length + 24)}`;
|
|
248
|
+
const sealed = { ...unsigned, proof_id: proof.proof_id };
|
|
249
|
+
if (proof.proof_id !== expectedProofId || proof.content_hash !== changeProofHash(sealed)) {
|
|
250
|
+
throw new Error("change proof seal is invalid");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=changeProofContract.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Finding } from "./types.js";
|
|
3
|
+
export declare const PROJECT_DNA_USEFULNESS_OBSERVATION_SCHEMA_VERSION: "hunch.project-dna-usefulness-observation/1";
|
|
4
|
+
export declare const PROJECT_DNA_USEFULNESS_SIGNALS: readonly ["used", "prevented", "near_miss", "contradicted", "stale", "unused", "unknown"];
|
|
5
|
+
export declare const PROJECT_DNA_USEFULNESS_EVIDENCE_KINDS: readonly ["explicit_human_observation", "independent_review"];
|
|
6
|
+
export declare const ProjectDnaUsefulnessObservationSchema: z.ZodObject<{
|
|
7
|
+
schema: z.ZodLiteral<"hunch.project-dna-usefulness-observation/1">;
|
|
8
|
+
observationId: z.ZodString;
|
|
9
|
+
episode: z.ZodObject<{
|
|
10
|
+
provider: z.ZodString;
|
|
11
|
+
schemaVersion: z.ZodString;
|
|
12
|
+
episodeId: z.ZodString;
|
|
13
|
+
episodeHash: z.ZodString;
|
|
14
|
+
terminalAt: z.ZodString;
|
|
15
|
+
result: z.ZodEnum<{
|
|
16
|
+
fail: "fail";
|
|
17
|
+
pass: "pass";
|
|
18
|
+
uncertain: "uncertain";
|
|
19
|
+
abandoned: "abandoned";
|
|
20
|
+
rolled_back: "rolled_back";
|
|
21
|
+
}>;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
delivery: z.ZodObject<{
|
|
24
|
+
receiptRef: z.ZodString;
|
|
25
|
+
receiptHash: z.ZodString;
|
|
26
|
+
repositoryId: z.ZodString;
|
|
27
|
+
repositoryRevision: z.ZodString;
|
|
28
|
+
profileId: z.ZodString;
|
|
29
|
+
profileContentHash: z.ZodString;
|
|
30
|
+
snapshotHash: z.ZodString;
|
|
31
|
+
retrievalHash: z.ZodString;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
projection: z.ZodObject<{
|
|
34
|
+
role: z.ZodString;
|
|
35
|
+
categories: z.ZodArray<z.ZodEnum<{
|
|
36
|
+
communication: "communication";
|
|
37
|
+
engineering: "engineering";
|
|
38
|
+
review: "review";
|
|
39
|
+
culture: "culture";
|
|
40
|
+
vocabulary: "vocabulary";
|
|
41
|
+
}>>;
|
|
42
|
+
traitIds: z.ZodArray<z.ZodString>;
|
|
43
|
+
evidenceHashes: z.ZodArray<z.ZodString>;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
artifact: z.ZodObject<{
|
|
46
|
+
kind: z.ZodEnum<{
|
|
47
|
+
message: "message";
|
|
48
|
+
commit: "commit";
|
|
49
|
+
pull_request: "pull_request";
|
|
50
|
+
issue: "issue";
|
|
51
|
+
}>;
|
|
52
|
+
ref: z.ZodString;
|
|
53
|
+
contentHash: z.ZodString;
|
|
54
|
+
}, z.core.$strict>;
|
|
55
|
+
assessment: z.ZodObject<{
|
|
56
|
+
schemaVersion: z.ZodString;
|
|
57
|
+
assessmentId: z.ZodString;
|
|
58
|
+
contentHash: z.ZodString;
|
|
59
|
+
projectMatchClassification: z.ZodEnum<{
|
|
60
|
+
unassessable: "unassessable";
|
|
61
|
+
conformant: "conformant";
|
|
62
|
+
mixed: "mixed";
|
|
63
|
+
nonconformant: "nonconformant";
|
|
64
|
+
}>;
|
|
65
|
+
causalInterpretation: z.ZodLiteral<"project_match_is_non_causal">;
|
|
66
|
+
}, z.core.$strict>;
|
|
67
|
+
signal: z.ZodEnum<{
|
|
68
|
+
unknown: "unknown";
|
|
69
|
+
stale: "stale";
|
|
70
|
+
used: "used";
|
|
71
|
+
prevented: "prevented";
|
|
72
|
+
near_miss: "near_miss";
|
|
73
|
+
contradicted: "contradicted";
|
|
74
|
+
unused: "unused";
|
|
75
|
+
}>;
|
|
76
|
+
evidence: z.ZodArray<z.ZodObject<{
|
|
77
|
+
kind: z.ZodEnum<{
|
|
78
|
+
explicit_human_observation: "explicit_human_observation";
|
|
79
|
+
independent_review: "independent_review";
|
|
80
|
+
}>;
|
|
81
|
+
ref: z.ZodString;
|
|
82
|
+
hash: z.ZodString;
|
|
83
|
+
}, z.core.$strict>>;
|
|
84
|
+
observedAt: z.ZodString;
|
|
85
|
+
retainUntil: z.ZodString;
|
|
86
|
+
privacy: z.ZodObject<{
|
|
87
|
+
payloadMode: z.ZodLiteral<"references_hashes_only">;
|
|
88
|
+
rawArtifactIncluded: z.ZodLiteral<false>;
|
|
89
|
+
rawFeedbackIncluded: z.ZodLiteral<false>;
|
|
90
|
+
rawProfileIncluded: z.ZodLiteral<false>;
|
|
91
|
+
}, z.core.$strict>;
|
|
92
|
+
authority: z.ZodObject<{
|
|
93
|
+
behavioralEffect: z.ZodLiteral<"none">;
|
|
94
|
+
mayChangeRanking: z.ZodLiteral<false>;
|
|
95
|
+
mayPromoteKnowledge: z.ZodLiteral<false>;
|
|
96
|
+
mayGrantAuthority: z.ZodLiteral<false>;
|
|
97
|
+
}, z.core.$strict>;
|
|
98
|
+
contentHash: z.ZodString;
|
|
99
|
+
}, z.core.$strict>;
|
|
100
|
+
export type ProjectDnaUsefulnessObservation = z.infer<typeof ProjectDnaUsefulnessObservationSchema>;
|
|
101
|
+
export type CreateProjectDnaUsefulnessObservationInput = Omit<ProjectDnaUsefulnessObservation, "schema" | "observationId" | "authority" | "contentHash">;
|
|
102
|
+
export declare function createProjectDnaUsefulnessObservation(input: CreateProjectDnaUsefulnessObservationInput): ProjectDnaUsefulnessObservation;
|
|
103
|
+
export declare function assertProjectDnaUsefulnessObservation(value: unknown): asserts value is ProjectDnaUsefulnessObservation;
|
|
104
|
+
/** Contradiction and staleness create review work, never a profile mutation or policy. */
|
|
105
|
+
export declare function projectDnaUsefulnessObservationFinding(value: unknown): Finding | null;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { findingId } from "./ids.js";
|
|
4
|
+
import { PROJECT_DNA_CATEGORIES, } from "./projectDna.js";
|
|
5
|
+
import { FindingSchema, isCredentialFreeText } from "./types.js";
|
|
6
|
+
export const PROJECT_DNA_USEFULNESS_OBSERVATION_SCHEMA_VERSION = "hunch.project-dna-usefulness-observation/1";
|
|
7
|
+
export const PROJECT_DNA_USEFULNESS_SIGNALS = [
|
|
8
|
+
"used",
|
|
9
|
+
"prevented",
|
|
10
|
+
"near_miss",
|
|
11
|
+
"contradicted",
|
|
12
|
+
"stale",
|
|
13
|
+
"unused",
|
|
14
|
+
"unknown",
|
|
15
|
+
];
|
|
16
|
+
export const PROJECT_DNA_USEFULNESS_EVIDENCE_KINDS = [
|
|
17
|
+
"explicit_human_observation",
|
|
18
|
+
"independent_review",
|
|
19
|
+
];
|
|
20
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
21
|
+
const GIT_OBJECT = /^[a-f0-9]{40,64}$/;
|
|
22
|
+
const RECEIPT_REF = /^hunch-memory:hmctx_[a-f0-9]{32}$/;
|
|
23
|
+
const REPOSITORY_ID = /^pdnar_[a-f0-9]{24}$/;
|
|
24
|
+
const PROFILE_ID = /^pdna_[a-f0-9]{24}$/;
|
|
25
|
+
const TRAIT_ID = /^pdnat_[a-f0-9]{20}$/;
|
|
26
|
+
const OBSERVATION_ID = /^pduo_[a-f0-9]{24}$/;
|
|
27
|
+
const MAX_RETENTION_MS = 365 * 24 * 60 * 60 * 1_000;
|
|
28
|
+
const ProjectDnaUsefulnessEvidenceSchema = z.object({
|
|
29
|
+
kind: z.enum(PROJECT_DNA_USEFULNESS_EVIDENCE_KINDS),
|
|
30
|
+
ref: z.string().min(1).max(512),
|
|
31
|
+
hash: z.string().regex(SHA256),
|
|
32
|
+
}).strict();
|
|
33
|
+
export const ProjectDnaUsefulnessObservationSchema = z.object({
|
|
34
|
+
schema: z.literal(PROJECT_DNA_USEFULNESS_OBSERVATION_SCHEMA_VERSION),
|
|
35
|
+
observationId: z.string().regex(OBSERVATION_ID),
|
|
36
|
+
episode: z.object({
|
|
37
|
+
provider: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
|
|
38
|
+
schemaVersion: z.string().min(3).max(128),
|
|
39
|
+
episodeId: z.string().min(3).max(256),
|
|
40
|
+
episodeHash: z.string().regex(SHA256),
|
|
41
|
+
terminalAt: z.string().min(1).max(64),
|
|
42
|
+
result: z.enum(["pass", "fail", "uncertain", "abandoned", "rolled_back"]),
|
|
43
|
+
}).strict(),
|
|
44
|
+
delivery: z.object({
|
|
45
|
+
receiptRef: z.string().regex(RECEIPT_REF),
|
|
46
|
+
receiptHash: z.string().regex(SHA256),
|
|
47
|
+
repositoryId: z.string().regex(REPOSITORY_ID),
|
|
48
|
+
repositoryRevision: z.string().regex(GIT_OBJECT),
|
|
49
|
+
profileId: z.string().regex(PROFILE_ID),
|
|
50
|
+
profileContentHash: z.string().regex(SHA256),
|
|
51
|
+
snapshotHash: z.string().regex(SHA256),
|
|
52
|
+
retrievalHash: z.string().regex(SHA256),
|
|
53
|
+
}).strict(),
|
|
54
|
+
projection: z.object({
|
|
55
|
+
role: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
|
|
56
|
+
categories: z.array(z.enum(PROJECT_DNA_CATEGORIES)).min(1).max(PROJECT_DNA_CATEGORIES.length),
|
|
57
|
+
traitIds: z.array(z.string().regex(TRAIT_ID)).min(1).max(64),
|
|
58
|
+
evidenceHashes: z.array(z.string().regex(SHA256)).min(1).max(512),
|
|
59
|
+
}).strict(),
|
|
60
|
+
artifact: z.object({
|
|
61
|
+
kind: z.enum(["commit", "pull_request", "issue", "message"]),
|
|
62
|
+
ref: z.string().min(3).max(512),
|
|
63
|
+
contentHash: z.string().regex(SHA256),
|
|
64
|
+
}).strict(),
|
|
65
|
+
assessment: z.object({
|
|
66
|
+
schemaVersion: z.string().min(3).max(128),
|
|
67
|
+
assessmentId: z.string().min(3).max(128),
|
|
68
|
+
contentHash: z.string().regex(SHA256),
|
|
69
|
+
projectMatchClassification: z.enum(["unassessable", "conformant", "mixed", "nonconformant"]),
|
|
70
|
+
causalInterpretation: z.literal("project_match_is_non_causal"),
|
|
71
|
+
}).strict(),
|
|
72
|
+
signal: z.enum(PROJECT_DNA_USEFULNESS_SIGNALS),
|
|
73
|
+
evidence: z.array(ProjectDnaUsefulnessEvidenceSchema).max(64),
|
|
74
|
+
observedAt: z.string().min(1).max(64),
|
|
75
|
+
retainUntil: z.string().min(1).max(64),
|
|
76
|
+
privacy: z.object({
|
|
77
|
+
payloadMode: z.literal("references_hashes_only"),
|
|
78
|
+
rawArtifactIncluded: z.literal(false),
|
|
79
|
+
rawFeedbackIncluded: z.literal(false),
|
|
80
|
+
rawProfileIncluded: z.literal(false),
|
|
81
|
+
}).strict(),
|
|
82
|
+
authority: z.object({
|
|
83
|
+
behavioralEffect: z.literal("none"),
|
|
84
|
+
mayChangeRanking: z.literal(false),
|
|
85
|
+
mayPromoteKnowledge: z.literal(false),
|
|
86
|
+
mayGrantAuthority: z.literal(false),
|
|
87
|
+
}).strict(),
|
|
88
|
+
contentHash: z.string().regex(SHA256),
|
|
89
|
+
}).strict().superRefine((observation, ctx) => {
|
|
90
|
+
for (const [path, value] of [
|
|
91
|
+
[["episode", "terminalAt"], observation.episode.terminalAt],
|
|
92
|
+
[["observedAt"], observation.observedAt],
|
|
93
|
+
[["retainUntil"], observation.retainUntil],
|
|
94
|
+
]) {
|
|
95
|
+
if (!Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value) {
|
|
96
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path], message: "Project DNA usefulness timestamp is invalid" });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const observedAt = Date.parse(observation.observedAt);
|
|
100
|
+
const terminalAt = Date.parse(observation.episode.terminalAt);
|
|
101
|
+
const retainUntil = Date.parse(observation.retainUntil);
|
|
102
|
+
if (Number.isFinite(observedAt) && Number.isFinite(terminalAt) && observedAt < terminalAt) {
|
|
103
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["observedAt"], message: "Project DNA usefulness cannot precede the terminal outcome" });
|
|
104
|
+
}
|
|
105
|
+
if (Number.isFinite(observedAt) && Number.isFinite(retainUntil)
|
|
106
|
+
&& (retainUntil <= observedAt || retainUntil - observedAt > MAX_RETENTION_MS)) {
|
|
107
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["retainUntil"], message: "Project DNA usefulness retention is invalid" });
|
|
108
|
+
}
|
|
109
|
+
if (observation.signal !== "unknown" && observation.evidence.length === 0) {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: z.ZodIssueCode.custom,
|
|
112
|
+
path: ["evidence"],
|
|
113
|
+
message: "classified Project DNA usefulness requires explicit human or independent review evidence",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const identities = new Set();
|
|
117
|
+
for (const [index, evidence] of observation.evidence.entries()) {
|
|
118
|
+
const identity = `${evidence.kind}:${evidence.ref}:${evidence.hash}`;
|
|
119
|
+
if (identities.has(identity)) {
|
|
120
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["evidence", index], message: "Project DNA usefulness evidence is duplicated" });
|
|
121
|
+
}
|
|
122
|
+
identities.add(identity);
|
|
123
|
+
if (!isCredentialFreeText(evidence.ref)) {
|
|
124
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["evidence", index, "ref"], message: "Project DNA usefulness evidence contains credential material" });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const categories = observation.projection.categories;
|
|
128
|
+
if (new Set(categories).size !== categories.length
|
|
129
|
+
|| new Set(observation.projection.traitIds).size !== observation.projection.traitIds.length
|
|
130
|
+
|| new Set(observation.projection.evidenceHashes).size !== observation.projection.evidenceHashes.length) {
|
|
131
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["projection"], message: "Project DNA usefulness projection contains duplicates" });
|
|
132
|
+
}
|
|
133
|
+
for (const [path, value] of [
|
|
134
|
+
[["episode", "schemaVersion"], observation.episode.schemaVersion],
|
|
135
|
+
[["episode", "episodeId"], observation.episode.episodeId],
|
|
136
|
+
[["projection", "role"], observation.projection.role],
|
|
137
|
+
[["artifact", "ref"], observation.artifact.ref],
|
|
138
|
+
[["assessment", "schemaVersion"], observation.assessment.schemaVersion],
|
|
139
|
+
[["assessment", "assessmentId"], observation.assessment.assessmentId],
|
|
140
|
+
]) {
|
|
141
|
+
if (!isCredentialFreeText(value)) {
|
|
142
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path], message: "Project DNA usefulness identity contains credential material" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const unsigned = projectDnaUsefulnessUnsigned(observation);
|
|
146
|
+
if (observation.contentHash !== projectDnaUsefulnessHash(unsigned)
|
|
147
|
+
|| observation.observationId !== projectDnaUsefulnessObservationId(observation)) {
|
|
148
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Project DNA usefulness observation seal is invalid" });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
function canonicalJson(value) {
|
|
152
|
+
if (Array.isArray(value))
|
|
153
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
154
|
+
if (value && typeof value === "object") {
|
|
155
|
+
return `{${Object.entries(value)
|
|
156
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
157
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
158
|
+
.join(",")}}`;
|
|
159
|
+
}
|
|
160
|
+
return JSON.stringify(value) ?? "null";
|
|
161
|
+
}
|
|
162
|
+
function projectDnaUsefulnessHash(value) {
|
|
163
|
+
return `sha256:${createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
|
|
164
|
+
}
|
|
165
|
+
function projectDnaUsefulnessObservationId(observation) {
|
|
166
|
+
const identityHash = projectDnaUsefulnessHash({
|
|
167
|
+
episodeId: observation.episode.episodeId,
|
|
168
|
+
receiptRef: observation.delivery.receiptRef,
|
|
169
|
+
receiptHash: observation.delivery.receiptHash,
|
|
170
|
+
profileId: observation.delivery.profileId,
|
|
171
|
+
artifactRef: observation.artifact.ref,
|
|
172
|
+
artifactContentHash: observation.artifact.contentHash,
|
|
173
|
+
});
|
|
174
|
+
return `pduo_${identityHash.slice(7, 31)}`;
|
|
175
|
+
}
|
|
176
|
+
function projectDnaUsefulnessUnsigned(observation) {
|
|
177
|
+
return {
|
|
178
|
+
schema: observation.schema,
|
|
179
|
+
episode: { ...observation.episode },
|
|
180
|
+
delivery: { ...observation.delivery },
|
|
181
|
+
projection: {
|
|
182
|
+
role: observation.projection.role,
|
|
183
|
+
categories: [...observation.projection.categories],
|
|
184
|
+
traitIds: [...observation.projection.traitIds],
|
|
185
|
+
evidenceHashes: [...observation.projection.evidenceHashes],
|
|
186
|
+
},
|
|
187
|
+
artifact: { ...observation.artifact },
|
|
188
|
+
assessment: { ...observation.assessment },
|
|
189
|
+
signal: observation.signal,
|
|
190
|
+
evidence: observation.evidence.map((item) => ({ ...item })),
|
|
191
|
+
observedAt: observation.observedAt,
|
|
192
|
+
retainUntil: observation.retainUntil,
|
|
193
|
+
privacy: { ...observation.privacy },
|
|
194
|
+
authority: { ...observation.authority },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export function createProjectDnaUsefulnessObservation(input) {
|
|
198
|
+
const unsigned = projectDnaUsefulnessUnsigned({
|
|
199
|
+
schema: PROJECT_DNA_USEFULNESS_OBSERVATION_SCHEMA_VERSION,
|
|
200
|
+
episode: { ...input.episode },
|
|
201
|
+
delivery: { ...input.delivery },
|
|
202
|
+
projection: {
|
|
203
|
+
role: input.projection.role,
|
|
204
|
+
categories: [...input.projection.categories],
|
|
205
|
+
traitIds: [...input.projection.traitIds],
|
|
206
|
+
evidenceHashes: [...input.projection.evidenceHashes],
|
|
207
|
+
},
|
|
208
|
+
artifact: { ...input.artifact },
|
|
209
|
+
assessment: { ...input.assessment },
|
|
210
|
+
signal: input.signal,
|
|
211
|
+
evidence: input.evidence.map((item) => ({ ...item })),
|
|
212
|
+
observedAt: input.observedAt,
|
|
213
|
+
retainUntil: input.retainUntil,
|
|
214
|
+
privacy: { ...input.privacy },
|
|
215
|
+
authority: {
|
|
216
|
+
behavioralEffect: "none",
|
|
217
|
+
mayChangeRanking: false,
|
|
218
|
+
mayPromoteKnowledge: false,
|
|
219
|
+
mayGrantAuthority: false,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return ProjectDnaUsefulnessObservationSchema.parse({
|
|
223
|
+
...unsigned,
|
|
224
|
+
observationId: projectDnaUsefulnessObservationId(unsigned),
|
|
225
|
+
contentHash: projectDnaUsefulnessHash(unsigned),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
export function assertProjectDnaUsefulnessObservation(value) {
|
|
229
|
+
ProjectDnaUsefulnessObservationSchema.parse(value);
|
|
230
|
+
}
|
|
231
|
+
/** Contradiction and staleness create review work, never a profile mutation or policy. */
|
|
232
|
+
export function projectDnaUsefulnessObservationFinding(value) {
|
|
233
|
+
const observation = ProjectDnaUsefulnessObservationSchema.parse(value);
|
|
234
|
+
if (observation.signal !== "contradicted" && observation.signal !== "stale")
|
|
235
|
+
return null;
|
|
236
|
+
const title = `Project DNA outcome ${observation.signal}: ${observation.delivery.profileId}`;
|
|
237
|
+
const evidence = [
|
|
238
|
+
`project-dna-usefulness:${observation.observationId}`,
|
|
239
|
+
`project-dna-usefulness-content:${observation.contentHash}`,
|
|
240
|
+
`episode:${observation.episode.episodeId}@${observation.episode.episodeHash}`,
|
|
241
|
+
`delivery:${observation.delivery.receiptRef}@${observation.delivery.receiptHash}`,
|
|
242
|
+
`profile:${observation.delivery.profileId}@${observation.delivery.profileContentHash}`,
|
|
243
|
+
`artifact:${observation.artifact.ref}@${observation.artifact.contentHash}`,
|
|
244
|
+
`assessment:${observation.assessment.assessmentId}@${observation.assessment.contentHash}`,
|
|
245
|
+
...observation.evidence.map((item) => `${item.kind}:${item.ref}@${item.hash}`),
|
|
246
|
+
];
|
|
247
|
+
return FindingSchema.parse({
|
|
248
|
+
id: findingId(title),
|
|
249
|
+
title,
|
|
250
|
+
observation: observation.signal === "contradicted"
|
|
251
|
+
? `Explicit outcome evidence may conflict with delivered Project DNA profile ${observation.delivery.profileId}; review the named traits and evidence before changing the profile.`
|
|
252
|
+
: `Explicit outcome evidence may show that delivered Project DNA profile ${observation.delivery.profileId} was stale; review currentness before changing the profile.`,
|
|
253
|
+
evidence,
|
|
254
|
+
method: null,
|
|
255
|
+
severity: observation.signal === "contradicted" ? "high" : "medium",
|
|
256
|
+
triage: "open",
|
|
257
|
+
affected_files: [],
|
|
258
|
+
affected_symbols: [...observation.projection.traitIds],
|
|
259
|
+
violates_constraint: null,
|
|
260
|
+
spawned_decision: null,
|
|
261
|
+
observed_at: observation.observedAt,
|
|
262
|
+
resolved_commit: null,
|
|
263
|
+
provenance: {
|
|
264
|
+
source: "project_dna_outcome_experience+candidate",
|
|
265
|
+
confidence: 0.75,
|
|
266
|
+
evidence,
|
|
267
|
+
last_verified: observation.observedAt,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=projectDnaOutcomeExperience.js.map
|
package/dist/mcp/server.js
CHANGED
|
@@ -27,6 +27,8 @@ import { compileVerifiedEvidenceMap, EvidenceExecutionSchema, EvidenceInterventi
|
|
|
27
27
|
import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
|
|
28
28
|
import { buildDeliveryEnvelope, DELIVERY_PROFILE_POLICY_VERSION, DELIVERY_PROFILES, } from "../core/delivery.js";
|
|
29
29
|
import { CHANGE_IDENTITY_ALGORITHM, CHANGE_IDENTITY_SCHEMA_VERSION, deriveChangeIdentity, } from "../core/changeIdentity.js";
|
|
30
|
+
import { deriveChangeProof } from "../core/changeProof.js";
|
|
31
|
+
import { ChangeProofSchema } from "../core/changeProofContract.js";
|
|
30
32
|
import { PROJECT_DNA_CATEGORIES, PROJECT_DNA_MATCH_SCHEMA_VERSION, PROJECT_DNA_SCHEMA_VERSION, discoverProjectDna, evaluateProjectDnaMatch, } from "../core/projectDna.js";
|
|
31
33
|
import { PROJECT_DNA_DELTA_SCHEMA_VERSION, diffProjectDna } from "../core/projectDnaDelta.js";
|
|
32
34
|
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
@@ -905,6 +907,32 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
905
907
|
return err(error.message);
|
|
906
908
|
}
|
|
907
909
|
});
|
|
910
|
+
// -- hunch_change_proof (exact-revision semantic evidence) ----------------
|
|
911
|
+
server.registerTool("hunch_change_proof", {
|
|
912
|
+
title: "Derive a sealed semantic proof for an exact change",
|
|
913
|
+
description: "Bind an exact committed Git transition to its change identity, Project DNA, base/result semantic graphs, current decisions and constraints, blast radius, conformance, guard verdict, and explicit gaps. Read-only and deterministic; grants no execution, CI, deployment, merge, ranking, promotion, or policy authority.",
|
|
914
|
+
inputSchema: {
|
|
915
|
+
base_ref: z.string().min(1).max(1_024).describe("Base commit or ref for the exact tree transition."),
|
|
916
|
+
result_ref: z.string().min(1).max(1_024).optional().describe("Result commit or ref (default HEAD)."),
|
|
917
|
+
public_only: z.boolean().optional().describe("Exclude the configured private-memory overlay. Required before publishing a proof."),
|
|
918
|
+
cwd: cwdHintField,
|
|
919
|
+
},
|
|
920
|
+
outputSchema: ChangeProofSchema,
|
|
921
|
+
}, async ({ base_ref, result_ref, public_only }) => {
|
|
922
|
+
try {
|
|
923
|
+
const proof = ChangeProofSchema.parse(deriveChangeProof(root, store, base_ref, result_ref ?? "HEAD", { publicOnly: public_only }));
|
|
924
|
+
return {
|
|
925
|
+
content: [{
|
|
926
|
+
type: "text",
|
|
927
|
+
text: `${proof.proof_id} — ${proof.verdict.toUpperCase()}; ${proof.changed_file_count} exact file delta(s), ${proof.blast_radius_count} dependent path(s), ${proof.omissions.length + proof.unknowns.length} explicit gap(s); sealed ${proof.content_hash}. Evidence only; no execution or merge authority.`,
|
|
928
|
+
}],
|
|
929
|
+
structuredContent: proof,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
return err(error.message);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
908
936
|
// -- hunch_project_dna ----------------------------------------------------
|
|
909
937
|
server.registerTool("hunch_project_dna", {
|
|
910
938
|
title: "Inspect this repository's evidence-backed Project DNA",
|