@@ -25,19 +25,29 @@ function unwrapSingleEnclosingFence(text) {
2525 return match [ 2 ] ;
2626}
2727
28- function isPlaceholderOnlyValue ( raw ) {
29- if ( typeof raw !== "string" ) return false ;
28+ /**
29+ * Shared strip/trim/unwrap used by placeholder and unusable-stand-in matchers.
30+ * Returns null when the value is absent after normalisation.
31+ */
32+ function normalizeRawSectionValue ( raw ) {
33+ if ( typeof raw !== "string" ) return null ;
3034 let value = raw . replace ( / < ! - - [ \s \S ] * ?- - > / g, "" ) . trim ( ) ;
31- if ( ! value ) return false ;
35+ if ( ! value ) return null ;
3236
33- // A lone fenced block whose entire body is a placeholder is still placeholder
34- // text (e.g. ```text\nN/A\n```), not a real example.
37+ // A lone fenced block whose entire body is a stand-in is still a stand-in
38+ // (e.g. ```text\nN/A\n```), not a real example.
3539 const unwrapped = unwrapSingleEnclosingFence ( value ) ;
3640 if ( unwrapped !== null ) {
3741 value = unwrapped . trim ( ) ;
38- if ( ! value ) return false ;
42+ if ( ! value ) return null ;
3943 }
4044
45+ return value ;
46+ }
47+
48+ function isPlaceholderOnlyValue ( raw ) {
49+ const value = normalizeRawSectionValue ( raw ) ;
50+ if ( value === null ) return false ;
4151 return PLACEHOLDER_ONLY_RE . test ( value ) ;
4252}
4353
@@ -374,7 +384,10 @@ function detectIssueKind(issue) {
374384// ---------------------------------------------------------------------------
375385
376386function isEmpty ( text ) {
377- return clean ( text ) . length === 0 ;
387+ const c = clean ( text ) ;
388+ if ( c . length === 0 ) return true ;
389+ // Stand-ins like "...", "…", "---" are not actionable report content.
390+ return / ^ [ \p{ P} \p{ S} \s ] + $ / u. test ( c ) ;
378391}
379392
380393function allSameCanonical ( sections ) {
@@ -395,6 +408,20 @@ function isPlaceholder(text) {
395408 return isPlaceholderOnlyValue ( text ) ;
396409}
397410
411+ /**
412+ * True when Version is an "I don't know" stand-in rather than an install id.
413+ * Kept separate from PLACEHOLDER_ONLY_RE so legacy N/A / No response soft-pass
414+ * behaviour is unchanged.
415+ */
416+ const UNUSABLE_VERSION_RE =
417+ / ^ [ \s _ * ~ ` ] * (?: u n k n o w n | u n k o w n | u k n o w n | d o n ' ? t \s + k n o w | d o \s + n o t \s + k n o w | i d k | d u n n o | n o t \s + s u r e | u n s u r e | \? + | 모 름 | 잘 \s * 모 름 | 모 르 겠 (?: 습 니 다 | 음 ) ? | 不 明 | わ か ら な い | 分 か ら な い | 不 知 道 | 不 清 楚 | k e i n e \s + a h n u n g | w e i [ s ß ] { 1 , 2 } \s + n i c h t ) [ \s _ * ~ ` ] * [ . ! ? ] * $ / i;
418+
419+ function isUnusableVersion ( raw ) {
420+ const value = normalizeRawSectionValue ( raw ) ;
421+ if ( value === null ) return false ;
422+ return UNUSABLE_VERSION_RE . test ( value ) ;
423+ }
424+
398425const CJK_RE =
399426 / [ \p{ Script= Han} \p{ Script= Hiragana} \p{ Script= Katakana} \p{ Script= Hangul} ] / gu;
400427
@@ -430,6 +457,16 @@ function isTooTerseFeatureSection(text) {
430457 return true ;
431458}
432459
460+ /**
461+ * Bug Reproduction needs steps or concrete signals. A title-like phrase with
462+ * no commands, paths, digits, or product keywords is not actionable.
463+ */
464+ function isTooTerseBugReproduction ( text ) {
465+ if ( isEmpty ( text ) || isPlaceholder ( text ) ) return false ;
466+ if ( hasConcreteDetail ( text ) ) return false ;
467+ return countWords ( text ) < 12 ;
468+ }
469+
433470/**
434471 * Check if raw section text is a placeholder-only variant without relying on
435472 * clean() first. Used to distinguish intentionally blank optional fields
@@ -625,6 +662,8 @@ function validateIssue(issue) {
625662 const repro = extractSection ( body , "Reproduction" ) ;
626663 const version = extractSection ( body , "Version" ) ;
627664 const os = extractSection ( body , "Operating system" ) ?? extractSection ( body , "OS" ) ;
665+ // New Bug report template always includes Client or integration.
666+ const isNewBugForm = extractSection ( body , "Client or integration" ) !== null ;
628667
629668 if ( isEmpty ( summary ) && isEmpty ( repro ) ) {
630669 // Soft-pass substantial non-English / freeform structured reports once
@@ -642,16 +681,65 @@ function validateIssue(issue) {
642681 reasons . push ( "Both Summary and Reproduction are empty." ) ;
643682 guidance . push ( "Describe what happened and how to reproduce it." ) ;
644683 }
684+ } else {
685+ // Each mapped field is required on its own — a filled Summary with an
686+ // empty / ellipsis Reproduction (e.g. #598) must not pass.
687+ if ( isEmpty ( summary ) ) {
688+ reasons . push ( "Summary is empty." ) ;
689+ guidance . push ( "Describe what happened (the symptom or error)." ) ;
690+ }
691+ if ( isEmpty ( repro ) ) {
692+ reasons . push ( "Reproduction is empty." ) ;
693+ guidance . push ( "List the exact steps to reproduce the problem." ) ;
694+ } else if ( ! softPass && isTooTerseBugReproduction ( repro ) ) {
695+ reasons . push ( "Reproduction is too vague to act on." ) ;
696+ guidance . push ( "List exact steps, commands, and the observed failure — not only a short phrase." ) ;
697+ }
645698 }
646699
647- // Required environment fields removed after submission.
648- // Only fire when the headings exist in the body (new form). Legacy bug
649- // reports never had Version or OS fields, so null means absent, not removed.
650- // Skip when the raw value is a "No response" placeholder -- the old form had
651- // both fields as optional, so legacy issues legitimately contain those headings
652- // with the GitHub placeholder. Only close when the field was actively cleared.
653- if ( ! softPass && version !== null && os !== null && isEmpty ( version ) && isEmpty ( os ) &&
654- ! isRawPlaceholder ( version ) && ! isRawPlaceholder ( os ) ) {
700+ // Version "Unknown" / "모름" / "idk" is never actionable, on any form.
701+ if ( ! softPass && version !== null && isUnusableVersion ( version ) ) {
702+ reasons . push ( "Version is missing or unknown." ) ;
703+ guidance . push ( "Report the installed `@bitkyc08/opencodex` version (for example `2.7.42`) or a commit SHA from `ocx --version`." ) ;
704+ } else if (
705+ ! softPass &&
706+ isNewBugForm &&
707+ ( version === null || isEmpty ( version ) || isRawPlaceholder ( version ) )
708+ ) {
709+ // New form requires Version (including when the heading was removed).
710+ // Legacy N/A / No response soft-pass stays only for bodies without
711+ // Client or integration.
712+ reasons . push ( "Version is missing." ) ;
713+ guidance . push ( "Add your OpenCodex version so we can reproduce the environment." ) ;
714+ }
715+
716+ if ( ! softPass && isNewBugForm && os !== null && isUnusableVersion ( os ) ) {
717+ reasons . push ( "Operating system is missing or unknown." ) ;
718+ guidance . push ( "Add your OS name and version (for example Windows 11 24H2)." ) ;
719+ } else if (
720+ ! softPass &&
721+ isNewBugForm &&
722+ ( os === null || isEmpty ( os ) || isRawPlaceholder ( os ) )
723+ ) {
724+ reasons . push ( "Operating system is missing." ) ;
725+ guidance . push ( "Add your OS name and version (for example Windows 11 24H2)." ) ;
726+ }
727+
728+ // Required environment fields removed after submission on bodies that are
729+ // not the new form (no Client or integration). Legacy reports never had
730+ // Version or OS fields, so null means absent, not removed. Skip when the
731+ // raw value is a "No response" placeholder — the old form had both fields
732+ // as optional. Only close when the field was actively cleared.
733+ if (
734+ ! softPass &&
735+ ! isNewBugForm &&
736+ version !== null &&
737+ os !== null &&
738+ isEmpty ( version ) &&
739+ isEmpty ( os ) &&
740+ ! isRawPlaceholder ( version ) &&
741+ ! isRawPlaceholder ( os )
742+ ) {
655743 reasons . push ( "Version and Operating system are both missing." ) ;
656744 guidance . push ( "Add your OpenCodex version and OS so we can reproduce the environment." ) ;
657745 }
@@ -859,6 +947,7 @@ module.exports = {
859947 isPlaceholderOnlyValue,
860948 isPlaceholder,
861949 isRawPlaceholder,
950+ isUnusableVersion,
862951 countWords,
863952 hasConcreteDetail,
864953 labelForKind,
0 commit comments