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
86 changes: 86 additions & 0 deletions src/runtime/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* The public `compileFpfSource()` API is unchanged.
*/

import { PROJECT_ALIGNMENT_ROUTE_NAME } from './constants.js';
import {
buildExplicitReferenceRelations,
buildLexiconRelations,
Expand All @@ -29,8 +30,11 @@ import { parseSource } from './source-parser.js';
import { buildValidation } from './validation-runner.js';
import type {
AnchorRef,
HeuristicSeedRule,
IndexMapNode,
LexiconEntry,
PatternRecord,
RouteRecord,
Snapshot,
} from './types.js';

Expand Down Expand Up @@ -86,6 +90,8 @@ export function compileFpfSource(params: {
);
const indexes = buildIndexes(compiledNodes, patternGraph.nodes, routeGraph.nodes, lexicon);

const heuristicSeedRules = buildHeuristicSeedRules(patternGraph.nodes, routeGraph.nodes);

// Stage 4: ValidationRunner — snapshot candidate → validation findings
const validation = buildValidation(
compiledNodes,
Expand Down Expand Up @@ -117,6 +123,7 @@ export function compileFpfSource(params: {
compiledNodes,
relationGraph: allRelations,
indexes,
heuristicSeedRules,
validation,
};

Expand All @@ -142,3 +149,82 @@ export {
scoreRouteQuery,
selectBestAnchors,
} from './query-helpers.js';

function buildHeuristicSeedRules(
patternNodes: Record<string, PatternRecord>,
routeNodes: Record<string, RouteRecord>,
): HeuristicSeedRule[] {
const rules: HeuristicSeedRule[] = [];

const creativityNodeIds = ['C.17', 'C.18', 'C.19', 'B.5.2.1', 'A.0'].filter(
(id) => id in patternNodes || id in routeNodes,
);
if (creativityNodeIds.length > 0) {
rules.push({
name: 'creative-search-heuristic',
allOf: [['creativity', 'creative']],
anyOf: [['open-ended', 'open ended'], ['search']],
seedNodeIds: creativityNodeIds,
seedScore: 64,
seedOrigin: 'lexical',
initialNodeIds: ['C.17', 'C.18', 'C.19'].filter((id) => id in patternNodes),
});
}

const alignmentRoute = Object.values(routeNodes).find(
(r) => r.name.toLowerCase() === PROJECT_ALIGNMENT_ROUTE_NAME,
);
const alignmentNodeIds = ['A.1.1', 'A.15', 'B.5.1', 'F.17'].filter(
(id) => id in patternNodes || id in routeNodes,
);
if (alignmentRoute) {
// Populate project-alignment constraints that were previously hard-coded in query-engine.ts
alignmentRoute.constraints = [
'Add F.11 and F.9 only when method/work vocabulary is explicitly at stake in the question.',
'Land on F.17 early rather than escalating to F.11 unless the asker names a cross-team mismatch.',
];
rules.push({
name: 'vocabulary-alignment',
allOf: [['vocabulary']],
anyOf: [['overloaded'], ['across teams'], ['across contexts']],
seedNodeIds: alignmentNodeIds,
seedScore: 20,
seedOrigin: 'route_expansion',
initialNodeIds: [],
routeId: alignmentRoute.id,
routeScore: 80,
});
}

const roleNodeIds = ['A.1.1', 'A.2.1', 'A.2.5'].filter(
(id) => id in patternNodes || id in routeNodes,
);
if (roleNodeIds.length > 0) {
rules.push({
name: 'role-assignment-connection',
allOf: [['role assignment']],
anyOf: [['connect'], ['relation']],
seedNodeIds: roleNodeIds,
seedScore: 36,
seedOrigin: 'lexical',
initialNodeIds: [],
});
}

const entityNodeIds = ['A.6.3.CR', 'A.6.3.RT', 'E.17.ID.CR'].filter(
(id) => id in patternNodes || id in routeNodes,
);
if (entityNodeIds.length > 0) {
rules.push({
name: 'same-entity-comparative-reading',
allOf: [['same entity', 'same-entity']],
anyOf: [['rewrite'], ['comparative']],
seedNodeIds: entityNodeIds,
seedScore: 40,
seedOrigin: 'lexical',
initialNodeIds: entityNodeIds.filter((id) => id in patternNodes),
});
}

return rules;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 2 additions & 0 deletions src/runtime/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const PREFACE_MARKER = '# **Preface** (non-normative)';
export const PREFACE_ROUTE_CITATION = 'Preface/Where to start';
export const ROUTE_INDEX_CITATION = 'J.4';

export const PROJECT_ALIGNMENT_ROUTE_NAME = 'project alignment';

export const PART_C_LABEL = 'Part C - Kernel Extension Specifications';

export const PART_C_CLUSTER_LABELS = [
Expand Down
3 changes: 3 additions & 0 deletions src/runtime/graph-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export function buildRouteGraph(
citations: unique([...existing.citations, ...route.citations]),
anchorIds: unique([...existing.anchorIds, ...route.anchorIds]),
searchableText: unique([existing.searchableText, route.searchableText]).join(' '),
constraints: unique([...existing.constraints, ...route.constraints]),
});
}

Expand Down Expand Up @@ -353,6 +354,7 @@ function parsePrefaceRoutes(
citations: [PREFACE_ROUTE_CITATION],
anchorIds: anchorMap[PREFACE_ROUTE_CITATION] ? [PREFACE_ROUTE_CITATION] : [],
searchableText: cleanMarkdown(trimmed),
constraints: [],
});
}

Expand Down Expand Up @@ -413,6 +415,7 @@ function parseJ4Routes(
citations: [ROUTE_INDEX_CITATION],
anchorIds: anchorMap[ROUTE_INDEX_CITATION] ? [ROUTE_INDEX_CITATION] : [],
searchableText: cleanMarkdown(cells.join(' ')),
constraints: [],
});
}
return routes;
Expand Down
85 changes: 26 additions & 59 deletions src/runtime/query-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
FrontierCandidate,
FrontierOrigin,
GraphExpansion,
HeuristicSeedRule,
InspectAnchorResult,
InspectNeighbor,
InspectResult,
Expand Down Expand Up @@ -597,71 +598,44 @@ export class QueryEngine {
}
}

