@@ -266,6 +266,21 @@ function runValidator(args, input = null) {
266266 return { status : result . status , parsed } ;
267267}
268268
269+ // runJson in this file passes env, not stdin; this variant feeds JSON on stdin.
270+ function runJsonInput ( script , input ) {
271+ const result = spawnSync ( process . execPath , [ script ] , { cwd : repo , input, encoding : "utf8" } ) ;
272+ if ( result . status !== 0 ) {
273+ errors . push ( `${ rel ( script ) } failed on stdin input: ${ result . stderr || result . stdout } ` ) ;
274+ return null ;
275+ }
276+ try {
277+ return JSON . parse ( result . stdout ) ;
278+ } catch ( error ) {
279+ errors . push ( `${ rel ( script ) } did not output JSON: ${ error . message } ` ) ;
280+ return null ;
281+ }
282+ }
283+
269284const invalidObs = runValidator ( [ path . join ( plugin , "fixtures/invalid-observation.json" ) ] ) ;
270285if ( invalidObs . parsed ) {
271286 if ( invalidObs . status !== 1 ) errors . push ( "Structural validator should exit 1 for the invalid Observation." ) ;
@@ -312,6 +327,82 @@ if (resolvedDetection && resolvedDetection.packageResolution?.resolvedCount !==
312327 errors . push ( "Detector should resolve hl7.fhir.r4.core when present in the cache." ) ;
313328}
314329
330+ // --- Extended structural validator coverage ---
331+ function validatorCodes ( args , input = null ) {
332+ const run = runValidator ( args , input ) ;
333+ return { status : run . status , codes : run . parsed ? run . parsed . operationOutcome . issue . map ( ( entry ) => `${ entry . code } :${ entry . expression [ 0 ] } ` ) : null } ;
334+ }
335+ const unknownElement = validatorCodes ( [ ] , '{"resourceType":"Patient","id":"x","bogusElement":1}' ) ;
336+ if ( ! unknownElement . codes ?. some ( ( entry ) => entry . startsWith ( "structure:" ) && entry . includes ( "bogusElement" ) ) ) errors . push ( "Validator should flag unknown elements as structure." ) ;
337+ const doubleChoice = validatorCodes ( [ ] , '{"resourceType":"Observation","status":"final","code":{},"valueString":"a","valueInteger":2}' ) ;
338+ if ( ! doubleChoice . codes ?. includes ( "structure:Observation.value[x]" ) ) errors . push ( "Validator should flag choice[x] exclusivity." ) ;
339+ const badEnum = validatorCodes ( [ ] , '{"resourceType":"Observation","status":"bogus","code":{}}' ) ;
340+ if ( ! badEnum . codes ?. includes ( "code-invalid:Observation.status" ) ) errors . push ( "Validator should flag invalid required code enums." ) ;
341+ const nullValue = validatorCodes ( [ ] , '{"resourceType":"Patient","id":"x","active":null}' ) ;
342+ if ( ! nullValue . codes ?. some ( ( entry ) => entry . startsWith ( "value:" ) ) ) errors . push ( "Validator should flag null values." ) ;
343+ const emptyArray = validatorCodes ( [ ] , '{"resourceType":"Patient","id":"x","name":[]}' ) ;
344+ if ( ! emptyArray . codes ?. some ( ( entry ) => entry . startsWith ( "value:" ) ) ) errors . push ( "Validator should flag empty arrays." ) ;
345+ const unknownType = runValidator ( [ ] , '{"resourceType":"Goober","id":"x"}' ) ;
346+ if ( unknownType . status !== 0 || ! unknownType . parsed ?. operationOutcome . issue . some ( ( entry ) => entry . code === "incomplete" ) ) errors . push ( "Validator should return incomplete info and exit 0 for unschemaed resource types." ) ;
347+ const bundleRecursion = validatorCodes ( [ ] , '{"resourceType":"Bundle","type":"collection","entry":[{"resource":{"resourceType":"Observation","status":12}}]}' ) ;
348+ if ( ! bundleRecursion . codes ?. some ( ( entry ) => entry . includes ( "entry[0].resource" ) ) ) errors . push ( "Validator should recurse into Bundle entries." ) ;
349+ const validatorBadJson = spawnSync ( process . execPath , [ path . join ( plugin , "skills/fhir-validation/scripts/validate-structural.mjs" ) ] , { cwd : repo , input : "{not json" , encoding : "utf8" } ) ;
350+ if ( validatorBadJson . status !== 2 ) errors . push ( "Validator should exit 2 on invalid JSON." ) ;
351+
352+ // --- Extended expression mapper coverage ---
353+ const functionExpr = runJson ( expressionMapper , [ "Bundle.entry.resource.ofType(Patient).name.where(use='official')" ] ) ;
354+ if ( ! functionExpr ?. functions ?. some ( ( entry ) => entry . name === "ofType" ) || ! functionExpr ?. functions ?. some ( ( entry ) => entry . name === "where" ) ) errors . push ( "Expression mapper should capture FHIRPath functions." ) ;
355+ const noArgExpr = spawnSync ( process . execPath , [ expressionMapper ] , { cwd : repo , encoding : "utf8" } ) ;
356+ if ( noArgExpr . status !== 2 ) errors . push ( "Expression mapper should exit 2 with no argument." ) ;
357+
358+ // --- Extended explainer coverage ---
359+ const allCodes = [ "required" , "value" , "code-invalid" , "structure" , "invariant" , "processing" , "not-found" , "duplicate" , "forbidden" , "incomplete" , "business-rule" , "profile-unknown" , "slicing" ] ;
360+ const allExplained = runJsonInput ( explainer , JSON . stringify ( { resourceType : "OperationOutcome" , issue : allCodes . map ( ( code ) => ( { severity : "error" , code } ) ) } ) ) ;
361+ if ( ! allExplained ?. issues ?. every ( ( entry ) => entry . meaning && ! / U n k n o w n / . test ( entry . meaning ) ) ) errors . push ( "Explainer should map every known issue code." ) ;
362+ const unknownCode = runJsonInput ( explainer , JSON . stringify ( { resourceType : "OperationOutcome" , issue : [ { severity : "error" , code : "made-up" } ] } ) ) ;
363+ if ( ! / U n k n o w n / . test ( unknownCode ?. issues ?. [ 0 ] ?. meaning || "" ) ) errors . push ( "Explainer should fall back for unknown codes." ) ;
364+ if ( spawnSync ( process . execPath , [ explainer ] , { cwd : repo , input : '{"resourceType":"Patient"}' , encoding : "utf8" } ) . status !== 2 ) errors . push ( "Explainer should exit 2 for non-OperationOutcome input." ) ;
365+
366+ // --- Extended analyzer coverage ---
367+ const withSnapshot = runJsonInput ( analyzer , JSON . stringify ( { resourceType : "StructureDefinition" , derivation : "constraint" , snapshot : { element : [ { path : "Observation" } ] } } ) ) ;
368+ if ( withSnapshot ?. needsSnapshot !== false ) errors . push ( "Analyzer should not flag profiles that already have a snapshot." ) ;
369+ const noDiscriminator = runJsonInput ( analyzer , JSON . stringify ( { resourceType : "StructureDefinition" , derivation : "constraint" , snapshot : { element : [ { path : "X" } ] } , differential : { element : [ { path : "Observation.category" , slicing : { rules : "open" } } ] } } ) ) ;
370+ if ( ! noDiscriminator ?. caveats ?. some ( ( entry ) => / n o d i s c r i m i n a t o r / . test ( entry ) ) ) errors . push ( "Analyzer should caveat slicing without a discriminator." ) ;
371+ if ( spawnSync ( process . execPath , [ analyzer ] , { cwd : repo , input : '{"resourceType":"Patient"}' , encoding : "utf8" } ) . status !== 2 ) errors . push ( "Analyzer should exit 2 for non-StructureDefinition input." ) ;
372+
373+ // --- Flat-directory detection ---
374+ const flatDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "records-flat-" ) ) ;
375+ await writeFile ( path . join ( flatDir , "obs.json" ) , JSON . stringify ( { resourceType : "Observation" , id : "a" , status : "final" , code : { } } ) , "utf8" ) ;
376+ const flatDetection = runJson ( detector , [ flatDir ] , { ...process . env , FHIR_PACKAGE_CACHE : emptyCache } ) ;
377+ if ( flatDetection ?. projectType !== "fhir-resources" || flatDetection ?. resourceInventory . byResourceType . Observation !== 1 ) {
378+ errors . push ( "Detector should classify a flat directory of resources as fhir-resources." ) ;
379+ }
380+ const emptyDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "records-emptydir-" ) ) ;
381+ const emptyDetection = runJson ( detector , [ emptyDir ] , { ...process . env , FHIR_PACKAGE_CACHE : emptyCache } ) ;
382+ if ( emptyDetection ?. projectType !== "unknown" ) errors . push ( "Detector should classify an empty directory as unknown." ) ;
383+
384+ // --- Redaction and quality-rule derivation ---
385+ const redactor = path . join ( plugin , "skills/fhir-validation/scripts/redact-fhir-summary.mjs" ) ;
386+ const bundleSummary = runJsonInput ( redactor , JSON . stringify ( { resourceType : "Bundle" , entry : [ { resource : { resourceType : "Patient" , id : "p1" } } ] } ) ) ;
387+ if ( bundleSummary ?. entryCount !== 1 || bundleSummary ?. privacyRiskLevel !== "high" ) errors . push ( "Redactor should summarize Bundles and raise risk for Patient entries." ) ;
388+ const qualityDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "records-quality-" ) ) ;
389+ for ( const id of [ "a" , "b" , "c" ] ) {
390+ await writeFile ( path . join ( qualityDir , `${ id } .json` ) , JSON . stringify ( { resourceType : "Observation" , id, status : "final" , code : { } , meta : { profile : [ "https://example.org/StructureDefinition/p" ] } } ) , "utf8" ) ;
391+ }
392+ const qualityRules = runJson ( path . join ( plugin , "skills/fhir-validation/scripts/derive-quality-rules.mjs" ) , [ qualityDir ] ) ;
393+ if ( ! qualityRules ?. proposedRules ?. some ( ( rule ) => rule . id . startsWith ( "profile-" ) && rule . confidence === "high" ) ) {
394+ errors . push ( "Quality-rule derivation should propose a high-confidence profile rule when all resources share a profile." ) ;
395+ }
396+
397+ // --- CI generation modes ---
398+ const ciGen = path . join ( plugin , "skills/fhir-validation/scripts/generate-ci.mjs" ) ;
399+ const apiCi = spawnSync ( process . execPath , [ ciGen , "--api" ] , { cwd : repo , encoding : "utf8" } ) . stdout ;
400+ if ( ! apiCi . includes ( "RECORDS_API_URL" ) ) errors . push ( "CI generator --api should reference RECORDS_API_URL." ) ;
401+ const sushiCi = spawnSync ( process . execPath , [ ciGen , "--sushi" ] , { cwd : repo , encoding : "utf8" } ) . stdout ;
402+ if ( ! sushiCi . includes ( "sushi ." ) ) errors . push ( "CI generator --sushi should include a SUSHI build step." ) ;
403+ const uploadCi = spawnSync ( process . execPath , [ ciGen , "--upload-artifact" ] , { cwd : repo , encoding : "utf8" } ) . stdout ;
404+ if ( ! uploadCi . includes ( "upload-artifact" ) ) errors . push ( "CI generator --upload-artifact should add an artifact upload step." ) ;
405+
315406if ( errors . length ) {
316407 console . error ( errors . map ( ( error ) => `- ${ error } ` ) . join ( "\n" ) ) ;
317408 process . exit ( 1 ) ;
0 commit comments