@@ -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
@@ -398,6 +408,20 @@ function isPlaceholder(text) {
398408 return isPlaceholderOnlyValue ( text ) ;
399409}
400410
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+
401425const CJK_RE =
402426 / [ \p{ Script= Han} \p{ Script= Hiragana} \p{ Script= Katakana} \p{ Script= Hangul} ] / gu;
403427
@@ -433,6 +457,16 @@ function isTooTerseFeatureSection(text) {
433457 return true ;
434458}
435459
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+
436470/**
437471 * Check if raw section text is a placeholder-only variant without relying on
438472 * clean() first. Used to distinguish intentionally blank optional fields
@@ -628,6 +662,8 @@ function validateIssue(issue) {
628662 const repro = extractSection ( body , "Reproduction" ) ;
629663 const version = extractSection ( body , "Version" ) ;
630664 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 ;
631667
632668 if ( isEmpty ( summary ) && isEmpty ( repro ) ) {
633669 // Soft-pass substantial non-English / freeform structured reports once
@@ -655,17 +691,55 @@ function validateIssue(issue) {
655691 if ( isEmpty ( repro ) ) {
656692 reasons . push ( "Reproduction is empty." ) ;
657693 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." ) ;
658697 }
659698 }
660699
661- // Required environment fields removed after submission.
662- // Only fire when the headings exist in the body (new form). Legacy bug
663- // reports never had Version or OS fields, so null means absent, not removed.
664- // Skip when the raw value is a "No response" placeholder -- the old form had
665- // both fields as optional, so legacy issues legitimately contain those headings
666- // with the GitHub placeholder. Only close when the field was actively cleared.
667- if ( ! softPass && version !== null && os !== null && isEmpty ( version ) && isEmpty ( os ) &&
668- ! 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+ ) {
669743 reasons . push ( "Version and Operating system are both missing." ) ;
670744 guidance . push ( "Add your OpenCodex version and OS so we can reproduce the environment." ) ;
671745 }
@@ -873,6 +947,7 @@ module.exports = {
873947 isPlaceholderOnlyValue,
874948 isPlaceholder,
875949 isRawPlaceholder,
950+ isUnusableVersion,
876951 countWords,
877952 hasConcreteDetail,
878953 labelForKind,
0 commit comments