@davesheffer/hunch 1.6.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 +220 -0
- package/bench/constitution-exp03-v1.json +70 -0
- package/dist/cli/index.js +1278 -44
- 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/dist/synthesis/provider.js +145 -37
- package/dist/synthesis/synthesize.js +4 -4
- package/package.json +5 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { shortHash } from "../core/ids.js";
|
|
3
|
+
import { canonicalHash } from "./canonical.js";
|
|
4
|
+
import { compileDecisionRecord } from "./compiler.js";
|
|
5
|
+
import { EvidenceEventSchema } from "./schema.js";
|
|
6
|
+
export function clampCandidateLimit(value) {
|
|
7
|
+
if (value == null || !Number.isFinite(value))
|
|
8
|
+
return 3;
|
|
9
|
+
return Math.max(1, Math.min(3, Math.trunc(value)));
|
|
10
|
+
}
|
|
11
|
+
export function durationCutoff(since, now) {
|
|
12
|
+
const match = /^(\d+)([dw])$/.exec(since.trim());
|
|
13
|
+
if (!match)
|
|
14
|
+
throw new Error(`--since must be a positive duration such as 30d or 12w (got "${since}")`);
|
|
15
|
+
const amount = Number(match[1]);
|
|
16
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
17
|
+
throw new Error("--since duration must be positive");
|
|
18
|
+
const days = match[2] === "w" ? amount * 7 : amount;
|
|
19
|
+
return Date.parse(now) - days * 86_400_000;
|
|
20
|
+
}
|
|
21
|
+
function eligible(decision, minDate) {
|
|
22
|
+
const at = Date.parse(decision.valid_from ?? decision.date);
|
|
23
|
+
return decision.status === "accepted"
|
|
24
|
+
&& !decision.superseded_by
|
|
25
|
+
&& !decision.valid_to
|
|
26
|
+
&& decision.provenance.source.includes("human_confirmed")
|
|
27
|
+
&& decision.conformance?.length === 1
|
|
28
|
+
&& Number.isFinite(at)
|
|
29
|
+
&& at >= minDate;
|
|
30
|
+
}
|
|
31
|
+
function eventFor(root, decision, isPrivate) {
|
|
32
|
+
const predicate = decision.conformance[0];
|
|
33
|
+
const contentHash = canonicalHash({
|
|
34
|
+
decision: decision.id,
|
|
35
|
+
predicate,
|
|
36
|
+
related_files: decision.related_files,
|
|
37
|
+
related_components: decision.related_components,
|
|
38
|
+
caused_by_bug: decision.caused_by_bug,
|
|
39
|
+
});
|
|
40
|
+
const symbols = [predicate.subject, predicate.object].filter((v) => !!v);
|
|
41
|
+
return EvidenceEventSchema.parse({
|
|
42
|
+
id: `ev_${shortHash(`${decision.id}:${contentHash}`)}`,
|
|
43
|
+
kind: "decision",
|
|
44
|
+
occurred_at: decision.valid_from ?? decision.date,
|
|
45
|
+
repository: basename(root),
|
|
46
|
+
...(decision.commit ? { commit: decision.commit } : {}),
|
|
47
|
+
files: decision.related_files.filter((f) => !f.startsWith("private:")),
|
|
48
|
+
symbols,
|
|
49
|
+
text_ref: decision.id,
|
|
50
|
+
...(decision.commit ? { diff_ref: `git:${decision.commit}` } : {}),
|
|
51
|
+
related_records: [decision.id],
|
|
52
|
+
data_class: isPrivate ? "private" : "public",
|
|
53
|
+
content_hash: contentHash,
|
|
54
|
+
compiler: { status: "eligible", policy: null, reason: "Structured human-confirmed Decision.conformance predicate." },
|
|
55
|
+
provenance: {
|
|
56
|
+
source: "derived",
|
|
57
|
+
confidence: 1,
|
|
58
|
+
evidence: [decision.id, ...decision.provenance.evidence],
|
|
59
|
+
last_verified: decision.provenance.last_verified,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function canReclassify(event) {
|
|
64
|
+
const status = event?.compiler?.status;
|
|
65
|
+
return !event || status === "eligible" || status === "uncompilable";
|
|
66
|
+
}
|
|
67
|
+
/** Model-free Phase-2A bootstrap. It accepts only explicit structured evidence,
|
|
68
|
+
* creates an auditable event, compiles at most three new candidates, and never
|
|
69
|
+
* changes lifecycle authority. */
|
|
70
|
+
export function bootstrapPolicies(store, root, repository, opts = {}) {
|
|
71
|
+
if (opts.publicOnly && opts.privateOnly)
|
|
72
|
+
throw new Error("choose only one of publicOnly or privateOnly");
|
|
73
|
+
if (opts.privateOnly && !store.hasPrivate)
|
|
74
|
+
throw new Error("private bootstrap needs a configured Hunch private overlay");
|
|
75
|
+
const now = opts.now ?? new Date().toISOString();
|
|
76
|
+
const minDate = durationCutoff(opts.since ?? "90d", now);
|
|
77
|
+
const decisions = opts.privateOnly
|
|
78
|
+
? store.recsInHome("decisions", "private")
|
|
79
|
+
: opts.publicOnly
|
|
80
|
+
? store.json.loadAll("decisions")
|
|
81
|
+
: store.recs("decisions");
|
|
82
|
+
const candidates = decisions
|
|
83
|
+
.filter((d) => eligible(d, minDate))
|
|
84
|
+
.sort((a, b) => (b.valid_from ?? b.date).localeCompare(a.valid_from ?? a.date) || a.id.localeCompare(b.id));
|
|
85
|
+
const maxCandidates = clampCandidateLimit(opts.maxCandidates);
|
|
86
|
+
const homeView = { publicOnly: opts.publicOnly, privateOnly: opts.privateOnly };
|
|
87
|
+
const openCandidates = repository.listPolicies(homeView).filter((p) => p.state === "compiled" || p.state === "validating" || p.state === "proposed").length;
|
|
88
|
+
const available = Math.max(0, maxCandidates - Math.min(maxCandidates, openCandidates));
|
|
89
|
+
const report = { scanned: decisions.length, eligible: candidates.length, compiled: [], covered: 0, deferred: 0, uncompilable: 0, conflicted: 0 };
|
|
90
|
+
for (const decision of candidates) {
|
|
91
|
+
const isPrivate = opts.privateOnly ? true : opts.publicOnly ? false : !!store.getPrivateRec("decisions", decision.id);
|
|
92
|
+
const baseEvent = eventFor(root, decision, isPrivate);
|
|
93
|
+
const priorEvent = repository.getEvidence(baseEvent.id, homeView);
|
|
94
|
+
try {
|
|
95
|
+
const compiled = compileDecisionRecord(store, decision, isPrivate, { now });
|
|
96
|
+
const incumbent = repository.getPolicy(compiled.policy.id, homeView);
|
|
97
|
+
if (incumbent) {
|
|
98
|
+
report.covered++;
|
|
99
|
+
if (canReclassify(priorEvent))
|
|
100
|
+
repository.putEvidence({
|
|
101
|
+
...baseEvent,
|
|
102
|
+
related_records: [...baseEvent.related_records, incumbent.id],
|
|
103
|
+
compiler: { status: "covered", policy: incumbent.id, reason: "Equivalent Policy IR already exists; lifecycle state preserved." },
|
|
104
|
+
}, { private: isPrivate });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (report.compiled.length >= available) {
|
|
108
|
+
report.deferred++;
|
|
109
|
+
if (canReclassify(priorEvent) && priorEvent?.compiler?.status !== "eligible") {
|
|
110
|
+
repository.putEvidence(baseEvent, { private: isPrivate });
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const policy = repository.putPolicy(compiled.policy, { private: compiled.private });
|
|
115
|
+
const event = repository.putEvidence({
|
|
116
|
+
...baseEvent,
|
|
117
|
+
related_records: [...baseEvent.related_records, policy.id],
|
|
118
|
+
compiler: { status: "compiled", policy: policy.id, reason: "Deterministic structured compatibility compilation." },
|
|
119
|
+
}, { private: isPrivate });
|
|
120
|
+
report.compiled.push({ evidence: event, policy });
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
report.uncompilable++;
|
|
124
|
+
if (canReclassify(priorEvent) && priorEvent?.compiler?.status !== "uncompilable")
|
|
125
|
+
repository.putEvidence({
|
|
126
|
+
...baseEvent,
|
|
127
|
+
compiler: { status: "uncompilable", policy: null, reason: e.message },
|
|
128
|
+
}, { private: isPrivate });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return report;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { sha1, shortHash } from "../core/ids.js";
|
|
2
|
+
function canonicalValue(value) {
|
|
3
|
+
if (Array.isArray(value))
|
|
4
|
+
return value.map(canonicalValue);
|
|
5
|
+
if (value && typeof value === "object") {
|
|
6
|
+
return Object.fromEntries(Object.entries(value)
|
|
7
|
+
.filter(([, v]) => v !== undefined)
|
|
8
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
9
|
+
.map(([k, v]) => [k, canonicalValue(v)]));
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** Stable, whitespace-free JSON for hashes and cross-client receipts. Arrays retain
|
|
14
|
+
* semantic order; object keys are recursively sorted. */
|
|
15
|
+
export function canonicalJson(value) {
|
|
16
|
+
return JSON.stringify(canonicalValue(value));
|
|
17
|
+
}
|
|
18
|
+
export function canonicalHash(value) {
|
|
19
|
+
return sha1(canonicalJson(value));
|
|
20
|
+
}
|
|
21
|
+
/** Proofs bind evaluator semantics, not mutable lifecycle fields such as approval,
|
|
22
|
+
* severity, timestamps, or state. */
|
|
23
|
+
export function policySemanticHash(policy) {
|
|
24
|
+
return canonicalHash({
|
|
25
|
+
id: policy.id,
|
|
26
|
+
ir_version: policy.ir_version,
|
|
27
|
+
statement: policy.statement,
|
|
28
|
+
scope: policy.scope,
|
|
29
|
+
assertion: policy.assertion,
|
|
30
|
+
exception_of: policy.exception_of,
|
|
31
|
+
data_class: policy.data_class,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function proofPlanContentHash(plan) {
|
|
35
|
+
const { id: _id, content_hash: _contentHash, created_at: _createdAt, ...body } = plan;
|
|
36
|
+
return canonicalHash(body);
|
|
37
|
+
}
|
|
38
|
+
/** A proof rerun is bound to policy semantics, not a mutable lifecycle revision.
|
|
39
|
+
* Live gate receipts keep policy_revision in their own deterministic hash; this
|
|
40
|
+
* projection is only for immutable proof/replay evidence. */
|
|
41
|
+
export function proofEvaluationHash(evaluation) {
|
|
42
|
+
const { policy_revision: _revision, deterministic_hash: _hash, ...semantic } = evaluation;
|
|
43
|
+
return canonicalHash(semantic);
|
|
44
|
+
}
|
|
45
|
+
export function policyId(seed) {
|
|
46
|
+
return `pol_${shortHash(canonicalJson(seed))}`;
|
|
47
|
+
}
|
|
48
|
+
export function proofId(seed) {
|
|
49
|
+
return `proof_${shortHash(canonicalJson(seed))}`;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=canonical.js.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { canonicalHash } from "./canonical.js";
|
|
2
|
+
import { assertCompositionBinding, policyProofHash } from "./composition.js";
|
|
3
|
+
import { blockingEvidenceError } from "./lifecycle.js";
|
|
4
|
+
import { assessHistoryDispositions } from "./disposition.js";
|
|
5
|
+
const proofRank = { P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5 };
|
|
6
|
+
export function buildProofCard(policy, proof, dispositions = [], composition = [], shadowPrecision = null) {
|
|
7
|
+
let semanticMatch = proof.policy_hash === policyProofHash(policy, composition);
|
|
8
|
+
try {
|
|
9
|
+
assertCompositionBinding(policy, composition, proof.composition);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
semanticMatch = false;
|
|
13
|
+
}
|
|
14
|
+
const baselineClean = proof.current.total === 1 && proof.current.satisfied === 1 && proof.current.unknown === 0 && proof.current.error === 0;
|
|
15
|
+
const dispositionAssessment = assessHistoryDispositions(proof, dispositions);
|
|
16
|
+
const evidenceError = blockingEvidenceError(proof, dispositions);
|
|
17
|
+
const proofStrongEnough = proofRank[proof.proof_class] >= proofRank.P3;
|
|
18
|
+
const eligible = semanticMatch && baselineClean && proofStrongEnough && !evidenceError;
|
|
19
|
+
const canBlock = eligible && policy.state === "active_blocking" && policy.severity === "blocking" && policy.authority?.kind === "human";
|
|
20
|
+
const unknownResults = proof.current.unknown + proof.known_bad.unknown + proof.known_good.unknown + proof.accepted_history.unknown + proof.mutations.unknown;
|
|
21
|
+
const errorResults = proof.current.error + proof.known_bad.error + proof.known_good.error + proof.accepted_history.error + proof.mutations.error;
|
|
22
|
+
const actions = [];
|
|
23
|
+
if (!semanticMatch)
|
|
24
|
+
actions.push("Regenerate the plan and proof for the current policy semantics.");
|
|
25
|
+
if (dispositionAssessment.unresolved_count)
|
|
26
|
+
actions.push("Classify every accepted-history violation with an exact human disposition before considering blocking approval.");
|
|
27
|
+
if (dispositionAssessment.counts.false_positive_selector + dispositionAssessment.counts.false_positive_semantics + dispositionAssessment.counts.false_positive_stale > 0)
|
|
28
|
+
actions.push("Repair and re-prove the policy before blocking because a human classified a historical hit as a false positive.");
|
|
29
|
+
if (dispositionAssessment.counts.true_positive_accepted_exception > 0)
|
|
30
|
+
actions.push("Prove combined parent/exception semantics before treating an accepted exception as resolved.");
|
|
31
|
+
if (dispositionAssessment.counts.unknown_insufficient_parser > 0)
|
|
32
|
+
actions.push("Improve parser support and re-prove before resolving an unknown historical hit.");
|
|
33
|
+
if (unknownResults || errorResults)
|
|
34
|
+
actions.push("Repair or explicitly resolve every unknown/error proof result.");
|
|
35
|
+
if (proof.mutation_controls.failed)
|
|
36
|
+
actions.push("Repair every failed required mutation control before considering blocking approval.");
|
|
37
|
+
if (policy.candidate.conflicts.length)
|
|
38
|
+
actions.push("Resolve every direct candidate conflict with a human disposition before lifecycle promotion.");
|
|
39
|
+
if (shadowPrecision?.recommendation === "eligible_for_p4_review")
|
|
40
|
+
actions.push("Shadow thresholds are met; a human may review P4 evidence, but measurement grants no authority.");
|
|
41
|
+
else if (shadowPrecision)
|
|
42
|
+
actions.push("Continue bounded shadow review until every reported precision threshold is met.");
|
|
43
|
+
if (eligible && !canBlock)
|
|
44
|
+
actions.push("A human may review and explicitly activate blocking mode; the proof and any earlier advisory approval grant no blocking authority by themselves.");
|
|
45
|
+
if (!eligible && actions.length === 0)
|
|
46
|
+
actions.push("Strengthen the evidence vector before requesting blocking approval.");
|
|
47
|
+
actions.push("Review the exact assertion, scope, evidence, and limitations before any lifecycle action.");
|
|
48
|
+
const body = {
|
|
49
|
+
policy: {
|
|
50
|
+
id: policy.id,
|
|
51
|
+
statement: policy.statement,
|
|
52
|
+
state: policy.state,
|
|
53
|
+
severity: policy.severity,
|
|
54
|
+
data_class: policy.data_class,
|
|
55
|
+
assertion: policy.assertion,
|
|
56
|
+
scope: policy.scope,
|
|
57
|
+
evidence: [...policy.evidence],
|
|
58
|
+
candidate: policy.candidate,
|
|
59
|
+
exception_of: policy.exception_of,
|
|
60
|
+
},
|
|
61
|
+
proof: { id: proof.id, proof_class: proof.proof_class, plan_hash: proof.plan_hash, generated_at: proof.generated_at },
|
|
62
|
+
evidence_vector: {
|
|
63
|
+
current: proof.current,
|
|
64
|
+
known_bad: proof.known_bad,
|
|
65
|
+
known_good: proof.known_good,
|
|
66
|
+
accepted_history: proof.accepted_history,
|
|
67
|
+
mutations: proof.mutations,
|
|
68
|
+
mutation_controls: proof.mutation_controls,
|
|
69
|
+
},
|
|
70
|
+
project_checks: proof.project_checks,
|
|
71
|
+
composition: proof.composition ?? null,
|
|
72
|
+
shadow_precision: shadowPrecision,
|
|
73
|
+
history_dispositions: {
|
|
74
|
+
current: dispositionAssessment.current,
|
|
75
|
+
counts: dispositionAssessment.counts,
|
|
76
|
+
missing_commits: dispositionAssessment.missing_commits,
|
|
77
|
+
unresolved_count: dispositionAssessment.unresolved_count,
|
|
78
|
+
},
|
|
79
|
+
uncertainty: {
|
|
80
|
+
unclassified_history_hits: dispositionAssessment.unresolved_count,
|
|
81
|
+
unknown_results: unknownResults,
|
|
82
|
+
error_results: errorResults,
|
|
83
|
+
limitations: [...proof.limitations],
|
|
84
|
+
compiler_uncertainty: [...policy.candidate.uncertainty],
|
|
85
|
+
candidate_conflicts: [...policy.candidate.conflicts],
|
|
86
|
+
},
|
|
87
|
+
authority: {
|
|
88
|
+
current: policy.authority,
|
|
89
|
+
eligible_for_human_blocking_approval: eligible,
|
|
90
|
+
can_block_now: canBlock,
|
|
91
|
+
blocking_evidence_error: evidenceError,
|
|
92
|
+
},
|
|
93
|
+
actions,
|
|
94
|
+
};
|
|
95
|
+
return { card_hash: canonicalHash(body), ...body };
|
|
96
|
+
}
|
|
97
|
+
function line(label, summary) {
|
|
98
|
+
return `${label}: ${summary.total} total · ${summary.satisfied} satisfied · ${summary.violated} violated · ${summary.not_applicable} n/a · ${summary.unknown} unknown · ${summary.error} error`;
|
|
99
|
+
}
|
|
100
|
+
export function renderProofCard(card) {
|
|
101
|
+
return [
|
|
102
|
+
`CONSTITUTION PROOF CARD ${card.policy.id}`,
|
|
103
|
+
` ${card.policy.statement}`,
|
|
104
|
+
` state: ${card.policy.state} · severity: ${card.policy.severity} · proof: ${card.proof.proof_class} (${card.proof.id})`,
|
|
105
|
+
` assertion: ${JSON.stringify(card.policy.assertion)}`,
|
|
106
|
+
` scope: ${JSON.stringify(card.policy.scope)}`,
|
|
107
|
+
` evidence: ${card.policy.evidence.join(", ") || "none"}`,
|
|
108
|
+
` candidate alternatives: ${card.policy.candidate.alternatives.length} · conflicts: ${card.policy.candidate.conflicts.length} · incumbent: ${card.policy.candidate.incumbent ?? "none"}`,
|
|
109
|
+
` scope suggestion: ${card.policy.candidate.scope_suggestion ? JSON.stringify(card.policy.candidate.scope_suggestion) : "none — narrow compiled scope retained"}`,
|
|
110
|
+
` exception parent: ${card.policy.exception_of ?? "none"}`,
|
|
111
|
+
` ${line("current", card.evidence_vector.current)}`,
|
|
112
|
+
` ${line("known bad", card.evidence_vector.known_bad)}`,
|
|
113
|
+
` ${line("known good", card.evidence_vector.known_good)}`,
|
|
114
|
+
` ${line("accepted history", card.evidence_vector.accepted_history)}`,
|
|
115
|
+
` history dispositions: ${card.history_dispositions.current.length} current · ${card.history_dispositions.counts.true_positive_actionable} actionable true positive · ${card.history_dispositions.unresolved_count} unresolved`,
|
|
116
|
+
` ${line("mutations", card.evidence_vector.mutations)}`,
|
|
117
|
+
` mutation controls: ${card.evidence_vector.mutation_controls.passed}/${card.evidence_vector.mutation_controls.total} passed · ${card.evidence_vector.mutation_controls.failed} failed`,
|
|
118
|
+
` project checks: build ${card.project_checks.build} · test ${card.project_checks.test} · never required for evaluator sensitivity`,
|
|
119
|
+
...(card.composition ? [` composition: ${card.composition.root_policy_id} + ${card.composition.members.length} exception(s) · ${card.composition.composite_hash}`] : []),
|
|
120
|
+
...(card.shadow_precision ? [` shadow: ${card.shadow_precision.window.applicable} recent applicable · ${card.shadow_precision.window.violated} violated · precision ${card.shadow_precision.precision.confirmed == null ? "n/a" : (card.shadow_precision.precision.confirmed * 100).toFixed(1) + "%"} · ${card.shadow_precision.recommendation}`] : []),
|
|
121
|
+
` uncertainty: ${card.uncertainty.unclassified_history_hits} unclassified history hit · ${card.uncertainty.unknown_results} unknown · ${card.uncertainty.error_results} error`,
|
|
122
|
+
` blocking readiness: ${card.authority.eligible_for_human_blocking_approval ? "eligible for explicit human review" : `not eligible${card.authority.blocking_evidence_error ? ` — ${card.authority.blocking_evidence_error}` : ""}`}`,
|
|
123
|
+
` authority: ${card.authority.current?.kind === "human" ? card.authority.current.actor : "none — proof cannot activate policy"}`,
|
|
124
|
+
...card.uncertainty.limitations.map((limitation) => ` limitation: ${limitation}`),
|
|
125
|
+
...card.uncertainty.compiler_uncertainty.map((uncertainty) => ` compiler uncertainty: ${uncertainty}`),
|
|
126
|
+
...card.uncertainty.candidate_conflicts.map((conflict) => ` candidate conflict: ${conflict}`),
|
|
127
|
+
...card.history_dispositions.current.map((disposition) => ` history disposition: ${disposition.commit} · ${disposition.classification} · ${disposition.actor} · ${disposition.id}`),
|
|
128
|
+
...card.policy.candidate.counterexamples.map((counterexample) => ` counterexample: ${counterexample}`),
|
|
129
|
+
...card.actions.map((action) => ` action: ${action}`),
|
|
130
|
+
` card: ${card.card_hash}`,
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=card.js.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { policyId } from "./canonical.js";
|
|
2
|
+
import { POLICY_IR_VERSION, PolicySpecSchema } from "./schema.js";
|
|
3
|
+
function selector(ref) {
|
|
4
|
+
if (ref.startsWith("symbol-id:") || ref.startsWith("symbol:"))
|
|
5
|
+
return { selector: ref };
|
|
6
|
+
return { selector: ref.startsWith("sym_") ? `symbol-id:${ref}` : `symbol:${ref}` };
|
|
7
|
+
}
|
|
8
|
+
function compileAssertion(predicate, through) {
|
|
9
|
+
if (predicate.assert === "exists")
|
|
10
|
+
return { kind: "exists", subject: selector(predicate.subject) };
|
|
11
|
+
if (!predicate.object)
|
|
12
|
+
throw new Error(`legacy ${predicate.assert} predicate has no object`);
|
|
13
|
+
const relation = {
|
|
14
|
+
// Legacy conformance deliberately treated calls/imports as one unified graph.
|
|
15
|
+
// Preserve that verdict exactly in the bridge instead of pretending edge-type
|
|
16
|
+
// precision already exists.
|
|
17
|
+
edges: ["calls", "imports", "depends_on", "contains"],
|
|
18
|
+
transitive: predicate.transitive,
|
|
19
|
+
max_depth: predicate.transitive ? 6 : 1,
|
|
20
|
+
};
|
|
21
|
+
const negative = predicate.assert === "not-calls" || predicate.assert === "not-imports";
|
|
22
|
+
if (through) {
|
|
23
|
+
if (!negative)
|
|
24
|
+
throw new Error("--through can only upgrade a not-calls/not-imports boundary into must-pass-through");
|
|
25
|
+
return {
|
|
26
|
+
kind: "must-pass-through",
|
|
27
|
+
subject: selector(predicate.subject),
|
|
28
|
+
relation: { ...relation, transitive: true, max_depth: 6 },
|
|
29
|
+
via: selector(through),
|
|
30
|
+
object: selector(predicate.object),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: negative ? "not-reaches" : "reaches",
|
|
35
|
+
subject: selector(predicate.subject),
|
|
36
|
+
relation,
|
|
37
|
+
object: selector(predicate.object),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function compileDecisionRecord(store, source, isPrivate, opts = {}) {
|
|
41
|
+
if (source.status === "rejected" || source.status === "superseded" || source.superseded_by) {
|
|
42
|
+
throw new Error(`decision ${source.id} is not in force`);
|
|
43
|
+
}
|
|
44
|
+
if (source.conformance?.length !== 1) {
|
|
45
|
+
throw new Error(`Gate G1 compiles exactly one structured conformance predicate; ${source.id} has ${source.conformance?.length ?? 0}`);
|
|
46
|
+
}
|
|
47
|
+
const assertion = compileAssertion(source.conformance[0], opts.through);
|
|
48
|
+
if (isPrivate && !store.hasPrivate)
|
|
49
|
+
throw new Error("private compilation needs a configured Hunch private overlay");
|
|
50
|
+
const now = opts.now ?? new Date().toISOString();
|
|
51
|
+
const id = policyId({ source: source.id, assertion });
|
|
52
|
+
const policy = PolicySpecSchema.parse({
|
|
53
|
+
id,
|
|
54
|
+
topic: source.topic ?? `decision.${source.id}`,
|
|
55
|
+
ir_version: POLICY_IR_VERSION,
|
|
56
|
+
revision: 1,
|
|
57
|
+
state: "compiled",
|
|
58
|
+
statement: source.title,
|
|
59
|
+
rationale: source.context || source.decision,
|
|
60
|
+
scope: {
|
|
61
|
+
repos: [],
|
|
62
|
+
paths: source.related_files.filter((f) => !f.startsWith("private:")),
|
|
63
|
+
components: [...source.related_components],
|
|
64
|
+
},
|
|
65
|
+
assertion,
|
|
66
|
+
severity: "warning",
|
|
67
|
+
surfaces: ["cli", "mcp", "ci"],
|
|
68
|
+
authority: null,
|
|
69
|
+
evidence: [source.id, ...(source.caused_by_bug ? [source.caused_by_bug] : [])],
|
|
70
|
+
proof: null,
|
|
71
|
+
reversal_conditions: [`Source decision ${source.id} is superseded or explicitly retired.`],
|
|
72
|
+
supersedes: null,
|
|
73
|
+
superseded_by: null,
|
|
74
|
+
valid_from: null,
|
|
75
|
+
valid_to: null,
|
|
76
|
+
data_class: isPrivate ? "private" : "public",
|
|
77
|
+
limitations: [
|
|
78
|
+
"TypeScript/JavaScript static graph only in Gate G1.",
|
|
79
|
+
"Dynamic calls and runtime dependency injection are not covered.",
|
|
80
|
+
...(opts.through ? [] : ["Legacy bridge preserves unified calls/imports/depends_on/contains reachability semantics."]),
|
|
81
|
+
],
|
|
82
|
+
legacy_refs: [source.id],
|
|
83
|
+
audit: [{ action: "compiled", actor_kind: "system", actor: "hunch:deterministic-compiler", at: now, reason: "Compiled from one structured Decision.conformance predicate.", proof: null }],
|
|
84
|
+
created_at: now,
|
|
85
|
+
updated_at: now,
|
|
86
|
+
provenance: { source: "derived", confidence: 1, evidence: [source.id], last_verified: now },
|
|
87
|
+
});
|
|
88
|
+
return { policy, private: isPrivate, source };
|
|
89
|
+
}
|
|
90
|
+
/** Deterministic compatibility compiler: one structured legacy conformance
|
|
91
|
+
* predicate becomes one Policy IR candidate. It never interprets prose into a
|
|
92
|
+
* nearest available rule. */
|
|
93
|
+
export function compileDecisionPolicy(store, decisionId, opts = {}) {
|
|
94
|
+
const source = store.getRec("decisions", decisionId);
|
|
95
|
+
if (!source)
|
|
96
|
+
throw new Error(`decision ${decisionId} not found`);
|
|
97
|
+
// A private source always taints its compiled artifact. `--private` can
|
|
98
|
+
// promote a public source into the overlay, but an omitted/false CLI flag
|
|
99
|
+
// must never declassify a same-id private overlay record into the public
|
|
100
|
+
// repository.
|
|
101
|
+
const isPrivate = !!opts.private || !!store.getPrivateRec("decisions", decisionId);
|
|
102
|
+
return compileDecisionRecord(store, source, isPrivate, { through: opts.through, now: opts.now });
|
|
103
|
+
}
|
|
104
|
+
/** Compile one already-enumerated structural assertion. This function does not
|
|
105
|
+
* infer or rank semantics; callers must pass an exact supported candidate. */
|
|
106
|
+
export function compileStructuralPolicy(store, input) {
|
|
107
|
+
const isPrivate = input.dataClass !== "public";
|
|
108
|
+
if (isPrivate && !store.hasPrivate)
|
|
109
|
+
throw new Error("private structural compilation needs a configured Hunch private overlay");
|
|
110
|
+
const now = input.now ?? new Date().toISOString();
|
|
111
|
+
const externalImport = input.assertion.kind === "not-reaches"
|
|
112
|
+
&& input.assertion.relation.edges.length === 1
|
|
113
|
+
&& input.assertion.relation.edges[0] === "imports"
|
|
114
|
+
&& input.assertion.object.selector.startsWith("external:");
|
|
115
|
+
const componentRelation = input.assertion.kind !== "exists"
|
|
116
|
+
&& input.assertion.kind !== "executable-behavior"
|
|
117
|
+
&& input.assertion.relation.edges.length === 1
|
|
118
|
+
&& input.assertion.relation.edges[0] === "depends_on"
|
|
119
|
+
&& input.assertion.subject.selector.startsWith("component");
|
|
120
|
+
const id = policyId({ assertion: input.assertion, scope: input.scope, data_class: input.dataClass });
|
|
121
|
+
const policy = PolicySpecSchema.parse({
|
|
122
|
+
id,
|
|
123
|
+
topic: input.source.topic ?? `decision.${input.source.id}`,
|
|
124
|
+
ir_version: POLICY_IR_VERSION,
|
|
125
|
+
revision: 1,
|
|
126
|
+
state: "compiled",
|
|
127
|
+
statement: input.source.title,
|
|
128
|
+
rationale: input.source.context || input.source.decision,
|
|
129
|
+
scope: input.scope,
|
|
130
|
+
assertion: input.assertion,
|
|
131
|
+
severity: "warning",
|
|
132
|
+
surfaces: ["cli", "mcp", "ci"],
|
|
133
|
+
authority: null,
|
|
134
|
+
evidence: [input.source.id, input.evidenceId, `commit:${input.commit}`],
|
|
135
|
+
proof: null,
|
|
136
|
+
reversal_conditions: [`Source decision ${input.source.id} is superseded or the structural interpretation is rejected.`],
|
|
137
|
+
supersedes: null,
|
|
138
|
+
superseded_by: null,
|
|
139
|
+
valid_from: null,
|
|
140
|
+
valid_to: null,
|
|
141
|
+
data_class: input.dataClass,
|
|
142
|
+
limitations: [
|
|
143
|
+
"Inferred from one exact first-parent Git structural delta; replay coverage is established only by a later plan-bound proof.",
|
|
144
|
+
externalImport
|
|
145
|
+
? "Scope is intentionally limited to the changed file and anchored to one stable symbol in that file."
|
|
146
|
+
: componentRelation
|
|
147
|
+
? "Scope is intentionally limited to the exact source and target components resolved from one relative static import."
|
|
148
|
+
: "Scope is intentionally limited to the changed caller or introduced-symbol file.",
|
|
149
|
+
externalImport
|
|
150
|
+
? "TypeScript/JavaScript static ESM import/export specifiers only; require(), dynamic import(), package aliases, and runtime loading are not covered."
|
|
151
|
+
: componentRelation
|
|
152
|
+
? "TypeScript/JavaScript static relative ESM import/export specifiers only; aliases, absolute paths, import maps, require(), dynamic import(), and runtime loading are not covered."
|
|
153
|
+
: "TypeScript/JavaScript static calls only; dynamic calls and runtime dependency injection are not covered.",
|
|
154
|
+
],
|
|
155
|
+
candidate: input.candidate,
|
|
156
|
+
legacy_refs: [input.source.id],
|
|
157
|
+
audit: [{
|
|
158
|
+
action: "compiled",
|
|
159
|
+
actor_kind: "system",
|
|
160
|
+
actor: "hunch:structural-delta-compiler",
|
|
161
|
+
at: now,
|
|
162
|
+
reason: `Compiled from one unambiguous assertion enumerated by evidence ${input.evidenceId}.`,
|
|
163
|
+
proof: null,
|
|
164
|
+
}],
|
|
165
|
+
created_at: now,
|
|
166
|
+
updated_at: now,
|
|
167
|
+
provenance: {
|
|
168
|
+
source: "derived",
|
|
169
|
+
confidence: 0.8,
|
|
170
|
+
evidence: [input.source.id, input.evidenceId, `commit:${input.commit}`],
|
|
171
|
+
last_verified: now,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return { policy, private: isPrivate };
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=compiler.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { pathMatchesGlob } from "../core/glob.js";
|
|
2
|
+
import { canonicalHash, canonicalJson, policySemanticHash } from "./canonical.js";
|
|
3
|
+
import { PolicyCompositionBindingSchema } from "./schema.js";
|
|
4
|
+
function sameValues(left, right) {
|
|
5
|
+
const a = [...left].sort();
|
|
6
|
+
const b = [...right].sort();
|
|
7
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
8
|
+
}
|
|
9
|
+
export function oppositeExceptionAssertions(child, parent) {
|
|
10
|
+
const left = child.assertion;
|
|
11
|
+
const right = parent.assertion;
|
|
12
|
+
if (!((left.kind === "reaches" && right.kind === "not-reaches") || (left.kind === "not-reaches" && right.kind === "reaches")))
|
|
13
|
+
return false;
|
|
14
|
+
return left.subject.selector === right.subject.selector
|
|
15
|
+
&& left.object.selector === right.object.selector
|
|
16
|
+
&& canonicalJson(left.relation) === canonicalJson(right.relation);
|
|
17
|
+
}
|
|
18
|
+
export function exceptionScopeIsNarrower(child, parent) {
|
|
19
|
+
const reposInside = !parent.scope.repos.length || child.scope.repos.every((repo) => parent.scope.repos.includes(repo));
|
|
20
|
+
const componentsInside = !parent.scope.components.length || child.scope.components.every((component) => parent.scope.components.includes(component));
|
|
21
|
+
const pathsInside = !parent.scope.paths.length || child.scope.paths.every((path) => parent.scope.paths.some((glob) => path === glob || pathMatchesGlob(path, glob)));
|
|
22
|
+
const strict = !sameValues(child.scope.repos, parent.scope.repos)
|
|
23
|
+
|| !sameValues(child.scope.paths, parent.scope.paths)
|
|
24
|
+
|| !sameValues(child.scope.components, parent.scope.components);
|
|
25
|
+
return reposInside && componentsInside && pathsInside && strict;
|
|
26
|
+
}
|
|
27
|
+
export function validateExceptionRelationship(child, parent) {
|
|
28
|
+
if (child.id === parent.id)
|
|
29
|
+
throw new Error("a policy cannot be its own exception parent");
|
|
30
|
+
if (child.exception_of !== parent.id)
|
|
31
|
+
throw new Error(`exception policy ${child.id} does not link parent ${parent.id}`);
|
|
32
|
+
if (child.data_class !== parent.data_class)
|
|
33
|
+
throw new Error("exception and parent must have the same data class");
|
|
34
|
+
if (!oppositeExceptionAssertions(child, parent))
|
|
35
|
+
throw new Error("exception must be the exact opposite reaches/not-reaches assertion over the same bindings and relation");
|
|
36
|
+
if (!exceptionScopeIsNarrower(child, parent))
|
|
37
|
+
throw new Error("exception scope must be strictly narrower than and contained by its parent scope");
|
|
38
|
+
}
|
|
39
|
+
/** Return every explicit descendant of a broad root. Parent links define the
|
|
40
|
+
* precedence tree; stable policy-id ordering keeps the binding canonical. */
|
|
41
|
+
export function compositionDescendants(root, policies) {
|
|
42
|
+
if (root.exception_of)
|
|
43
|
+
return [];
|
|
44
|
+
const byParent = new Map();
|
|
45
|
+
for (const policy of policies) {
|
|
46
|
+
if (!policy.exception_of)
|
|
47
|
+
continue;
|
|
48
|
+
const children = byParent.get(policy.exception_of) ?? [];
|
|
49
|
+
children.push(policy);
|
|
50
|
+
byParent.set(policy.exception_of, children);
|
|
51
|
+
}
|
|
52
|
+
const out = [];
|
|
53
|
+
const visited = new Set([root.id]);
|
|
54
|
+
const queue = [...(byParent.get(root.id) ?? [])].sort((a, b) => a.id.localeCompare(b.id));
|
|
55
|
+
while (queue.length) {
|
|
56
|
+
const policy = queue.shift();
|
|
57
|
+
if (visited.has(policy.id))
|
|
58
|
+
throw new Error(`exception composition contains a cycle at ${policy.id}`);
|
|
59
|
+
visited.add(policy.id);
|
|
60
|
+
out.push(policy);
|
|
61
|
+
queue.push(...[...(byParent.get(policy.id) ?? [])].sort((a, b) => a.id.localeCompare(b.id)));
|
|
62
|
+
}
|
|
63
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
64
|
+
}
|
|
65
|
+
export function policyCompositionBinding(root, members) {
|
|
66
|
+
if (!members.length)
|
|
67
|
+
return undefined;
|
|
68
|
+
const byId = new Map([root, ...members].map((policy) => [policy.id, policy]));
|
|
69
|
+
if (byId.size !== members.length + 1)
|
|
70
|
+
throw new Error("exception composition contains duplicate policy ids");
|
|
71
|
+
for (const member of members) {
|
|
72
|
+
const parent = member.exception_of ? byId.get(member.exception_of) : undefined;
|
|
73
|
+
if (!parent)
|
|
74
|
+
throw new Error(`exception policy ${member.id} has missing composition parent ${member.exception_of ?? "null"}`);
|
|
75
|
+
validateExceptionRelationship(member, parent);
|
|
76
|
+
}
|
|
77
|
+
const body = {
|
|
78
|
+
kind: "parent_with_exceptions",
|
|
79
|
+
root_policy_id: root.id,
|
|
80
|
+
root_policy_hash: policySemanticHash(root),
|
|
81
|
+
members: [...members]
|
|
82
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
83
|
+
.map((policy) => ({
|
|
84
|
+
policy_id: policy.id,
|
|
85
|
+
policy_hash: policySemanticHash(policy),
|
|
86
|
+
exception_of: policy.exception_of,
|
|
87
|
+
scope: policy.scope,
|
|
88
|
+
})),
|
|
89
|
+
};
|
|
90
|
+
return PolicyCompositionBindingSchema.parse({ ...body, composite_hash: canonicalHash(body) });
|
|
91
|
+
}
|
|
92
|
+
export function policyProofHash(root, members = []) {
|
|
93
|
+
return policyCompositionBinding(root, members)?.composite_hash ?? policySemanticHash(root);
|
|
94
|
+
}
|
|
95
|
+
export function assertCompositionBinding(root, members, binding) {
|
|
96
|
+
const expected = policyCompositionBinding(root, members);
|
|
97
|
+
if (canonicalJson(expected) !== canonicalJson(binding)) {
|
|
98
|
+
throw new Error(`proof artifacts do not match the current parent/exception composition for policy ${root.id}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=composition.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { shortHash } from "../core/ids.js";
|
|
3
|
+
import { revExists, revParse } from "../extractors/git.js";
|
|
4
|
+
import { canonicalHash, policySemanticHash } from "./canonical.js";
|
|
5
|
+
import { ProofCorpusInputSchema, ProofCorpusSchema, } from "./schema.js";
|
|
6
|
+
function resolveFixture(root, fixture, expected) {
|
|
7
|
+
if (!revExists(fixture.ref, root))
|
|
8
|
+
throw new Error(`corpus fixture ref ${fixture.ref} does not resolve to a commit`);
|
|
9
|
+
const ref = revParse(`${fixture.ref}^{commit}`, root);
|
|
10
|
+
if (!/^[a-f0-9]{40}$/.test(ref))
|
|
11
|
+
throw new Error(`corpus fixture ref ${fixture.ref} did not resolve to a full commit SHA`);
|
|
12
|
+
return {
|
|
13
|
+
kind: "commit",
|
|
14
|
+
ref,
|
|
15
|
+
label: fixture.label,
|
|
16
|
+
expected,
|
|
17
|
+
...(fixture.attestation ? { attestation: fixture.attestation } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function sorted(fixtures) {
|
|
21
|
+
return fixtures.sort((a, b) => a.ref.localeCompare(b.ref) || a.label.localeCompare(b.label));
|
|
22
|
+
}
|
|
23
|
+
/** Resolve human-authored Git refs once, then persist an immutable, policy-bound
|
|
24
|
+
* fixture manifest. No checkout, evaluator, model, provider, or project command runs. */
|
|
25
|
+
export function compileProofCorpus(root, policy, raw, opts = {}) {
|
|
26
|
+
const input = ProofCorpusInputSchema.parse(raw);
|
|
27
|
+
const knownBad = sorted(input.known_bad.map((fixture) => resolveFixture(root, fixture, "violated")));
|
|
28
|
+
const knownGood = sorted(input.known_good.map((fixture) => resolveFixture(root, fixture, "satisfied")));
|
|
29
|
+
const seen = new Map();
|
|
30
|
+
for (const [leg, fixtures] of [["known_bad", knownBad], ["known_good", knownGood]]) {
|
|
31
|
+
for (const fixture of fixtures) {
|
|
32
|
+
const existing = seen.get(fixture.ref);
|
|
33
|
+
if (existing)
|
|
34
|
+
throw new Error(`corpus fixture commit ${fixture.ref} is duplicated in ${existing} and ${leg}`);
|
|
35
|
+
seen.set(fixture.ref, leg);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const body = {
|
|
39
|
+
policy_id: policy.id,
|
|
40
|
+
policy_hash: policySemanticHash(policy),
|
|
41
|
+
repository: basename(root),
|
|
42
|
+
data_class: policy.data_class,
|
|
43
|
+
known_bad: knownBad,
|
|
44
|
+
known_good: knownGood,
|
|
45
|
+
};
|
|
46
|
+
const contentHash = canonicalHash(body);
|
|
47
|
+
return ProofCorpusSchema.parse({
|
|
48
|
+
id: `corpus_${shortHash(contentHash)}`,
|
|
49
|
+
content_hash: contentHash,
|
|
50
|
+
...body,
|
|
51
|
+
created_at: opts.now ?? new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function proofCorpusContentHash(corpus) {
|
|
55
|
+
const { id: _id, content_hash: _contentHash, created_at: _createdAt, ...body } = corpus;
|
|
56
|
+
return canonicalHash(body);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=corpus.js.map
|