@@ -694,7 +694,7 @@ export function extractRequiredDeliverables(text: string): RequiredDeliverables
694694 // required makes the deliverable gate reject the correct implementation
695695 // forever, so a negated verb disqualifies the whole line.
696696 if ( verbIsNegated ( line , verb . index ) ) continue
697- let spans = modelCandidateSpans ( requirementHead ( line , verb . index ) )
697+ let spans = modelCandidateSpans ( requirementHead ( line , verb . index ) , RENAME_VERB_RE . test ( verb [ 0 ] ) )
698698 // "Rename `old_orders` to `new_orders`" names two artifacts, but only the
699699 // destination is required to exist once the rename is done — the source
700700 // is expected to be GONE. `to` is not a qualifier `requirementHead` cuts
@@ -779,15 +779,34 @@ function inlineCodeSpans(line: string): string[] {
779779 * requiring a model by that name blocks a correct implementation forever.
780780 */
781781const NON_MODEL_KIND = "columns?|fields?|attributes?|variables?|vars?|macros?|settings?|parameters?"
782- /** Kind word, then at most a short phrase, then "called"/"named" or a colon, ending the gap before a span. */
783- const INTRODUCED_AS_NON_MODEL_RE = new RegExp (
784- `\\b(?:${ NON_MODEL_KIND } )\\b(?:(?:[^.;:!?]|\\.(?=\\w)){0,60}\\b(?:called|named)|\\s*:|\\s*-|\\s*\\()?\\s*$` ,
782+ /**
783+ * Kind word, a short phrase, then "called"/"named"; or a kind word and a colon,
784+ * dash or opening parenthesis — ending the gap before a span. Applied to one
785+ * clause of the gap that holds no deliverable noun, so a later "called" that
786+ * belongs to a model ("a settings model called `app_settings`") does not hand
787+ * its name to the kind word.
788+ */
789+ const INTRODUCED_CALLED_RE = new RegExp (
790+ `\\b(?:${ NON_MODEL_KIND } )\\b(?:[^.;:,!?]|\\.(?=\\w)){0,60}?\\b(?:called|named)\\s*$` ,
785791 "i" ,
786792)
787- /** "`x` column", "`x` variable": the kind word follows the span. */
788- const FOLLOWED_BY_NON_MODEL_RE = new RegExp ( `^\\s*(?:${ NON_MODEL_KIND } )\\b` , "i" )
789- /** "should have `a`, `b`": what a model has is its columns. */
790- const HAS_LIST_RE = / \b (?: h a v e | h a s | h a v i n g ) \s + (?: t h e \s + ) ? (?: f o l l o w i n g \s + ) ? (?: (?: n e w | e x t r a | a d d i t i o n a l ) \s + ) ? $ / i
793+ const INTRODUCED_COLON_RE = new RegExp ( `\\b(?:${ NON_MODEL_KIND } )\\b\\s*[:\\-(]?\\s*$` , "i" )
794+ /** A span directly introduced by a deliverable noun ("the model `x`", "table called `x`") is a model. */
795+ const INTRODUCED_AS_MODEL_RE =
796+ / \b (?: m o d e l s ? | t a b l e s ? | v i e w s ? | s e e d s ? | s n a p s h o t s ? | m a r t s ? | f i l e s ? ) \s * (?: (?: c a l l e d | n a m e d ) \s * ) ? : ? \s * $ / i
797+ /** ... and so is a span followed by one ("`x` model"). */
798+ const FOLLOWED_BY_MODEL_NOUN_RE = / ^ \s * (?: m o d e l s ? | t a b l e s ? | v i e w s ? | s e e d s ? | s n a p s h o t s ? | f i l e s ? ) \b / i
799+ /**
800+ * "`x` column", "`x` variable": the kind word follows the span and ends the
801+ * noun phrase. "`x` column names" is attributive and does not qualify.
802+ */
803+ const FOLLOWED_BY_NON_MODEL_RE = new RegExp (
804+ `^\\s*(?:${ NON_MODEL_KIND } )\\b(?=\\s*(?:$|[,.;:)]|(?:to|in|of|on|from|that|which|with|for|as|and|or|is|are|should|must|so|called|named|by)\\b))` ,
805+ "i" ,
806+ )
807+ /** "should have `a`, `b`": what a model has or exposes is its columns. */
808+ const HAS_LIST_RE =
809+ / \b (?: h a v e | h a s | h a v i n g | c o n t a i n s ? | c o n t a i n i n g | e x p o s e s ? | e x p o s i n g ) \s + (?: t h e \s + ) ? (?: f o l l o w i n g \s + ) ? (?: (?: n e w | e x t r a | a d d i t i o n a l ) \s + ) ? $ / i
791810/** Separator between items of a list of spans, allowing a short parenthetical note after an item. */
792811const LIST_SEPARATOR_RE = / ^ \s * (?: \( [ ^ ) ` ] * \) ) ? \s * (?: , | ; | , ? \s * (?: a n d | o r | & ) ) ? \s * $ / i
793812/** "rename column `a` to `b`": the target of a column rename is a column. */
@@ -799,8 +818,15 @@ const RENAME_TARGET_RE = new RegExp(`^\\s*(?:(?:${NON_MODEL_KIND})\\s+)?(?:to|in
799818 * A span is dropped only on explicit wording around it ("column called `x`",
800819 * "`x` column", "should have `a`, `b`", a list continuing such a span); with no
801820 * such wording it stays, so a bare name is still required.
821+ *
822+ * Dropping errs toward keeping: a span introduced or followed by a deliverable
823+ * noun is never dropped, "have `x`" drops only when an earlier span of the line
824+ * is already kept (so "the project should have `stg_a` and `stg_b`" keeps both),
825+ * and the target of "to"/"as" is dropped only on a rename line. Dropping a real
826+ * model could leave the line with no name at all, which makes the whole
827+ * contract read as absent and silences both completion gates.
802828 */
803- function modelCandidateSpans ( line : string ) : string [ ] {
829+ function modelCandidateSpans ( line : string , renameLine : boolean ) : string [ ] {
804830 const out : string [ ] = [ ]
805831 CODE_SPAN_RE . lastIndex = 0
806832 let prevEnd = 0
@@ -814,11 +840,17 @@ function modelCandidateSpans(line: string): string[] {
814840 const cur = matches [ i ] !
815841 const before = line . slice ( prevEnd , cur . start )
816842 const after = line . slice ( cur . end , matches [ i + 1 ] ?. start ?? line . length )
843+ // "column ... called `x`" counts only inside one clause that names no deliverable noun.
844+ const clause = before . split ( / [ ; : ! ? , ] | \. (? ! \w ) / ) . pop ( ) ?? ""
845+ const introducedAsNonModel =
846+ ( INTRODUCED_CALLED_RE . test ( clause ) && ! DELIVERABLE_NOUN_RE . test ( clause ) ) || INTRODUCED_COLON_RE . test ( before )
817847 const nonModel : boolean =
818- INTRODUCED_AS_NON_MODEL_RE . test ( before ) ||
819- FOLLOWED_BY_NON_MODEL_RE . test ( after ) ||
820- HAS_LIST_RE . test ( before ) ||
821- ( prevNonModel && ( LIST_SEPARATOR_RE . test ( before ) || RENAME_TARGET_RE . test ( before ) ) )
848+ ! INTRODUCED_AS_MODEL_RE . test ( before ) &&
849+ ! FOLLOWED_BY_MODEL_NOUN_RE . test ( after ) &&
850+ ( introducedAsNonModel ||
851+ FOLLOWED_BY_NON_MODEL_RE . test ( after ) ||
852+ ( out . length > 0 && HAS_LIST_RE . test ( before ) ) ||
853+ ( prevNonModel && ( LIST_SEPARATOR_RE . test ( before ) || ( renameLine && RENAME_TARGET_RE . test ( before ) ) ) ) )
822854 if ( ! nonModel ) out . push ( cur . text )
823855 prevNonModel = nonModel
824856 prevEnd = cur . end
0 commit comments