mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 15:56:35 +00:00
fix(domain): preserve partial reference comparison facts
Own endpoint identity and per-edge kind knowledge in one parsed reference representation. Preserve exact same-artifact coordinates and cross-artifact name uncertainty, including generated and nonunique procedure labels. Use exact multiplicity-preserving sparse compatibility flow instead of positional/greedy pairing or a quadratic adjacency matrix. Share the native coordinate parser with call paths and remove the superseded projections. Validate public MCP comparison/reconstruction outcomes, independent tiny permutation oracles, dense matching and advertised JSON Schemas. Fixes #4210 Fixes #5131 Co-authored-by: Henglie <ebhenglie@gmail.com>
This commit is contained in:
1 parent
559af3d3ff
commit
92dadf9334
10 files changed
+1198
-191
No files matched your search
@@ -0,0 +1,228 @@
|
||||
/** Only these three facts can be unknown; direction is always observed. */
|
||||
export interface ReferenceCompatibilityRow {
|
||||
readonly direction: "in" | "out";
|
||||
readonly source: string | null;
|
||||
readonly target: string | null;
|
||||
readonly kind: string | null;
|
||||
}
|
||||
|
||||
interface ReferenceGroup extends ReferenceCompatibilityRow {
|
||||
readonly mask: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface FlowEdge {
|
||||
readonly to: number;
|
||||
readonly reverse: number;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
/** A local residual network: no recursive walk or pairwise adjacency matrix. */
|
||||
class ReferenceFlow {
|
||||
private readonly edges: FlowEdge[][] = [];
|
||||
|
||||
node(): number {
|
||||
this.edges.push([]);
|
||||
return this.edges.length - 1;
|
||||
}
|
||||
|
||||
connect(from: number, to: number, capacity: number): void {
|
||||
const forward = this.edges[from];
|
||||
const reverse = this.edges[to];
|
||||
if (forward === undefined || reverse === undefined)
|
||||
throw new Error("Reference flow endpoint was not allocated.");
|
||||
forward.push({ to, reverse: reverse.length, remaining: capacity });
|
||||
reverse.push({ to: from, reverse: forward.length - 1, remaining: 0 });
|
||||
}
|
||||
|
||||
covers(source: number, sink: number, required: number): boolean {
|
||||
let delivered = 0;
|
||||
while (delivered < required) {
|
||||
const levels = Array<number>(this.edges.length).fill(-1);
|
||||
const queue = [source];
|
||||
levels[source] = 0;
|
||||
for (let index = 0; index < queue.length; index++) {
|
||||
const node = queue[index];
|
||||
if (node === undefined) break;
|
||||
for (const edge of this.edges[node] ?? []) {
|
||||
if (edge.remaining <= 0 || levels[edge.to] !== -1) continue;
|
||||
levels[edge.to] = (levels[node] ?? -1) + 1;
|
||||
queue.push(edge.to);
|
||||
}
|
||||
}
|
||||
if (levels[sink] === -1) return false;
|
||||
const next = Array<number>(this.edges.length).fill(0);
|
||||
let amount: number;
|
||||
while (
|
||||
delivered < required &&
|
||||
(amount = this.augment(
|
||||
source,
|
||||
sink,
|
||||
required - delivered,
|
||||
levels,
|
||||
next,
|
||||
)) > 0
|
||||
)
|
||||
delivered += amount;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private augment(
|
||||
source: number,
|
||||
sink: number,
|
||||
capacity: number,
|
||||
levels: number[],
|
||||
next: number[],
|
||||
): number {
|
||||
const nodes = [source];
|
||||
const path: FlowEdge[] = [];
|
||||
const capacities = [capacity];
|
||||
while (nodes.length > 0) {
|
||||
const node = nodes[nodes.length - 1];
|
||||
if (node === undefined) break;
|
||||
if (node === sink) {
|
||||
const amount = capacities[capacities.length - 1] ?? 0;
|
||||
for (const edge of path) {
|
||||
edge.remaining -= amount;
|
||||
const reverse = this.edges[edge.to]?.[edge.reverse];
|
||||
if (reverse === undefined)
|
||||
throw new Error("Reference flow reverse edge was not allocated.");
|
||||
reverse.remaining += amount;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
const adjacent = this.edges[node] ?? [];
|
||||
let edge = adjacent[next[node] ?? 0];
|
||||
while (
|
||||
edge !== undefined &&
|
||||
(edge.remaining <= 0 || levels[edge.to] !== (levels[node] ?? -1) + 1)
|
||||
) {
|
||||
next[node] = (next[node] ?? 0) + 1;
|
||||
edge = adjacent[next[node] ?? 0];
|
||||
}
|
||||
if (edge === undefined) {
|
||||
levels[node] = -1;
|
||||
nodes.pop();
|
||||
path.pop();
|
||||
capacities.pop();
|
||||
continue;
|
||||
}
|
||||
path.push(edge);
|
||||
nodes.push(edge.to);
|
||||
capacities.push(
|
||||
Math.min(capacities[capacities.length - 1] ?? 0, edge.remaining),
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const groupRows = (
|
||||
rows: readonly ReferenceCompatibilityRow[],
|
||||
): Map<string, ReferenceGroup> => {
|
||||
const groups = new Map<string, ReferenceGroup>();
|
||||
for (const row of rows) {
|
||||
const key = JSON.stringify([
|
||||
row.direction,
|
||||
row.source,
|
||||
row.target,
|
||||
row.kind,
|
||||
]);
|
||||
const previous = groups.get(key);
|
||||
if (previous !== undefined) previous.count += 1;
|
||||
else
|
||||
groups.set(key, {
|
||||
...row,
|
||||
mask:
|
||||
(row.source === null ? 0 : 1) |
|
||||
(row.target === null ? 0 : 2) |
|
||||
(row.kind === null ? 0 : 4),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decide whether every row can pair without contradicting a known fact.
|
||||
* A bucket represents one pair of knowledge masks and their shared facts.
|
||||
* Each group joins at most eight buckets, so graph storage is linear in the
|
||||
* number of distinct rows. Integer flow preserves multiplicities exactly.
|
||||
*/
|
||||
export const referenceFactsCompatible = (
|
||||
left: readonly ReferenceCompatibilityRow[],
|
||||
right: readonly ReferenceCompatibilityRow[],
|
||||
): boolean => {
|
||||
if (left.length !== right.length) return false;
|
||||
if (left.length === 0) return true;
|
||||
const leftGroups = groupRows(left);
|
||||
const rightGroups = groupRows(right);
|
||||
if (
|
||||
leftGroups.size === rightGroups.size &&
|
||||
[...leftGroups].every(
|
||||
([key, group]) => rightGroups.get(key)?.count === group.count,
|
||||
)
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
[...leftGroups.values(), ...rightGroups.values()].every(
|
||||
({ mask }) => mask === 7,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// Cancelling an identical complete row is safe: any two partial partners of
|
||||
// that row agree on their shared facts and can instead pair with each other.
|
||||
// Cancelling identical partial rows is unsafe and must not be done.
|
||||
let required = left.length;
|
||||
for (const [key, group] of leftGroups) {
|
||||
if (group.mask !== 7) continue;
|
||||
const other = rightGroups.get(key);
|
||||
if (other === undefined) continue;
|
||||
const cancelled = Math.min(group.count, other.count);
|
||||
group.count -= cancelled;
|
||||
other.count -= cancelled;
|
||||
required -= cancelled;
|
||||
if (group.count === 0) leftGroups.delete(key);
|
||||
if (other.count === 0) rightGroups.delete(key);
|
||||
}
|
||||
const leftMasks = new Set([...leftGroups.values()].map(({ mask }) => mask));
|
||||
const rightMasks = new Set([...rightGroups.values()].map(({ mask }) => mask));
|
||||
const flow = new ReferenceFlow();
|
||||
const source = flow.node();
|
||||
const sink = flow.node();
|
||||
const buckets = new Map<string, number>();
|
||||
const bucket = (
|
||||
group: ReferenceGroup,
|
||||
leftMask: number,
|
||||
rightMask: number,
|
||||
): number => {
|
||||
const intersection = leftMask & rightMask;
|
||||
const key = JSON.stringify([
|
||||
group.direction,
|
||||
leftMask,
|
||||
rightMask,
|
||||
intersection & 1 ? group.source : null,
|
||||
intersection & 2 ? group.target : null,
|
||||
intersection & 4 ? group.kind : null,
|
||||
]);
|
||||
const existing = buckets.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
const node = flow.node();
|
||||
buckets.set(key, node);
|
||||
return node;
|
||||
};
|
||||
for (const group of leftGroups.values()) {
|
||||
const node = flow.node();
|
||||
flow.connect(source, node, group.count);
|
||||
for (const mask of rightMasks)
|
||||
flow.connect(node, bucket(group, group.mask, mask), group.count);
|
||||
}
|
||||
for (const group of rightGroups.values()) {
|
||||
const node = flow.node();
|
||||
flow.connect(node, sink, group.count);
|
||||
for (const mask of leftMasks)
|
||||
flow.connect(bucket(group, mask, group.mask), node, group.count);
|
||||
}
|
||||
return flow.covers(source, sink, required);
|
||||
};
|
||||
Reference in new issue
Block a user