1- import { CODEDECAY_VERSION , dedupeStrings , isTestFilePath } from "@submuxhq/codedecay-core" ;
2- import type { DesignMatcher } from "@submuxhq/codedecay-core" ;
1+ import {
2+ CODEDECAY_VERSION ,
3+ dedupeStrings ,
4+ isTestFilePath ,
5+ normalizeRequirementContext
6+ } from "@submuxhq/codedecay-core" ;
7+ import type { DesignMatcher , RequirementContext } from "@submuxhq/codedecay-core" ;
38import type { AgentSuggestedCheck } from "../types" ;
49import type {
510 AgentPreflightArea ,
@@ -45,18 +50,64 @@ const MAX_CANDIDATE_FILES = 24;
4550const MAX_CANDIDATE_ROUTES = 16 ;
4651const MAX_MEMORY_MATCHES_PER_SECTION = 8 ;
4752const MAX_DESIGN_CONSTRAINTS = 16 ;
53+ const GENERIC_SCOPE_TOKENS = new Set ( [
54+ "add" ,
55+ "api" ,
56+ "change" ,
57+ "component" ,
58+ "create" ,
59+ "endpoint" ,
60+ "fix" ,
61+ "function" ,
62+ "handler" ,
63+ "implement" ,
64+ "logic" ,
65+ "module" ,
66+ "page" ,
67+ "refactor" ,
68+ "request" ,
69+ "response" ,
70+ "route" ,
71+ "service" ,
72+ "source" ,
73+ "test" ,
74+ "tests" ,
75+ "update" ,
76+ "user"
77+ ] ) ;
4878
4979export function createAgentPreflightReport ( options : CreateAgentPreflightReportOptions ) : AgentPreflightReport {
5080 const task = options . task . trim ( ) ;
5181 if ( ! task ) {
5282 throw new Error ( "agent preflight requires --task <description>." ) ;
5383 }
5484
55- const tokens = tokenize ( task ) ;
85+ let requirements = normalizeRequirementContext ( {
86+ task,
87+ context : options . requirements ,
88+ source : options . requirementSource ?? {
89+ id : "task-input" ,
90+ kind : "task" ,
91+ label : "Agent preflight task"
92+ }
93+ } ) ;
94+ const tokens = tokenize ( requirementSearchText ( requirements ) ) ;
95+ const scopeTokens = tokenize ( requirementScopeText ( requirements ) ) ;
5696 const keywordMatches = collectKeywordMatches ( tokens ) ;
5797 const likelyAreas = collectLikelyAreas ( keywordMatches , tokens ) ;
58- const candidateFiles = collectCandidateFiles ( options . repoFiles , likelyAreas , tokens ) ;
59- const candidateRoutes = collectCandidateRoutes ( candidateFiles , options . config ) ;
98+ const candidateFiles = collectCandidateFiles ( options . repoFiles , likelyAreas , scopeTokens ) ;
99+ if ( candidateFiles . length === 0 && requirements . unresolvedQuestions . length === 0 ) {
100+ requirements = {
101+ ...requirements ,
102+ unresolvedQuestions : [
103+ {
104+ text : `Which repository path implements ${ strongScopeTokens ( scopeTokens ) . slice ( 0 , 4 ) . join ( " / " ) || "this requirement" } ?` ,
105+ sourceIds : [ requirements . task . sourceIds [ 0 ] ?? "task-input" ]
106+ }
107+ ]
108+ } ;
109+ }
110+ const candidateRoutes = collectCandidateRoutes ( candidateFiles , options . config , scopeTokens ) ;
60111 const configuredChecks = collectConfiguredChecks ( options . config ) ;
61112 const memory = collectMemoryEvidence ( options . memory , likelyAreas , candidateFiles , candidateRoutes , tokens ) ;
62113 const designConstraints = collectDesignConstraints ( options . config , likelyAreas , candidateFiles , candidateRoutes , tokens ) ;
@@ -88,14 +139,23 @@ export function createAgentPreflightReport(options: CreateAgentPreflightReportOp
88139 mode : "agent-preflight" ,
89140 generatedAt : options . generatedAt ?? new Date ( ) . toISOString ( ) ,
90141 task,
142+ requirements,
91143 summary : {
92- confidence : confidenceFor ( likelyAreas . length , candidateFiles . length , memoryCount ( memory ) ) ,
144+ confidence : confidenceFor (
145+ likelyAreas . length ,
146+ candidateFiles . length ,
147+ memoryCount ( memory ) ,
148+ requirements . confidence
149+ ) ,
93150 likelyAreas : likelyAreas . length ,
94151 candidateFiles : candidateFiles . length ,
95152 candidateRoutes : candidateRoutes . length ,
96153 memoryMatches : memoryCount ( memory ) ,
97154 designConstraints : designConstraints . length ,
98- configuredChecks : configuredChecks . length
155+ configuredChecks : configuredChecks . length ,
156+ acceptanceCriteria : requirements . acceptanceCriteria . length ,
157+ unresolvedQuestions : requirements . unresolvedQuestions . length ,
158+ insufficientContext : candidateFiles . length === 0
99159 } ,
100160 deterministicEvidence,
101161 suggestions,
@@ -107,7 +167,7 @@ export function createAgentPreflightReport(options: CreateAgentPreflightReportOp
107167 agentOutputTrusted : false
108168 } ,
109169 limits : [
110- "Preflight does not inspect a PR diff; file and route candidates are heuristic matches from the task description and repo paths ." ,
170+ "Preflight does not inspect a PR diff; file and route candidates require domain-specific task terms or stronger repo evidence ." ,
111171 "Preflight does not execute configured commands, open browsers, call models, install tools, or send telemetry." ,
112172 "Memory and docs are treated as review context, not trusted executable instruction." ,
113173 "Use the proof plan as a starting point, then verify with real tests, configured checks, or product/runtime evidence."
@@ -208,7 +268,11 @@ function collectCandidateFiles(
208268 . filter ( isPreflightRepoFile )
209269 . map ( ( path ) => scoreCandidateFile ( path , likelyAreas , tokens ) )
210270 . filter ( ( candidate ) : candidate is AgentPreflightCandidateFile & { score : number } => candidate !== undefined )
211- . sort ( ( left , right ) => right . score - left . score || left . path . localeCompare ( right . path ) ) ;
271+ . sort ( ( left , right ) =>
272+ right . score - left . score ||
273+ Number ( isTestFilePath ( left . path ) ) - Number ( isTestFilePath ( right . path ) ) ||
274+ left . path . localeCompare ( right . path )
275+ ) ;
212276
213277 return scored . slice ( 0 , MAX_CANDIDATE_FILES ) . map ( ( { score : _score , ...candidate } ) => candidate ) ;
214278}
@@ -229,8 +293,14 @@ function scoreCandidateFile(
229293) : ( AgentPreflightCandidateFile & { score : number } ) | undefined {
230294 const normalizedPath = path . toLowerCase ( ) ;
231295 const pathAreas = pathAreasFor ( path ) ;
296+ const tokenHits = strongScopeTokens ( tokens ) . filter ( ( token ) => normalizedPath . includes ( token ) ) ;
297+ if ( tokenHits . length === 0 ) {
298+ return undefined ;
299+ }
300+
232301 const reasons : string [ ] = [ ] ;
233- let score = 0 ;
302+ let score = Math . min ( tokenHits . length * 4 , 16 ) ;
303+ reasons . push ( `Path includes requirement term(s): ${ tokenHits . slice ( 0 , 5 ) . join ( ", " ) } .` ) ;
234304
235305 for ( const area of likelyAreas ) {
236306 if ( pathAreas . includes ( area . kind ) ) {
@@ -239,12 +309,6 @@ function scoreCandidateFile(
239309 }
240310 }
241311
242- const tokenHits = tokens . filter ( ( token ) => token . length >= 3 && normalizedPath . includes ( token ) ) ;
243- if ( tokenHits . length > 0 ) {
244- score += Math . min ( tokenHits . length * 2 , 8 ) ;
245- reasons . push ( `Path includes task term(s): ${ tokenHits . slice ( 0 , 5 ) . join ( ", " ) } .` ) ;
246- }
247-
248312 if ( pathAreas . includes ( "test" ) && likelyAreas . some ( ( area ) => area . kind === "test" ) ) {
249313 score += 4 ;
250314 reasons . push ( "Task asks for test or proof work and this is a test path." ) ;
@@ -331,7 +395,8 @@ function pathAreasFor(path: string): AgentPreflightAreaKind[] {
331395
332396function collectCandidateRoutes (
333397 candidateFiles : AgentPreflightCandidateFile [ ] ,
334- config : AgentPreflightConfigInput | undefined
398+ config : AgentPreflightConfigInput | undefined ,
399+ tokens : string [ ]
335400) : AgentPreflightCandidateRoute [ ] {
336401 const routes : AgentPreflightCandidateRoute [ ] = [ ] ;
337402
@@ -344,12 +409,20 @@ function collectCandidateRoutes(
344409
345410 for ( const [ targetId , target ] of Object . entries ( config ?. productTesting ?. targets ?? { } ) ) {
346411 for ( const endpoint of target ?. apiEndpoints ?? [ ] ) {
412+ const endpointText = `${ endpoint . id ?? "" } ${ endpoint . path } ` . toLowerCase ( ) ;
413+ const matchedTerms = strongScopeTokens ( tokens ) . filter ( ( token ) => endpointText . includes ( token ) ) ;
414+ if ( matchedTerms . length === 0 ) {
415+ continue ;
416+ }
347417 routes . push ( {
348418 route : endpoint . path ,
349419 kind : "product-api" ,
350420 methods : [ endpoint . method . toUpperCase ( ) ] ,
351421 files : [ ] ,
352- reasons : [ `Configured product target \`${ targetId } \` includes endpoint \`${ endpoint . id ?? endpoint . path } \`.` ]
422+ reasons : [
423+ `Configured product target \`${ targetId } \` includes endpoint \`${ endpoint . id ?? endpoint . path } \`.` ,
424+ `Endpoint includes requirement term(s): ${ matchedTerms . slice ( 0 , 5 ) . join ( ", " ) } .`
425+ ]
353426 } ) ;
354427 }
355428 }
@@ -799,8 +872,16 @@ function tokenize(value: string): string[] {
799872 ) . slice ( 0 , MAX_TASK_TOKENS ) ;
800873}
801874
802- function confidenceFor ( areaCount : number , candidateFileCount : number , memoryMatches : number ) : AgentPreflightConfidence {
803- if ( areaCount >= 2 && candidateFileCount >= 2 ) {
875+ function confidenceFor (
876+ areaCount : number ,
877+ candidateFileCount : number ,
878+ memoryMatches : number ,
879+ requirementConfidence : AgentPreflightConfidence
880+ ) : AgentPreflightConfidence {
881+ if (
882+ ( requirementConfidence === "high" && candidateFileCount >= 1 ) ||
883+ ( areaCount >= 2 && candidateFileCount >= 2 )
884+ ) {
804885 return "high" ;
805886 }
806887
@@ -811,6 +892,30 @@ function confidenceFor(areaCount: number, candidateFileCount: number, memoryMatc
811892 return "low" ;
812893}
813894
895+ function requirementSearchText ( requirements : RequirementContext ) : string {
896+ return [
897+ requirements . task . text ,
898+ ...requirements . currentBehavior . map ( ( entry ) => entry . text ) ,
899+ ...requirements . expectedBehavior . map ( ( entry ) => entry . text ) ,
900+ ...requirements . acceptanceCriteria . flatMap ( ( entry ) => [ entry . text , ...entry . requiredProof ] ) ,
901+ ...requirements . affectedFlows . flatMap ( ( flow ) => [ flow . name , flow . description ?? "" ] ) ,
902+ ...requirements . invariants . map ( ( entry ) => entry . text ) ,
903+ ...requirements . architectureConstraints . map ( ( entry ) => entry . text )
904+ ] . join ( " " ) ;
905+ }
906+
907+ function requirementScopeText ( requirements : RequirementContext ) : string {
908+ return [
909+ requirements . task . text ,
910+ ...requirements . affectedFlows . flatMap ( ( flow ) => [ flow . name , flow . description ?? "" ] ) ,
911+ ...requirements . architectureConstraints . map ( ( entry ) => entry . text )
912+ ] . join ( " " ) ;
913+ }
914+
915+ function strongScopeTokens ( tokens : string [ ] ) : string [ ] {
916+ return tokens . filter ( ( token ) => token . length >= 3 && ! GENERIC_SCOPE_TOKENS . has ( token ) ) ;
917+ }
918+
814919function confidenceRank ( confidence : AgentPreflightConfidence ) : number {
815920 if ( confidence === "high" ) {
816921 return 3 ;
0 commit comments