@holdyourvoice/hyv 3.3.5 → 3.4.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.
Files changed (87) hide show
  1. package/Readme.md +66 -116
  2. package/dist/agents/catalog.js +118 -0
  3. package/dist/agents/catalog.test.js +60 -0
  4. package/dist/agents/emit.js +42 -0
  5. package/dist/agents/emit.test.js +64 -0
  6. package/dist/agents/index.js +4 -0
  7. package/dist/agents/load.js +184 -0
  8. package/dist/agents/load.test.js +149 -0
  9. package/dist/agents/types.js +18 -0
  10. package/dist/cli.js +464 -317
  11. package/dist/cli.test.js +76 -0
  12. package/dist/fact-linter.js +107 -98
  13. package/dist/mcp.js +43 -192
  14. package/dist/rebuild-task.test.js +1 -1
  15. package/dist/release-audit.test.js +5 -1
  16. package/dist/stage1-evaluation.js +74 -48
  17. package/dist/version.js +1 -1
  18. package/package.json +3 -3
  19. package/skills/hyv-analyze/SKILL.md +24 -0
  20. package/skills/hyv-analyze/agent.json +49 -0
  21. package/skills/hyv-analyze/agents/openai.yaml +4 -0
  22. package/skills/hyv-apply-hidden-text-policy/SKILL.md +24 -0
  23. package/skills/hyv-apply-hidden-text-policy/agent.json +49 -0
  24. package/skills/hyv-apply-hidden-text-policy/agents/openai.yaml +4 -0
  25. package/skills/hyv-apply-rebuild/SKILL.md +24 -0
  26. package/skills/hyv-apply-rebuild/agent.json +49 -0
  27. package/skills/hyv-apply-rebuild/agents/openai.yaml +4 -0
  28. package/skills/hyv-apply-rewrite/SKILL.md +24 -0
  29. package/skills/hyv-apply-rewrite/agent.json +48 -0
  30. package/skills/hyv-apply-rewrite/agents/openai.yaml +4 -0
  31. package/skills/hyv-batch-analyze/SKILL.md +24 -0
  32. package/skills/hyv-batch-analyze/agent.json +45 -0
  33. package/skills/hyv-batch-analyze/agents/openai.yaml +4 -0
  34. package/skills/hyv-fact-lint/SKILL.md +24 -0
  35. package/skills/hyv-fact-lint/agent.json +50 -0
  36. package/skills/hyv-fact-lint/agents/openai.yaml +4 -0
  37. package/skills/hyv-final-check/SKILL.md +24 -0
  38. package/skills/hyv-final-check/agent.json +43 -0
  39. package/skills/hyv-final-check/agents/openai.yaml +4 -0
  40. package/skills/hyv-hygiene/SKILL.md +24 -0
  41. package/skills/hyv-hygiene/agent.json +50 -0
  42. package/skills/hyv-hygiene/agents/openai.yaml +4 -0
  43. package/skills/hyv-inspect-hidden-text/SKILL.md +24 -0
  44. package/skills/hyv-inspect-hidden-text/agent.json +48 -0
  45. package/skills/hyv-inspect-hidden-text/agents/openai.yaml +4 -0
  46. package/skills/hyv-learning/SKILL.md +24 -0
  47. package/skills/hyv-learning/agent.json +48 -0
  48. package/skills/hyv-learning/agents/openai.yaml +4 -0
  49. package/skills/hyv-lifecycle/SKILL.md +24 -0
  50. package/skills/hyv-lifecycle/agent.json +48 -0
  51. package/skills/hyv-lifecycle/agents/openai.yaml +4 -0
  52. package/skills/hyv-logic-lint/SKILL.md +24 -0
  53. package/skills/hyv-logic-lint/agent.json +47 -0
  54. package/skills/hyv-logic-lint/agents/openai.yaml +4 -0
  55. package/skills/hyv-mcp/SKILL.md +24 -0
  56. package/skills/hyv-mcp/agent.json +43 -0
  57. package/skills/hyv-mcp/agents/openai.yaml +4 -0
  58. package/skills/hyv-patterns/SKILL.md +24 -0
  59. package/skills/hyv-patterns/agent.json +43 -0
  60. package/skills/hyv-patterns/agents/openai.yaml +4 -0
  61. package/skills/hyv-prepare-judgment/SKILL.md +24 -0
  62. package/skills/hyv-prepare-judgment/agent.json +52 -0
  63. package/skills/hyv-prepare-judgment/agents/openai.yaml +4 -0
  64. package/skills/hyv-prepare-rebuild/SKILL.md +24 -0
  65. package/skills/hyv-prepare-rebuild/agent.json +54 -0
  66. package/skills/hyv-prepare-rebuild/agents/openai.yaml +4 -0
  67. package/skills/hyv-prepare-rewrite/SKILL.md +24 -0
  68. package/skills/hyv-prepare-rewrite/agent.json +51 -0
  69. package/skills/hyv-prepare-rewrite/agents/openai.yaml +4 -0
  70. package/skills/hyv-profile/SKILL.md +24 -0
  71. package/skills/hyv-profile/agent.json +50 -0
  72. package/skills/hyv-profile/agents/openai.yaml +4 -0
  73. package/skills/hyv-rebuild-writer-request/SKILL.md +24 -0
  74. package/skills/hyv-rebuild-writer-request/agent.json +47 -0
  75. package/skills/hyv-rebuild-writer-request/agents/openai.yaml +4 -0
  76. package/skills/hyv-reduce-judgment/SKILL.md +24 -0
  77. package/skills/hyv-reduce-judgment/agent.json +45 -0
  78. package/skills/hyv-reduce-judgment/agents/openai.yaml +4 -0
  79. package/skills/hyv-rewrite-prompt/SKILL.md +24 -0
  80. package/skills/hyv-rewrite-prompt/agent.json +48 -0
  81. package/skills/hyv-rewrite-prompt/agents/openai.yaml +4 -0
  82. package/skills/hyv-verify/SKILL.md +24 -0
  83. package/skills/hyv-verify/agent.json +49 -0
  84. package/skills/hyv-verify/agents/openai.yaml +4 -0
  85. package/skills/hyv-verify-spec/SKILL.md +24 -0
  86. package/skills/hyv-verify-spec/agent.json +50 -0
  87. package/skills/hyv-verify-spec/agents/openai.yaml +4 -0
