Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Active tickets

- [ ] [`ticket-088`](project/ticket-088/README.md) — prevent overlapping
projections of one source sentence from creating a false blocking polarity
contradiction. Current state: `IN_PROGRESS / EDIT`; workstream: `core-dsl`.

- [ ] [`ticket-054`](project/ticket-054/README.md) — restore skills-agent
discovery, prove a todo2code → Repair PR → independent Validator hand-off,
then add three bounded todo2code-grounded skills. Current state:
Expand Down
1 change: 1 addition & 0 deletions project/TICKETS.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions project/ticket-088/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions project/ticket-088/ai-codex-logs.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions project/ticket-088/ai-codex.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions project/ticket-088/changelog.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions project/ticket-088/intent.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions project/ticket-088/preprompt.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions src/graph/linker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createRelationId, graphFingerprint } from '../core/id.js';
import { assertIntentRecords } from '../core/schema.js';
import { keywords, topicKeywords } from '../core/text.js';
import { pathAliases, symbolAliases } from '../core/target.js';
import { normalizePath, pathAliases, symbolAliases } from '../core/target.js';
import type { IntentGraph, IntentRecord, IntentRelation, RelationType, SourceKind } from '../core/types.js';
import { buildSymbolResolutionIndex, hasResolvedNlAstSymbolPair, type SymbolResolutionIndex } from './symbol-resolution.js';
import { aggregateCapabilityOverlap, isFileAggregate } from './capability-evidence.js';
Expand Down Expand Up @@ -443,7 +443,9 @@ function isModuleTopicEvidencePair(left: IntentRecord, right: IntentRecord): boo
function determineRelation(left: IntentRecord, right: IntentRecord, evidence: PairEvidence): DirectedRelation {
// `scorePair` already computed this over the same two strings.
const textScore = evidence.textScore;
if (left.statement.polarity !== right.statement.polarity && textScore >= 0.45) {
if (left.statement.polarity !== right.statement.polarity
&& textScore >= 0.45
&& !isOverlappingSameSourceProjection(left, right)) {
return { from: left, to: right, type: 'contradicts' };
}
if (left.source.kind === right.source.kind && textScore >= 0.82) {
Expand All @@ -455,6 +457,23 @@ function determineRelation(left: IntentRecord, right: IntentRecord, evidence: Pa
return { from: left, to: right, type: 'related_to' };
}

/**
* Two extractors may project different spans from one physical sentence. A
* line-level NL projection can end before a continuation containing negation,
* while a document projection covers the complete sentence. Those records are
* alternate observations of one source location, not independent contrary
* claims. Disjoint ranges in the same file remain eligible for contradiction.
*/
function isOverlappingSameSourceProjection(left: IntentRecord, right: IntentRecord): boolean {
const leftPath = left.source.path ? normalizePath(left.source.path) : '';
const rightPath = right.source.path ? normalizePath(right.source.path) : '';
if (!leftPath || leftPath !== rightPath) return false;
const leftLines = left.source.lines;
const rightLines = right.source.lines;
if (!leftLines || !rightLines) return false;
return leftLines.start <= rightLines.end && rightLines.start <= leftLines.end;
}

function relationForSourceKinds(left: IntentRecord, right: IntentRecord): DirectedRelation | null {
for (const rule of SOURCE_RELATION_RULES) {
const relation = matchSourceRule(left, right, rule);
Expand Down
68 changes: 68 additions & 0 deletions test/linker-pairing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,74 @@ function astFact(path: string, symbol: string, object: string): IntentRecord {
});
}

function polarityProjection(options: {
sourceKind: 'document' | 'nl';
sourcePath: string;
sourceLines: { start: number; end: number };
text: string;
polarity: 'positive' | 'negative';
}): IntentRecord {
return buildRecord({
kind: options.sourceKind === 'document' ? 'documentation_statement' : 'declared_intent',
action: 'document',
object: 'unverified renders as terminal bounded observation',
target: { symbols: ['unverified'] },
text: options.text,
polarity: options.polarity,
lifecycle: 'proposed',
sourceKind: options.sourceKind,
sourcePath: options.sourcePath,
sourceLines: options.sourceLines,
extractor: 'test',
epistemicClass: 'declaration',
confidence: 0.8,
basis: ['fixture'],
});
}

test('overlapping excerpts from one source location cannot contradict each other', () => {
const document = polarityProjection({
sourceKind: 'document',
sourcePath: './project/ticket-071/README.md',
sourceLines: { start: 32, end: 33 },
text: 'unverified renders as a terminal bounded observation, not as Checking',
polarity: 'negative',
});
const shortenedNl = polarityProjection({
sourceKind: 'nl',
sourcePath: 'project/ticket-071/README.md',
sourceLines: { start: 32, end: 32 },
text: 'unverified renders as a terminal bounded observation',
polarity: 'positive',
});

const graph = linkIntentRecords([document, shortenedNl], AT);
assert.equal(graph.relations.length, 1);
assert.equal(graph.relations[0]?.type, 'documents');
assert.ok(!diagnoseGraph(graph, AT).diagnostics.some((item) => item.code === 'CONFLICTING_INTENT'));
});

test('independent opposite-polarity sources still create a blocking contradiction', () => {
const document = polarityProjection({
sourceKind: 'document',
sourcePath: 'project/ticket-071/README.md',
sourceLines: { start: 32, end: 33 },
text: 'unverified renders as a terminal bounded observation, not as Checking',
polarity: 'negative',
});
const independentNl = polarityProjection({
sourceKind: 'nl',
sourcePath: 'TASK.md',
sourceLines: { start: 1, end: 1 },
text: 'unverified renders as a terminal bounded observation',
polarity: 'positive',
});

const graph = linkIntentRecords([document, independentNl], AT);
assert.equal(graph.relations[0]?.type, 'contradicts');
assert.ok(diagnoseGraph(graph, AT).diagnostics.some((item) => item.code === 'CONFLICTING_INTENT'));
});

test('Two unrelated AST facts sharing only a file are not linked', () => {
const graph = linkIntentRecords([
astFact('src/module.ts', 'parseHeaders', 'nagłówki żądania HTTP'),
Expand Down
Loading