@@ -154,10 +154,18 @@ function extractRules(
154154 for ( const sentence of sentences ) {
155155 const lower = sentence . toLowerCase ( ) ;
156156
157- // Find which spec entities are mentioned in this sentence
158- const mentionedTags = vocabulary . tags . filter ( ( t ) =>
159- lower . includes ( t . toLowerCase ( ) ) ,
160- ) ;
157+ // Find which spec entities are mentioned in this sentence.
158+ // Match both exact tag names and their singular/plural variants
159+ // ("payment" matches tag "payments" and vice versa).
160+ const mentionedTags = vocabulary . tags . filter ( ( t ) => {
161+ const tl = t . toLowerCase ( ) ;
162+ if ( lower . includes ( tl ) ) return true ;
163+ // Singular form: "payments" → "payment"
164+ if ( tl . endsWith ( 's' ) && lower . includes ( tl . slice ( 0 , - 1 ) ) ) return true ;
165+ // Plural form: "webhook" → "webhooks"
166+ if ( ! tl . endsWith ( 's' ) && lower . includes ( tl + 's' ) ) return true ;
167+ return false ;
168+ } ) ;
161169 const mentionedEndpoints = vocabulary . endpoints . filter ( ( e ) =>
162170 lower . includes ( e . toLowerCase ( ) ) ,
163171 ) ;
@@ -167,10 +175,13 @@ function extractRules(
167175
168176 // Workflow ordering: "before X, you must Y" / "X before Y" / "followed by"
169177 const orderingPatterns = [
170- / b e f o r e \s + (?: p r o c e s s i n g | c r e a t i n g | c a l l i n g | u s i n g ) \s + ( \w + ) / i,
171- / m u s t \s + (?: f i r s t | c r e a t e | a t t a c h | c o n f i g u r e ) \s + ( \w + ) .* ?b e f o r e / i,
172- / f o l l o w e d \s + b y \s + ( \w + ) / i,
173- / (?: t h e n | a f t e r ) \s + (?: c a l l | u s e | c r e a t e ) \s + ( (?: P O S T | G E T | P U T | D E L E T E ) \s + \/ \S + ) / i,
178+ / b e f o r e \s + (?: p r o c e s s i n g | c r e a t i n g | c a l l i n g | u s i n g | c o n f i r m i n g ) \s + / i,
179+ / m u s t \s + (?: f i r s t | c r e a t e | a t t a c h | c o n f i g u r e ) \s + / i,
180+ / f o l l o w e d \s + b y \s + / i,
181+ / (?: t h e n | a f t e r ) \s + (?: c a l l | u s e | c r e a t e ) \s + / i,
182+ / (?: y o u m u s t | m u s t f i r s t ) \s + .* ?\s + b e f o r e \b / i,
183+ / t h e \s + (?: p a y m e n t | s u b s c r i p t i o n ) \s + f l o w \s + f o l l o w s / i,
184+ / s t r i c t \s + s e q u e n c e / i,
174185 ] ;
175186
176187 for ( const pattern of orderingPatterns ) {
@@ -191,7 +202,7 @@ function extractRules(
191202 }
192203 }
193204
194- // Mutual exclusion: "do not X when using Y" / "do not mix"
205+ // Mutual exclusion / prohibitions : "do not X when using Y" / "do not mix"
195206 if ( / d o \s + n o t | d o n ' t | s h o u l d \s + n o t | n e v e r / i. test ( lower ) ) {
196207 if ( mentionedTags . length >= 2 || ( mentionedTags . length >= 1 && mentionedEndpoints . length >= 1 ) ) {
197208 const entities = resolveEntitiesFromSentence ( sentence , vocabulary ) ;
@@ -205,6 +216,16 @@ function extractRules(
205216 } ) ;
206217 }
207218 }
219+ // Single-entity prohibition: "do not attempt to X" is a constraint
220+ if ( mentionedTags . length === 1 ) {
221+ rules . push ( {
222+ type : 'constrains' ,
223+ sourceEntity : '[prohibition]' ,
224+ targetEntity : mentionedTags [ 0 ] ! ,
225+ evidence : `constraint: "${ sentence . trim ( ) . slice ( 0 , 120 ) } "` ,
226+ confidence : 0.8 ,
227+ } ) ;
228+ }
208229 }
209230
210231 // Time constraints: "within X minutes/hours/days"
@@ -263,7 +284,7 @@ function extractRules(
263284 }
264285
265286 // Security requirements: "verify" / "authenticate" / "signature"
266- if ( / v e r i f y | a u t h e n t i c a t e | s i g n a t u r e | a u t h o r i z a t i o n / i. test ( lower ) && / r e q u i r e d | m u s t | a l w a y s / i. test ( lower ) ) {
287+ if ( / v e r i f y | a u t h e n t i c a t e | s i g n a t u r e | a u t h o r i z a t i o n / i. test ( lower ) && / r e q u i r e d | m u s t | a l w a y s | b e f o r e \s + p r o c e s s i n g / i. test ( lower ) ) {
267288 const entity = mentionedTags [ 0 ] ?? mentionedEndpoints [ 0 ] ?? 'unknown' ;
268289 rules . push ( {
269290 type : 'depends_on' ,
@@ -298,9 +319,12 @@ function resolveEntitiesFromSentence(
298319 const lower = sentence . toLowerCase ( ) ;
299320 const found : Array < { entity : string ; position : number } > = [ ] ;
300321
301- // Find tags mentioned in order of appearance
322+ // Find tags mentioned in order of appearance (with singular/plural)
302323 for ( const tag of vocabulary . tags ) {
303- const idx = lower . indexOf ( tag . toLowerCase ( ) ) ;
324+ const tl = tag . toLowerCase ( ) ;
325+ let idx = lower . indexOf ( tl ) ;
326+ if ( idx === - 1 && tl . endsWith ( 's' ) ) idx = lower . indexOf ( tl . slice ( 0 , - 1 ) ) ;
327+ if ( idx === - 1 && ! tl . endsWith ( 's' ) ) idx = lower . indexOf ( tl + 's' ) ;
304328 if ( idx !== - 1 ) {
305329 found . push ( { entity : tag , position : idx } ) ;
306330 }
0 commit comments