package/dist/cli.test.js CHANGED
@@ -556,3 +556,79 @@ test('prepares and applies an authorized rebuild through CLI', () => {
556
556
  rmSync(directory, { recursive: true, force: true });
557
557
  }
558
558
  });
559
+ test('agent list enumerates every command as one portable package', () => {
560
+ const result = run(['agent', 'list']);
561
+ assert.equal(result.status, 0, result.stderr);
562
+ const entries = JSON.parse(result.stdout);
563
+ assert.equal(entries.length, 23);
564
+ const ids = entries.map((entry) => entry.id);
565
+ assert.equal(new Set(ids).size, ids.length);
566
+ for (const id of ['hyv-profile', 'hyv-analyze', 'hyv-verify', 'hyv-mcp', 'hyv-patterns']) {
567
+ assert.ok(ids.includes(id), `missing ${id}`);
568
+ }
569
+ for (const entry of entries) {
570
+ assert.ok(entry.role);
571
+ assert.ok(entry.workflow_phase);
572
+ assert.ok(entry.description);
573
+ }
574
+ });
575
+ test('agent validate passes over the full package tree and rejects unknown ids', () => {
576
+ const result = run(['agent', 'validate']);
577
+ assert.equal(result.status, 0, result.stderr);
578
+ assert.equal(JSON.parse(result.stdout).status, 'PASS');
579
+ assert.equal(run(['agent', 'validate', 'hyv-verify']).status, 0);
580
+ const unknown = run(['agent', 'validate', 'hyv-does-not-exist']);
581
+ assert.equal(unknown.status, 1);
582
+ assert.match(unknown.stderr, /unknown agent/);
583
+ });
584
+ test('agent describe resolves a host and reports the runtime descriptor', () => {
585
+ const result = run(['agent', 'describe', 'hyv-verify', '--host', 'codex']);
586
+ assert.equal(result.status, 0, result.stderr);
587
+ const descriptor = JSON.parse(result.stdout);
588
+ assert.equal(descriptor.agent.id, 'hyv-verify');
589
+ assert.equal(descriptor.host.id, 'codex');
590
+ assert.ok(descriptor.available_capabilities.includes('read_repository'));
591
+ assert.ok(Array.isArray(descriptor.unavailable_capabilities));
592
+ });
593
+ test('agent emit writes a prompt or json contract to stdout or a named file', () => {
594
+ const prompt = run(['agent', 'emit', 'hyv-verify', '--mode=prompt', '--host=generic']);
595
+ assert.equal(prompt.status, 0, prompt.stderr);
596
+ assert.match(prompt.stdout, /# hyv-verify/);
597
+ assert.match(prompt.stdout, /Required input: original path, candidate path, profile path/);
598
+ const jsonOut = run(['agent', 'emit', 'hyv-verify', '--mode', 'json', '--host', 'generic']);
599
+ assert.equal(jsonOut.status, 0, jsonOut.stderr);
600
+ assert.equal(JSON.parse(jsonOut.stdout).agent.id, 'hyv-verify');
601
+ const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-agent-'));
602
+ try {
603
+ const target = join(directory, 'verify.json');
604
+ const emitted = run(['agent', 'emit', 'hyv-verify', '--mode', 'json', '--output', target]);
605
+ assert.equal(emitted.status, 0, emitted.stderr);
606
+ assert.equal(JSON.parse(readFileSync(target, 'utf8')).agent.id, 'hyv-verify');
607
+ }
608
+ finally {
609
+ rmSync(directory, { recursive: true, force: true });
610
+ }
611
+ });
612
+ test('agent commands locate the installed package tree outside a repository', () => {
613
+ const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-agent-cwd-'));
614
+ try {
615
+ mkdirSync(join(directory, 'skills'));
616
+ const result = spawnSync(process.execPath, [cli, 'agent', 'validate'], { cwd: directory, encoding: 'utf8' });
617
+ assert.equal(result.status, 0, result.stderr);
618
+ assert.equal(JSON.parse(result.stdout).status, 'PASS');
619
+ }
620
+ finally {
621
+ rmSync(directory, { recursive: true, force: true });
622
+ }
623
+ });
624
+ test('agent usage errors return exit 1 and never mutate', () => {
625
+ assert.equal(run(['agent']).status, 1);
626
+ assert.equal(run(['agent', 'emit', 'hyv-verify']).status, 1);
627
+ assert.equal(run(['agent', 'emit', 'hyv-verify', '--mode', 'bad']).status, 1);
628
+ assert.equal(run(['agent', 'describe']).status, 1);
629
+ assert.equal(run(['agent', 'describe', 'hyv-verify', '--mode', 'json']).status, 1);
630
+ assert.equal(run(['agent', 'describe', 'hyv-verify', '--output', 'ignored.json']).status, 1);
631
+ assert.equal(run(['agent', 'list', '--host', 'codex']).status, 1);
632
+ assert.equal(run(['agent', 'list', 'extra']).status, 1);
633
+ assert.equal(run(['agent', 'nope']).status, 1);
634
+ });
@@ -67,6 +67,110 @@ export function extractFactClaims(draft) {
67
67
  }
68
68
  return output;
69
69
  }
