@@ -59,7 +59,7 @@ import { validateObjectNamespacePrefix } from '@objectstack/spec/kernel';
5959import { singularToPlural } from '@objectstack/spec/shared' ;
6060import { printHeader , printSuccess , printError , printInfo , printStep , printWarning , createTimer , isReportedError , CLI_ALIAS } from '../utils/format.js' ;
6161import { metadataFileName } from '../utils/metadata-file-name.js' ;
62- import { readProjectNamespace , type ProjectNamespace } from '../utils/project-namespace.js' ;
62+ import { namespacedItemName , readProjectNamespace , type ProjectNamespace } from '../utils/project-namespace.js' ;
6363import { findEmissionParseFailures } from '../utils/emitted-source-parses.js' ;
6464import { findBarrelAliasRefusal } from '../utils/importable-binding.js' ;
6565import {
@@ -150,15 +150,19 @@ export interface ScaffoldBinds {
150150 * In a project whose manifest declares a `namespace`, that is
151151 * `${namespace}_${name}`, which is the name `defineStack`'s namespace-prefix
152152 * gate demands. `namesObject` says which generators write one, so that
153- * `runMetadataGeneration` reads the manifest only for those.
154- * `generate-object-namespace-prefix.test.ts` pins both the prefix and the
155- * flag against the templates.
153+ * `runMetadataGeneration` refuses for those when the config does not load:
154+ * with the namespace unknown, the object name it would write is one
155+ * `defineStack` may refuse. `generate-object-namespace-prefix.test.ts` pins
156+ * both the prefix and the flag against the templates.
156157 *
157- * Only object names are prefixed. The scaffold's own `name` on an action, a
158- * flow, a dashboard, an app, a skill or a picklist is not judged against the
159- * namespace by any gate `os validate` runs, so it stays the name the author
160- * typed. A view container writes no `name` at all: it is registered under
161- * the object it binds to (#21325, see the `view` generator).
158+ * Every other metadata name carries it too (ruling C on #30798). The
159+ * scaffold's own `name` on an action, a flow, a dashboard, an app, a skill, a
160+ * picklist, a permission set or a dataset goes through {@link itemNameFor}:
161+ * `${namespace}_${name}` in a namespaced project, unless the derived name
162+ * already carries the namespace. `os lint`'s `naming/namespace-prefix` warns
163+ * on a bare one, and `generate-scaffold-validates.test.ts` holds every
164+ * scaffold to zero findings. A view container writes no `name` at all: it is
165+ * registered under the object it binds to (#21325, see the `view` generator).
162166 *
163167 * ## A binding is read off the stack, never derived from the new item's name
164168 *
@@ -418,8 +422,8 @@ export default ${toCamelCase(name)}Views;
418422 */
419423 namesObject : true ,
420424 binds : { object : 'flag' , flow : 'flag' } ,
421- itemName : ( name : string ) => toSnakeCase ( name ) ,
422- generate : ( name : string , _namespace ?: string , bindings ?: ScaffoldBindings ) => {
425+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( toSnakeCase ( name ) , namespace ) ,
426+ generate : ( name : string , namespace ?: string , bindings ?: ScaffoldBindings ) => {
423427 const object = requireBinding ( 'action' , bindings , 'object' ) ;
424428 const flow = requireBinding ( 'action' , bindings , 'flow' ) ;
425429 return `import * as UI from '@objectstack/spec/ui';
@@ -428,7 +432,7 @@ export default ${toCamelCase(name)}Views;
428432 * ${ toTitleCase ( name ) } Action
429433 */
430434const ${ toCamelCase ( name ) } Action: UI.Action = {
431- name: '${ toSnakeCase ( name ) } ',
435+ name: '${ itemNameFor ( toSnakeCase ( name ) , namespace ) } ',
432436 label: '${ toTitleCase ( name ) } ',
433437 // Runs the flow named in \`target\` against the record it is invoked on.
434438 type: 'flow',
@@ -494,9 +498,9 @@ export default ${toCamelCase(name)}Action;
494498 */
495499 namesObject : true ,
496500 binds : { object : 'flag' } ,
497- itemName : ( name : string ) => `${ toSnakeCase ( name ) } _flow` ,
501+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( `${ toSnakeCase ( name ) } _flow` , namespace ) ,
498502 requires : FLOW_SCAFFOLD_REQUIRES ,
499- generate : ( name : string , _namespace ?: string , bindings ?: ScaffoldBindings ) => {
503+ generate : ( name : string , namespace ?: string , bindings ?: ScaffoldBindings ) => {
500504 const object = requireBinding ( 'flow' , bindings , 'object' ) ;
501505 return `import * as Automation from '@objectstack/spec/automation';
502506
@@ -509,7 +513,7 @@ export default ${toCamelCase(name)}Action;
509513 * not load.
510514 */
511515const ${ toCamelCase ( name ) } Flow: Automation.Flow = {
512- name: '${ toSnakeCase ( name ) } _flow',
516+ name: '${ itemNameFor ( ` ${ toSnakeCase ( name ) } _flow` , namespace ) } ',
513517 label: '${ toTitleCase ( name ) } Flow',
514518 type: 'record_change',
515519 // Armed: the trigger below fires it. Set 'obsolete' to switch it off.
@@ -550,14 +554,14 @@ export default ${toCamelCase(name)}Flow;
550554 description : 'Analytics dashboard' ,
551555 defaultDir : 'src/dashboards' ,
552556 namesObject : false ,
553- itemName : ( name : string ) => `${ toSnakeCase ( name ) } _dashboard` ,
554- generate : ( name : string ) => `import * as UI from '@objectstack/spec/ui';
557+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( `${ toSnakeCase ( name ) } _dashboard` , namespace ) ,
558+ generate : ( name : string , namespace ?: string ) => `import * as UI from '@objectstack/spec/ui';
555559
556560/**
557561 * ${ toTitleCase ( name ) } Dashboard
558562 */
559563const ${ toCamelCase ( name ) } Dashboard: UI.Dashboard = {
560- name: '${ toSnakeCase ( name ) } _dashboard',
564+ name: '${ itemNameFor ( ` ${ toSnakeCase ( name ) } _dashboard` , namespace ) } ',
561565 label: '${ toTitleCase ( name ) } Dashboard',
562566 widgets: [],
563567};
@@ -588,16 +592,16 @@ export default ${toCamelCase(name)}Dashboard;
588592 */
589593 namesObject : true ,
590594 binds : { object : 'flag' } ,
591- itemName : ( name : string ) => `${ toSnakeCase ( name ) } _app` ,
592- generate : ( name : string , _namespace ?: string , bindings ?: ScaffoldBindings ) => {
595+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( `${ toSnakeCase ( name ) } _app` , namespace ) ,
596+ generate : ( name : string , namespace ?: string , bindings ?: ScaffoldBindings ) => {
593597 const object = requireBinding ( 'app' , bindings , 'object' ) ;
594598 return `import * as UI from '@objectstack/spec/ui';
595599
596600/**
597601 * ${ toTitleCase ( name ) } App
598602 */
599603const ${ toCamelCase ( name ) } App: UI.App = {
600- name: '${ toSnakeCase ( name ) } _app',
604+ name: '${ itemNameFor ( ` ${ toSnakeCase ( name ) } _app` , namespace ) } ',
601605 label: '${ toTitleCase ( name ) } ',
602606 navigation: [
603607 {
@@ -638,8 +642,8 @@ export default ${toCamelCase(name)}App;
638642 * harness default.
639643 */
640644 namesObject : false ,
641- itemName : ( name : string ) => toSnakeCase ( name ) ,
642- generate : ( name : string ) => `import { defineSkill } from '@objectstack/spec/ai';
645+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( toSnakeCase ( name ) , namespace ) ,
646+ generate : ( name : string , namespace ?: string ) => `import { defineSkill } from '@objectstack/spec/ai';
643647
644648/**
645649 * ${ toTitleCase ( name ) } Skill
@@ -653,7 +657,7 @@ export default ${toCamelCase(name)}App;
653657 * a startup error naming the key, not a field that goes missing later.
654658 */
655659const ${ toCamelCase ( name ) } Skill = defineSkill({
656- name: '${ toSnakeCase ( name ) } ',
660+ name: '${ itemNameFor ( toSnakeCase ( name ) , namespace ) } ',
657661 label: '${ toTitleCase ( name ) } ',
658662 description: 'One line on what this skill is for — the model routes on it.',
659663
@@ -682,7 +686,7 @@ const ${toCamelCase(name)}Skill = defineSkill({
682686 // it (\`ai-skill-tool-unresolved\`), and at runtime the reference is dropped
683687 // while the instructions keep promising the capability.
684688 //
685- // tools: ['action_${ toSnakeCase ( name ) } ', 'query_records'],
689+ // tools: ['action_${ itemNameFor ( toSnakeCase ( name ) , namespace ) } ', 'query_records'],
686690 tools: [],
687691});
688692
@@ -720,18 +724,18 @@ export default ${toCamelCase(name)}Skill;
720724 * `FieldSchema` refuses the two together.
721725 */
722726 namesObject : false ,
723- itemName : ( name : string ) => toSnakeCase ( name ) ,
724- generate : ( name : string ) => `import { definePicklist } from '@objectstack/spec/data';
727+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( toSnakeCase ( name ) , namespace ) ,
728+ generate : ( name : string , namespace ?: string ) => `import { definePicklist } from '@objectstack/spec/data';
725729
726730/**
727731 * ${ toTitleCase ( name ) } Picklist
728732 *
729733 * A shared option list. A select field offers these options by naming the
730- * list — Field.select({ picklist: '${ toSnakeCase ( name ) } ' }) — and declares no
734+ * list — Field.select({ picklist: '${ itemNameFor ( toSnakeCase ( name ) , namespace ) } ' }) — and declares no
731735 * \`options\` of its own: a field declaring both is refused.
732736 */
733737const ${ toCamelCase ( name ) } Picklist = definePicklist({
734- name: '${ toSnakeCase ( name ) } ',
738+ name: '${ itemNameFor ( toSnakeCase ( name ) , namespace ) } ',
735739 label: '${ toTitleCase ( name ) } ',
736740 options: [
737741 { label: 'Option A', value: 'option_a' },
@@ -779,8 +783,8 @@ export default ${toCamelCase(name)}Picklist;
779783 */
780784 namesObject : true ,
781785 binds : { object : 'flag' } ,
782- itemName : ( name : string ) => toSnakeCase ( name ) ,
783- generate : ( name : string , _namespace ?: string , bindings ?: ScaffoldBindings ) => {
786+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( toSnakeCase ( name ) , namespace ) ,
787+ generate : ( name : string , namespace ?: string , bindings ?: ScaffoldBindings ) => {
784788 const object = requireBinding ( 'permission' , bindings , 'object' ) ;
785789 return `import { definePermissionSet } from '@objectstack/spec/security';
786790
@@ -793,7 +797,7 @@ export default ${toCamelCase(name)}Picklist;
793797 * by any one of them is granted.
794798 */
795799const ${ toCamelCase ( name ) } PermissionSet = definePermissionSet({
796- name: '${ toSnakeCase ( name ) } ',
800+ name: '${ itemNameFor ( toSnakeCase ( name ) , namespace ) } ',
797801 label: '${ toTitleCase ( name ) } ',
798802
799803 // One entry per object, keyed by its machine name. readScope is which of
@@ -841,8 +845,8 @@ export default ${toCamelCase(name)}PermissionSet;
841845 */
842846 namesObject : true ,
843847 binds : { object : 'flag' } ,
844- itemName : ( name : string ) => toSnakeCase ( name ) ,
845- generate : ( name : string , _namespace ?: string , bindings ?: ScaffoldBindings ) => {
848+ itemName : ( name : string , namespace ?: string ) => itemNameFor ( toSnakeCase ( name ) , namespace ) ,
849+ generate : ( name : string , namespace ?: string , bindings ?: ScaffoldBindings ) => {
846850 const object = requireBinding ( 'dataset' , bindings , 'object' ) ;
847851 const dimension = object . displayField
848852 ? `\n { name: ${ tsString ( object . displayField ) } , field: ${ tsString ( object . displayField ) } },\n `
@@ -857,7 +861,7 @@ export default ${toCamelCase(name)}PermissionSet;
857861 * name; it declares no object, field or aggregate of its own.
858862 */
859863const ${ toCamelCase ( name ) } Dataset = defineDataset({
860- name: '${ toSnakeCase ( name ) } ',
864+ name: '${ itemNameFor ( toSnakeCase ( name ) , namespace ) } ',
861865 label: '${ toTitleCase ( name ) } ',
862866 object: ${ tsString ( object . name ) } ,
863867
@@ -1093,6 +1097,19 @@ function prefixedObjectName(shortName: string, namespace?: string): string {
10931097 : `${ namespace } _${ shortName } ` ;
10941098}
10951099
1100+ /**
1101+ * The NON-object machine name a scaffold writes (ruling C on #30798): every
1102+ * metadata name a package authors carries its namespace, the way an object
1103+ * name does. `shortName` is the name as the generator derives it
1104+ * (`task_done_flow`); it gains `${namespace}_` unless it already carries the
1105+ * namespace — the predicate `os lint`'s `naming/namespace-prefix` judges with,
1106+ * so a scaffold is never written in a spelling the lint reports. With no
1107+ * namespace it is written as derived.
1108+ */
1109+ function itemNameFor ( shortName : string , namespace ?: string ) : string {
1110+ return namespacedItemName ( shortName , namespace ) ;
1111+ }
1112+
10961113/**
10971114 * A TypeScript single-quoted string literal for `value`. Bindings carry text
10981115 * read off the author's stack (an object's label), so it is escaped rather
@@ -1585,7 +1602,7 @@ async function runMetadataGeneration(
15851602 console . log ( chalk . dim (
15861603 // #20197: "always the same string" stopped being true for an object
15871604 // name in a namespaced project, which gains the manifest's prefix.
1588- ' write lands in the file unchanged (a namespaced project only prefixes an object name ).' ,
1605+ ' write lands in the file unchanged (a namespaced project only adds its namespace prefix ).' ,
15891606 ) ) ;
15901607 console . log ( chalk . dim (
15911608 // ⛔ The examples are deliberately NOT built from what the author
@@ -1603,15 +1620,17 @@ async function runMetadataGeneration(
16031620
16041621 // The project's config, loaded once before anything is written.
16051622 //
1606- // Its `manifest.namespace` is applied only by a generator that writes an
1607- // object machine name (#20197) — see `objectNameFor` — and only such a
1608- // generator refuses when the config does not load. Every generator reads
1609- // it (#20215), because whether the config loaded BEFORE this command wrote
1610- // anything is what the reach check below needs: a config that loaded then
1611- // and does not load once the scaffold is in place was broken by this
1612- // command, and the write is taken back out. A generator that names no
1613- // object still generates into a project whose config does not load, as it
1614- // always has.
1623+ // Its `manifest.namespace` prefixes an object machine name (#20197) — see
1624+ // `objectNameFor` — and every other metadata name a scaffold writes (ruling
1625+ // C on #30798) — see `itemNameFor`. Only a generator that writes an object
1626+ // name refuses when the config does not load, because `defineStack`
1627+ // refuses an unprefixed object; a bare non-object name is a lint warning,
1628+ // so the others still generate into such a project, as they always have,
1629+ // with the name as derived. Every generator reads it (#20215), because
1630+ // whether the config loaded BEFORE this command wrote anything is what the
1631+ // reach check below needs: a config that loaded then and does not load
1632+ // once the scaffold is in place was broken by this command, and the write
1633+ // is taken back out.
16151634 //
16161635 // BELOW the charset gate, because the prefix is a derivation and the
16171636 // #16726 position puts every derivation after that gate. ABOVE the render,
@@ -1644,8 +1663,8 @@ async function runMetadataGeneration(
16441663 console . log ( '' ) ;
16451664 process . exit ( 1 ) ;
16461665 }
1647- if ( project . kind === 'loaded' ) namespace = project . namespace ;
16481666 }
1667+ if ( project . kind === 'loaded' ) namespace = project . namespace ;
16491668
16501669 // The object name derived from what the author TYPED as this item's name:
16511670 // the `object` scaffold's own name, and a view's, which names the object
0 commit comments