@holdyourvoice/hyv 3.6.0 → 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 +93 -154
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +23 -33
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +88 -0
- package/dist/cli/checks.js +226 -0
- package/dist/cli/io.js +141 -0
- package/dist/cli/lifecycle.js +162 -0
- package/dist/cli/profiles.js +246 -0
- package/dist/cli/rewriting.js +111 -0
- package/dist/cli.js +5 -974
- 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 +77 -71
- 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 +10 -19
- package/dist/mcp.js +2 -267
- package/dist/pipeline.js +18 -105
- 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 +18 -52
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +82 -0
- package/dist/rewrite-response.js +27 -0
- package/dist/rewrite-task.js +73 -98
- 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.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/copy-spec.js
CHANGED
|
@@ -40,12 +40,12 @@ export function verifyClaims(candidate, spec) {
|
|
|
40
40
|
const atoms = claim.atoms.map(normalized);
|
|
41
41
|
const presentAtoms = new Set();
|
|
42
42
|
for (const sentence of draftSentences) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const matches = atoms.filter((atom) => atomMatches(sentence.normalizedText, atom));
|
|
44
|
+
if (!matches.length)
|
|
45
|
+
continue;
|
|
46
|
+
for (const atom of matches)
|
|
47
|
+
presentAtoms.add(atom);
|
|
48
|
+
(sentenceClaims[sentence.index] ??= []).push(claim.id);
|
|
49
49
|
}
|
|
50
50
|
const missingAtoms = atoms.filter((atom) => !presentAtoms.has(atom));
|
|
51
51
|
if (!claim.mutable && missingAtoms.length) {
|
package/dist/editorial-packs.js
CHANGED
|
@@ -23,9 +23,8 @@ function isDenied(text) {
|
|
|
23
23
|
function isFollowUpDenial(text) {
|
|
24
24
|
return /^(?:that|this) (?:statement|claim|fact|assertion|point) (?:is|was) (?:not|false|untrue|incorrect)\b/i.test(text.trim());
|
|
25
25
|
}
|
|
26
|
-
function hasAffirmedSourceText(
|
|
26
|
+
function hasAffirmedSourceText(sourceSentences, text) {
|
|
27
27
|
const expected = text.toLowerCase();
|
|
28
|
-
const sourceSentences = sentences(source);
|
|
29
28
|
return sourceSentences.some((sentence, index) => sentence.text.toLowerCase().includes(expected) && !isDenied(sentence.text) && !isFollowUpDenial(sourceSentences[index + 1]?.text ?? ''));
|
|
30
29
|
}
|
|
31
30
|
function requiredFactsAreSourced(brief) {
|
|
@@ -33,7 +32,9 @@ function requiredFactsAreSourced(brief) {
|
|
|
33
32
|
return true;
|
|
34
33
|
if (!brief.factSources?.length)
|
|
35
34
|
return false;
|
|
36
|
-
|
|
35
|
+
const sources = brief.factSources.map((source) => sentences(source.text));
|
|
36
|
+
return brief.requiredFacts.every((fact) => sources.some((source) => hasAffirmedSourceText(source, fact.text)
|
|
37
|
+
|| (fact.atoms?.length && fact.atoms.every((atom) => hasAffirmedSourceText(source, atom)))));
|
|
37
38
|
}
|
|
38
39
|
function isArgumentMap(value) {
|
|
39
40
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
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) {
|
|
@@ -170,37 +182,46 @@ function receipt(profile, event, status) {
|
|
|
170
182
|
const publicMetadata = { mutationId: event.mutationId, eventId: event.eventId, profileId: identity(profile), profileRevision: revision(profile), status, timestamp: event.timestamp };
|
|
171
183
|
return { version: '1', ...publicMetadata, digest: digest(publicMetadata) };
|
|
172
184
|
}
|
|
173
|
-
function
|
|
185
|
+
function requestDigest(event) {
|
|
186
|
+
return digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
187
|
+
}
|
|
188
|
+
function eventBase(profile, kind, options, details = {}) {
|
|
174
189
|
const mutationId = options.mutationId ?? randomUUID();
|
|
175
190
|
const base = {
|
|
176
191
|
version: '2', eventId: digest({ profile: identity(profile), mutationId }), mutationId, timestamp: new Date().toISOString(),
|
|
177
192
|
profileRevision: revision(profile), revisionDigest: revisionDigest(profile), authority: options.authority ?? 'team',
|
|
178
193
|
provenance: options.provenance ?? 'local', weight: options.weight ?? 1, compatibility: options.compatibility ?? 'same-or-newer', kind,
|
|
179
194
|
};
|
|
180
|
-
|
|
195
|
+
const event = { ...base, requestDigest: '', ...details };
|
|
196
|
+
event.requestDigest = requestDigest(event);
|
|
197
|
+
return event;
|
|
181
198
|
}
|
|
182
|
-
function
|
|
199
|
+
function withMutation(profile, event, options, operation, source) {
|
|
183
200
|
try {
|
|
184
|
-
|
|
185
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
201
|
+
mkdirSync(learningDirectory(options), { recursive: true, mode: 0o700 });
|
|
186
202
|
const file = eventFile(profile, options);
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (existing)
|
|
191
|
-
return receipt(profile, existing, existing.requestDigest === event.requestDigest ? 'already_recorded' : 'conflict');
|
|
192
|
-
const invalid = validate?.(events);
|
|
193
|
-
if (invalid)
|
|
194
|
-
return receipt(profile, event, invalid);
|
|
195
|
-
writeEventsAtomically(file, [...events, event]);
|
|
196
|
-
return receipt(profile, event, 'recorded');
|
|
197
|
-
});
|
|
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);
|
|
198
206
|
return result ?? receipt(profile, event, 'lock_timeout');
|
|
199
207
|
}
|
|
200
208
|
catch (error) {
|
|
201
209
|
return receipt(profile, event, error.message.includes('corrupt') ? 'corrupt' : 'write_failed');
|
|
202
210
|
}
|
|
203
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
|
+
}
|
|
204
225
|
function countFindings(findings) {
|
|
205
226
|
const counted = new Map();
|
|
206
227
|
for (const finding of findings) {
|
|
@@ -224,8 +245,7 @@ export function recordVerifiedCandidate(profile, verification, candidate, option
|
|
|
224
245
|
const outcome = createHash('sha256').update(`${identity(profile)}\0${candidate}`).digest('hex');
|
|
225
246
|
if (readEvents(profile, options).some((event) => event.kind === 'verified_candidate' && event.outcome === outcome))
|
|
226
247
|
return 'nothing_to_learn';
|
|
227
|
-
const event =
|
|
228
|
-
event.requestDigest = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
248
|
+
const event = eventBase(profile, 'verified_candidate', { ...options, mutationId: options.mutationId ?? outcome }, { resolved: resolved.slice(0, MAX_RESOLVED_FINDINGS), outcome });
|
|
229
249
|
const result = mutate(profile, event, options);
|
|
230
250
|
return result.status === 'recorded' ? 'recorded' : result.status === 'already_recorded' ? 'nothing_to_learn' : 'write_failed';
|
|
231
251
|
}
|
|
@@ -235,76 +255,62 @@ export function recordLearningInstruction(profile, instruction, options = {}) {
|
|
|
235
255
|
throw new Error('Learning instructions cannot be empty.');
|
|
236
256
|
if (normalized.length > MAX_INSTRUCTION_CHARACTERS)
|
|
237
257
|
throw new Error(`Learning instructions must be ${MAX_INSTRUCTION_CHARACTERS} characters or fewer.`);
|
|
238
|
-
const event =
|
|
239
|
-
event.requestDigest = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
258
|
+
const event = eventBase(profile, 'instruction', options, { instruction: normalized });
|
|
240
259
|
return mutate(profile, event, options);
|
|
241
260
|
}
|
|
242
261
|
export function addLearningInstruction(profile, instruction, options = {}) { return recordLearningInstruction(profile, instruction, options).status === 'recorded'; }
|
|
262
|
+
function recordControlEvent(profile, kind, targetEventId, options) {
|
|
263
|
+
const event = eventBase(profile, kind, options, { targetEventId });
|
|
264
|
+
return mutate(profile, event, options, (events) => {
|
|
265
|
+
const target = events.find((item) => item.eventId === targetEventId);
|
|
266
|
+
return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
243
269
|
export function ratifyLearningEvent(profile, targetEventId, options = {}) {
|
|
244
|
-
|
|
245
|
-
event.requestDigest = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
246
|
-
return mutate(profile, event, options, (events) => { const target = events.find((item) => item.eventId === targetEventId); return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined; });
|
|
270
|
+
return recordControlEvent(profile, 'ratification', targetEventId, options);
|
|
247
271
|
}
|
|
248
272
|
export function supersedeLearningEvent(profile, targetEventId, options = {}) {
|
|
249
|
-
|
|
250
|
-
event.requestDigest = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
251
|
-
return mutate(profile, event, options, (events) => { const target = events.find((item) => item.eventId === targetEventId); return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined; });
|
|
273
|
+
return recordControlEvent(profile, 'supersession', targetEventId, options);
|
|
252
274
|
}
|
|
253
275
|
export function migrateLearningV2ToV3(source, target, options = {}) {
|
|
254
276
|
const sourceFingerprint = profileFingerprint(source);
|
|
255
277
|
const migrationId = options.mutationId ?? `migration:${digest({ source: sourceFingerprint, target: target.id, revision: target.revision })}`;
|
|
256
278
|
const migrationOptions = { ...options, mutationId: migrationId };
|
|
257
279
|
const marker = { ...eventBase(target, 'migration', migrationOptions), sourceProfile: sourceFingerprint };
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (migrated.some((event) => !compacted.some((item) => item.eventId === event.eventId)) || !compacted.some((item) => item.eventId === marker.eventId))
|
|
275
|
-
return receipt(target, marker, 'capacity_exceeded');
|
|
276
|
-
writeEventsAtomically(file, intended);
|
|
277
|
-
return receipt(target, marker, 'recorded');
|
|
278
|
-
});
|
|
279
|
-
return result ?? receipt(target, marker, 'lock_timeout');
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
return receipt(target, marker, error.message.includes('corrupt') ? 'corrupt' : 'write_failed');
|
|
283
|
-
}
|
|
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);
|
|
284
296
|
}
|
|
285
297
|
export function inspectLearning(profile, options = {}) {
|
|
286
298
|
const events = readEvents(profile, options);
|
|
287
|
-
const
|
|
299
|
+
const statusOf = learningState(events, profile);
|
|
288
300
|
return events.slice(-MAX_EVENTS).map((event) => {
|
|
289
|
-
const control = event.kind === 'ratification' || event.kind === 'supersession' || event.kind === 'migration';
|
|
290
|
-
const status = control ? 'control' : superseded.has(event.eventId) ? 'superseded' : isCompatible(event, profile) || ratified.has(event.eventId) ? 'active' : 'incompatible';
|
|
291
301
|
return {
|
|
292
302
|
version: '1', eventId: event.eventId, mutationId: event.mutationId, eventType: event.kind, timestamp: event.timestamp,
|
|
293
303
|
profileRevision: event.profileRevision, authority: event.authority, weight: event.weight,
|
|
294
|
-
compatibility: event.compatibility, status, ...(event.targetEventId ? { targetEventId: event.targetEventId } : {}),
|
|
304
|
+
compatibility: event.compatibility, status: statusOf(event), ...(event.targetEventId ? { targetEventId: event.targetEventId } : {}),
|
|
295
305
|
};
|
|
296
306
|
});
|
|
297
307
|
}
|
|
298
308
|
export function composeLearning(profile, options = {}) {
|
|
299
309
|
const events = readEvents(profile, options);
|
|
300
|
-
const
|
|
310
|
+
const statusOf = learningState(events, profile);
|
|
301
311
|
const preferences = new Map();
|
|
302
312
|
for (const event of events) {
|
|
303
|
-
if (event
|
|
304
|
-
continue;
|
|
305
|
-
if (superseded.has(event.eventId))
|
|
306
|
-
continue;
|
|
307
|
-
if (!isCompatible(event, profile) && !ratified.has(event.eventId))
|
|
313
|
+
if (statusOf(event) !== 'active')
|
|
308
314
|
continue;
|
|
309
315
|
const texts = event.kind === 'instruction' && event.instruction ? [{ text: event.instruction, count: event.weight }]
|
|
310
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
|
}
|