@@ -18,6 +18,8 @@ import {
1818 type DesignSetSource ,
1919 type ResolvedDesignFile ,
2020 checkDesignDocument ,
21+ designInputBlockers ,
22+ type ItemCatalogItem ,
2123 designBaseFiles ,
2224 designCoverage ,
2325 designFilesFor ,
@@ -54,10 +56,14 @@ import {
5456 parseRequirementsMarkdown ,
5557 unitDesignFilePath ,
5658 unitRulesPath ,
59+ unitAsIsPath ,
60+ unitUnderstandingPath ,
61+ unitRulesReviewPath ,
5762 unitStatePath ,
5863} from "@kintsugi/domain" ;
5964import { parseKintsugiYaml } from "@kintsugi/manifest" ;
6065import { chunk } from "#common/util/async.js" ;
66+ import { CatalogService } from "#modules/delivery/catalog/index.js" ;
6167import { type DocumentContext , DocumentsService } from "#modules/delivery/documents/index.js" ;
6268import type {
6369 CommonComponentView ,
@@ -209,7 +215,7 @@ export class DesignService {
209215 private readonly projectSets = new Map < string , Promise < ProjectSets > > ( ) ;
210216 private readonly scans = new Map < string , Promise < AppScan > > ( ) ;
211217
212- constructor ( private readonly documents : DocumentsService ) { }
218+ constructor ( private readonly documents : DocumentsService , private readonly catalog : CatalogService ) { }
213219
214220 /** The design sets of the project at a ref, with the faults of their definitions; with an application, how its units stand with each. */
215221 async sets ( projectId : string , ref : string | undefined , applicationId : string | undefined ) : Promise < DesignSetsView > {
@@ -485,12 +491,24 @@ export class DesignService {
485491 if ( keys . length < MAX_UNITS ) keys . push ( { module, unit } ) ;
486492 }
487493 }
494+ const catalog = await this . catalog . load ( context . project . id , applicationId , commit ) . catch ( ( error : unknown ) => {
495+ if ( error instanceof NotFoundException ) return undefined ;
496+ throw error ;
497+ } ) ;
498+ const active = new Map ( ( catalog ?. catalog . items ?? [ ] ) . filter ( ( item ) => item . status === "active" ) . map ( ( item ) => [ item . id , item ] ) ) ;
499+ const knownKeys = new Set ( keys . map ( ( key ) => `${ key . module } /${ key . unit } ` ) ) ;
500+ for ( const item of active . values ( ) ) {
501+ if ( ! knownKeys . has ( item . id ) && keys . length < MAX_UNITS ) {
502+ const [ module , unit ] = item . id . split ( "/" ) as [ string , string ] ;
503+ keys . push ( { module, unit } ) ;
504+ }
505+ }
488506 const shared : Omit < DesignContext , "items" > = { ruleIds : [ ] , requirementIds, componentIds : components . map ( ( component ) => component . id ) } ;
489507 // The application's own design first: a unit's design may point to the items of the sets of the application it needs.
490508 const application = await this . scanApplication ( context , commit , applicationId , project , shared ) ;
491509 const units : UnitScan [ ] = [ ] ;
492510 for ( const group of chunk ( keys , READS ) ) {
493- const scanned = await Promise . all ( group . map ( ( key ) => this . scanUnit ( context , commit , applicationId , key . module , key . unit , project , shared , application ) ) ) ;
511+ const scanned = await Promise . all ( group . map ( ( key ) => this . scanUnit ( context , commit , applicationId , key . module , key . unit , project , shared , application , active . get ( ` ${ key . module } / ${ key . unit } ` ) ) ) ) ;
494512 units . push ( ...scanned . filter ( ( unit ) : unit is UnitScan => unit !== undefined ) ) ;
495513 }
496514 return { project, application, units, components } ;
@@ -541,25 +559,31 @@ export class DesignService {
541559 }
542560 for ( const detail of details ) {
543561 const summary = summaries [ detail . set ] as UnitSetSummary ;
544- const needsMet = detail . needs . every ( ( need ) => summaries [ need ] === undefined || finished ( ( summaries [ need ] as UnitSetSummary ) . state ) ) ;
545- summary . ready = needsMet && summary . state === "missing" && summary . files . conformant + summary . files . problems === 0 ;
562+ summary . blockers = detail . needs . filter ( ( need ) => summaries [ need ] === undefined || ! finished ( summaries [ need ] ! . state ) ) . map ( ( need ) => `Needs the merged, conformant ${ need } design first.` ) ;
563+ summary . ready = summary . blockers . length === 0 && summary . state === "missing" && summary . files . conformant + summary . files . problems === 0 ;
546564 }
547565 return { summaries, details, unresolved, componentRefs, anyStale, items } ;
548566 }
549567
550568 /** One unit: its state file, its rules, its design documents, and what the checks say. Undefined without a unit file. */
551- private async scanUnit ( context : DocumentContext , commit : string , app : string , module : string , unit : string , project : ProjectSets , shared : Omit < DesignContext , "items" > , application : ApplicationScan ) : Promise < UnitScan | undefined > {
569+ private async scanUnit ( context : DocumentContext , commit : string , app : string , module : string , unit : string , project : ProjectSets , shared : Omit < DesignContext , "items" > , application : ApplicationScan , catalogItem ?: ItemCatalogItem ) : Promise < UnitScan | undefined > {
552570 const stateText = await this . documents . readText ( context , commit , unitStatePath ( app , module , unit ) , MAX_DESIGN_BYTES ) ;
553- const summary = stateText === undefined ? null : parseWorkItem ( stateText ) ;
554- if ( stateText === undefined || summary === null || summary . kind === undefined ) return undefined ;
555- const data = documentOf ( stateText ) . data ;
571+ const summary = stateText === undefined ? ( catalogItem === undefined ? null : { kind : catalogItem . kind , title : catalogItem . name , assignee : undefined } ) : parseWorkItem ( stateText ) ;
572+ if ( summary === null || summary . kind === undefined ) return undefined ;
573+ const data = documentOf ( stateText ?? "" ) . data ;
556574 const waived = new Map < string , string > ( ) ;
557575 for ( const entry of Array . isArray ( data [ "waived" ] ) ? data [ "waived" ] : [ ] ) {
558576 const waiver = typeof entry === "string" ? parseDesignWaiver ( entry ) : undefined ;
559577 if ( waiver !== undefined ) waived . set ( `${ waiver . set } /${ waiver . file } ` , waiver . reason ) ;
560578 }
561579 const rulesText = await this . documents . readText ( context , commit , unitRulesPath ( app , module , unit ) , MAX_RULES_BYTES ) ;
562580 const rules = rulesText === undefined ? [ ] : ruleCardsOf ( rulesText ) ;
581+ const [ specification , understanding , review ] = await Promise . all ( [
582+ this . documents . readText ( context , commit , unitAsIsPath ( app , module , unit ) , MAX_DESIGN_BYTES ) ,
583+ this . documents . readText ( context , commit , unitUnderstandingPath ( app , module , unit ) , MAX_DESIGN_BYTES ) ,
584+ this . documents . readText ( context , commit , unitRulesReviewPath ( app , module , unit ) , MAX_RULES_BYTES ) ,
585+ ] ) ;
586+ const evidenceBlockers = designInputBlockers ( { specification, understanding, rules : rulesText , review, catalogDigest : catalogItem ?. digest } ) ;
563587 const kind = summary . kind ;
564588 const applicable = project . sets . filter ( ( loaded ) => designFilesFor ( loaded , kind ) . length > 0 ) ;
565589
@@ -621,11 +645,15 @@ export class DesignService {
621645 }
622646 for ( const detail of details ) {
623647 const summary = sets [ detail . set ] as UnitSetSummary ;
624- const needsMet = detail . needs . every ( ( need ) => {
648+ summary . draftAllowed = catalogItem !== undefined ;
649+ summary . blockers = [ ...evidenceBlockers , ...detail . needs . filter ( ( need ) => {
625650 const target = sets [ need ] ?? application . summaries [ need ] ;
626- return target === undefined || finished ( target . state ) ;
627- } ) ;
628- summary . ready = needsMet && summary . state === "missing" && summary . files . conformant + summary . files . problems === 0 ;
651+ // A unit dependency deliberately inapplicable to this kind has nothing to generate.
652+ const definition = project . sets . find ( ( loaded ) => loaded . set . id === need ) ;
653+ const inapplicable = definition !== undefined && definition . set . scope !== "application" && designFilesFor ( definition , kind ) . length === 0 ;
654+ return ! inapplicable && ( target === undefined || ! finished ( target . state ) ) ;
655+ } ) . map ( ( need ) => `Needs the merged, conformant ${ need } design first.` ) ] ;
656+ summary . ready = summary . blockers . length === 0 && summary . state === "missing" && summary . files . conformant + summary . files . problems === 0 ;
629657 }
630658 const coverage : UnitCoverageRow [ ] = rules
631659 . map ( ( rule ) => ( { rule : rule . id , title : rule . title , coveredBy : coveredBy . get ( rule . id ) ?? [ ] } ) )
0 commit comments