@holdyourvoice/hyv 3.6.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +91 -96
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +19 -13
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +15 -35
- package/dist/cli/io.js +41 -37
- package/dist/copy-spec.js +6 -6
- package/dist/editorial-packs.js +4 -3
- package/dist/fact-linter.js +45 -27
- package/dist/hidden-text.js +6 -6
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/internal.js +8 -0
- package/dist/judgment-task.js +10 -14
- package/dist/learning.js +66 -67
- package/dist/lifecycle-adapter.js +2 -5
- package/dist/local-eval.js +16 -31
- package/dist/mcp-server.js +272 -0
- package/dist/mcp-tools.js +2 -2
- package/dist/mcp.js +2 -261
- package/dist/pipeline.js +9 -85
- package/dist/profile-compose.js +18 -11
- package/dist/profile-score.js +6 -6
- package/dist/profile.js +17 -19
- package/dist/provenance-status.js +1 -3
- package/dist/rebuild-task.js +14 -25
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +82 -0
- package/dist/rewrite-task.js +66 -65
- package/dist/rule-allowances.js +6 -10
- package/dist/semantic-review.js +71 -47
- package/dist/stage1-evaluation.js +29 -14
- package/dist/strict-quality.js +10 -18
- package/dist/version.js +1 -1
- package/dist/voice-dna.js +34 -21
- package/dist/writing-examples.js +2 -7
- package/package.json +6 -3
- package/skills/hyv-prepare-judgment/SKILL.md +4 -0
- package/dist/agents/catalog.test.js +0 -60
- package/dist/agents/emit.test.js +0 -64
- package/dist/agents/load.test.js +0 -149
- package/dist/ai-editor.test.js +0 -265
- package/dist/approval-capability.test.js +0 -52
- package/dist/approval-context.test.js +0 -38
- package/dist/backtest.test.js +0 -20
- package/dist/benchmark.test.js +0 -328
- package/dist/canonical-json.test.js +0 -24
- package/dist/cli/context.test.js +0 -55
- package/dist/cli.test.js +0 -731
- package/dist/editorial-packs.test.js +0 -94
- package/dist/fact-linter.test.js +0 -85
- package/dist/hidden-text.test.js +0 -26
- package/dist/hygiene.test.js +0 -83
- package/dist/judgment-task.test.js +0 -162
- package/dist/learning.test.js +0 -325
- package/dist/lifecycle-adapter.test.js +0 -56
- package/dist/local-eval.test.js +0 -20
- package/dist/logic-linter-corpus.test.js +0 -22
- package/dist/logic-linter.test.js +0 -39
- package/dist/mcp-tools.test.js +0 -286
- package/dist/mcp.test.js +0 -312
- package/dist/mirror-refs.test.js +0 -63
- package/dist/pipeline.test.js +0 -247
- package/dist/preservation.test.js +0 -22
- package/dist/production-gates.test.js +0 -34
- package/dist/profile-compose.test.js +0 -32
- package/dist/profile-score.test.js +0 -22
- package/dist/profile-watch.test.js +0 -23
- package/dist/profile.test.js +0 -141
- package/dist/provenance-status.test.js +0 -22
- package/dist/rebuild-task.test.js +0 -206
- package/dist/recomposition.test.js +0 -34
- package/dist/release-audit.test.js +0 -292
- package/dist/rewrite-task.test.js +0 -166
- package/dist/rule-allowances.test.js +0 -17
- package/dist/rule-reconciliation.test.js +0 -50
- package/dist/sample-ingest.test.js +0 -52
- package/dist/semantic-review.test.js +0 -101
- package/dist/stage1-dry-run.test.js +0 -39
- package/dist/stage1-evaluation.test.js +0 -184
- package/dist/stage1-human-packet.test.js +0 -102
- package/dist/stage1-schema-contract.test.js +0 -95
- package/dist/stage2-human-packet.test.js +0 -81
- package/dist/strict-quality.test.js +0 -62
- package/dist/text-provenance.feature.test.js +0 -45
- package/dist/text.test.js +0 -16
- package/dist/voice-dna.test.js +0 -121
- package/dist/writing-examples.test.js +0 -35
package/dist/fact-linter.js
CHANGED
|
@@ -5,11 +5,13 @@ const QUOTE = /["“]([^"”]+)["”]/;
|
|
|
5
5
|
function normal(value) { return value.toLowerCase().replace(/[^\p{L}\p{N}%]+/gu, ' ').trim(); }
|
|
6
6
|
function tokens(value) { return normal(value).split(' ').filter((word) => word.length > 1 && !STOP_WORDS.has(word)); }
|
|
7
7
|
function evidence(source, text) { const start = source.text.indexOf(text); return { sourceId: source.id, excerpt: text, start: Math.max(0, start), end: Math.max(0, start) + text.length }; }
|
|
8
|
-
function sourceSentences(sources) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
function sourceSentences(sources) {
|
|
9
|
+
return sources.flatMap((source) => sentences(source.text).map((sentence) => ({
|
|
10
|
+
source, text: sentence.text, terms: new Set(tokens(sentence.text)),
|
|
11
|
+
})));
|
|
12
|
+
}
|
|
13
|
+
function hasOverlap(claimTerms, source) {
|
|
14
|
+
return claimTerms.length > 0 && claimTerms.filter((term) => source.terms.has(term)).length / claimTerms.length >= 0.8;
|
|
13
15
|
}
|
|
14
16
|
function dates(value) { return [...value.matchAll(DATE)].map((match) => new Date(match[0]).toISOString().slice(0, 10)).filter((value) => value !== ''); }
|
|
15
17
|
function numbers(value) { return value.match(/\b\d+(?:\.\d+)?\s*(?:%|days?|hours?|weeks?|months?|years?)?\b/gi) ?? []; }
|
|
@@ -46,13 +48,11 @@ function kindFor(text) {
|
|
|
46
48
|
kinds.push('fact');
|
|
47
49
|
return kinds;
|
|
48
50
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
return sources.filter(({ text }) => terms.some((term) => tokens(text).includes(term)));
|
|
51
|
+
function evidenceFor(lines) {
|
|
52
|
+
return lines.map(({ source, text }) => evidence(source, text));
|
|
52
53
|
}
|
|
53
54
|
function fallbackEvidence(sources) {
|
|
54
|
-
|
|
55
|
-
return line ? [evidence(line.source, line.text)] : [];
|
|
55
|
+
return evidenceFor(sources.slice(0, 1));
|
|
56
56
|
}
|
|
57
57
|
function finding(claim, kind, severity, reason, evidenceItems, confidence, suggestedAction) {
|
|
58
58
|
return { severity, kind, claim: claim.text, draftLocation: { sentence: claim.sentence, start: claim.start, end: claim.end }, reason, evidence: evidenceItems, confidence, suggestedAction };
|
|
@@ -91,14 +91,15 @@ function capabilityFinding(claim, relevant, evidenceItems) {
|
|
|
91
91
|
return finding(claim, 'missing_evidence', 'needs_human_review', 'The product capability is not established by the relevant source wording.', evidenceItems, 'medium', 'Confirm the capability with a reviewer or add evidence.');
|
|
92
92
|
}
|
|
93
93
|
function findingsForClaim(claim, input, sourceLines, semanticAdapter) {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
94
|
+
const claimTerms = tokens(claim.text);
|
|
95
|
+
const relevant = sourceLines.filter((source) => claimTerms.some((term) => source.terms.has(term)));
|
|
96
|
+
const same = sourceLines.find((source) => hasOverlap(claimTerms, source));
|
|
97
|
+
const evidenceItems = relevant.length ? evidenceFor(relevant.slice(0, 2)) : fallbackEvidence(sourceLines);
|
|
97
98
|
const quote = claim.text.match(QUOTE)?.[1];
|
|
98
99
|
const sourceHasAttribution = input.sources.some((source) => /\b(said|according to|reported)\b/i.test(source.text));
|
|
99
100
|
if (quote && sourceHasAttribution && !input.sources.some((source) => source.text.includes(quote))) {
|
|
100
101
|
const quoteRelevant = sourceLines.filter(({ text }) => /\b(said|according to|reported)\b/i.test(text));
|
|
101
|
-
const quoteEvidence = quoteRelevant.length ? quoteRelevant.slice(0, 2)
|
|
102
|
+
const quoteEvidence = quoteRelevant.length ? evidenceFor(quoteRelevant.slice(0, 2)) : input.sources.slice(0, 1).map((source) => evidence(source, source.text));
|
|
102
103
|
return [finding(claim, 'quote_drift', 'error', 'The quoted wording differs from the supplied source.', quoteEvidence, 'high', 'Use the source wording or label the text as a paraphrase.')];
|
|
103
104
|
}
|
|
104
105
|
const claimDates = dates(claim.text);
|
|
@@ -132,7 +133,7 @@ function findingsForClaim(claim, input, sourceLines, semanticAdapter) {
|
|
|
132
133
|
return overreach;
|
|
133
134
|
if (claimDates.some((date) => sourceDates.includes(date)) || same)
|
|
134
135
|
return [];
|
|
135
|
-
if (!relevant.length && claim.kinds.includes('fact') &&
|
|
136
|
+
if (!relevant.length && claim.kinds.includes('fact') && claimTerms.length <= 4) {
|
|
136
137
|
return [finding(claim, 'missing_evidence', 'needs_human_review', 'No close source evidence was found; the wording is too sparse for a reliable deterministic verdict.', evidenceItems, 'low', 'Confirm with a reviewer or provide a source.')];
|
|
137
138
|
}
|
|
138
139
|
if (!relevant.length || (claim.kinds.includes('number') && !relevant.some(({ text }) => /\b\d+(?:\.\d+)?%?\b/.test(text)))) {
|
|
@@ -147,29 +148,46 @@ function findingsForClaim(claim, input, sourceLines, semanticAdapter) {
|
|
|
147
148
|
return [finding(claim, 'missing_evidence', 'needs_human_review', 'Relevant source material exists, but deterministic matching could not establish support.', evidenceItems, 'low', 'Review the source context or enable an approved semantic adapter.')];
|
|
148
149
|
}
|
|
149
150
|
function draftContradictions(claims, sourceLines) {
|
|
150
|
-
const normalized = claims.map((claim) =>
|
|
151
|
+
const normalized = claims.map((claim) => {
|
|
152
|
+
const text = normal(claim.text);
|
|
153
|
+
return { claim, core: normal(text.replace(/\bnot\b/g, '')), negated: /\bnot\b/.test(text) };
|
|
154
|
+
});
|
|
155
|
+
const byCore = new Map();
|
|
156
|
+
normalized.forEach(({ core }, index) => {
|
|
157
|
+
const indexes = byCore.get(core) ?? [];
|
|
158
|
+
indexes.push(index);
|
|
159
|
+
byCore.set(core, indexes);
|
|
160
|
+
});
|
|
151
161
|
const findings = [];
|
|
152
162
|
for (let index = 0; index < normalized.length; index += 1) {
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
const left = normalized[index];
|
|
164
|
+
for (const other of byCore.get(left.core)) {
|
|
155
165
|
const right = normalized[other];
|
|
156
|
-
|
|
157
|
-
const rightCore = normal(right.text.replace(/\bnot\b/g, ''));
|
|
158
|
-
if (leftCore === rightCore && /\bnot\b/.test(left.text) !== /\bnot\b/.test(right.text)) {
|
|
166
|
+
if (other > index && left.negated !== right.negated) {
|
|
159
167
|
findings.push(finding(right.claim, 'draft_contradiction', 'error', 'This draft claim contradicts an earlier draft claim.', fallbackEvidence(sourceLines), 'high', 'Resolve the two claims before publishing.'));
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
return findings;
|
|
164
172
|
}
|
|
173
|
+
const CONTRADICTIONS = new Set(['draft_contradiction', 'number_drift', 'date_drift', 'entity_drift', 'quote_drift', 'capability_drift', 'semantic_contradiction']);
|
|
165
174
|
function summarizeFindings(claims, findings) {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
175
|
+
const unsupported = new Set();
|
|
176
|
+
const contradicted = new Set();
|
|
177
|
+
const humanReview = new Set();
|
|
178
|
+
const affected = new Set();
|
|
179
|
+
for (const item of findings) {
|
|
180
|
+
const key = `${item.draftLocation.start}:${item.draftLocation.end}`;
|
|
181
|
+
affected.add(key);
|
|
182
|
+
if (item.kind === 'unsupported_claim')
|
|
183
|
+
unsupported.add(key);
|
|
184
|
+
if (CONTRADICTIONS.has(item.kind))
|
|
185
|
+
contradicted.add(key);
|
|
186
|
+
if (item.severity === 'needs_human_review' || item.severity === 'warning')
|
|
187
|
+
humanReview.add(key);
|
|
188
|
+
}
|
|
170
189
|
const checked = claims.filter((claim) => !claim.kinds.includes('opinion')).length;
|
|
171
|
-
|
|
172
|
-
return { checked, supported: Math.max(0, checked - affected), unsupported, contradicted, humanReview };
|
|
190
|
+
return { checked, supported: Math.max(0, checked - affected.size), unsupported: unsupported.size, contradicted: contradicted.size, humanReview: humanReview.size };
|
|
173
191
|
}
|
|
174
192
|
export function lintFacts(input) {
|
|
175
193
|
if (!input.sources.length)
|
package/dist/hidden-text.js
CHANGED
|
@@ -48,13 +48,13 @@ export function inspectHiddenText(text, policy = minimalHiddenTextPolicy) {
|
|
|
48
48
|
}
|
|
49
49
|
export function applyHiddenTextPolicy(text, policy = minimalHiddenTextPolicy) {
|
|
50
50
|
const report = inspectHiddenText(text, policy);
|
|
51
|
-
const output =
|
|
52
|
-
const
|
|
53
|
-
const again =
|
|
54
|
-
return { ...report, outputHash: hash(output), output, remaining, idempotent: again === output };
|
|
51
|
+
const output = removeProposedChanges(text, report.proposedChanges);
|
|
52
|
+
const remainingReport = inspectHiddenText(output, policy);
|
|
53
|
+
const again = removeProposedChanges(output, remainingReport.proposedChanges);
|
|
54
|
+
return { ...report, outputHash: hash(output), output, remaining: remainingReport.findings, idempotent: again === output };
|
|
55
55
|
}
|
|
56
|
-
function
|
|
57
|
-
const offsets = new Set(
|
|
56
|
+
function removeProposedChanges(text, changes) {
|
|
57
|
+
const offsets = new Set(changes.map((item) => item.offset));
|
|
58
58
|
let output = '';
|
|
59
59
|
for (let offset = 0; offset < text.length;) {
|
|
60
60
|
const value = text.codePointAt(offset);
|
|
Binary file
|
package/dist/internal.js
CHANGED
|
@@ -28,3 +28,11 @@ export function isText(value, maximum = 256) {
|
|
|
28
28
|
export function escaped(value) {
|
|
29
29
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
30
|
}
|
|
31
|
+
export function verificationMatchesBinding(verification, binding) {
|
|
32
|
+
return verification.artifactFingerprint === binding.deterministicArtifactFingerprint
|
|
33
|
+
&& verification.sourceHash === binding.sourceHash
|
|
34
|
+
&& verification.candidateHash === binding.candidateHash
|
|
35
|
+
&& verification.profileId === binding.profileId
|
|
36
|
+
&& verification.profileRevisionDigest === binding.profileRevisionDigest
|
|
37
|
+
&& verification.rulesetVersion === binding.rulesetVersion;
|
|
38
|
+
}
|
package/dist/judgment-task.js
CHANGED
|
@@ -17,9 +17,6 @@ function isRange(value) {
|
|
|
17
17
|
&& value.startSentenceId >= 1
|
|
18
18
|
&& value.endSentenceId >= value.startSentenceId;
|
|
19
19
|
}
|
|
20
|
-
function rangesContiguous(ranges) {
|
|
21
|
-
return ranges.every((range) => range.endSentenceId >= range.startSentenceId);
|
|
22
|
-
}
|
|
23
20
|
export function parseJudgmentEnvelope(value) {
|
|
24
21
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
25
22
|
throw new Error('Judgment envelope must be an object.');
|
|
@@ -105,12 +102,15 @@ export function fingerprintPreEditReduction(reduction) {
|
|
|
105
102
|
function reduced(decision, editScope, reason) {
|
|
106
103
|
return { decision, editScope, ...(reason ? { reason } : {}), recommendationFingerprint: fingerprintReduction(decision, editScope, reason) };
|
|
107
104
|
}
|
|
108
|
-
|
|
109
|
-
if (envelopes.length !==
|
|
110
|
-
throw new Error(
|
|
105
|
+
function requireKinds(envelopes, required, message) {
|
|
106
|
+
if (envelopes.length !== required.length)
|
|
107
|
+
throw new Error(message);
|
|
111
108
|
const kinds = new Set(envelopes.map((envelope) => envelope.judgmentType));
|
|
112
|
-
if (
|
|
113
|
-
throw new Error(
|
|
109
|
+
if (required.some((kind) => !kinds.has(kind)))
|
|
110
|
+
throw new Error(message);
|
|
111
|
+
}
|
|
112
|
+
export function reducePreEdit(envelopes) {
|
|
113
|
+
requireKinds(envelopes, PRE_EDIT_KINDS, 'Pre-edit reduction requires triage, argument, and form envelopes.');
|
|
114
114
|
if (envelopes.some((envelope) => envelope.stage !== 'pre-edit'))
|
|
115
115
|
throw new Error('Pre-edit reduction rejects post-candidate envelopes.');
|
|
116
116
|
const argument = envelopes.find((envelope) => envelope.judgmentType === 'argument');
|
|
@@ -121,7 +121,7 @@ export function reducePreEdit(envelopes) {
|
|
|
121
121
|
return reduced('REBUILD', { ranges: [] });
|
|
122
122
|
}
|
|
123
123
|
const ranges = envelopes.flatMap((envelope) => envelope.editScope?.ranges ?? namedRanges(envelope.findings));
|
|
124
|
-
if (!
|
|
124
|
+
if (!ranges.every((range) => range.endSentenceId >= range.startSentenceId)) {
|
|
125
125
|
throw new Error('Edit scope must name contiguous sentence ranges.');
|
|
126
126
|
}
|
|
127
127
|
if (envelopes.every((envelope) => envelope.decision === 'SHIP') && ranges.length === 0) {
|
|
@@ -132,11 +132,7 @@ export function reducePreEdit(envelopes) {
|
|
|
132
132
|
return reduced('EDIT', { ranges });
|
|
133
133
|
}
|
|
134
134
|
export function reducePostCandidate(envelopes) {
|
|
135
|
-
|
|
136
|
-
throw new Error('Post-candidate reduction requires argument, polarity, form, flatness, and semantic envelopes.');
|
|
137
|
-
const kinds = new Set(envelopes.map((envelope) => envelope.judgmentType));
|
|
138
|
-
if (POST_CANDIDATE_KINDS.some((kind) => !kinds.has(kind)))
|
|
139
|
-
throw new Error('Post-candidate reduction requires argument, polarity, form, flatness, and semantic envelopes.');
|
|
135
|
+
requireKinds(envelopes, POST_CANDIDATE_KINDS, 'Post-candidate reduction requires argument, polarity, form, flatness, and semantic envelopes.');
|
|
140
136
|
if (envelopes.some((envelope) => envelope.stage !== 'post-candidate'))
|
|
141
137
|
throw new Error('Post-candidate reduction rejects pre-edit envelopes.');
|
|
142
138
|
if (envelopes.some((envelope) => envelope.decision === 'REBUILD'))
|
package/dist/learning.js
CHANGED
|
@@ -18,7 +18,7 @@ function canonicalJson(value) {
|
|
|
18
18
|
return JSON.stringify(value);
|
|
19
19
|
}
|
|
20
20
|
export function profileFingerprint(profile) {
|
|
21
|
-
return
|
|
21
|
+
return digest(profile);
|
|
22
22
|
}
|
|
23
23
|
function identity(profile) { return profile.version === '3' ? profile.id : profileFingerprint(profile); }
|
|
24
24
|
function revision(profile) { return profile.version === '3' ? profile.revision : 1; }
|
|
@@ -99,15 +99,27 @@ function readEventsFromFile(file) {
|
|
|
99
99
|
function readEvents(profile, options = {}) { return readEventsFromFile(eventFile(profile, options)); }
|
|
100
100
|
function serialize(events) { return events.length ? `${events.map((event) => JSON.stringify(event)).join('\n')}\n` : ''; }
|
|
101
101
|
function learningState(events, profile) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const superseded = new Set();
|
|
103
|
+
const ratified = new Set();
|
|
104
|
+
const currentRevision = revision(profile);
|
|
105
|
+
for (const event of events) {
|
|
106
|
+
if (event.kind === 'supersession')
|
|
107
|
+
superseded.add(event.targetEventId);
|
|
108
|
+
if (event.kind === 'ratification' && event.profileRevision <= currentRevision)
|
|
109
|
+
ratified.add(event.targetEventId);
|
|
110
|
+
}
|
|
111
|
+
return (event) => {
|
|
112
|
+
if (event.kind === 'ratification' || event.kind === 'supersession' || event.kind === 'migration')
|
|
113
|
+
return 'control';
|
|
114
|
+
if (superseded.has(event.eventId))
|
|
115
|
+
return 'superseded';
|
|
116
|
+
if (ratified.has(event.eventId))
|
|
117
|
+
return 'active';
|
|
118
|
+
const compatible = event.profileRevision <= currentRevision
|
|
119
|
+
&& (event.compatibility === 'same-or-newer' || event.profileRevision === currentRevision);
|
|
120
|
+
return compatible ? 'active' : 'incompatible';
|
|
105
121
|
};
|
|
106
122
|
}
|
|
107
|
-
function isCompatible(event, profile) {
|
|
108
|
-
return event.profileRevision <= revision(profile)
|
|
109
|
-
&& (event.compatibility === 'same-or-newer' || event.profileRevision === revision(profile));
|
|
110
|
-
}
|
|
111
123
|
function withLock(file, operation) {
|
|
112
124
|
const lock = `${file}.lock`;
|
|
113
125
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
@@ -143,7 +155,7 @@ function compact(events) {
|
|
|
143
155
|
const selected = new Set(content.slice(0, MAX_EVENTS).map((event) => event.eventId));
|
|
144
156
|
const controls = events.filter((event) => event.kind === 'migration' || ((event.kind === 'ratification' || event.kind === 'supersession') && selected.has(event.targetEventId ?? '')));
|
|
145
157
|
while (selected.size + controls.length > MAX_EVENTS)
|
|
146
|
-
selected.delete(content[
|
|
158
|
+
selected.delete(content[selected.size - 1]?.eventId ?? '');
|
|
147
159
|
const retained = events.filter((event) => selected.has(event.eventId) || (event.kind === 'migration') || ((event.kind === 'ratification' || event.kind === 'supersession') && selected.has(event.targetEventId ?? '')))
|
|
148
160
|
.sort((a, b) => a.timestamp.localeCompare(b.timestamp) || a.eventId.localeCompare(b.eventId));
|
|
149
161
|
while (retained.length && Buffer.byteLength(serialize(retained)) > MAX_STORAGE_BYTES) {
|
|
@@ -173,37 +185,43 @@ function receipt(profile, event, status) {
|
|
|
173
185
|
function requestDigest(event) {
|
|
174
186
|
return digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
175
187
|
}
|
|
176
|
-
function eventBase(profile, kind, options) {
|
|
188
|
+
function eventBase(profile, kind, options, details = {}) {
|
|
177
189
|
const mutationId = options.mutationId ?? randomUUID();
|
|
178
190
|
const base = {
|
|
179
191
|
version: '2', eventId: digest({ profile: identity(profile), mutationId }), mutationId, timestamp: new Date().toISOString(),
|
|
180
192
|
profileRevision: revision(profile), revisionDigest: revisionDigest(profile), authority: options.authority ?? 'team',
|
|
181
193
|
provenance: options.provenance ?? 'local', weight: options.weight ?? 1, compatibility: options.compatibility ?? 'same-or-newer', kind,
|
|
182
194
|
};
|
|
183
|
-
|
|
195
|
+
const event = { ...base, requestDigest: '', ...details };
|
|
196
|
+
event.requestDigest = requestDigest(event);
|
|
197
|
+
return event;
|
|
184
198
|
}
|
|
185
|
-
function
|
|
199
|
+
function withMutation(profile, event, options, operation, source) {
|
|
186
200
|
try {
|
|
187
|
-
|
|
188
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
201
|
+
mkdirSync(learningDirectory(options), { recursive: true, mode: 0o700 });
|
|
189
202
|
const file = eventFile(profile, options);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (existing)
|
|
194
|
-
return receipt(profile, existing, existing.requestDigest === event.requestDigest ? 'already_recorded' : 'conflict');
|
|
195
|
-
const invalid = validate?.(events);
|
|
196
|
-
if (invalid)
|
|
197
|
-
return receipt(profile, event, invalid);
|
|
198
|
-
writeEventsAtomically(file, [...events, event]);
|
|
199
|
-
return receipt(profile, event, 'recorded');
|
|
200
|
-
});
|
|
203
|
+
const sourceFile = source ? eventFile(source, options) : undefined;
|
|
204
|
+
const run = () => operation(file, sourceFile);
|
|
205
|
+
const result = sourceFile === undefined ? withLock(file, run) : withLocks([sourceFile, file], run);
|
|
201
206
|
return result ?? receipt(profile, event, 'lock_timeout');
|
|
202
207
|
}
|
|
203
208
|
catch (error) {
|
|
204
209
|
return receipt(profile, event, error.message.includes('corrupt') ? 'corrupt' : 'write_failed');
|
|
205
210
|
}
|
|
206
211
|
}
|
|
212
|
+
function mutate(profile, event, options, validate) {
|
|
213
|
+
return withMutation(profile, event, options, (file) => {
|
|
214
|
+
const events = readEventsFromFile(file);
|
|
215
|
+
const existing = events.find((item) => item.mutationId === event.mutationId);
|
|
216
|
+
if (existing)
|
|
217
|
+
return receipt(profile, existing, existing.requestDigest === event.requestDigest ? 'already_recorded' : 'conflict');
|
|
218
|
+
const invalid = validate?.(events);
|
|
219
|
+
if (invalid)
|
|
220
|
+
return receipt(profile, event, invalid);
|
|
221
|
+
writeEventsAtomically(file, [...events, event]);
|
|
222
|
+
return receipt(profile, event, 'recorded');
|
|
223
|
+
});
|
|
224
|
+
}
|
|
207
225
|
function countFindings(findings) {
|
|
208
226
|
const counted = new Map();
|
|
209
227
|
for (const finding of findings) {
|
|
@@ -227,8 +245,7 @@ export function recordVerifiedCandidate(profile, verification, candidate, option
|
|
|
227
245
|
const outcome = createHash('sha256').update(`${identity(profile)}\0${candidate}`).digest('hex');
|
|
228
246
|
if (readEvents(profile, options).some((event) => event.kind === 'verified_candidate' && event.outcome === outcome))
|
|
229
247
|
return 'nothing_to_learn';
|
|
230
|
-
const event =
|
|
231
|
-
event.requestDigest = requestDigest(event);
|
|
248
|
+
const event = eventBase(profile, 'verified_candidate', { ...options, mutationId: options.mutationId ?? outcome }, { resolved: resolved.slice(0, MAX_RESOLVED_FINDINGS), outcome });
|
|
232
249
|
const result = mutate(profile, event, options);
|
|
233
250
|
return result.status === 'recorded' ? 'recorded' : result.status === 'already_recorded' ? 'nothing_to_learn' : 'write_failed';
|
|
234
251
|
}
|
|
@@ -238,14 +255,12 @@ export function recordLearningInstruction(profile, instruction, options = {}) {
|
|
|
238
255
|
throw new Error('Learning instructions cannot be empty.');
|
|
239
256
|
if (normalized.length > MAX_INSTRUCTION_CHARACTERS)
|
|
240
257
|
throw new Error(`Learning instructions must be ${MAX_INSTRUCTION_CHARACTERS} characters or fewer.`);
|
|
241
|
-
const event =
|
|
242
|
-
event.requestDigest = requestDigest(event);
|
|
258
|
+
const event = eventBase(profile, 'instruction', options, { instruction: normalized });
|
|
243
259
|
return mutate(profile, event, options);
|
|
244
260
|
}
|
|
245
261
|
export function addLearningInstruction(profile, instruction, options = {}) { return recordLearningInstruction(profile, instruction, options).status === 'recorded'; }
|
|
246
262
|
function recordControlEvent(profile, kind, targetEventId, options) {
|
|
247
|
-
const event =
|
|
248
|
-
event.requestDigest = requestDigest(event);
|
|
263
|
+
const event = eventBase(profile, kind, options, { targetEventId });
|
|
249
264
|
return mutate(profile, event, options, (events) => {
|
|
250
265
|
const target = events.find((item) => item.eventId === targetEventId);
|
|
251
266
|
return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined;
|
|
@@ -262,56 +277,40 @@ export function migrateLearningV2ToV3(source, target, options = {}) {
|
|
|
262
277
|
const migrationId = options.mutationId ?? `migration:${digest({ source: sourceFingerprint, target: target.id, revision: target.revision })}`;
|
|
263
278
|
const migrationOptions = { ...options, mutationId: migrationId };
|
|
264
279
|
const marker = { ...eventBase(target, 'migration', migrationOptions), sourceProfile: sourceFingerprint };
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (migrated.some((event) => !compacted.some((item) => item.eventId === event.eventId)) || !compacted.some((item) => item.eventId === marker.eventId))
|
|
282
|
-
return receipt(target, marker, 'capacity_exceeded');
|
|
283
|
-
writeEventsAtomically(file, intended);
|
|
284
|
-
return receipt(target, marker, 'recorded');
|
|
285
|
-
});
|
|
286
|
-
return result ?? receipt(target, marker, 'lock_timeout');
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
return receipt(target, marker, error.message.includes('corrupt') ? 'corrupt' : 'write_failed');
|
|
290
|
-
}
|
|
280
|
+
return withMutation(target, marker, options, (file, sourceFile) => {
|
|
281
|
+
const targetEvents = readEventsFromFile(file);
|
|
282
|
+
const existing = targetEvents.find((event) => event.mutationId === marker.mutationId);
|
|
283
|
+
if (existing)
|
|
284
|
+
return receipt(target, existing, 'already_recorded');
|
|
285
|
+
const migrated = readEventsFromFile(sourceFile).filter((event) => event.kind === 'instruction' || event.kind === 'verified_candidate').map((event) => ({
|
|
286
|
+
...event, eventId: digest({ target: target.id, source: event.eventId }), mutationId: `migration:${marker.mutationId}:${event.mutationId}`,
|
|
287
|
+
profileRevision: target.revision, revisionDigest: target.revisionDigest, provenance: `migration:${sourceFingerprint}`,
|
|
288
|
+
}));
|
|
289
|
+
const intended = [...targetEvents, ...migrated, marker];
|
|
290
|
+
const retainedIds = new Set(compact(intended).map((event) => event.eventId));
|
|
291
|
+
if (!retainedIds.has(marker.eventId) || migrated.some((event) => !retainedIds.has(event.eventId)))
|
|
292
|
+
return receipt(target, marker, 'capacity_exceeded');
|
|
293
|
+
writeEventsAtomically(file, intended);
|
|
294
|
+
return receipt(target, marker, 'recorded');
|
|
295
|
+
}, source);
|
|
291
296
|
}
|
|
292
297
|
export function inspectLearning(profile, options = {}) {
|
|
293
298
|
const events = readEvents(profile, options);
|
|
294
|
-
const
|
|
299
|
+
const statusOf = learningState(events, profile);
|
|
295
300
|
return events.slice(-MAX_EVENTS).map((event) => {
|
|
296
|
-
const control = event.kind === 'ratification' || event.kind === 'supersession' || event.kind === 'migration';
|
|
297
|
-
const status = control ? 'control' : superseded.has(event.eventId) ? 'superseded' : isCompatible(event, profile) || ratified.has(event.eventId) ? 'active' : 'incompatible';
|
|
298
301
|
return {
|
|
299
302
|
version: '1', eventId: event.eventId, mutationId: event.mutationId, eventType: event.kind, timestamp: event.timestamp,
|
|
300
303
|
profileRevision: event.profileRevision, authority: event.authority, weight: event.weight,
|
|
301
|
-
compatibility: event.compatibility, status, ...(event.targetEventId ? { targetEventId: event.targetEventId } : {}),
|
|
304
|
+
compatibility: event.compatibility, status: statusOf(event), ...(event.targetEventId ? { targetEventId: event.targetEventId } : {}),
|
|
302
305
|
};
|
|
303
306
|
});
|
|
304
307
|
}
|
|
305
308
|
export function composeLearning(profile, options = {}) {
|
|
306
309
|
const events = readEvents(profile, options);
|
|
307
|
-
const
|
|
310
|
+
const statusOf = learningState(events, profile);
|
|
308
311
|
const preferences = new Map();
|
|
309
312
|
for (const event of events) {
|
|
310
|
-
if (event
|
|
311
|
-
continue;
|
|
312
|
-
if (superseded.has(event.eventId))
|
|
313
|
-
continue;
|
|
314
|
-
if (!isCompatible(event, profile) && !ratified.has(event.eventId))
|
|
313
|
+
if (statusOf(event) !== 'active')
|
|
315
314
|
continue;
|
|
316
315
|
const texts = event.kind === 'instruction' && event.instruction ? [{ text: event.instruction, count: event.weight }]
|
|
317
316
|
: (event.resolved ?? []).map((finding) => ({ text: `Previously verified repair: ${finding.engine}/${finding.id}.`, count: finding.count * event.weight }));
|
|
@@ -3,7 +3,7 @@ import { verifyApprovalCapability } from './approval-capability.js';
|
|
|
3
3
|
import { createInitialLifecycleArtifact, isValidLifecycleArtifact, parseSemanticVerdict, prepareSemanticReviewTask, reduceRewriteLifecycle } from './semantic-review.js';
|
|
4
4
|
import { verifyDeterministically } from './pipeline.js';
|
|
5
5
|
import { recordVerifiedCandidate } from './learning.js';
|
|
6
|
-
import { MAX_JSON_BYTES } from './internal.js';
|
|
6
|
+
import { MAX_JSON_BYTES, verificationMatchesBinding } from './internal.js';
|
|
7
7
|
export function prepareLifecycle(deterministic, binding, receipt, policy, allowedViolations) {
|
|
8
8
|
const task = prepareSemanticReviewTask(binding, policy, receipt, allowedViolations);
|
|
9
9
|
return { task, artifact: createInitialLifecycleArtifact(task, deterministic) };
|
|
@@ -67,10 +67,7 @@ export function recordApprovedLearning(request) {
|
|
|
67
67
|
if (!replay.ok || canonicalJson(replay.artifact) !== canonicalJson(approved) || approved.status !== 'approved')
|
|
68
68
|
throw new Error('Approved learning is not authorized.');
|
|
69
69
|
const deterministic = verifyDeterministically(source, candidate, profile, copySpec, writingBrief);
|
|
70
|
-
if (!deterministic.verification.passed || deterministic.artifact
|
|
71
|
-
|| deterministic.artifact.sourceHash !== approved.binding.sourceHash || deterministic.artifact.candidateHash !== approved.binding.candidateHash
|
|
72
|
-
|| deterministic.artifact.profileId !== approved.binding.profileId || deterministic.artifact.profileRevisionDigest !== approved.binding.profileRevisionDigest
|
|
73
|
-
|| deterministic.artifact.rulesetVersion !== approved.binding.rulesetVersion)
|
|
70
|
+
if (!deterministic.verification.passed || !verificationMatchesBinding(deterministic.artifact, approved.binding))
|
|
74
71
|
throw new Error('Approved learning binding does not match deterministic verification.');
|
|
75
72
|
return recordVerifiedCandidate(profile, deterministic.verification, candidate, { mutationId: `approved:${approved.artifactFingerprint}`, authority: 'team', provenance: `approved:${approved.capabilityFingerprint}`, compatibility: 'exact' });
|
|
76
73
|
}
|
package/dist/local-eval.js
CHANGED
|
@@ -23,42 +23,27 @@ function groupedParagraphs(values, label) {
|
|
|
23
23
|
if (values.length < 2 || values.some((value) => !value.paragraphId || !value.text.trim()) || new Set(values.map((value) => value.paragraphId)).size < 2)
|
|
24
24
|
throw new Error(`${label} needs at least two paragraph IDs with text.`);
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
const df = new Map();
|
|
29
|
-
for (const document of documentTerms)
|
|
30
|
-
for (const term of new Set(document))
|
|
31
|
-
df.set(term, (df.get(term) ?? 0) + 1);
|
|
32
|
-
const vocabulary = [...df.keys()].sort();
|
|
33
|
-
const idf = new Map([...df].map(([term, frequency]) => [term, Math.log((documents.length + 1) / (frequency + 1)) + 1]));
|
|
34
|
-
const toVector = (document) => {
|
|
35
|
-
const termsInDocument = terms(document);
|
|
36
|
-
const count = new Map();
|
|
37
|
-
for (const term of termsInDocument)
|
|
38
|
-
count.set(term, (count.get(term) ?? 0) + 1);
|
|
39
|
-
const vector = {};
|
|
40
|
-
for (const [term, occurrences] of count)
|
|
41
|
-
if (idf.has(term))
|
|
42
|
-
vector[term] = (occurrences / Math.max(1, termsInDocument.length)) * idf.get(term);
|
|
43
|
-
return vector;
|
|
44
|
-
};
|
|
45
|
-
return {
|
|
46
|
-
vocabulary,
|
|
47
|
-
idf,
|
|
48
|
-
vectors: documents.map(toVector),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function vectorWithIdf(document, idf) {
|
|
52
|
-
const documentTerms = terms(document);
|
|
53
|
-
const count = new Map();
|
|
26
|
+
function vectorWithIdf(documentTerms, idf) {
|
|
27
|
+
const counts = new Map();
|
|
54
28
|
for (const term of documentTerms)
|
|
55
|
-
|
|
29
|
+
counts.set(term, (counts.get(term) ?? 0) + 1);
|
|
56
30
|
const vector = {};
|
|
57
|
-
for (const [term, occurrences] of
|
|
31
|
+
for (const [term, occurrences] of counts) {
|
|
58
32
|
if (idf.has(term))
|
|
59
33
|
vector[term] = (occurrences / Math.max(1, documentTerms.length)) * idf.get(term);
|
|
34
|
+
}
|
|
60
35
|
return vector;
|
|
61
36
|
}
|
|
37
|
+
function tfIdf(documents) {
|
|
38
|
+
const documentTerms = documents.map(terms);
|
|
39
|
+
const frequencies = new Map();
|
|
40
|
+
for (const document of documentTerms) {
|
|
41
|
+
for (const term of new Set(document))
|
|
42
|
+
frequencies.set(term, (frequencies.get(term) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
const idf = new Map([...frequencies].map(([term, frequency]) => [term, Math.log((documents.length + 1) / (frequency + 1)) + 1]));
|
|
45
|
+
return { idf, vectors: documentTerms.map((document) => vectorWithIdf(document, idf)) };
|
|
46
|
+
}
|
|
62
47
|
function sigmoid(value) { return value >= 0 ? 1 / (1 + Math.exp(-value)) : Math.exp(value) / (1 + Math.exp(value)); }
|
|
63
48
|
/** Deterministic, train-only logistic regression over sparse TF-IDF vectors. */
|
|
64
49
|
function localAuthorshipProbability(candidate, user, shadow) {
|
|
@@ -75,7 +60,7 @@ function localAuthorshipProbability(candidate, user, shadow) {
|
|
|
75
60
|
weights[term] = (weights[term] ?? 0) + 0.12 * error * value;
|
|
76
61
|
}
|
|
77
62
|
}
|
|
78
|
-
const candidateVector = vectorWithIdf(candidate, transformed.idf);
|
|
63
|
+
const candidateVector = vectorWithIdf(terms(candidate), transformed.idf);
|
|
79
64
|
const score = bias + Object.entries(candidateVector).reduce((sum, [term, value]) => sum + (weights[term] ?? 0) * value, 0);
|
|
80
65
|
return Number(sigmoid(score).toFixed(3));
|
|
81
66
|
}
|