@holdyourvoice/hyv 3.5.1 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +5 -59
- package/dist/ai-editor.js +4 -20
- package/dist/cli/agents.js +108 -0
- package/dist/cli/checks.js +226 -0
- package/dist/cli/context.test.js +55 -0
- package/dist/cli/io.js +137 -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/cli.test.js +1 -1
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/learning.js +16 -9
- package/dist/mcp-tools.js +8 -17
- package/dist/mcp.js +12 -18
- package/dist/mirror-refs.test.js +4 -4
- package/dist/pipeline.js +30 -31
- package/dist/pipeline.test.js +5 -3
- package/dist/rebuild-task.js +4 -27
- package/dist/rebuild-task.test.js +1 -1
- package/dist/rewrite-response.js +27 -0
- package/dist/rewrite-task.js +9 -35
- package/dist/rewrite-task.test.js +10 -9
- package/dist/strict-quality.js +1 -2
- package/dist/strict-quality.test.js +3 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/hyv-analyze/SKILL.md +1 -1
- package/skills/hyv-analyze/agents/openai.yaml +1 -1
- package/skills/hyv-apply-rebuild/SKILL.md +1 -0
- package/skills/hyv-apply-rebuild/agents/openai.yaml +1 -1
- package/skills/hyv-apply-rewrite/SKILL.md +1 -0
- package/skills/hyv-apply-rewrite/agents/openai.yaml +1 -1
- package/skills/hyv-prepare-rebuild/SKILL.md +1 -0
- package/skills/hyv-prepare-rebuild/agents/openai.yaml +1 -1
- package/skills/hyv-prepare-rewrite/SKILL.md +1 -0
- package/skills/hyv-prepare-rewrite/agents/openai.yaml +1 -1
- package/skills/hyv-rebuild-writer-request/SKILL.md +1 -0
- package/skills/hyv-rebuild-writer-request/agents/openai.yaml +1 -1
- package/skills/hyv-rewrite-prompt/SKILL.md +4 -3
- package/skills/hyv-rewrite-prompt/agent.json +1 -1
- package/skills/hyv-rewrite-prompt/agents/openai.yaml +2 -2
- package/skills/hyv-strict-check/SKILL.md +3 -3
- package/skills/hyv-verify/SKILL.md +2 -1
- package/skills/hyv-verify/agents/openai.yaml +1 -1
- package/skills/hyv-verify-spec/SKILL.md +3 -2
- package/skills/hyv-verify-spec/agent.json +1 -1
- package/skills/hyv-verify-spec/agents/openai.yaml +2 -2
package/dist/cli.test.js
CHANGED
|
@@ -272,7 +272,7 @@ test('uses exit code 2 for a failed candidate gate and 1 for misuse', () => {
|
|
|
272
272
|
assert.equal(run(['profile', profile, first, second, '--avoid=unlock']).status, 0);
|
|
273
273
|
const verification = run(['verify', original, candidate, profile]);
|
|
274
274
|
assert.equal(verification.status, 2);
|
|
275
|
-
assert.deepEqual(Object.keys(JSON.parse(verification.stdout)).sort(), ['candidate', 'finalOutput', 'logicLint', 'original', 'passed', 'preservationScore', 'regressions', 'version']);
|
|
275
|
+
assert.deepEqual(Object.keys(JSON.parse(verification.stdout)).sort(), ['candidate', 'finalOutput', 'logicLint', 'original', 'passed', 'preservationScore', 'regressions', 'strictFindings', 'version']);
|
|
276
276
|
assert.equal(run(['unknown-command']).status, 1);
|
|
277
277
|
assert.equal(run(['mcp', 'unexpected']).status, 1);
|
|
278
278
|
}
|
|
Binary file
|
package/dist/learning.js
CHANGED
|
@@ -170,6 +170,9 @@ function receipt(profile, event, status) {
|
|
|
170
170
|
const publicMetadata = { mutationId: event.mutationId, eventId: event.eventId, profileId: identity(profile), profileRevision: revision(profile), status, timestamp: event.timestamp };
|
|
171
171
|
return { version: '1', ...publicMetadata, digest: digest(publicMetadata) };
|
|
172
172
|
}
|
|
173
|
+
function requestDigest(event) {
|
|
174
|
+
return digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
175
|
+
}
|
|
173
176
|
function eventBase(profile, kind, options) {
|
|
174
177
|
const mutationId = options.mutationId ?? randomUUID();
|
|
175
178
|
const base = {
|
|
@@ -177,7 +180,7 @@ function eventBase(profile, kind, options) {
|
|
|
177
180
|
profileRevision: revision(profile), revisionDigest: revisionDigest(profile), authority: options.authority ?? 'team',
|
|
178
181
|
provenance: options.provenance ?? 'local', weight: options.weight ?? 1, compatibility: options.compatibility ?? 'same-or-newer', kind,
|
|
179
182
|
};
|
|
180
|
-
return { ...base, requestDigest:
|
|
183
|
+
return { ...base, requestDigest: requestDigest(base) };
|
|
181
184
|
}
|
|
182
185
|
function mutate(profile, event, options, validate) {
|
|
183
186
|
try {
|
|
@@ -225,7 +228,7 @@ export function recordVerifiedCandidate(profile, verification, candidate, option
|
|
|
225
228
|
if (readEvents(profile, options).some((event) => event.kind === 'verified_candidate' && event.outcome === outcome))
|
|
226
229
|
return 'nothing_to_learn';
|
|
227
230
|
const event = { ...eventBase(profile, 'verified_candidate', { ...options, mutationId: options.mutationId ?? outcome }), resolved: resolved.slice(0, MAX_RESOLVED_FINDINGS), outcome };
|
|
228
|
-
event.requestDigest =
|
|
231
|
+
event.requestDigest = requestDigest(event);
|
|
229
232
|
const result = mutate(profile, event, options);
|
|
230
233
|
return result.status === 'recorded' ? 'recorded' : result.status === 'already_recorded' ? 'nothing_to_learn' : 'write_failed';
|
|
231
234
|
}
|
|
@@ -236,19 +239,23 @@ export function recordLearningInstruction(profile, instruction, options = {}) {
|
|
|
236
239
|
if (normalized.length > MAX_INSTRUCTION_CHARACTERS)
|
|
237
240
|
throw new Error(`Learning instructions must be ${MAX_INSTRUCTION_CHARACTERS} characters or fewer.`);
|
|
238
241
|
const event = { ...eventBase(profile, 'instruction', options), instruction: normalized };
|
|
239
|
-
event.requestDigest =
|
|
242
|
+
event.requestDigest = requestDigest(event);
|
|
240
243
|
return mutate(profile, event, options);
|
|
241
244
|
}
|
|
242
245
|
export function addLearningInstruction(profile, instruction, options = {}) { return recordLearningInstruction(profile, instruction, options).status === 'recorded'; }
|
|
246
|
+
function recordControlEvent(profile, kind, targetEventId, options) {
|
|
247
|
+
const event = { ...eventBase(profile, kind, options), targetEventId };
|
|
248
|
+
event.requestDigest = requestDigest(event);
|
|
249
|
+
return mutate(profile, event, options, (events) => {
|
|
250
|
+
const target = events.find((item) => item.eventId === targetEventId);
|
|
251
|
+
return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
243
254
|
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; });
|
|
255
|
+
return recordControlEvent(profile, 'ratification', targetEventId, options);
|
|
247
256
|
}
|
|
248
257
|
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; });
|
|
258
|
+
return recordControlEvent(profile, 'supersession', targetEventId, options);
|
|
252
259
|
}
|
|
253
260
|
export function migrateLearningV2ToV3(source, target, options = {}) {
|
|
254
261
|
const sourceFingerprint = profileFingerprint(source);
|
package/dist/mcp-tools.js
CHANGED
|
@@ -21,14 +21,17 @@ import { scoreHeldoutProfile } from './profile-score.js';
|
|
|
21
21
|
import { findWritingExamples } from './writing-examples.js';
|
|
22
22
|
import { evaluateIsolatedBacktest } from './backtest.js';
|
|
23
23
|
import { evaluateLocalComposite } from './local-eval.js';
|
|
24
|
-
function
|
|
24
|
+
function parseDocument(text, parse, label) {
|
|
25
25
|
try {
|
|
26
|
-
return
|
|
26
|
+
return parse(JSON.parse(text));
|
|
27
27
|
}
|
|
28
28
|
catch (error) {
|
|
29
|
-
throw new Error(error instanceof Error ? error.message :
|
|
29
|
+
throw new Error(error instanceof Error ? error.message : `${label} is not valid JSON.`);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
function profileFromJson(profileJson) {
|
|
33
|
+
return parseDocument(profileJson, parseProfile, 'Profile');
|
|
34
|
+
}
|
|
32
35
|
function profileV3FromJson(profileJson) {
|
|
33
36
|
const profile = profileFromJson(profileJson);
|
|
34
37
|
if (profile.version !== '3')
|
|
@@ -36,22 +39,10 @@ function profileV3FromJson(profileJson) {
|
|
|
36
39
|
return profile;
|
|
37
40
|
}
|
|
38
41
|
function copySpecFromJson(copySpecJson) {
|
|
39
|
-
|
|
40
|
-
return parseCopySpec(JSON.parse(copySpecJson));
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
throw new Error(error instanceof Error ? error.message : 'CopySpec is not valid JSON.');
|
|
44
|
-
}
|
|
42
|
+
return parseDocument(copySpecJson, parseCopySpec, 'CopySpec');
|
|
45
43
|
}
|
|
46
44
|
function writingBriefFromJson(writingBriefJson) {
|
|
47
|
-
|
|
48
|
-
return undefined;
|
|
49
|
-
try {
|
|
50
|
-
return parseWritingBrief(JSON.parse(writingBriefJson));
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
throw new Error(error instanceof Error ? error.message : 'WritingBrief is not valid JSON.');
|
|
54
|
-
}
|
|
45
|
+
return writingBriefJson ? parseDocument(writingBriefJson, parseWritingBrief, 'WritingBrief') : undefined;
|
|
55
46
|
}
|
|
56
47
|
export function buildProfileForMcp(samples, avoid = []) {
|
|
57
48
|
return buildProfile(samples, avoid);
|
package/dist/mcp.js
CHANGED
|
@@ -12,7 +12,6 @@ const profileJson = z.string().min(1).max(50_000);
|
|
|
12
12
|
const copySpecJson = z.string().min(1).max(250_000);
|
|
13
13
|
const writingBriefJson = z.string().min(1).max(50_000);
|
|
14
14
|
const samples = z.array(writing).min(2).max(20);
|
|
15
|
-
const strictSamples = z.array(writing).min(2).max(20);
|
|
16
15
|
const heldoutSamples = z.array(writing).min(3).max(20);
|
|
17
16
|
const evalParagraphs = z.array(z.object({ paragraph_id: z.string().min(1).max(160), text: writing })).min(2).max(50);
|
|
18
17
|
const writingExamples = z.array(z.object({ basename: z.string().min(1).max(160), text: writing })).min(1).max(64);
|
|
@@ -39,9 +38,6 @@ function json(value) {
|
|
|
39
38
|
function failure(error) {
|
|
40
39
|
return { content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], isError: true };
|
|
41
40
|
}
|
|
42
|
-
function lifecycleResult(result) {
|
|
43
|
-
return json(result.ok ? result.artifact : { error: result.error });
|
|
44
|
-
}
|
|
45
41
|
function guardedJson(run, publicError) {
|
|
46
42
|
try {
|
|
47
43
|
return json(run());
|
|
@@ -51,12 +47,10 @@ function guardedJson(run, publicError) {
|
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
49
|
function guardedLifecycle(run, publicError) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return failure(publicError ? new Error(publicError) : error);
|
|
59
|
-
}
|
|
50
|
+
return guardedJson(() => {
|
|
51
|
+
const result = run();
|
|
52
|
+
return result.ok ? result.artifact : { error: result.error };
|
|
53
|
+
}, publicError);
|
|
60
54
|
}
|
|
61
55
|
const server = new McpServer({ name: 'hold-your-voice', version: HYV_VERSION });
|
|
62
56
|
server.registerTool('hyv_build_profile', {
|
|
@@ -74,8 +68,8 @@ server.registerTool('hyv_analyze', {
|
|
|
74
68
|
annotations: { readOnlyHint: true },
|
|
75
69
|
}, async ({ draft, profile_json, writing_brief_json }) => guardedJson(() => analyzeForMcp(draft, profile_json, writing_brief_json)));
|
|
76
70
|
server.registerTool('hyv_strict_check', {
|
|
77
|
-
description: 'Run the
|
|
78
|
-
inputSchema: { draft: writing, profile_json: profileJson, samples
|
|
71
|
+
description: 'Run the calibrated local strict-quality gate. It requires a Profile v3 and local writing samples; it returns strict-ready, needs-human-review, or blocked without changing text or learning state.',
|
|
72
|
+
inputSchema: { draft: writing, profile_json: profileJson, samples, writing_brief_json: writingBriefJson.optional() },
|
|
79
73
|
annotations: { readOnlyHint: true },
|
|
80
74
|
}, async ({ draft, profile_json, samples: localSamples, writing_brief_json }) => guardedJson(() => strictCheckForMcp(draft, profile_json, localSamples, writing_brief_json)));
|
|
81
75
|
server.registerTool('hyv_score', {
|
|
@@ -128,7 +122,7 @@ server.registerTool('hyv_logic_lint', {
|
|
|
128
122
|
annotations: { readOnlyHint: true },
|
|
129
123
|
}, async ({ draft, writing_brief_json }) => guardedJson(() => logicLintForMcp(draft, writing_brief_json)));
|
|
130
124
|
server.registerTool('hyv_rewrite_prompt', {
|
|
131
|
-
description: 'Create a constrained editing brief.
|
|
125
|
+
description: 'Create a strict constrained editing brief. Every active AI Editor finding is a required repair; explicit local examples remain advisory cadence evidence. It does not rewrite the draft or call a model.',
|
|
132
126
|
inputSchema: { draft: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional(), examples: writingExamples.optional() },
|
|
133
127
|
annotations: { readOnlyHint: true },
|
|
134
128
|
}, async ({ draft, profile_json, writing_brief_json, examples }) => guardedJson(() => rewritePromptForMcp(draft, profile_json, {}, writing_brief_json, examples)));
|
|
@@ -138,12 +132,12 @@ server.registerTool('hyv_find_writing_examples', {
|
|
|
138
132
|
annotations: { readOnlyHint: true },
|
|
139
133
|
}, async ({ query, examples }) => guardedJson(() => findWritingExamplesForMcp(query, examples)));
|
|
140
134
|
server.registerTool('hyv_prepare_rewrite', {
|
|
141
|
-
description: 'Prepare a local, versioned rewrite task.
|
|
135
|
+
description: 'Prepare a strict local, versioned rewrite task. Every active AI Editor finding is eligible for source-faithful repair; the caller may forward the task to a provider only by explicit choice.',
|
|
142
136
|
inputSchema: { draft: writing, profile_json: profileJson, copy_spec_json: copySpecJson.optional(), writing_brief_json: writingBriefJson.optional() },
|
|
143
137
|
annotations: { readOnlyHint: true },
|
|
144
138
|
}, async ({ draft, profile_json, copy_spec_json, writing_brief_json }) => guardedJson(() => prepareRewriteForMcp(draft, profile_json, copy_spec_json, writing_brief_json)));
|
|
145
139
|
server.registerTool('hyv_apply_rewrite', {
|
|
146
|
-
description: 'Validate
|
|
140
|
+
description: 'Validate a model response to a prepared task, then reject any candidate with an unresolved active AI Editor finding. It never calls a provider or stores source or candidate text.',
|
|
147
141
|
inputSchema: { task_json: z.string().min(1).max(250_000), response_json: z.string().min(1).max(100_000), profile_json: profileJson },
|
|
148
142
|
annotations: { readOnlyHint: true },
|
|
149
143
|
}, async ({ task_json, response_json, profile_json }) => guardedJson(() => applyRewriteForMcp(task_json, response_json, profile_json)));
|
|
@@ -164,12 +158,12 @@ server.registerTool('hyv_reduce_judgment', {
|
|
|
164
158
|
annotations: { readOnlyHint: true },
|
|
165
159
|
}, async ({ envelopes_json }) => guardedJson(() => reduceJudgmentForMcp(envelopes_json)));
|
|
166
160
|
server.registerTool('hyv_verify', {
|
|
167
|
-
description: 'Verify a revised candidate against an original draft and portable profile
|
|
161
|
+
description: 'Verify a revised candidate against an original draft and portable profile. Every active AI Editor finding fails default verification; learning state remains unchanged.',
|
|
168
162
|
inputSchema: { original: writing, candidate: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
|
|
169
163
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
170
164
|
}, async ({ original, candidate, profile_json, writing_brief_json }) => guardedJson(() => verifyForMcp(original, candidate, profile_json, writing_brief_json)));
|
|
171
165
|
server.registerTool('hyv_verify_copy_spec', {
|
|
172
|
-
description: 'Verify a candidate against
|
|
166
|
+
description: 'Verify a candidate against strict default voice gates and a local CopySpec. Every active AI Editor finding fails verification; immutable claims remain verbatim unless atoms are supplied, and prohibited claims fail closed.',
|
|
173
167
|
inputSchema: { original: writing, candidate: writing, profile_json: profileJson, copy_spec_json: copySpecJson, writing_brief_json: writingBriefJson.optional() },
|
|
174
168
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
175
169
|
}, async ({ original, candidate, profile_json, copy_spec_json, writing_brief_json }) => guardedJson(() => verifyCopySpecForMcp(original, candidate, profile_json, copy_spec_json, writing_brief_json)));
|
|
@@ -250,7 +244,7 @@ if (redactsSensitiveInputs) {
|
|
|
250
244
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
251
245
|
}, async (args) => guardedJson(() => prepareRebuildForMcp(args.draft, args.profile_json, args.reduction_json, args.copy_spec_json, args.capability_json, loadApprovalContext(), args.writing_brief_json, args.recomposition_policy_json), 'Rebuild preparation failed.'));
|
|
252
246
|
server.registerTool('hyv_apply_rebuild', {
|
|
253
|
-
description: 'Validate
|
|
247
|
+
description: 'Validate a whole-document rebuild response against a prepared authorized rebuild task and reject unresolved active AI Editor findings. Capability input requires host-guaranteed sensitive-input redaction. It never calls a provider.',
|
|
254
248
|
inputSchema: { task_json: lifecycleJson, response_json: z.string().min(1).max(100_000), profile_json: profileJson, capability_json: lifecycleJson },
|
|
255
249
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
256
250
|
}, async (args) => guardedJson(() => applyRebuildForMcp(args.task_json, args.response_json, args.profile_json, args.capability_json, loadApprovalContext()), 'Rebuild application failed.'));
|
package/dist/mirror-refs.test.js
CHANGED
|
@@ -23,7 +23,7 @@ test('reconciles branch and tag creates, rewrites, and deletions', () => {
|
|
|
23
23
|
git(root, 'clone', '--quiet', source, checkout);
|
|
24
24
|
git(checkout, 'config', 'user.name', 'Mirror Test');
|
|
25
25
|
git(checkout, 'config', 'user.email', 'mirror-test@example.invalid');
|
|
26
|
-
git(checkout, 'switch', '--quiet', '-c', 'main');
|
|
26
|
+
git(checkout, 'switch', '--quiet', '-c', 'fixture/main');
|
|
27
27
|
writeFileSync(join(checkout, 'main.txt'), 'first\n');
|
|
28
28
|
git(checkout, 'add', 'main.txt');
|
|
29
29
|
git(checkout, 'commit', '--quiet', '-m', 'first');
|
|
@@ -34,20 +34,20 @@ test('reconciles branch and tag creates, rewrites, and deletions', () => {
|
|
|
34
34
|
git(checkout, 'tag', 'v1.0.0');
|
|
35
35
|
git(checkout, 'push', '--quiet', '--all', 'origin');
|
|
36
36
|
git(checkout, 'push', '--quiet', '--tags', 'origin');
|
|
37
|
-
git(source, 'symbolic-ref', 'HEAD', 'refs/heads/main');
|
|
37
|
+
git(source, 'symbolic-ref', 'HEAD', 'refs/heads/fixture/main');
|
|
38
38
|
git(checkout, 'remote', 'set-head', 'origin', '-a');
|
|
39
39
|
git(checkout, 'remote', 'add', 'mirror', mirror);
|
|
40
40
|
execFileSync(process.execPath, [mirrorScript], { cwd: checkout, stdio: 'pipe' });
|
|
41
41
|
assert.deepEqual(refs(mirror), refs(source));
|
|
42
42
|
assert.equal(refs(mirror).some((ref) => ref.endsWith('refs/heads/HEAD')), false);
|
|
43
|
-
git(checkout, 'switch', '--quiet', 'main');
|
|
43
|
+
git(checkout, 'switch', '--quiet', 'fixture/main');
|
|
44
44
|
writeFileSync(join(checkout, 'main.txt'), 'rewritten\n');
|
|
45
45
|
git(checkout, 'add', 'main.txt');
|
|
46
46
|
git(checkout, 'commit', '--quiet', '--amend', '-m', 'rewritten main');
|
|
47
47
|
git(checkout, 'branch', '-D', 'feature/test');
|
|
48
48
|
git(checkout, 'tag', '-d', 'v1.0.0');
|
|
49
49
|
git(checkout, 'tag', 'v2.0.0');
|
|
50
|
-
git(checkout, 'push', '--quiet', '--force', 'origin', 'main');
|
|
50
|
+
git(checkout, 'push', '--quiet', '--force', 'origin', 'fixture/main');
|
|
51
51
|
git(checkout, 'push', '--quiet', 'origin', '--delete', 'feature/test');
|
|
52
52
|
git(checkout, 'push', '--quiet', 'origin', ':refs/tags/v1.0.0');
|
|
53
53
|
git(checkout, 'push', '--quiet', 'origin', 'v2.0.0');
|
package/dist/pipeline.js
CHANGED
|
@@ -29,10 +29,19 @@ function formatFindings(findings) {
|
|
|
29
29
|
export function isBlockingFinding(finding) {
|
|
30
30
|
return finding.engine === 'ai_editor' ? finding.appliedPolicy === 'blocking' : finding.severity === 'red';
|
|
31
31
|
}
|
|
32
|
-
export function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
export function isStrictFinding(finding) {
|
|
33
|
+
return finding.engine === 'ai_editor' || isBlockingFinding(finding);
|
|
34
|
+
}
|
|
35
|
+
function analysisFindings(result) {
|
|
36
|
+
return [...result.voiceDna.findings, ...result.aiEditor.findings, ...(result.editorial?.findings ?? [])];
|
|
37
|
+
}
|
|
38
|
+
export function strictFindings(result) {
|
|
39
|
+
return analysisFindings(result).filter(isStrictFinding);
|
|
40
|
+
}
|
|
41
|
+
export function deriveEditScope(result, strict = false) {
|
|
42
|
+
const findings = analysisFindings(result);
|
|
43
|
+
const blocking = findings.filter(strict ? isStrictFinding : isBlockingFinding);
|
|
44
|
+
const pendingJudgment = strict ? [] : findings.filter((finding) => finding.appliedPolicy === 'judgment-required');
|
|
36
45
|
return {
|
|
37
46
|
eligibleSentenceIds: [...new Set(blocking.map((finding) => finding.sentence))].sort((left, right) => left - right),
|
|
38
47
|
blocking,
|
|
@@ -40,17 +49,17 @@ export function deriveEditScope(result) {
|
|
|
40
49
|
};
|
|
41
50
|
}
|
|
42
51
|
export function renderRewritePrompt(draft, profile, result, learning = [], brief, examples = []) {
|
|
43
|
-
const allFindings =
|
|
44
|
-
const scope = deriveEditScope(result);
|
|
52
|
+
const allFindings = analysisFindings(result);
|
|
53
|
+
const scope = deriveEditScope(result, true);
|
|
45
54
|
const redFindings = scope.blocking;
|
|
46
|
-
const yellowFindings = allFindings.filter((finding) => !
|
|
55
|
+
const yellowFindings = allFindings.filter((finding) => !isStrictFinding(finding) && finding.appliedPolicy !== 'judgment-required');
|
|
47
56
|
const metrics = profile.metrics;
|
|
48
57
|
return [
|
|
49
58
|
'# Tier 0 — non-negotiable preservation',
|
|
50
59
|
'Preserve facts, names, numbers, claims, and every unflagged sentence exactly. Do not add claims, examples, sections, hooks, or CTAs.',
|
|
51
60
|
'',
|
|
52
|
-
'# Tier 1 —
|
|
53
|
-
'
|
|
61
|
+
'# Tier 1 — strict repair requirements',
|
|
62
|
+
'Every active AI Editor finding is a required repair. Replace each flagged sentence with a stronger, source-faithful sentence; do not merely swap one stock phrase for another.',
|
|
54
63
|
'Use only facts already present in the draft, CopySpec, WritingBrief, or supplied source context. Do not invent a source, metric, date, quotation, mechanism, example, CTA, or opinion.',
|
|
55
64
|
`VoiceDNA: ${result.voiceDna.score}/100 (${result.voiceDna.passed ? 'pass' : 'fail'}).`,
|
|
56
65
|
`AI Editor: ${result.aiEditor.score}/100 (${result.aiEditor.passed ? 'pass' : 'fail'}).`,
|
|
@@ -87,8 +96,8 @@ export function rewritePrompt(draft, profile, learning = [], brief, examples = [
|
|
|
87
96
|
function compareCandidates(original, candidate, profile, brief) {
|
|
88
97
|
const baseline = analyze(original, profile, brief);
|
|
89
98
|
const checked = analyze(candidate, profile, brief);
|
|
90
|
-
const baselineFindings =
|
|
91
|
-
const checkedFindings =
|
|
99
|
+
const baselineFindings = analysisFindings(baseline);
|
|
100
|
+
const checkedFindings = analysisFindings(checked);
|
|
92
101
|
const known = new Set(baselineFindings.map((finding) => `${finding.engine}:${finding.id}:${finding.sentence}`));
|
|
93
102
|
const regressions = checkedFindings.filter((finding) => !known.has(`${finding.engine}:${finding.id}:${finding.sentence}`));
|
|
94
103
|
const preservation = legacySetPreservation(original, candidate).score;
|
|
@@ -114,48 +123,38 @@ function verifyRequiredFacts(candidate, brief) {
|
|
|
114
123
|
return result;
|
|
115
124
|
return { ...result, passed: false, failures: [...result.failures, ...reversed.map((fact) => ({ id: fact.id, code: 'missing_immutable_claim', message: `Required fact ${fact.id} is negated or denied.`, evidence: 'WritingBrief required fact.' }))] };
|
|
116
125
|
}
|
|
117
|
-
|
|
126
|
+
function verifyCandidate(original, candidate, profile, brief, rebuild) {
|
|
118
127
|
const { baseline, checked, regressions, preservation } = compareCandidates(original, candidate, profile, brief);
|
|
128
|
+
const claims = rebuild ? verifyClaims(candidate, rebuild.copySpec) : undefined;
|
|
119
129
|
const finalOutput = finalOutputCheck(candidate);
|
|
120
130
|
const logicLint = lintLogic(candidate, brief);
|
|
121
131
|
const factLint = brief?.factSources?.length ? lintFacts({ sources: brief.factSources, draft: candidate, metadata: brief.factMetadata }) : undefined;
|
|
122
132
|
const requiredFacts = verifyRequiredFacts(candidate, brief);
|
|
133
|
+
const unresolvedStrictFindings = strictFindings(checked);
|
|
123
134
|
return {
|
|
124
135
|
version: '2',
|
|
125
136
|
original: baseline,
|
|
126
137
|
candidate: checked,
|
|
127
138
|
preservationScore: preservation,
|
|
128
139
|
regressions,
|
|
140
|
+
strictFindings: unresolvedStrictFindings,
|
|
141
|
+
...(claims ? { claims } : {}),
|
|
129
142
|
finalOutput,
|
|
130
143
|
logicLint,
|
|
131
144
|
...(factLint ? { factLint } : {}), ...(requiredFacts ? { requiredFacts } : {}),
|
|
132
|
-
passed: checked.passed && !regressions.some(isBlockingFinding) && preservation >= 70 && finalOutput.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
|
|
145
|
+
passed: checked.passed && unresolvedStrictFindings.length === 0 && !regressions.some(isBlockingFinding) && (claims ? claims.passed : preservation >= 70) && finalOutput.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
|
|
133
146
|
};
|
|
134
147
|
}
|
|
148
|
+
export function verify(original, candidate, profile, brief) {
|
|
149
|
+
return verifyCandidate(original, candidate, profile, brief);
|
|
150
|
+
}
|
|
135
151
|
export function verifyWithCopySpec(original, candidate, profile, spec, brief) {
|
|
136
152
|
const verification = verify(original, candidate, profile, brief);
|
|
137
153
|
const claims = verifyClaims(candidate, spec);
|
|
138
154
|
return { ...verification, claims, passed: verification.passed && claims.passed };
|
|
139
155
|
}
|
|
140
156
|
export function verifyRebuildWithCopySpec(original, candidate, profile, spec, brief) {
|
|
141
|
-
|
|
142
|
-
const claims = verifyClaims(candidate, spec);
|
|
143
|
-
const finalCheck = finalOutputCheck(candidate);
|
|
144
|
-
const logicLint = lintLogic(candidate, brief);
|
|
145
|
-
const factLint = brief?.factSources?.length ? lintFacts({ sources: brief.factSources, draft: candidate, metadata: brief.factMetadata }) : undefined;
|
|
146
|
-
const requiredFacts = verifyRequiredFacts(candidate, brief);
|
|
147
|
-
return {
|
|
148
|
-
version: '2',
|
|
149
|
-
original: baseline,
|
|
150
|
-
candidate: checked,
|
|
151
|
-
preservationScore: preservation,
|
|
152
|
-
regressions,
|
|
153
|
-
claims,
|
|
154
|
-
finalOutput: finalCheck,
|
|
155
|
-
logicLint,
|
|
156
|
-
...(factLint ? { factLint } : {}), ...(requiredFacts ? { requiredFacts } : {}),
|
|
157
|
-
passed: checked.passed && !regressions.some(isBlockingFinding) && claims.passed && finalCheck.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
|
|
158
|
-
};
|
|
157
|
+
return verifyCandidate(original, candidate, profile, brief, { copySpec: spec });
|
|
159
158
|
}
|
|
160
159
|
function projectDeterministicVerificationArtifact(source, candidate, profile, verification, copySpec, writingBrief, verificationKind = 'claims' in verification ? 'copy_spec' : 'standard') {
|
|
161
160
|
const identity = profileIdentity(profile);
|
package/dist/pipeline.test.js
CHANGED
|
@@ -83,7 +83,7 @@ test('orders rewrite instructions by importance tier', () => {
|
|
|
83
83
|
});
|
|
84
84
|
test('gives strong, source-faithful repair feedback without authorizing a broader rewrite', () => {
|
|
85
85
|
const prompt = rewritePrompt('The scheduler failed — twice. Experts say this changes everything. The launch is on 14 August.', profile);
|
|
86
|
-
assert.match(prompt, /
|
|
86
|
+
assert.match(prompt, /Every active AI Editor finding is a required repair/);
|
|
87
87
|
assert.match(prompt, /Do not invent a source, metric, date, quotation, mechanism, example, CTA, or opinion/);
|
|
88
88
|
assert.match(prompt, /otherwise remove the unsupported framing without widening the claim/);
|
|
89
89
|
assert.match(prompt, /Before responding, check every Tier 1 finding against its replacement/);
|
|
@@ -119,9 +119,11 @@ test('advisory and pending-judgment findings pass while blocking findings fail',
|
|
|
119
119
|
const blocking = analyze('The scheduler failed — twice.', profile);
|
|
120
120
|
assert.equal(blocking.passed, false);
|
|
121
121
|
});
|
|
122
|
-
test('verify rejects
|
|
122
|
+
test('verify rejects unresolved active AI Editor findings under the default strict policy', () => {
|
|
123
123
|
const neutralProfile = buildProfile(['I write plainly.', 'I name the mechanism.']);
|
|
124
|
-
|
|
124
|
+
const unresolved = verify('The scheduler failed twice.', 'The scheduler failed twice. We leverage logs.', neutralProfile);
|
|
125
|
+
assert.equal(unresolved.passed, false);
|
|
126
|
+
assert.ok(unresolved.strictFindings.some((finding) => finding.id === 'ai.leverage'));
|
|
125
127
|
assert.equal(verify('The scheduler failed twice.', 'The scheduler failed — twice.', profile).passed, false);
|
|
126
128
|
});
|
|
127
129
|
test('keeps verify pass/fail on the legacy metric while calibration reports both metrics', () => {
|
package/dist/rebuild-task.js
CHANGED
|
@@ -10,26 +10,13 @@ import { HYV_VERSION } from './version.js';
|
|
|
10
10
|
import { buildRecompositionBrief, measureLexicalResidual, parseRecompositionPolicy } from './recomposition.js';
|
|
11
11
|
import { provenanceStatusForRebuild, writerRequestForRebuild } from './provenance-status.js';
|
|
12
12
|
import { fingerprint, profileIdentity, sha256 as digest } from './internal.js';
|
|
13
|
-
|
|
13
|
+
import { failure, parseResponseJson, projectLifecycleBinding } from './rewrite-response.js';
|
|
14
14
|
const MAX_CANDIDATE_CHARACTERS = 100_000;
|
|
15
|
-
function failure(code, message, path) {
|
|
16
|
-
return { code, message, ...(path ? { path } : {}) };
|
|
17
|
-
}
|
|
18
15
|
function isFailure(value) {
|
|
19
16
|
return typeof value === 'object' && value !== null && 'code' in value && 'message' in value;
|
|
20
17
|
}
|
|
21
|
-
function parseJson(value) {
|
|
22
|
-
if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
|
|
23
|
-
return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(value);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return failure('invalid_json', 'Response must be valid JSON.');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
18
|
function parseRebuildResponse(value) {
|
|
32
|
-
const raw = typeof value === 'string' ?
|
|
19
|
+
const raw = typeof value === 'string' ? parseResponseJson(value) : value;
|
|
33
20
|
if (isFailure(raw))
|
|
34
21
|
return raw;
|
|
35
22
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
@@ -159,7 +146,7 @@ function rejected(task, raw, failures) {
|
|
|
159
146
|
};
|
|
160
147
|
}
|
|
161
148
|
export function applyRebuildResponse(task, raw) {
|
|
162
|
-
const response = parseRebuildResponse(typeof raw === 'string' ?
|
|
149
|
+
const response = parseRebuildResponse(typeof raw === 'string' ? parseResponseJson(raw) : raw);
|
|
163
150
|
if (isFailure(response))
|
|
164
151
|
return rejected(task, raw, [response]);
|
|
165
152
|
if (response.taskFingerprint !== task.fingerprint) {
|
|
@@ -227,15 +214,5 @@ export function createRebuildLifecycleBinding(task, receipt, deterministic) {
|
|
|
227
214
|
if (!deterministic.passed || receipt.taskFingerprint !== task.fingerprint || receipt.mode !== 'REBUILD' || deterministic.verificationKind !== 'rebuild') {
|
|
228
215
|
throw new Error('Lifecycle binding requires a passed rebuild artifact for this rebuild task.');
|
|
229
216
|
}
|
|
230
|
-
return
|
|
231
|
-
rewriteTaskFingerprint: task.fingerprint,
|
|
232
|
-
rewriteResponseFingerprint: receipt.responseFingerprint,
|
|
233
|
-
deterministicArtifactFingerprint: deterministic.artifactFingerprint,
|
|
234
|
-
sourceHash: deterministic.sourceHash,
|
|
235
|
-
candidateHash: deterministic.candidateHash,
|
|
236
|
-
profileId: deterministic.profileId,
|
|
237
|
-
profileRevisionDigest: deterministic.profileRevisionDigest,
|
|
238
|
-
rulesetVersion: deterministic.rulesetVersion,
|
|
239
|
-
schemaVersion: '1',
|
|
240
|
-
};
|
|
217
|
+
return projectLifecycleBinding(task.fingerprint, receipt, deterministic);
|
|
241
218
|
}
|
|
@@ -174,7 +174,7 @@ test('CLI and MCP rebuild helpers share fingerprints', () => {
|
|
|
174
174
|
version: '1', audience: 'operators', intent: 'explain', format: 'outreach',
|
|
175
175
|
});
|
|
176
176
|
assert.match(briefTask.prompt, /# WritingBrief/);
|
|
177
|
-
assert.equal(HYV_VERSION, '3.
|
|
177
|
+
assert.equal(HYV_VERSION, '3.6.1');
|
|
178
178
|
});
|
|
179
179
|
test('apply rejects forged tasks, missing capability, and substituted profiles', () => {
|
|
180
180
|
const reduction = rebuildRecommendation();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const MAX_RESPONSE_BYTES = 100_000;
|
|
2
|
+
export function failure(code, message, path) {
|
|
3
|
+
return { code, message, ...(path ? { path } : {}) };
|
|
4
|
+
}
|
|
5
|
+
export function parseResponseJson(value) {
|
|
6
|
+
if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
|
|
7
|
+
return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(value);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return failure('invalid_json', 'Response must be valid JSON.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function projectLifecycleBinding(taskFingerprint, receipt, deterministic) {
|
|
16
|
+
return {
|
|
17
|
+
rewriteTaskFingerprint: taskFingerprint,
|
|
18
|
+
rewriteResponseFingerprint: receipt.responseFingerprint,
|
|
19
|
+
deterministicArtifactFingerprint: deterministic.artifactFingerprint,
|
|
20
|
+
sourceHash: deterministic.sourceHash,
|
|
21
|
+
candidateHash: deterministic.candidateHash,
|
|
22
|
+
profileId: deterministic.profileId,
|
|
23
|
+
profileRevisionDigest: deterministic.profileRevisionDigest,
|
|
24
|
+
rulesetVersion: deterministic.rulesetVersion,
|
|
25
|
+
schemaVersion: '1',
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/rewrite-task.js
CHANGED
|
@@ -3,30 +3,14 @@ import { finalOutputCheck, hygieneSourceFindings } from './hygiene.js';
|
|
|
3
3
|
import { analyze, deriveEditScope, renderRewritePrompt, verifyDeterministically } from './pipeline.js';
|
|
4
4
|
import { sentences } from './text.js';
|
|
5
5
|
import { fingerprint } from './internal.js';
|
|
6
|
-
|
|
6
|
+
import { failure, parseResponseJson, projectLifecycleBinding } from './rewrite-response.js';
|
|
7
7
|
const MAX_REPLACEMENTS = 100;
|
|
8
8
|
const MAX_REPLACEMENT_CHARACTERS = 10_000;
|
|
9
|
-
function failure(code, message, path) {
|
|
10
|
-
return { code, message, ...(path ? { path } : {}) };
|
|
11
|
-
}
|
|
12
|
-
function responseFingerprint(response) {
|
|
13
|
-
return fingerprint(response);
|
|
14
|
-
}
|
|
15
|
-
function parseJson(value) {
|
|
16
|
-
if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
|
|
17
|
-
return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(value);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return failure('invalid_json', 'Response must be valid JSON.');
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
9
|
function isFailure(value) {
|
|
26
10
|
return typeof value === 'object' && value !== null && 'code' in value;
|
|
27
11
|
}
|
|
28
12
|
function parseResponse(value) {
|
|
29
|
-
const raw = typeof value === 'string' ?
|
|
13
|
+
const raw = typeof value === 'string' ? parseResponseJson(value) : value;
|
|
30
14
|
if (isFailure(raw))
|
|
31
15
|
return raw;
|
|
32
16
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
@@ -106,7 +90,7 @@ export function prepareRewriteTask(draft, profile, copySpec, writingBrief, autho
|
|
|
106
90
|
const result = analyze(draft, profile, writingBrief);
|
|
107
91
|
const prompt = renderRewritePrompt(draft, profile, result, [], writingBrief);
|
|
108
92
|
const mapped = sentences(draft);
|
|
109
|
-
const eligibleSentenceIds = new Set([...deriveEditScope(result).eligibleSentenceIds, ...authorizedSentenceIds]);
|
|
93
|
+
const eligibleSentenceIds = new Set([...deriveEditScope(result, true).eligibleSentenceIds, ...authorizedSentenceIds]);
|
|
110
94
|
const taskBase = {
|
|
111
95
|
version: '1',
|
|
112
96
|
draft,
|
|
@@ -133,7 +117,7 @@ export function parseRewriteTask(value) {
|
|
|
133
117
|
return task;
|
|
134
118
|
}
|
|
135
119
|
function rejected(task, raw, failures, adapterIds = []) {
|
|
136
|
-
return { status: 'repairable', failures, receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint:
|
|
120
|
+
return { status: 'repairable', failures, receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint: fingerprint(raw), adapterIds, replacementSentenceIds: [] } };
|
|
137
121
|
}
|
|
138
122
|
export function applyShip(task) {
|
|
139
123
|
return {
|
|
@@ -151,7 +135,7 @@ export function applyShip(task) {
|
|
|
151
135
|
};
|
|
152
136
|
}
|
|
153
137
|
export function applyRewriteResponse(task, raw) {
|
|
154
|
-
const source = typeof raw === 'string' ?
|
|
138
|
+
const source = typeof raw === 'string' ? parseResponseJson(raw) : raw;
|
|
155
139
|
const parsed = isFailure(source) ? source : parseResponse(source);
|
|
156
140
|
const fenced = isFailure(parsed) && parsed.code === 'invalid_json' ? repairFencedJson(raw) : { value: source };
|
|
157
141
|
const repaired = isFailure(parsed) && parsed.code === 'invalid_response_shape' ? repairStringifiedReplacements(source) : fenced;
|
|
@@ -163,7 +147,7 @@ export function applyRewriteResponse(task, raw) {
|
|
|
163
147
|
return rejected(task, raw, [failure('task_fingerprint_mismatch', 'Response task fingerprint does not match this task.', 'taskFingerprint')], adapterIds);
|
|
164
148
|
if ('mode' in response && response.mode === 'SHIP') {
|
|
165
149
|
const shipped = applyShip(task);
|
|
166
|
-
return { ...shipped, receipt: { ...shipped.receipt, adapterIds, responseFingerprint:
|
|
150
|
+
return { ...shipped, receipt: { ...shipped.receipt, adapterIds, responseFingerprint: fingerprint(raw) } };
|
|
167
151
|
}
|
|
168
152
|
if (response.version === '2')
|
|
169
153
|
return applyRangeResponse(task, response, raw, adapterIds);
|
|
@@ -189,7 +173,7 @@ export function applyRewriteResponse(task, raw) {
|
|
|
189
173
|
if (replacement !== undefined)
|
|
190
174
|
candidate = `${candidate.slice(0, sentence.start)}${replacement}${candidate.slice(sentence.end)}`;
|
|
191
175
|
}
|
|
192
|
-
return { status: 'accepted', candidate, failures: [], receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint:
|
|
176
|
+
return { status: 'accepted', candidate, failures: [], receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint: fingerprint(raw), adapterIds, replacementSentenceIds: [...seen].sort((left, right) => left - right), mode: 'EDIT' } };
|
|
193
177
|
}
|
|
194
178
|
function applyRangeResponse(task, response, raw, adapterIds) {
|
|
195
179
|
const sentenceMap = new Map(task.sentences.map((sentence) => [sentence.id, sentence]));
|
|
@@ -236,7 +220,7 @@ function applyRangeResponse(task, response, raw, adapterIds) {
|
|
|
236
220
|
receipt: {
|
|
237
221
|
version: '1',
|
|
238
222
|
taskFingerprint: task.fingerprint,
|
|
239
|
-
responseFingerprint:
|
|
223
|
+
responseFingerprint: fingerprint(raw),
|
|
240
224
|
adapterIds,
|
|
241
225
|
operationRanges: response.operations.map((operation) => ({ startSentenceId: operation.startSentenceId, endSentenceId: operation.endSentenceId })),
|
|
242
226
|
mode: 'EDIT',
|
|
@@ -262,15 +246,5 @@ export function evaluateRewriteResponse(task, raw, profile) {
|
|
|
262
246
|
export function createRewriteLifecycleBinding(task, receipt, deterministic) {
|
|
263
247
|
if (!deterministic.passed || receipt.taskFingerprint !== task.fingerprint)
|
|
264
248
|
throw new Error('Lifecycle binding requires a passed deterministic artifact for this rewrite task.');
|
|
265
|
-
return
|
|
266
|
-
rewriteTaskFingerprint: task.fingerprint,
|
|
267
|
-
rewriteResponseFingerprint: receipt.responseFingerprint,
|
|
268
|
-
deterministicArtifactFingerprint: deterministic.artifactFingerprint,
|
|
269
|
-
sourceHash: deterministic.sourceHash,
|
|
270
|
-
candidateHash: deterministic.candidateHash,
|
|
271
|
-
profileId: deterministic.profileId,
|
|
272
|
-
profileRevisionDigest: deterministic.profileRevisionDigest,
|
|
273
|
-
rulesetVersion: deterministic.rulesetVersion,
|
|
274
|
-
schemaVersion: '1',
|
|
275
|
-
};
|
|
249
|
+
return projectLifecycleBinding(task.fingerprint, receipt, deterministic);
|
|
276
250
|
}
|
|
@@ -21,18 +21,19 @@ test('applies only eligible numbered replacements and preserves all clean bytes'
|
|
|
21
21
|
assert.equal(result.candidate, 'I use the answer. The launch is on 14 August.');
|
|
22
22
|
assert.deepEqual(result.receipt.adapterIds, []);
|
|
23
23
|
});
|
|
24
|
-
test('
|
|
24
|
+
test('makes every active AI Editor finding eligible under the default strict rewrite policy', () => {
|
|
25
25
|
const task = prepareRewriteTask('I write clear notes. This work is meaningful. I keep the mechanism visible.', profile);
|
|
26
|
-
assert.deepEqual(task.eligibleSentenceIds, []);
|
|
27
|
-
assert.deepEqual(task.sentences.map((sentence) => sentence.eligible), [false,
|
|
26
|
+
assert.deepEqual(task.eligibleSentenceIds, [2]);
|
|
27
|
+
assert.deepEqual(task.sentences.map((sentence) => sentence.eligible), [false, true, false]);
|
|
28
|
+
assert.match(task.prompt, /Every active AI Editor finding is a required repair/);
|
|
28
29
|
});
|
|
29
|
-
test('
|
|
30
|
+
test('makes advisory and judgment-required findings required repairs', () => {
|
|
30
31
|
const advisory = prepareRewriteTask('Firstly, check the invoice.', profile);
|
|
31
|
-
assert.deepEqual(advisory.eligibleSentenceIds, []);
|
|
32
|
+
assert.deepEqual(advisory.eligibleSentenceIds, [1]);
|
|
32
33
|
const neutralProfile = buildProfile(['I write plainly.', 'I name the mechanism.']);
|
|
33
34
|
const pending = prepareRewriteTask('We leverage the scheduler.', neutralProfile);
|
|
34
|
-
assert.deepEqual(pending.eligibleSentenceIds, []);
|
|
35
|
-
assert.match(pending.prompt, /
|
|
35
|
+
assert.deepEqual(pending.eligibleSentenceIds, [1]);
|
|
36
|
+
assert.match(pending.prompt, /Every active AI Editor finding is a required repair/);
|
|
36
37
|
const blocking = prepareRewriteTask('The scheduler failed — twice. The owner checked it.', profile);
|
|
37
38
|
assert.deepEqual(blocking.eligibleSentenceIds, [1]);
|
|
38
39
|
});
|
|
@@ -40,9 +41,9 @@ test('keeps blocking VoiceDNA avoid findings eligible', () => {
|
|
|
40
41
|
const task = prepareRewriteTask('The plan uses leverage.', profile);
|
|
41
42
|
assert.deepEqual(task.eligibleSentenceIds, [1]);
|
|
42
43
|
});
|
|
43
|
-
test('
|
|
44
|
+
test('makes reconciled active founder reframes eligible under the default strict policy', () => {
|
|
44
45
|
const task = prepareRewriteTask("This isn't positioning. This is proof.", profile);
|
|
45
|
-
assert.deepEqual(task.eligibleSentenceIds, []);
|
|
46
|
+
assert.deepEqual(task.eligibleSentenceIds, [1]);
|
|
46
47
|
});
|
|
47
48
|
test('preserves a clean draft byte-for-byte when the rewrite response is empty', () => {
|
|
48
49
|
const draft = 'I write clear notes.\n\nI keep the mechanism visible.\n';
|