@davesheffer/hunch 1.7.0 → 1.7.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 +214 -0
- package/bench/constitution-exp03-v1.json +70 -0
- package/dist/cli/index.js +1203 -24
- package/dist/constitution/adapters.js +487 -0
- package/dist/constitution/behaviorAttestationBinding.js +17 -0
- package/dist/constitution/behaviorEvaluator.js +220 -0
- package/dist/constitution/behaviorProof.js +205 -0
- package/dist/constitution/behaviorWorkspace.js +124 -0
- package/dist/constitution/bootstrap.js +133 -0
- package/dist/constitution/canonical.js +51 -0
- package/dist/constitution/card.js +133 -0
- package/dist/constitution/compiler.js +176 -0
- package/dist/constitution/composition.js +101 -0
- package/dist/constitution/corpus.js +58 -0
- package/dist/constitution/delta.js +154 -0
- package/dist/constitution/disposition.js +141 -0
- package/dist/constitution/evaluator.js +435 -0
- package/dist/constitution/experiment.js +948 -0
- package/dist/constitution/experimentRunner.js +344 -0
- package/dist/constitution/g2.js +291 -0
- package/dist/constitution/g2BehaviorAttestation.js +209 -0
- package/dist/constitution/g2BehaviorCandidates.js +703 -0
- package/dist/constitution/g2BehaviorDependencies.js +379 -0
- package/dist/constitution/g2BehaviorMaterialization.js +171 -0
- package/dist/constitution/g2BehaviorPolicyMaterializer.js +241 -0
- package/dist/constitution/g2CandidateAttestation.js +179 -0
- package/dist/constitution/g2Candidates.js +195 -0
- package/dist/constitution/g2Drills.js +122 -0
- package/dist/constitution/g3.js +511 -0
- package/dist/constitution/g3Conformance.js +115 -0
- package/dist/constitution/lifecycle.js +189 -0
- package/dist/constitution/mutation.js +262 -0
- package/dist/constitution/nodeTestEvidence.js +47 -0
- package/dist/constitution/plan.js +172 -0
- package/dist/constitution/policyRuntime.js +8 -0
- package/dist/constitution/proof.js +166 -0
- package/dist/constitution/replay.js +361 -0
- package/dist/constitution/replayCache.js +89 -0
- package/dist/constitution/replayWorker.js +34 -0
- package/dist/constitution/repository.js +533 -0
- package/dist/constitution/schema.js +545 -0
- package/dist/constitution/scorecard.js +106 -0
- package/dist/constitution/service.js +1149 -0
- package/dist/constitution/shadow.js +235 -0
- package/dist/constitution/sourceMutation.js +316 -0
- package/dist/constitution/structural.js +601 -0
- package/dist/core/autoreview.js +27 -3
- package/dist/core/dupdetect.js +10 -3
- package/dist/core/events.js +61 -0
- package/dist/core/externalImports.js +24 -0
- package/dist/core/hookpolicy.js +3 -0
- package/dist/core/relativeImports.js +33 -0
- package/dist/core/stats.js +115 -0
- package/dist/extractors/git.js +81 -0
- package/dist/extractors/indexer.js +39 -38
- package/dist/extractors/nativeTreeSitter.js +108 -0
- package/dist/extractors/parse.js +5 -15
- package/dist/integrations/claudemd.js +8 -1
- package/dist/integrations/gitignore.js +8 -0
- package/dist/integrations/providers.js +32 -10
- package/dist/integrations/sync.js +16 -1
- package/dist/mcp/server.js +284 -0
- package/package.json +5 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { shortHash } from "../core/ids.js";
|
|
5
|
+
import { parseDocAnchors } from "../core/docanchors.js";
|
|
6
|
+
import { toPosixTarget } from "../core/paths.js";
|
|
7
|
+
import { commitMeta, revExists, revParse } from "../extractors/git.js";
|
|
8
|
+
import { durationCutoff } from "./bootstrap.js";
|
|
9
|
+
import { canonicalHash } from "./canonical.js";
|
|
10
|
+
import { EvidenceEventSchema, EvidenceImportSchema, } from "./schema.js";
|
|
11
|
+
const MAX_INSTRUCTION_FILES = 64;
|
|
12
|
+
const MAX_INSTRUCTION_FILE_BYTES = 512 * 1024;
|
|
13
|
+
const MAX_INSTRUCTION_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
const MAX_INSTRUCTION_HISTORY_COMMITS = 128;
|
|
15
|
+
const MAX_IMPORT_FILE_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
function homeView(opts) {
|
|
17
|
+
return { publicOnly: opts.publicOnly, privateOnly: opts.privateOnly };
|
|
18
|
+
}
|
|
19
|
+
function normalizeRepoFile(file) {
|
|
20
|
+
const normalized = toPosixTarget(file.trim());
|
|
21
|
+
if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || normalized.split("/").includes("..")) {
|
|
22
|
+
throw new Error(`evidence file path must be repository-relative: ${file}`);
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
function coverageCompiler(related, policies, constraints, unsupportedReason) {
|
|
27
|
+
const refs = new Set(related);
|
|
28
|
+
const policy = policies.find((candidate) => refs.has(candidate.id)
|
|
29
|
+
|| candidate.legacy_refs.some((ref) => refs.has(ref))
|
|
30
|
+
|| candidate.evidence.some((ref) => refs.has(ref)));
|
|
31
|
+
if (policy)
|
|
32
|
+
return { status: "covered", policy: policy.id, reason: "Imported evidence explicitly relates to an existing deterministic Policy IR record." };
|
|
33
|
+
const constraint = constraints.find((candidate) => candidate.status === "active" && refs.has(candidate.id));
|
|
34
|
+
if (constraint)
|
|
35
|
+
return { status: "covered", policy: null, reason: "Imported evidence explicitly relates to an active deterministic legacy Constraint." };
|
|
36
|
+
return { status: "uncompilable", policy: null, reason: unsupportedReason };
|
|
37
|
+
}
|
|
38
|
+
function privateOnlyRelatedRef(store, repository, ref) {
|
|
39
|
+
if (ref.startsWith("pol_")) {
|
|
40
|
+
return !!repository.getPolicy(ref, { privateOnly: true }) && !repository.getPolicy(ref, { publicOnly: true });
|
|
41
|
+
}
|
|
42
|
+
const kind = ref.startsWith("dec_") ? "decisions"
|
|
43
|
+
: ref.startsWith("bug_") ? "bugs"
|
|
44
|
+
: ref.startsWith("con_") ? "constraints"
|
|
45
|
+
: null;
|
|
46
|
+
return !!kind && !!store.getPrivateRec(kind, ref) && !store.json.get(kind, ref);
|
|
47
|
+
}
|
|
48
|
+
function instructionFile(file) {
|
|
49
|
+
return /(^|\/)(AGENTS|CLAUDE|GEMINI)\.md$/i.test(file)
|
|
50
|
+
|| /^\.github\/copilot-instructions\.md$/i.test(file)
|
|
51
|
+
|| /^\.(cursor|windsurf)\/rules\/.+\.(md|mdc)$/i.test(file)
|
|
52
|
+
|| /^(docs\/)?(adr|adrs|decisions)\/.+\.md$/i.test(file);
|
|
53
|
+
}
|
|
54
|
+
function committedInstructionFiles(root) {
|
|
55
|
+
try {
|
|
56
|
+
const raw = execFileSync("git", ["-C", root, "ls-tree", "-r", "-z", "--name-only", "HEAD"], {
|
|
57
|
+
encoding: "buffer",
|
|
58
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
59
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
60
|
+
});
|
|
61
|
+
return raw.toString("utf8").split("\0").filter(instructionFile).sort();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error("instruction ingestion could not enumerate committed repository files");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function committedFileAt(root, revision, file, maxBytes) {
|
|
68
|
+
try {
|
|
69
|
+
const object = `${revision}:${file}`;
|
|
70
|
+
const size = Number(execFileSync("git", ["-C", root, "cat-file", "-s", object], {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
73
|
+
}).trim());
|
|
74
|
+
if (!Number.isFinite(size) || size < 0 || size > maxBytes)
|
|
75
|
+
return null;
|
|
76
|
+
return execFileSync("git", ["-C", root, "cat-file", "blob", object], {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
maxBuffer: maxBytes + 1,
|
|
79
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function authoredInstructionContent(source) {
|
|
87
|
+
let authored = source.replace(/<!-- HUNCH:START[^]*?<!-- HUNCH:END -->/g, "");
|
|
88
|
+
authored = authored.replace(/^---\r?\n[^]*?\r?\n---\r?\n?/, "");
|
|
89
|
+
authored = authored.replace(/^# Copilot instructions\s*/i, "");
|
|
90
|
+
const substantive = authored.replace(/^\s{0,3}#{1,6}\s+.*$/gm, "").trim();
|
|
91
|
+
return substantive ? authored.trim() : null;
|
|
92
|
+
}
|
|
93
|
+
function authoredInstructionCommit(root, file, sourceHash) {
|
|
94
|
+
let commits;
|
|
95
|
+
try {
|
|
96
|
+
commits = execFileSync("git", [
|
|
97
|
+
"-C", root,
|
|
98
|
+
"log",
|
|
99
|
+
`--max-count=${MAX_INSTRUCTION_HISTORY_COMMITS}`,
|
|
100
|
+
"--format=%H",
|
|
101
|
+
"--",
|
|
102
|
+
file,
|
|
103
|
+
], {
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
maxBuffer: 1024 * 1024,
|
|
106
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
107
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
let introducedAt = null;
|
|
113
|
+
for (const commit of commits) {
|
|
114
|
+
const blob = committedFileAt(root, commit, file, MAX_INSTRUCTION_FILE_BYTES);
|
|
115
|
+
const authored = blob == null ? null : authoredInstructionContent(blob);
|
|
116
|
+
if (authored == null || canonicalHash(authored) !== sourceHash)
|
|
117
|
+
break;
|
|
118
|
+
introducedAt = commit;
|
|
119
|
+
}
|
|
120
|
+
return introducedAt;
|
|
121
|
+
}
|
|
122
|
+
function instructionEvents(root, dataClass, policies, constraints) {
|
|
123
|
+
const files = committedInstructionFiles(root);
|
|
124
|
+
const selected = files.slice(0, MAX_INSTRUCTION_FILES);
|
|
125
|
+
let excluded = files.length - selected.length;
|
|
126
|
+
let totalBytes = 0;
|
|
127
|
+
const pending = [];
|
|
128
|
+
for (const file of selected) {
|
|
129
|
+
const committed = committedFileAt(root, "HEAD", file, MAX_INSTRUCTION_FILE_BYTES);
|
|
130
|
+
const source = committed == null ? null : authoredInstructionContent(committed);
|
|
131
|
+
if (source == null || totalBytes + Buffer.byteLength(source, "utf8") > MAX_INSTRUCTION_TOTAL_BYTES) {
|
|
132
|
+
excluded++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
totalBytes += Buffer.byteLength(source, "utf8");
|
|
136
|
+
const sourceHash = canonicalHash(source);
|
|
137
|
+
const introducedAt = authoredInstructionCommit(root, file, sourceHash);
|
|
138
|
+
if (!introducedAt || !revExists(introducedAt, root)) {
|
|
139
|
+
excluded++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const commit = revParse(`${introducedAt}^{commit}`, root);
|
|
143
|
+
const meta = commitMeta(commit, root);
|
|
144
|
+
if (!meta || !Number.isFinite(Date.parse(meta.date))) {
|
|
145
|
+
excluded++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const anchors = parseDocAnchors(source);
|
|
149
|
+
const related = [...new Set(anchors.map((anchor) => anchor.pin).filter((pin) => !!pin))].sort();
|
|
150
|
+
const contentHash = canonicalHash({
|
|
151
|
+
kind: "instruction",
|
|
152
|
+
file,
|
|
153
|
+
commit,
|
|
154
|
+
source_hash: sourceHash,
|
|
155
|
+
anchors,
|
|
156
|
+
data_class: dataClass,
|
|
157
|
+
});
|
|
158
|
+
const event = EvidenceEventSchema.parse({
|
|
159
|
+
id: `ev_${shortHash(`instruction:${contentHash}`)}`,
|
|
160
|
+
kind: "instruction",
|
|
161
|
+
occurred_at: meta.date,
|
|
162
|
+
actor: meta.author,
|
|
163
|
+
repository: basename(root),
|
|
164
|
+
commit,
|
|
165
|
+
files: [file],
|
|
166
|
+
symbols: [],
|
|
167
|
+
text_ref: file,
|
|
168
|
+
diff_ref: `git:${commit}:${file}`,
|
|
169
|
+
related_records: related,
|
|
170
|
+
data_class: dataClass,
|
|
171
|
+
content_hash: contentHash,
|
|
172
|
+
compiler: coverageCompiler(related, policies, constraints, "Committed instruction/ADR content was hash-normalized, but it declares no exact supported structural assertion tied to an existing guard."),
|
|
173
|
+
provenance: {
|
|
174
|
+
source: "extracted",
|
|
175
|
+
confidence: 0.7,
|
|
176
|
+
evidence: [file, commit, sourceHash],
|
|
177
|
+
last_verified: meta.date,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
pending.push({ occurredAt: meta.date, event, private: dataClass !== "public" });
|
|
181
|
+
}
|
|
182
|
+
return { scanned: files.length, excluded, pending };
|
|
183
|
+
}
|
|
184
|
+
function importEvents(root, file, store, repository, opts, policies, constraints, publicPolicies, publicConstraints) {
|
|
185
|
+
const target = resolve(root, file);
|
|
186
|
+
let raw;
|
|
187
|
+
try {
|
|
188
|
+
const size = statSync(target).size;
|
|
189
|
+
if (size > MAX_IMPORT_FILE_BYTES)
|
|
190
|
+
throw new Error("too large");
|
|
191
|
+
raw = readFileSync(target, "utf8");
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw new Error(`evidence import ${basename(file)} is unreadable or exceeds ${MAX_IMPORT_FILE_BYTES} bytes`);
|
|
195
|
+
}
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = EvidenceImportSchema.parse(JSON.parse(raw));
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
throw new Error(`invalid evidence import ${basename(file)}: ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
const pending = [];
|
|
204
|
+
for (const item of parsed.items) {
|
|
205
|
+
let dataClass = item.data_class;
|
|
206
|
+
if (opts.privateOnly && dataClass === "public")
|
|
207
|
+
dataClass = "private";
|
|
208
|
+
if (opts.publicOnly && dataClass !== "public") {
|
|
209
|
+
throw new Error(`evidence import ${basename(file)} contains ${dataClass} item ${item.id}; refusing public-only ingestion`);
|
|
210
|
+
}
|
|
211
|
+
if (dataClass !== "public" && !store.hasPrivate) {
|
|
212
|
+
throw new Error(`evidence import ${basename(file)} needs a configured private overlay for ${dataClass} item ${item.id}`);
|
|
213
|
+
}
|
|
214
|
+
let commit;
|
|
215
|
+
if (item.commit) {
|
|
216
|
+
if (!revExists(item.commit, root))
|
|
217
|
+
throw new Error(`evidence import item ${item.id} commit ${item.commit} does not resolve`);
|
|
218
|
+
commit = revParse(`${item.commit}^{commit}`, root);
|
|
219
|
+
}
|
|
220
|
+
const files = [...new Set(item.files.map(normalizeRepoFile))].sort();
|
|
221
|
+
const symbols = [...new Set(item.symbols)].sort();
|
|
222
|
+
const related = [...new Set(item.related_records)].sort();
|
|
223
|
+
if (dataClass === "public") {
|
|
224
|
+
const privateRef = related.find((ref) => privateOnlyRelatedRef(store, repository, ref));
|
|
225
|
+
if (privateRef)
|
|
226
|
+
throw new Error(`public evidence import item ${item.id} references private-only record ${privateRef}`);
|
|
227
|
+
}
|
|
228
|
+
const textHash = item.text ? canonicalHash(item.text) : undefined;
|
|
229
|
+
const textRef = item.text_ref ?? `export:${parsed.source}:${item.id}`;
|
|
230
|
+
const body = {
|
|
231
|
+
source: parsed.source,
|
|
232
|
+
external_id: item.id,
|
|
233
|
+
kind: item.kind,
|
|
234
|
+
occurred_at: item.occurred_at,
|
|
235
|
+
actor: item.actor,
|
|
236
|
+
commit,
|
|
237
|
+
files,
|
|
238
|
+
symbols,
|
|
239
|
+
text_ref: textRef,
|
|
240
|
+
text_hash: textHash,
|
|
241
|
+
related_records: related,
|
|
242
|
+
data_class: dataClass,
|
|
243
|
+
maintainer_confirmed: item.maintainer_confirmed,
|
|
244
|
+
};
|
|
245
|
+
const contentHash = canonicalHash(body);
|
|
246
|
+
const event = EvidenceEventSchema.parse({
|
|
247
|
+
id: `ev_${shortHash(`${parsed.source}:${contentHash}`)}`,
|
|
248
|
+
kind: item.kind,
|
|
249
|
+
occurred_at: item.occurred_at,
|
|
250
|
+
...(item.actor ? { actor: item.actor } : {}),
|
|
251
|
+
repository: basename(root),
|
|
252
|
+
...(commit ? { commit, diff_ref: `git:${commit}` } : {}),
|
|
253
|
+
files,
|
|
254
|
+
symbols,
|
|
255
|
+
text_ref: textRef,
|
|
256
|
+
related_records: related,
|
|
257
|
+
data_class: dataClass,
|
|
258
|
+
content_hash: contentHash,
|
|
259
|
+
compiler: coverageCompiler(related, dataClass === "public" ? publicPolicies : policies, dataClass === "public" ? publicConstraints : constraints, item.maintainer_confirmed
|
|
260
|
+
? "Maintainer-confirmed review/instruction evidence was normalized, but no exact supported structural assertion or existing deterministic guard is linked."
|
|
261
|
+
: "External review/conversation evidence is not maintainer-confirmed and cannot become a policy candidate."),
|
|
262
|
+
provenance: {
|
|
263
|
+
source: item.maintainer_confirmed ? "human_confirmed+imported" : "imported",
|
|
264
|
+
confidence: item.maintainer_confirmed ? 1 : 0.5,
|
|
265
|
+
evidence: [parsed.source, basename(file), item.id, ...(commit ? [commit] : []), ...(textHash ? [textHash] : [])],
|
|
266
|
+
last_verified: item.occurred_at,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
pending.push({ occurredAt: item.occurred_at, event, private: dataClass !== "public" });
|
|
270
|
+
}
|
|
271
|
+
return { scanned: parsed.items.length, pending };
|
|
272
|
+
}
|
|
273
|
+
function limit(value) {
|
|
274
|
+
if (value == null || !Number.isFinite(value))
|
|
275
|
+
return 100;
|
|
276
|
+
return Math.max(1, Math.min(200, Math.trunc(value)));
|
|
277
|
+
}
|
|
278
|
+
function exactRecords(store, opts) {
|
|
279
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
280
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
281
|
+
if (opts.privateOnly) {
|
|
282
|
+
if (!store.hasPrivate)
|
|
283
|
+
throw new Error("private evidence ingestion needs a configured Hunch private overlay");
|
|
284
|
+
return { constraints: store.recsInHome("constraints", "private"), bugs: store.recsInHome("bugs", "private") };
|
|
285
|
+
}
|
|
286
|
+
if (opts.publicOnly)
|
|
287
|
+
return { constraints: store.json.loadAll("constraints"), bugs: store.json.loadAll("bugs") };
|
|
288
|
+
return { constraints: store.recs("constraints"), bugs: store.recs("bugs") };
|
|
289
|
+
}
|
|
290
|
+
function isPrivateRecord(store, kind, id, opts) {
|
|
291
|
+
if (opts.privateOnly)
|
|
292
|
+
return true;
|
|
293
|
+
if (opts.publicOnly)
|
|
294
|
+
return false;
|
|
295
|
+
return !!store.getPrivateRec(kind, id);
|
|
296
|
+
}
|
|
297
|
+
function correctionEvent(root, constraint, isPrivate) {
|
|
298
|
+
const occurredAt = constraint.valid_from ?? constraint.provenance.last_verified;
|
|
299
|
+
if (!occurredAt || !Number.isFinite(Date.parse(occurredAt)))
|
|
300
|
+
return null;
|
|
301
|
+
const contentHash = canonicalHash({
|
|
302
|
+
constraint: constraint.id,
|
|
303
|
+
statement: constraint.statement,
|
|
304
|
+
scope: constraint.scope,
|
|
305
|
+
forbids: constraint.forbids,
|
|
306
|
+
match: constraint.match,
|
|
307
|
+
source_decision: constraint.source_decision,
|
|
308
|
+
});
|
|
309
|
+
const event = EvidenceEventSchema.parse({
|
|
310
|
+
id: `ev_${shortHash(`correction:${contentHash}`)}`,
|
|
311
|
+
kind: "correction",
|
|
312
|
+
occurred_at: occurredAt,
|
|
313
|
+
repository: basename(root),
|
|
314
|
+
files: constraint.scope.filter((scope) => scope !== "**"),
|
|
315
|
+
symbols: constraint.forbids?.symbols ?? [],
|
|
316
|
+
text_ref: constraint.id,
|
|
317
|
+
related_records: [constraint.id, ...(constraint.source_decision ? [constraint.source_decision] : [])],
|
|
318
|
+
data_class: isPrivate ? "private" : "public",
|
|
319
|
+
content_hash: contentHash,
|
|
320
|
+
compiler: {
|
|
321
|
+
status: "covered",
|
|
322
|
+
policy: null,
|
|
323
|
+
reason: "Active human-confirmed legacy Constraint already delivers deterministic correction enforcement; Policy IR bridge remains explicit follow-on work.",
|
|
324
|
+
},
|
|
325
|
+
provenance: {
|
|
326
|
+
source: "derived",
|
|
327
|
+
confidence: 1,
|
|
328
|
+
evidence: [constraint.id, ...constraint.provenance.evidence],
|
|
329
|
+
last_verified: constraint.provenance.last_verified,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
return { occurredAt, event, private: isPrivate };
|
|
333
|
+
}
|
|
334
|
+
function bugEvent(root, store, repository, bug, isPrivate, opts) {
|
|
335
|
+
const commit = bug.lineage.fixed_commit ?? bug.lineage.introduced_commit;
|
|
336
|
+
const meta = commit ? commitMeta(commit, root) : null;
|
|
337
|
+
const occurredAt = meta?.date ?? bug.provenance.last_verified;
|
|
338
|
+
if (!occurredAt || !Number.isFinite(Date.parse(occurredAt)))
|
|
339
|
+
return null;
|
|
340
|
+
const kind = bug.lineage.detected ? "test_failure" : "incident";
|
|
341
|
+
const related = [
|
|
342
|
+
bug.id,
|
|
343
|
+
bug.lineage.recurrence_of,
|
|
344
|
+
bug.lineage.spawned_decision,
|
|
345
|
+
bug.lineage.spawned_constraint,
|
|
346
|
+
].filter((value) => !!value);
|
|
347
|
+
const contentHash = canonicalHash({
|
|
348
|
+
bug: bug.id,
|
|
349
|
+
root_cause: bug.root_cause,
|
|
350
|
+
files: bug.affected_files,
|
|
351
|
+
symbols: bug.affected_symbols,
|
|
352
|
+
lineage: bug.lineage,
|
|
353
|
+
});
|
|
354
|
+
const homeView = { publicOnly: opts.publicOnly, privateOnly: opts.privateOnly };
|
|
355
|
+
const policy = bug.lineage.spawned_decision
|
|
356
|
+
? repository.listPolicies(homeView).find((candidate) => candidate.legacy_refs.includes(bug.lineage.spawned_decision))
|
|
357
|
+
: undefined;
|
|
358
|
+
const constraint = bug.lineage.spawned_constraint
|
|
359
|
+
? opts.publicOnly
|
|
360
|
+
? store.json.get("constraints", bug.lineage.spawned_constraint)
|
|
361
|
+
: opts.privateOnly
|
|
362
|
+
? store.getPrivateRec("constraints", bug.lineage.spawned_constraint)
|
|
363
|
+
: store.getRec("constraints", bug.lineage.spawned_constraint)
|
|
364
|
+
: undefined;
|
|
365
|
+
const covered = !!policy || constraint?.status === "active";
|
|
366
|
+
const event = EvidenceEventSchema.parse({
|
|
367
|
+
id: `ev_${shortHash(`${kind}:${contentHash}`)}`,
|
|
368
|
+
kind,
|
|
369
|
+
occurred_at: occurredAt,
|
|
370
|
+
repository: basename(root),
|
|
371
|
+
...(meta ? { actor: meta.author, commit: meta.sha, diff_ref: `git:${meta.sha}` } : {}),
|
|
372
|
+
files: bug.affected_files,
|
|
373
|
+
symbols: bug.affected_symbols,
|
|
374
|
+
text_ref: bug.id,
|
|
375
|
+
related_records: [...related, ...(policy ? [policy.id] : [])],
|
|
376
|
+
data_class: isPrivate ? "private" : "public",
|
|
377
|
+
content_hash: contentHash,
|
|
378
|
+
compiler: covered
|
|
379
|
+
? {
|
|
380
|
+
status: "covered",
|
|
381
|
+
policy: policy?.id ?? null,
|
|
382
|
+
reason: policy
|
|
383
|
+
? "Spawned decision already has equivalent Policy IR coverage."
|
|
384
|
+
: "Spawned active legacy Constraint already covers this failure; Policy IR bridge remains pending.",
|
|
385
|
+
}
|
|
386
|
+
: {
|
|
387
|
+
status: "uncompilable",
|
|
388
|
+
policy: null,
|
|
389
|
+
reason: "Incident/test evidence normalized, but no attributable supported assertion or existing deterministic guard is linked.",
|
|
390
|
+
},
|
|
391
|
+
provenance: {
|
|
392
|
+
source: "derived",
|
|
393
|
+
confidence: bug.provenance.confidence,
|
|
394
|
+
evidence: [bug.id, ...bug.provenance.evidence],
|
|
395
|
+
last_verified: bug.provenance.last_verified,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
return { occurredAt, event, private: isPrivate };
|
|
399
|
+
}
|
|
400
|
+
/** Normalize existing local Hunch truth into Constitution EvidenceEvents. This
|
|
401
|
+
* adapter never synthesizes intent and never creates or activates a policy. */
|
|
402
|
+
export function ingestLocalEvidence(store, root, repository, opts = {}) {
|
|
403
|
+
const now = opts.now ?? new Date().toISOString();
|
|
404
|
+
const minDate = durationCutoff(opts.since ?? "90d", now);
|
|
405
|
+
const records = exactRecords(store, opts);
|
|
406
|
+
const view = homeView(opts);
|
|
407
|
+
const policies = repository.listPolicies(view);
|
|
408
|
+
const publicPolicies = repository.listPolicies({ publicOnly: true });
|
|
409
|
+
const publicConstraints = store.json.loadAll("constraints");
|
|
410
|
+
const pending = [];
|
|
411
|
+
let scanned = records.constraints.length + records.bugs.length;
|
|
412
|
+
let excluded = 0;
|
|
413
|
+
for (const constraint of records.constraints) {
|
|
414
|
+
if (constraint.status !== "active" || !constraint.provenance.source.includes("human_confirmed")) {
|
|
415
|
+
excluded++;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const item = correctionEvent(root, constraint, isPrivateRecord(store, "constraints", constraint.id, opts));
|
|
419
|
+
if (!item || Date.parse(item.occurredAt) < minDate)
|
|
420
|
+
excluded++;
|
|
421
|
+
else
|
|
422
|
+
pending.push(item);
|
|
423
|
+
}
|
|
424
|
+
for (const bug of records.bugs) {
|
|
425
|
+
const attributable = !!bug.lineage.detected || (!!bug.root_cause.trim() && bug.provenance.confidence >= 0.7);
|
|
426
|
+
if (!attributable) {
|
|
427
|
+
excluded++;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const item = bugEvent(root, store, repository, bug, isPrivateRecord(store, "bugs", bug.id, opts), opts);
|
|
431
|
+
if (!item || Date.parse(item.occurredAt) < minDate)
|
|
432
|
+
excluded++;
|
|
433
|
+
else
|
|
434
|
+
pending.push(item);
|
|
435
|
+
}
|
|
436
|
+
if (opts.instructions) {
|
|
437
|
+
const instructions = instructionEvents(root, opts.privateOnly ? "private" : "public", opts.privateOnly ? policies : publicPolicies, opts.privateOnly ? records.constraints : publicConstraints);
|
|
438
|
+
scanned += instructions.scanned;
|
|
439
|
+
excluded += instructions.excluded;
|
|
440
|
+
for (const item of instructions.pending) {
|
|
441
|
+
if (Date.parse(item.occurredAt) < minDate)
|
|
442
|
+
excluded++;
|
|
443
|
+
else
|
|
444
|
+
pending.push(item);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const file of opts.importFiles ?? []) {
|
|
448
|
+
const imported = importEvents(root, file, store, repository, opts, policies, records.constraints, publicPolicies, publicConstraints);
|
|
449
|
+
scanned += imported.scanned;
|
|
450
|
+
for (const item of imported.pending) {
|
|
451
|
+
if (Date.parse(item.occurredAt) < minDate)
|
|
452
|
+
excluded++;
|
|
453
|
+
else
|
|
454
|
+
pending.push(item);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
pending.sort((a, b) => b.occurredAt.localeCompare(a.occurredAt) || a.event.id.localeCompare(b.event.id));
|
|
458
|
+
const unique = [...new Map(pending.map((item) => [item.event.id, item])).values()];
|
|
459
|
+
excluded += pending.length - unique.length;
|
|
460
|
+
const selected = unique.slice(0, limit(opts.maxEvents));
|
|
461
|
+
excluded += Math.max(0, unique.length - selected.length);
|
|
462
|
+
const report = {
|
|
463
|
+
scanned,
|
|
464
|
+
eligible: unique.length,
|
|
465
|
+
normalized: 0,
|
|
466
|
+
existing: 0,
|
|
467
|
+
covered: 0,
|
|
468
|
+
uncompilable: 0,
|
|
469
|
+
excluded,
|
|
470
|
+
events: [],
|
|
471
|
+
};
|
|
472
|
+
for (const item of selected) {
|
|
473
|
+
const existing = repository.getEvidence(item.event.id, view);
|
|
474
|
+
const event = existing ?? repository.putEvidence(item.event, { private: item.private });
|
|
475
|
+
if (existing)
|
|
476
|
+
report.existing++;
|
|
477
|
+
else
|
|
478
|
+
report.normalized++;
|
|
479
|
+
if (event.compiler?.status === "covered")
|
|
480
|
+
report.covered++;
|
|
481
|
+
if (event.compiler?.status === "uncompilable")
|
|
482
|
+
report.uncompilable++;
|
|
483
|
+
report.events.push(event);
|
|
484
|
+
}
|
|
485
|
+
return report;
|
|
486
|
+
}
|
|
487
|
+
//# sourceMappingURL=adapters.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function executableBehaviorAttestationError(policy, currentAttestations) {
|
|
2
|
+
if (policy.assertion.kind !== "executable-behavior")
|
|
3
|
+
return null;
|
|
4
|
+
const binding = policy.assertion.attestation;
|
|
5
|
+
const current = currentAttestations.find((attestation) => attestation.id === binding.id);
|
|
6
|
+
if (!current
|
|
7
|
+
|| current.disposition !== "selected"
|
|
8
|
+
|| current.content_hash !== binding.content_hash
|
|
9
|
+
|| current.candidate_id !== binding.candidate_id
|
|
10
|
+
|| current.candidate_hash !== binding.candidate_hash
|
|
11
|
+
|| current.replay_id !== binding.replay_id
|
|
12
|
+
|| current.replay_hash !== binding.replay_hash) {
|
|
13
|
+
return "executable behavior policy is not bound to a current exact selected human attestation";
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=behaviorAttestationBinding.js.map
|