@@ -171,6 +171,76 @@ function stripMarkdownInline(s) {
171171 . trim ( ) ;
172172}
173173
174+ function truncate ( s , max = 140 ) {
175+ const t = String ( s ?? "" ) . trim ( ) ;
176+ if ( ! t ) return "" ;
177+ return t . length > max ? `${ t . slice ( 0 , max - 1 ) } ?` : t ;
178+ }
179+
180+ function readmeTableValue ( markdown , fieldName ) {
181+ const wanted = String ( fieldName ?? "" ) . trim ( ) . toLowerCase ( ) ;
182+ if ( ! wanted ) return "" ;
183+
184+ const lines = String ( markdown ?? "" ) . split ( / \r ? \n / ) ;
185+ for ( const line of lines ) {
186+ const t = String ( line ?? "" ) . trim ( ) ;
187+ if ( ! t . startsWith ( "|" ) ) continue ;
188+
189+ // Separator rows like: |-----|-----|
190+ if ( / ^ \| \s * : ? - + : ? \s * \| / . test ( t ) ) continue ;
191+
192+ let cols = t . split ( "|" ) . map ( ( c ) => c . trim ( ) ) ;
193+ while ( cols . length && cols [ 0 ] === "" ) cols = cols . slice ( 1 ) ;
194+ while ( cols . length && cols [ cols . length - 1 ] === "" ) cols = cols . slice ( 0 , - 1 ) ;
195+ if ( cols . length < 2 ) continue ;
196+
197+ const key = String ( cols [ 0 ] ?? "" ) . trim ( ) . toLowerCase ( ) ;
198+ if ( ! key || key !== wanted ) continue ;
199+
200+ const value = stripMarkdownInline ( cols . slice ( 1 ) . join ( " | " ) ) ;
201+ if ( ! value ) continue ;
202+ return value ;
203+ }
204+
205+ return "" ;
206+ }
207+
208+ function normalizeLanguageLabel ( v ) {
209+ const s = String ( v ?? "" ) . trim ( ) ;
210+ if ( ! s ) return "" ;
211+ const lower = s . toLowerCase ( ) ;
212+ if ( lower === "en" || / \b e n g l i s h \b / i. test ( lower ) ) return "English" ;
213+ if ( lower === "zh" || / ^ z h \b / . test ( lower ) || / \b c h i n e s e \b / i. test ( lower ) || / \u4e2d \u6587 / . test ( s ) ) return "Chinese" ;
214+ return s ;
215+ }
216+
217+ function normalizeMaturity ( v ) {
218+ const s = String ( v ?? "" ) . trim ( ) ;
219+ if ( ! s ) return "" ;
220+ return s . toLowerCase ( ) . replace ( / \s + / g, "_" ) ;
221+ }
222+
223+ function extractMaturityFromReadme ( markdown ) {
224+ const fromTable = readmeTableValue ( markdown , "Maturity" ) ;
225+ if ( fromTable ) return fromTable ;
226+
227+ const m1 = / ! \[ \s * M a t u r i t y \s * : \s * ( [ ^ \] ] + ?) \s * \] / i. exec ( String ( markdown ?? "" ) ) ;
228+ if ( m1 ?. [ 1 ] ) return m1 [ 1 ] . trim ( ) ;
229+
230+ // shields.io style: .../badge/Maturity-smoke_tested-...
231+ const m2 = / b a d g e \/ M a t u r i t y - ( [ ^ \- \s \) ] + ) - / i. exec ( String ( markdown ?? "" ) ) ;
232+ if ( m2 ?. [ 1 ] ) {
233+ try {
234+ return decodeURIComponent ( m2 [ 1 ] ) . trim ( ) ;
235+ } catch {
236+ return String ( m2 [ 1 ] ) . trim ( ) ;
237+ }
238+ }
239+
240+ return "" ;
241+ }
242+
243+
174244function firstParagraphDescription ( markdown ) {
175245 const text = String ( markdown ?? "" ) . replace ( / < ! - - ( [ \s \S ] * ?) - - > / g, "" ) ;
176246 const lines = text . split ( / \r ? \n / ) . map ( ( l ) => l . trim ( ) ) ;
@@ -254,7 +324,9 @@ function inferTagsFromRepo(repoName, markdown) {
254324 const language = [ ] ;
255325 // Only infer language when explicitly stated.
256326 if ( / \b l a n g u a g e \s * : \s * e n \b / i. test ( hay ) || / \b e n g l i s h \b / i. test ( hay ) )
257- language . push ( "en" ) ;
327+ language . push ( "English" ) ;
328+ if ( / \b l a n g u a g e \s * : \s * z h \b / i. test ( hay ) || / \b c h i n e s e \b / i. test ( hay ) || / \u4e2d \u6587 / . test ( hay ) )
329+ language . push ( "Chinese" ) ;
258330
259331 return {
260332 paradigm : uniq ( paradigm ) ,
@@ -354,9 +426,15 @@ async function buildIndex() {
354426 const readmeUrl = `${ GH_API } /repos/${ encodeURIComponent ( ORG ) } /${ encodeURIComponent ( repo ) } /readme` ;
355427 const readme = await ghGetRaw ( readmeUrl , { allow404 : true } ) ;
356428
429+ const shortFromReadmeTable = readmeTableValue ( readme , "Short Description" ) ;
357430 const shortFromReadme = firstParagraphDescription ( readme ) ;
358431 const shortFromRepo = String ( r . description ?? "" ) . trim ( ) ;
359432
433+ const maturity = normalizeMaturity (
434+ ( typeof meta ?. maturity === "string" ? meta . maturity : "" ) ||
435+ extractMaturityFromReadme ( readme )
436+ ) || null ;
437+
360438 const tagsFromMeta = normalizeTags ( meta ?. tags ) ;
361439 const tagsInferred = inferTagsFromRepo ( repo , readme ) ;
362440
@@ -367,6 +445,14 @@ async function buildIndex() {
367445 language : uniq ( [ ...( tagsFromMeta . language ?? [ ] ) , ...( tagsInferred . language ?? [ ] ) ] )
368446 } ;
369447
448+ // Prefer explicit README table language if present.
449+ const languageFromReadme = normalizeLanguageLabel ( readmeTableValue ( readme , "Language" ) ) ;
450+ if ( languageFromReadme ) {
451+ tags . language = uniq ( [ ...( tags . language ?? [ ] ) , languageFromReadme ] ) ;
452+ }
453+
454+ tags . language = uniq ( ( tags . language ?? [ ] ) . map ( normalizeLanguageLabel ) . filter ( Boolean ) ) ;
455+
370456 const keywords = uniq ( [
371457 ...toArray ( meta ?. keywords ) . map ( String ) ,
372458 ...tags . paradigm ,
@@ -377,15 +463,31 @@ async function buildIndex() {
377463 ] ) ;
378464
379465 let short_description = String ( meta ?. short_description ?? "" ) . trim ( ) ;
380- if ( ! short_description ) short_description = shortFromRepo ;
466+ if ( ! short_description ) short_description = shortFromReadmeTable ;
381467 if ( ! short_description ) short_description = shortFromReadme ;
468+ if ( ! short_description ) short_description = shortFromRepo ;
382469 if ( ! short_description ) {
383470 const p = tags . paradigm ?. [ 0 ] ;
384471 short_description = p
385472 ? `${ p } task template (PsyFlow/TAPS).`
386473 : `${ repo } task template (PsyFlow/TAPS).` ;
387474 }
388475
476+ short_description = truncate ( short_description ) ;
477+
478+ let psyflow_version = meta ?. psyflow_version ?? null ;
479+ if ( ! psyflow_version ) {
480+ const v = readmeTableValue ( readme , "PsyFlow Version" ) ;
481+ psyflow_version = v || null ;
482+ }
483+
484+ let has_voiceover =
485+ typeof meta ?. has_voiceover === "boolean" ? meta . has_voiceover : null ;
486+ if ( has_voiceover === null ) {
487+ const voiceName = readmeTableValue ( readme , "Voice Name" ) ;
488+ if ( voiceName ) has_voiceover = true ;
489+ }
490+
389491 const run_anchor = readme ? detectRunAnchor ( readme ) : "#run" ;
390492
391493 const item = {
@@ -394,11 +496,11 @@ async function buildIndex() {
394496 html_url : r . html_url ,
395497 default_branch : r . default_branch ,
396498 short_description,
499+ maturity,
397500 tags,
398501 keywords,
399- psyflow_version : meta ?. psyflow_version ?? null ,
400- has_voiceover :
401- typeof meta ?. has_voiceover === "boolean" ? meta . has_voiceover : null ,
502+ psyflow_version,
503+ has_voiceover,
402504 last_updated : r . pushed_at || r . updated_at ,
403505 structure,
404506 readme_run_anchor : run_anchor
@@ -416,7 +518,7 @@ async function buildIndex() {
416518 tasks . sort ( ( a , b ) => ( a . last_updated < b . last_updated ? 1 : - 1 ) ) ;
417519
418520 const index = {
419- schema_version : 1 ,
521+ schema_version : 2 ,
420522 generated_at : new Date ( ) . toISOString ( ) ,
421523 org : ORG ,
422524 tasks
0 commit comments