private matchesSeedRule(normalizedQuestion: string, rule: HeuristicSeedRule): boolean {
const matchesGroup = (alternatives: string[]): boolean =>
alternatives.some((term) => term.length > 0 && normalizedQuestion.includes(term));
return (
rule.allOf.every(matchesGroup) &&
rule.anyOf.some(matchesGroup)
);
}

private addHeuristicSeeds(
normalizedQuestion: string,
addCandidate: (nodeId: string, delta: number, reason: string, origin: FrontierOrigin) => void,
): void {
if (
(normalizedQuestion.includes('creativity') || normalizedQuestion.includes('creative')) &&
(normalizedQuestion.includes('open-ended') ||
normalizedQuestion.includes('open ended') ||
normalizedQuestion.includes('search'))
) {
for (const nodeId of ['C.17', 'C.18', 'C.19', 'B.5.2.1', 'A.0']) {
addCandidate(nodeId, 64, 'creative-search-heuristic', 'lexical');
for (const rule of this.snapshot.heuristicSeedRules ?? []) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rebuild stale snapshots when heuristic rules are missing

addHeuristicSeeds now treats missing snapshot.heuristicSeedRules as an empty list, which avoids a crash but silently disables all heuristic boosts on pre-refactor snapshots. In runtime.refresh(), old snapshots are still considered compatible when sourceHash is unchanged because snapshotNeedsRebuild() only validates indexMap, so upgraded deployments can keep serving without any of the new seed logic (creative search, vocabulary alignment, role-assignment, same-entity) until a forced rebuild happens. Please make missing heuristicSeedRules a rebuild trigger (or synthesize defaults) so behavior does not regress after upgrade.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — fixed in bb83e63. Added a check for !Array.isArray(snapshot.heuristicSeedRules) in snapshotNeedsRebuild(), so old persisted snapshots missing the field will trigger a full recompile on the next refresh. This ensures heuristic seed logic is always active after upgrade rather than silently disabled.

if (!this.matchesSeedRule(normalizedQuestion, rule)) {
continue;
}
}

if (
normalizedQuestion.includes('vocabulary') &&
(normalizedQuestion.includes('overloaded') ||
normalizedQuestion.includes('across teams') ||
normalizedQuestion.includes('across contexts'))
) {
addCandidate('route:project-alignment', 80, 'burden:project-alignment', 'route_expansion');
for (const nodeId of ['A.1.1', 'A.15', 'B.5.1', 'F.17']) {
addCandidate(nodeId, 20, 'project-alignment-route-surface', 'route_expansion');
if (rule.routeId !== undefined && rule.routeScore !== undefined) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check if (rule.routeId && rule.routeScore) will fail if routeScore is 0, which is a valid (though unlikely) score. It is safer to check for undefined explicitly.

Suggested change
if (rule.routeId !== undefined && rule.routeScore !== undefined) {
if (rule.routeId !== undefined && rule.routeScore !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in f6f4bef — changed to rule.routeId !== undefined && rule.routeScore !== undefined to correctly handle a score of 0.

addCandidate(rule.routeId, rule.routeScore, `burden:${rule.name}`, 'route_expansion');
}
}

if (
normalizedQuestion.includes('role assignment') &&
(normalizedQuestion.includes('connect') || normalizedQuestion.includes('relation'))
) {
for (const nodeId of ['A.1.1', 'A.2.1', 'A.2.5']) {
addCandidate(nodeId, 36, 'role-assignment-connection', 'lexical');
}
}

if (
(normalizedQuestion.includes('same entity') || normalizedQuestion.includes('same-entity')) &&
(normalizedQuestion.includes('rewrite') || normalizedQuestion.includes('comparative'))
) {
for (const nodeId of ['A.6.3.CR', 'A.6.3.RT', 'E.17.ID.CR']) {
addCandidate(nodeId, 40, 'same-entity-comparative-reading', 'lexical');
for (const nodeId of rule.seedNodeIds) {
addCandidate(nodeId, rule.seedScore, rule.name, rule.seedOrigin);
}
}
}

private heuristicInitialNodeIds(normalizedQuestion: string): string[] {
if (
(normalizedQuestion.includes('creativity') || normalizedQuestion.includes('creative')) &&
(normalizedQuestion.includes('open-ended') ||
normalizedQuestion.includes('open ended') ||
normalizedQuestion.includes('search'))
) {
return ['C.17', 'C.18', 'C.19'].filter((nodeId) => Boolean(this.snapshot.patternGraph.nodes[nodeId]));
}

if (
(normalizedQuestion.includes('same entity') || normalizedQuestion.includes('same-entity')) &&
(normalizedQuestion.includes('rewrite') || normalizedQuestion.includes('comparative'))
) {
return ['A.6.3.CR', 'A.6.3.RT', 'E.17.ID.CR'].filter((nodeId) =>
for (const rule of this.snapshot.heuristicSeedRules ?? []) {
if (rule.initialNodeIds.length === 0) {
continue;
}
if (!this.matchesSeedRule(normalizedQuestion, rule)) {
continue;
}
return rule.initialNodeIds.filter((nodeId) =>
Boolean(this.snapshot.patternGraph.nodes[nodeId]),
);
}

return [];
}

Expand Down Expand Up @@ -1243,15 +1217,8 @@ export class QueryEngine {
route.firstHonestBurden
? `First honest burden: ${route.firstHonestBurden}.`
: 'Choose this route only when the stated burden matches the present problem.',
...(route.constraints ?? []),
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore project-alignment route guardrails

buildRouteAnswer() now relies on route.constraints, but in this commit parsePrefaceRoutes() and parseJ4Routes() both initialize constraints to [] and no later step populates them, so the two project-alignment safety constraints that were previously emitted are now silently dropped for all route answers. This is a behavioral regression for users asking about the project-alignment route, because they no longer receive the route-specific guidance that prevented over-escalation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive — same as the Devin Review finding above. The constraints are populated in compiler.ts:595-601 after the parsers run. The parsers initialize constraints: [], then the compiler pushes the two project-alignment constraint strings before the snapshot is built. No behavioral regression — route.constraints is correctly populated when buildRouteAnswer reads it.

];
if (route.name.toLowerCase() === 'project alignment') {
constraints.push(
'Add F.11 and F.9 only when method/work vocabulary itself must be aligned across contexts.',
);
constraints.push(
'Land on F.17 early rather than escalating directly into heavier governance or assurance surfaces.',
);
}
const answer = [
`${route.name} is the matched first-practical route.`,
route.firstHonestBurden ? `Burden: ${route.firstHonestBurden}.` : '',
Expand Down
3 changes: 3 additions & 0 deletions src/runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ function buildCompilerSummary(snapshot: Snapshot): BuildAudit['compiler'] {
}

function snapshotNeedsRebuild(snapshot: Snapshot): boolean {
if (!Array.isArray(snapshot.heuristicSeedRules)) {
return true;
}
return Object.values(snapshot.indexMap).some(
(node) =>
typeof node.description !== 'string' ||
Expand Down
16 changes: 16 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export interface RouteRecord {
citations: string[];
anchorIds: string[];
searchableText: string;
constraints: string[];
}

export interface LexiconEntry {
Expand Down Expand Up @@ -164,6 +165,20 @@ export interface BuildValidation {
brokenRoutes: string[];
}

export interface HeuristicSeedRule {
name: string;
/** Outer array = AND, inner array = OR alternatives for each term group. */
allOf: string[][];
/** Outer array = OR groups, inner array = OR alternatives within each group. */
anyOf: string[][];
seedNodeIds: string[];
seedScore: number;
seedOrigin: FrontierOrigin;
initialNodeIds: string[];
routeId?: string;
routeScore?: number;
}

export interface Snapshot {
sourcePath: string;
sourceHash: string;
Expand All @@ -184,6 +199,7 @@ export interface Snapshot {
compiledNodes: Record<string, CompiledNode>;
relationGraph: RelationEdge[];
indexes: SnapshotIndexes;
heuristicSeedRules?: HeuristicSeedRule[];
validation: BuildValidation;
}

Expand Down
Loading