@@ -132,31 +132,64 @@ afterAll(() => {
132132 fs . rmSync ( TMP_ROOT , { recursive : true , force : true } ) ;
133133} ) ;
134134
135- /** A legal, minimal host stack. Only the collection under test is populated. */
136- const hostStack = ( collection : string , artifact : unknown ) => ( {
135+ /**
136+ * A legal, minimal host stack: the collection under test, plus the objects a
137+ * binding scaffold needs present (see {@link boundObjects}).
138+ */
139+ const hostStack = ( collection : string , artifact : unknown , objects : readonly unknown [ ] = [ ] ) => ( {
137140 manifest : {
138141 id : 'com.example.scaffold' ,
139142 name : 'scaffold' ,
140143 version : '1.0.0' ,
141144 type : 'app' as const ,
142145 namespace : 'scaffold' ,
143146 } ,
147+ ...( objects . length > 0 ? { objects : [ ...objects ] } : { } ) ,
144148 [ collection ] : [ artifact ] ,
145149} ) ;
146150
151+ /** Materialize one scaffold through the loader `os validate` uses (see the header). */
152+ async function loadScaffold ( fileStem : string , source : string ) : Promise < unknown > {
153+ const file = path . join ( TMP_ROOT , `${ fileStem } .scaffold.ts` ) ;
154+ fs . writeFileSync ( file , source , 'utf8' ) ;
155+ const { mod } = await bundleRequire ( { filepath : file , external : BUNDLE_REQUIRE_EXTERNALS } ) ;
156+ return ( mod as { default ?: unknown } ) . default ?? mod ;
157+ }
158+
159+ /**
160+ * The object a BINDING scaffold names, as `os g object` writes it for the same
161+ * name — the precondition the author's own project supplies.
162+ *
163+ * A generator flagged `namesObject` (other than `object` itself) writes a
164+ * binding to `objectNameFor(STEM)`: a view container's `object`, an action's or
165+ * a flow start node's `objectName`, an app nav entry's `objectName`. The
166+ * templates are written to COMPOSE — `os g object NAME` then `os g view NAME`
167+ * — so each binding names exactly the object the object scaffold declares.
168+ * Validating a binding scaffold in a stack WITHOUT that object judged it against
169+ * an empty object set, and once the author-time rules resolved a view
170+ * container's `object` (`object-reference-unknown` at `views[0].object`), the
171+ * harness's own omission read as the scaffold's defect. So the object is
172+ * scaffolded here, through the same loader, and carried beside the artifact —
173+ * ⛔ never special-cased in a rule, and ⛔ never the scaffold under test edited
174+ * to fit the harness.
175+ */
176+ async function boundObjects ( type : string ) : Promise < unknown [ ] > {
177+ const target = GENERATOR_SCAFFOLD_TARGETS . find ( ( t ) => t . type === type ) ;
178+ if ( ! target ?. namesObject || type === 'object' ) return [ ] ;
179+ const objectTarget = GENERATOR_SCAFFOLD_TARGETS . find ( ( t ) => t . type === 'object' ) ;
180+ if ( ! objectTarget ) throw new Error ( 'the `object` generator must exist to seed a binding scaffold' ) ;
181+ return [ await loadScaffold ( 'bound-object' , objectTarget . generate ( STEM ) ) ] ;
182+ }
183+
147184/**
148185 * Load a scaffold the way `os validate` loads authored TypeScript, then run
149186 * the two steps `Validate.run()` runs on it.
150187 */
151188async function validateScaffold ( type : string , source : string ) {
152- const file = path . join ( TMP_ROOT , `${ type } .scaffold.ts` ) ;
153- fs . writeFileSync ( file , source , 'utf8' ) ;
154-
155- const { mod } = await bundleRequire ( { filepath : file , external : BUNDLE_REQUIRE_EXTERNALS } ) ;
156- const artifact = ( mod as { default ?: unknown } ) . default ?? mod ;
189+ const artifact = await loadScaffold ( type , source ) ;
157190
158191 const normalized = normalizeStackInput (
159- hostStack ( singularToPlural ( type ) , artifact ) as Record < string , unknown > ,
192+ hostStack ( singularToPlural ( type ) , artifact , await boundObjects ( type ) ) as Record < string , unknown > ,
160193 ) as Record < string , unknown > ;
161194
162195 const unknownKeys = [
@@ -203,6 +236,22 @@ describe('[#14087] every `os generate` scaffold passes `os validate`', () => {
203236 expect ( Object . keys ( KNOWN_UNVALIDATED_SCAFFOLDS ) ) . not . toContain ( 'flow' ) ;
204237 } ) ;
205238
239+ it ( 'a binding scaffold is judged beside the object `os g object` writes for the same name' , async ( ) => {
240+ // The precondition `boundObjects` supplies is only honest while the view's
241+ // binding and the object's name are the SAME spelling. Pinned directly, so
242+ // a template drifting one side of the pair turns this red rather than
243+ // quietly handing the view scaffold an object it does not bind.
244+ const [ object ] = ( await boundObjects ( 'view' ) ) as { name ?: unknown } [ ] ;
245+ const view = GENERATOR_SCAFFOLD_TARGETS . find ( ( t ) => t . type === 'view' ) ;
246+ expect ( view , 'the view generator must exist' ) . toBeDefined ( ) ;
247+ const container = ( await loadScaffold ( 'view-binding' , view ! . generate ( STEM ) ) ) as { object ?: unknown } ;
248+ expect ( object ?. name ) . toBe ( STEM ) ;
249+ expect ( container . object ) . toBe ( object ?. name ) ;
250+ // …and a non-binding generator is judged with no object carried at all.
251+ expect ( await boundObjects ( 'dashboard' ) ) . toEqual ( [ ] ) ;
252+ expect ( await boundObjects ( 'object' ) ) . toEqual ( [ ] ) ;
253+ } ) ;
254+
206255 const clean = GENERATOR_SCAFFOLD_TARGETS . filter ( ( t ) => ! ( t . type in KNOWN_UNVALIDATED_SCAFFOLDS ) ) ;
207256 const known = GENERATOR_SCAFFOLD_TARGETS . filter ( ( t ) => t . type in KNOWN_UNVALIDATED_SCAFFOLDS ) ;
208257
0 commit comments