70
+ function capabilityFinding(claim, relevant, evidenceItems) {
71
+ const claimCapabilities = capabilityObjects(claim.text);
72
+ if (!claimCapabilities.length || !relevant.length)
73
+ return undefined;
74
+ const sourceCapabilities = relevant.flatMap(({ text }) => capabilityObjects(text));
75
+ const supported = claimCapabilities.every((capability) => sourceCapabilities.some((sourceCapability) => capability.every((token) => sourceCapability.includes(token))));
76
+ if (supported && relevant.some(({ text }) => negated(text) !== negated(claim.text))) {
77
+ return finding(claim, 'capability_drift', 'error', 'The draft reverses the source capability.', evidenceItems, 'high', 'Match the source capability polarity or cite contrary evidence.');
78
+ }
79
+ if (supported)
80
+ return undefined;
81
+ const formatMismatch = claimCapabilities.some((capability) => {
82
+ const claimFormats = capability.filter((token) => CAPABILITY_FORMATS.has(token));
83
+ return claimFormats.length > 0 && sourceCapabilities.some((sourceCapability) => {
84
+ const sourceFormats = sourceCapability.filter((token) => CAPABILITY_FORMATS.has(token));
85
+ return sourceFormats.length > 0 && claimFormats.some((format) => !sourceFormats.includes(format));
86
+ });
87
+ });
88
+ if (formatMismatch) {
89
+ return finding(claim, 'capability_drift', 'error', 'The product capability differs from the supplied source.', evidenceItems, 'high', 'Match the source capability or cite contrary evidence.');
90
+ }
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
+ }
93
+ function findingsForClaim(claim, input, sourceLines, semanticAdapter) {
94
+ const relevant = findRelevant(claim.text, sourceLines);
95
+ const same = sourceLines.find(({ text }) => hasOverlap(claim.text, text));
96
+ const evidenceItems = relevant.length ? relevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : fallbackEvidence(sourceLines);
97
+ const quote = claim.text.match(QUOTE)?.[1];
98
+ const sourceHasAttribution = input.sources.some((source) => /\b(said|according to|reported)\b/i.test(source.text));
99
+ if (quote && sourceHasAttribution && !input.sources.some((source) => source.text.includes(quote))) {
100
+ const quoteRelevant = sourceLines.filter(({ text }) => /\b(said|according to|reported)\b/i.test(text));
101
+ const quoteEvidence = quoteRelevant.length ? quoteRelevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : input.sources.slice(0, 1).map((source) => evidence(source, source.text));
102
+ 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
+ const claimDates = dates(claim.text);
105
+ const sourceDates = relevant.flatMap(({ text }) => dates(text));
106
+ const claimNumbers = numbers(claim.text);
107
+ const sourceNumbers = relevant.flatMap(({ text }) => numbers(text));
108
+ if (!claimDates.length && !claim.kinds.includes('attribution_quote') && claimNumbers.length && sourceNumbers.length && claimNumbers.some((number) => !sourceNumbers.some((sourceNumber) => normal(sourceNumber) === normal(number)))) {
109
+ return [finding(claim, 'number_drift', 'error', 'A number or unit differs from relevant source evidence.', evidenceItems, 'high', 'Correct the number or unit, or cite a newer source.')];
110
+ }
111
+ if (claimDates.length && sourceDates.length && claimDates.some((date) => !sourceDates.includes(date))) {
112
+ return [finding(claim, 'date_drift', 'error', 'The draft date differs from relevant source evidence.', evidenceItems, 'high', 'Correct the date or cite a newer source.')];
113
+ }
114
+ const claimEntities = entities(claim.text);
115
+ const sourceEntities = relevant.flatMap(({ text }) => entities(text));
116
+ if (claimEntities.length && sourceEntities.length && claimEntities.some((entity) => !sourceEntities.some((sourceEntity) => normal(sourceEntity) === normal(entity)))) {
117
+ return [finding(claim, 'entity_drift', 'error', 'A named entity differs from relevant source evidence.', evidenceItems, 'high', 'Correct the name or cite the source that supports it.')];
118
+ }
119
+ const capability = capabilityFinding(claim, relevant, evidenceItems);
120
+ if (capability)
121
+ return [capability];
122
+ if (capabilityObjects(claim.text).length && relevant.length)
123
+ return [];
124
+ const overreach = [];
125
+ if (claim.kinds.includes('causal') && relevant.length && !relevant.some(({ text }) => /\b(caused?|because|led to|resulted in)\b/i.test(text))) {
126
+ overreach.push(finding(claim, 'causal_overreach', 'warning', 'The sources describe an outcome but do not establish causation.', evidenceItems, 'medium', 'Use an association claim or add causal evidence.'));
127
+ }
128
+ if (claim.kinds.includes('comparative') && relevant.length && !relevant.some(({ text }) => /\b(better|more|less|than|best|largest|fastest)\b/i.test(text))) {
129
+ overreach.push(finding(claim, 'comparative_overreach', 'warning', 'The sources do not establish the comparison.', evidenceItems, 'medium', 'Narrow the comparison or add comparative evidence.'));
130
+ }
131
+ if (overreach.length)
132
+ return overreach;
133
+ if (claimDates.some((date) => sourceDates.includes(date)) || same)
134
+ return [];
135
+ if (!relevant.length && claim.kinds.includes('fact') && tokens(claim.text).length <= 4) {
136
+ 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
+ if (!relevant.length || (claim.kinds.includes('number') && !relevant.some(({ text }) => /\b\d+(?:\.\d+)?%?\b/.test(text)))) {
139
+ return [finding(claim, 'unsupported_claim', 'error', 'No supplied source supports this checkable claim.', evidenceItems, 'medium', 'Add a source, remove the claim, or mark it as an approved hypothesis.')];
140
+ }
141
+ const semantic = semanticAdapter?.compare({ claim: claim.text, sources: input.sources });
142
+ if (semantic === 'supported')
143
+ return [];
144
+ if (semantic === 'contradicted') {
145
+ return [finding(claim, 'semantic_contradiction', 'error', 'The configured semantic adapter found contradictory source evidence.', evidenceItems, 'medium', 'Review the cited sources and correct or qualify the claim.')];
146
+ }
147
+ 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
+ function draftContradictions(claims, sourceLines) {
150
+ const normalized = claims.map((claim) => ({ claim, text: normal(claim.text) }));
151
+ const findings = [];
152
+ for (let index = 0; index < normalized.length; index += 1) {
153
+ for (let other = index + 1; other < normalized.length; other += 1) {
154
+ const left = normalized[index];
155
+ const right = normalized[other];
156
+ const leftCore = normal(left.text.replace(/\bnot\b/g, ''));
157
+ const rightCore = normal(right.text.replace(/\bnot\b/g, ''));
158
+ if (leftCore === rightCore && /\bnot\b/.test(left.text) !== /\bnot\b/.test(right.text)) {
159
+ 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
+ }
161
+ }
162
+ }
163
+ return findings;
164
+ }
165
+ function summarizeFindings(claims, findings) {
166
+ const claimKey = (item) => `${item.draftLocation.start}:${item.draftLocation.end}`;
167
+ const unsupported = new Set(findings.filter((item) => item.kind === 'unsupported_claim').map(claimKey)).size;
168
+ const contradicted = new Set(findings.filter((item) => ['draft_contradiction', 'number_drift', 'date_drift', 'entity_drift', 'quote_drift', 'capability_drift', 'semantic_contradiction'].includes(item.kind)).map(claimKey)).size;
169
+ const humanReview = new Set(findings.filter((item) => item.severity === 'needs_human_review' || item.severity === 'warning').map(claimKey)).size;
170
+ const checked = claims.filter((claim) => !claim.kinds.includes('opinion')).length;
171
+ const affected = new Set(findings.map(claimKey)).size;
172
+ return { checked, supported: Math.max(0, checked - affected), unsupported, contradicted, humanReview };
173
+ }
70
174
  export function lintFacts(input) {
71
175
  if (!input.sources.length)
72
176
  throw new Error('Fact lint requires at least one source document.');
@@ -81,105 +185,10 @@ export function lintFacts(input) {
81
185
  for (const claim of claims) {
82
186
  if (claim.kinds.includes('opinion') || allowed.has(normal(claim.text)) || (claim.kinds.includes('hypothesis') && approved.has(normal(claim.text))))
83
187
  continue;
84
- const relevant = findRelevant(claim.text, sourceLines);
85
- const same = sourceLines.find(({ text }) => hasOverlap(claim.text, text));
86
- const evidenceItems = relevant.length ? relevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : fallbackEvidence(sourceLines);
87
- const quote = claim.text.match(QUOTE)?.[1];
88
- const quoteRelevant = sourceLines.filter(({ text }) => /\b(said|according to|reported)\b/i.test(text));
89
- if (quote && input.sources.some((source) => /\b(said|according to|reported)\b/i.test(source.text)) && !input.sources.some((source) => source.text.includes(quote))) {
90
- const quoteEvidence = quoteRelevant.length ? quoteRelevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : input.sources.slice(0, 1).map((source) => evidence(source, source.text));
91
- findings.push(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.'));
92
- continue;
93
- }
94
- const claimDates = dates(claim.text);
95
- const sourceDates = relevant.flatMap(({ text }) => dates(text));
96
- const claimNumbers = numbers(claim.text);
97
- const sourceNumbers = relevant.flatMap(({ text }) => numbers(text));
98
- if (!claimDates.length && !claim.kinds.includes('attribution_quote') && claimNumbers.length && sourceNumbers.length && claimNumbers.some((number) => !sourceNumbers.some((sourceNumber) => normal(sourceNumber) === normal(number)))) {
99
- findings.push(finding(claim, 'number_drift', 'error', 'A number or unit differs from relevant source evidence.', evidenceItems, 'high', 'Correct the number or unit, or cite a newer source.'));
100
- continue;
101
- }
102
- if (claimDates.length && sourceDates.length && claimDates.some((date) => !sourceDates.includes(date))) {
103
- findings.push(finding(claim, 'date_drift', 'error', 'The draft date differs from relevant source evidence.', evidenceItems, 'high', 'Correct the date or cite a newer source.'));
104
- continue;
105
- }
106
- const claimEntities = entities(claim.text);
107
- const sourceEntities = relevant.flatMap(({ text }) => entities(text));
108
- if (claimEntities.length && sourceEntities.length && claimEntities.some((entity) => !sourceEntities.some((sourceEntity) => normal(sourceEntity) === normal(entity)))) {
109
- findings.push(finding(claim, 'entity_drift', 'error', 'A named entity differs from relevant source evidence.', evidenceItems, 'high', 'Correct the name or cite the source that supports it.'));
110
- continue;
111
- }
112
- const claimCapabilities = capabilityObjects(claim.text);
113
- if (claimCapabilities.length && relevant.length) {
114
- const sourceCapabilities = relevant.flatMap(({ text }) => capabilityObjects(text));
115
- const supportedCapability = claimCapabilities.every((capability) => sourceCapabilities.some((sourceCapability) => capability.every((token) => sourceCapability.includes(token))));
116
- if (supportedCapability && relevant.some(({ text }) => negated(text) !== negated(claim.text))) {
117
- findings.push(finding(claim, 'capability_drift', 'error', 'The draft reverses the source capability.', evidenceItems, 'high', 'Match the source capability polarity or cite contrary evidence.'));
118
- continue;
119
- }
120
- if (supportedCapability)
121
- continue;
122
- const knownFormatMismatch = claimCapabilities.some((capability) => {
123
- const claimFormats = capability.filter((token) => CAPABILITY_FORMATS.has(token));
124
- return claimFormats.length > 0 && sourceCapabilities.some((sourceCapability) => {
125
- const sourceFormats = sourceCapability.filter((token) => CAPABILITY_FORMATS.has(token));
126
- return sourceFormats.length > 0 && claimFormats.some((format) => !sourceFormats.includes(format));
127
- });
128
- });
129
- if (knownFormatMismatch) {
130
- findings.push(finding(claim, 'capability_drift', 'error', 'The product capability differs from the supplied source.', evidenceItems, 'high', 'Match the source capability or cite contrary evidence.'));
131
- continue;
132
- }
133
- findings.push(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.'));
134
- continue;
135
- }
136
- const causalOverreach = claim.kinds.includes('causal') && relevant.length && !relevant.some(({ text }) => /\b(caused?|because|led to|resulted in)\b/i.test(text));
137
- const comparativeOverreach = claim.kinds.includes('comparative') && relevant.length && !relevant.some(({ text }) => /\b(better|more|less|than|best|largest|fastest)\b/i.test(text));
138
- if (causalOverreach)
139
- findings.push(finding(claim, 'causal_overreach', 'warning', 'The sources describe an outcome but do not establish causation.', evidenceItems, 'medium', 'Use an association claim or add causal evidence.'));
140
- if (comparativeOverreach)
141
- findings.push(finding(claim, 'comparative_overreach', 'warning', 'The sources do not establish the comparison.', evidenceItems, 'medium', 'Narrow the comparison or add comparative evidence.'));
142
- if (causalOverreach || comparativeOverreach)
143
- continue;
144
- if (claimDates.length && claimDates.some((date) => sourceDates.includes(date)))
145
- continue;
146
- if (same)
147
- continue;
148
- if (!relevant.length && claim.kinds.includes('fact') && tokens(claim.text).length <= 4) {
149
- findings.push(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.'));
150
- continue;
151
- }
152
- if ((!relevant.length || (claim.kinds.includes('number') && !relevant.some(({ text }) => /\b\d+(?:\.\d+)?%?\b/.test(text))))) {
153
- findings.push(finding(claim, 'unsupported_claim', 'error', 'No supplied source supports this checkable claim.', evidenceItems, 'medium', 'Add a source, remove the claim, or mark it as an approved hypothesis.'));
154
- continue;
155
- }
156
- const semantic = semanticAdapter?.compare({ claim: claim.text, sources: input.sources });
157
- if (semantic === 'supported')
158
- continue;
159
- if (semantic === 'contradicted') {
160
- findings.push(finding(claim, 'semantic_contradiction', 'error', 'The configured semantic adapter found contradictory source evidence.', evidenceItems, 'medium', 'Review the cited sources and correct or qualify the claim.'));
161
- continue;
162
- }
163
- findings.push(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.'));
188
+ findings.push(...findingsForClaim(claim, input, sourceLines, semanticAdapter));
164
189
  }
165
- const normalizedClaims = claims.map((claim) => ({ claim, text: normal(claim.text) }));
166
- for (let index = 0; index < normalizedClaims.length; index += 1)
167
- for (let other = index + 1; other < normalizedClaims.length; other += 1) {
168
- const left = normalizedClaims[index];
169
- const right = normalizedClaims[other];
170
- const leftCore = normal(left.text.replace(/\bnot\b/g, ''));
171
- const rightCore = normal(right.text.replace(/\bnot\b/g, ''));
172
- if (leftCore === rightCore && /\bnot\b/.test(left.text) !== /\bnot\b/.test(right.text)) {
173
- 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.'));
174
- }
175
- }
176
- const claimKey = (item) => `${item.draftLocation.start}:${item.draftLocation.end}`;
177
- const unsupported = new Set(findings.filter((item) => item.kind === 'unsupported_claim').map(claimKey)).size;
178
- const contradicted = new Set(findings.filter((item) => ['draft_contradiction', 'number_drift', 'date_drift', 'entity_drift', 'quote_drift', 'capability_drift', 'semantic_contradiction'].includes(item.kind)).map(claimKey)).size;
179
- const humanReview = new Set(findings.filter((item) => item.severity === 'needs_human_review' || item.severity === 'warning').map(claimKey)).size;
180
- const checked = claims.filter((claim) => !claim.kinds.includes('opinion')).length;
181
- const affected = new Set(findings.map(claimKey)).size;
182
- return { version: '1', summary: { checked, supported: Math.max(0, checked - affected), unsupported, contradicted, humanReview }, claims, findings, skippedChecks: semanticAdapter ? [] : ['semantic_matching'] };
190
+ findings.push(...draftContradictions(claims, sourceLines));
191
+ return { version: '1', summary: summarizeFindings(claims, findings), claims, findings, skippedChecks: semanticAdapter ? [] : ['semantic_matching'] };
183
192
  }
184
193
  export function formatFactLintReport(report) {
185
194
  const lines = [`fact lint: ${report.summary.checked} checked, ${report.summary.supported} supported, ${report.findings.length} findings`];