@neocompose/cli 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@
7
7
  - Document the final relation-only world layer-link contract and keep the
8
8
  format-4 Hello World corpus free of the retired value-side binding field.
9
9
 
10
+ ## [0.10.0] - 2026-07-25
11
+
12
+ ### Added
13
+
14
+ - Declare List indexes and column settings in source with `@index(member: ...)`
15
+ and `@column(member: ...)`. Both were already in the schema contract and
16
+ offered by the language server, but lowering carried them from the manifest
17
+ cache and the emitter never wrote them back, so a workspace that pushed
18
+ without `neo/obj/` silently lost them.
19
+ - Declare a class hidden from the member selector and classes tree with
20
+ `@hidden`, the class-level counterpart to `@locked`. It maps to
21
+ `NeoSchemaClassAttribute.Hidden`, which the contract already carried; absent
22
+ means visible.
23
+
10
24
  ## [0.9.4] - 2026-07-25
11
25
 
12
26
  ### Fixed
package/README.md CHANGED
@@ -141,8 +141,23 @@ abstract class InventoryItem<
141
141
  The source identifier is the persisted name. `@settings` and `@storage` are
142
142
  typed by their declaration context; invalid fields and enum cases are
143
143
  diagnostics. Swift-like `.EnumCase` syntax works whenever the expected enum
144
- type is known. Focused annotations such as `@id`, `@locked`, `@system`, and
145
- `@relations` carry cross-cutting behavior.
144
+ type is known. Focused annotations such as `@id`, `@locked`, `@hidden`,
145
+ `@system`, and `@relations` carry cross-cutting behavior. `@hidden` marks a
146
+ class the member selector and classes tree leave out; absent means visible.
147
+
148
+ Lists take repeatable `@index` and `@column` annotations naming a field on the
149
+ entry class. An index requires a non-localizable `string` or a single-select
150
+ enum; a column may also target the reserved `__other__` key, which carries the
151
+ default layout for every field a subclass adds.
152
+
153
+ ```neo
154
+ @id("outpost-list-id")
155
+ @settings(kind: .Unordered)
156
+ @index(member: Slug, unique: true)
157
+ @column(member: Slug, width: 158, frozen: true)
158
+ @column(member: Notes, hidden: true)
159
+ public List<Outpost> Outposts = [];
160
+ ```
146
161
 
147
162
  Stable IDs preserve identity across rename, reorder, and file moves. Omit an
148
163
  `@id` only to create something. A successful real push assigns the ID and
package/dist/neo.mjs CHANGED
@@ -14285,6 +14285,9 @@ function validateMember(uri, member, documentKind, ownerScope, environment, diag
14285
14285
  diagnostics
14286
14286
  );
14287
14287
  validateTypeAnnotations(uri, member.type, environment, diagnostics);
14288
+ if (documentKind !== "flow") {
14289
+ validateListAnnotations(uri, member, environment, diagnostics);
14290
+ }
14288
14291
  const scope = new Map(ownerScope);
14289
14292
  if (member.kind === "function") {
14290
14293
  for (const parameter3 of member.parameters) {
@@ -14435,6 +14438,9 @@ function validateAnnotations(uri, annotations, context, environment, diagnostics
14435
14438
  continue;
14436
14439
  }
14437
14440
  seen.add(field.name);
14441
+ if (field.name === "member" && (annotation2.name === "index" || annotation2.name === "column")) {
14442
+ continue;
14443
+ }
14438
14444
  validateExpressionText(
14439
14445
  uri,
14440
14446
  argument2.text,
@@ -14458,6 +14464,7 @@ function fieldsForAnnotation(name, context, environment) {
14458
14464
  if (name === "locked") {
14459
14465
  return context.kind === "member" || context.kind === "flowBinding" ? [] : null;
14460
14466
  }
14467
+ if (name === "hidden") return context.kind === "class" ? [] : null;
14461
14468
  if (name === "settings") return settingsFields(context);
14462
14469
  if (name === "storage") return storageFields(context);
14463
14470
  if (name === "relations") return relationFields(context, environment);
@@ -15489,6 +15496,121 @@ function contextualAnnotationEnum(annotations, name, field) {
15489
15496
  const value = annotations.find((annotation2) => annotation2.name === name)?.arguments.find((argument2) => argument2.name === field)?.text.trim();
15490
15497
  return value?.startsWith(".") ? value.slice(1) : null;
15491
15498
  }
15499
+ function validateListAnnotations(uri, member, environment, diagnostics) {
15500
+ const declared = member.annotations.filter(
15501
+ (annotation2) => annotation2.name === "index" || annotation2.name === "column"
15502
+ );
15503
+ if (declared.length === 0) return;
15504
+ const entryClass = listEntryClass(member.type, environment);
15505
+ const seen = /* @__PURE__ */ new Set();
15506
+ for (const annotation2 of declared) {
15507
+ const target = memberKeyArgument(annotation2);
15508
+ if (target === null) {
15509
+ pushDiagnostic(
15510
+ diagnostics,
15511
+ uri,
15512
+ annotation2.range,
15513
+ "missing-list-annotation-member",
15514
+ `@${annotation2.name} on '${member.name}' requires a member naming a field on the list entry, such as @${annotation2.name}(member: Name).`
15515
+ );
15516
+ continue;
15517
+ }
15518
+ if (target.key === null) {
15519
+ pushDiagnostic(
15520
+ diagnostics,
15521
+ uri,
15522
+ target.range,
15523
+ "invalid-list-annotation-member",
15524
+ `@${annotation2.name} on '${member.name}' must name a list entry field directly, such as @${annotation2.name}(member: Name).`
15525
+ );
15526
+ continue;
15527
+ }
15528
+ if (seen.has(`${annotation2.name} ${target.key}`)) {
15529
+ pushDiagnostic(
15530
+ diagnostics,
15531
+ uri,
15532
+ target.range,
15533
+ "duplicate-list-annotation",
15534
+ `@${annotation2.name}(member: ${target.key}) is declared more than once on '${member.name}'.`
15535
+ );
15536
+ continue;
15537
+ }
15538
+ seen.add(`${annotation2.name} ${target.key}`);
15539
+ if (annotation2.name === "column" && target.key === LIST_COLUMN_INHERITANCE_KEY) {
15540
+ continue;
15541
+ }
15542
+ if (entryClass === null) continue;
15543
+ const field = resolveEntryField(entryClass, target.key, environment);
15544
+ if (field === null) {
15545
+ pushDiagnostic(
15546
+ diagnostics,
15547
+ uri,
15548
+ target.range,
15549
+ "unresolved-list-annotation-member",
15550
+ `@${annotation2.name}(member: ${target.key}) on '${member.name}' does not resolve on list entry class '${entryClass.name}'.`
15551
+ );
15552
+ continue;
15553
+ }
15554
+ if (annotation2.name !== "index") continue;
15555
+ const domainError = indexKeyDomainError(field, environment);
15556
+ if (domainError !== null) {
15557
+ pushDiagnostic(
15558
+ diagnostics,
15559
+ uri,
15560
+ target.range,
15561
+ "invalid-list-index-member",
15562
+ `@index(member: ${target.key}) on '${member.name}' is invalid: ${domainError}.`
15563
+ );
15564
+ }
15565
+ }
15566
+ }
15567
+ function memberKeyArgument(annotation2) {
15568
+ const positional = annotation2.arguments[0];
15569
+ const argument2 = annotation2.arguments.find((entry) => entry.name === "member") ?? (positional !== void 0 && positional.name === void 0 ? positional : void 0);
15570
+ if (argument2 === void 0) return null;
15571
+ const text = argument2.text.trim();
15572
+ return {
15573
+ key: IDENTIFIER_PATTERN2.test(text) ? text : null,
15574
+ range: argument2.range
15575
+ };
15576
+ }
15577
+ function listEntryClass(type, environment) {
15578
+ if (type.name !== "List" && type.name !== "Set") return null;
15579
+ const entry = type.typeArguments[0];
15580
+ if (entry === void 0) return null;
15581
+ return environment.types.get(entry.name) ?? null;
15582
+ }
15583
+ function resolveEntryField(entryClass, key, environment) {
15584
+ const visited = /* @__PURE__ */ new Set();
15585
+ let current = entryClass;
15586
+ while (current !== void 0 && !visited.has(current.name)) {
15587
+ visited.add(current.name);
15588
+ const match = current.members.find((entry) => entry.name === key);
15589
+ if (match !== void 0) return match;
15590
+ current = current.baseTypes.map((base) => environment.types.get(base.name)).find((base) => base !== void 0);
15591
+ }
15592
+ return null;
15593
+ }
15594
+ function indexKeyDomainError(field, environment) {
15595
+ if (field.kind !== "field") {
15596
+ return `'${field.name}' is a ${field.kind}, and index keys require a stored field`;
15597
+ }
15598
+ if (field.type.name === "string") {
15599
+ return annotationBoolean(field.annotations, "settings", "localizable") === false ? null : `'${field.name}' is localizable, and String index keys must be non-localizable`;
15600
+ }
15601
+ if (environment.enums.has(field.type.name)) return null;
15602
+ const entry = field.type.typeArguments[0];
15603
+ if (field.type.name === "List" && entry !== void 0 && environment.enums.has(entry.name) && annotationBoolean(field.annotations, "settings", "multiselect") === true) {
15604
+ return `'${field.name}' is a multiselect Enum, and index keys require a single-select Enum`;
15605
+ }
15606
+ return `'${field.name}' is a ${field.type.name} field, and index keys require a non-localizable String or a single-select Enum`;
15607
+ }
15608
+ function annotationBoolean(annotations, name, field) {
15609
+ const value = annotations.find((annotation2) => annotation2.name === name)?.arguments.find((argument2) => argument2.name === field)?.text.trim();
15610
+ if (value === "true") return true;
15611
+ if (value === "false") return false;
15612
+ return null;
15613
+ }
15492
15614
  function isListContext(context) {
15493
15615
  return (context.kind === "member" || context.kind === "type") && (context.type.name === "List" || context.type.name === "Set");
15494
15616
  }
@@ -15533,7 +15655,7 @@ function pushDiagnostic(diagnostics, uri, range2, code, message) {
15533
15655
  message
15534
15656
  });
15535
15657
  }
15536
- var primitiveType;
15658
+ var primitiveType, IDENTIFIER_PATTERN2, LIST_COLUMN_INHERITANCE_KEY;
15537
15659
  var init_project_source_semantics = __esm({
15538
15660
  "../packages/neoscript-language/src/project-source-semantics.ts"() {
15539
15661
  "use strict";
@@ -15542,6 +15664,8 @@ var init_project_source_semantics = __esm({
15542
15664
  init_strict_parser();
15543
15665
  init_project_root();
15544
15666
  primitiveType = (name) => ({ name });
15667
+ IDENTIFIER_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
15668
+ LIST_COLUMN_INHERITANCE_KEY = "__other__";
15545
15669
  }
15546
15670
  });
15547
15671
 
@@ -21308,6 +21432,16 @@ function annotationSignatureParameters(name) {
21308
21432
  return ["relation descriptors"];
21309
21433
  case "incomplete":
21310
21434
  return ["string reason"];
21435
+ case "index":
21436
+ return ["entry member", "bool unique"];
21437
+ case "column":
21438
+ return [
21439
+ "entry member",
21440
+ "int width",
21441
+ "bool hidden",
21442
+ "bool frozen",
21443
+ "bool wrapContent"
21444
+ ];
21311
21445
  default:
21312
21446
  return null;
21313
21447
  }
@@ -28783,7 +28917,7 @@ function lowerClass(context, declaration) {
28783
28917
  schemaKeyOrder: persistedSchemaOrder,
28784
28918
  extendsClassId,
28785
28919
  implementsInterfaceIds,
28786
- hiddenInMemberSelector: base?.hiddenInMemberSelector ?? false,
28920
+ hiddenInMemberSelector: hasAnnotation(declaration.annotations, "hidden"),
28787
28921
  system: systemMetadata(declaration.annotations),
28788
28922
  allowedStorage: storage ? storageValue(storage, "allowed") : null,
28789
28923
  // Class storage keys never exist in source. Retain only the historical
@@ -28800,6 +28934,11 @@ function lowerClass(context, declaration) {
28800
28934
  };
28801
28935
  }
28802
28936
  function lowerMember(context, ownerClass, declaration, id2, owner) {
28937
+ const member = lowerMemberKind(context, ownerClass, declaration, id2, owner);
28938
+ assertListAnnotationPlacement(ownerClass, declaration, member.kind);
28939
+ return member;
28940
+ }
28941
+ function lowerMemberKind(context, ownerClass, declaration, id2, owner) {
28803
28942
  const base = context.baseMembers.get(id2);
28804
28943
  const common = memberCommon(
28805
28944
  context,
@@ -29139,8 +29278,8 @@ function lowerFieldMember(context, ownerClass, declaration, id2, commonInput, ba
29139
29278
  kind: "list",
29140
29279
  entryMemberId: entryId,
29141
29280
  listKind: contextualEnumArgument(settings, "kind") === "Unordered" ? "unordered" : "ordered",
29142
- indexes: base?.kind === "list" ? base.indexes : [],
29143
- columns: base?.kind === "list" ? base.columns : []
29281
+ indexes: lowerListIndexes(ownerClass, declaration),
29282
+ columns: lowerListColumns(ownerClass, declaration)
29144
29283
  };
29145
29284
  }
29146
29285
  if (name === "Dictionary") {
@@ -29210,6 +29349,70 @@ function lowerFieldMember(context, ownerClass, declaration, id2, commonInput, ba
29210
29349
  `Unsupported field type ${JSON.stringify(name)} on ${ownerClass.name}.${declaration.name}.`
29211
29350
  );
29212
29351
  }
29352
+ function lowerListIndexes(ownerClass, declaration) {
29353
+ const declared = /* @__PURE__ */ new Set();
29354
+ return declaration.annotations.filter((entry) => entry.name === "index").map((entry) => {
29355
+ const schemaKey = memberKeyArgument2(entry, ownerClass, declaration);
29356
+ if (declared.has(schemaKey)) {
29357
+ throw new Error(
29358
+ `@index(member: ${schemaKey}) is declared more than once on ${ownerClass.name}.${declaration.name}.`
29359
+ );
29360
+ }
29361
+ declared.add(schemaKey);
29362
+ return { schemaKey, unique: booleanArgument(entry, "unique") ?? false };
29363
+ });
29364
+ }
29365
+ function lowerListColumns(ownerClass, declaration) {
29366
+ const declared = /* @__PURE__ */ new Set();
29367
+ return declaration.annotations.filter((entry) => entry.name === "column").map((entry) => {
29368
+ const memberKey = memberKeyArgument2(entry, ownerClass, declaration);
29369
+ if (declared.has(memberKey)) {
29370
+ throw new Error(
29371
+ `@column(member: ${memberKey}) is declared more than once on ${ownerClass.name}.${declaration.name}.`
29372
+ );
29373
+ }
29374
+ declared.add(memberKey);
29375
+ const width = numberArgument(entry, "width");
29376
+ return {
29377
+ memberKey,
29378
+ // `-1` is the generated C# marker's "unset" sentinel for an int
29379
+ // property that has no nullable form; the record spells that as null.
29380
+ width: width === null || width === UNSET_LIST_COLUMN_WIDTH ? null : width,
29381
+ hidden: booleanArgument(entry, "hidden") ?? false,
29382
+ frozen: booleanArgument(entry, "frozen") ?? false,
29383
+ wrapContent: booleanArgument(entry, "wrapContent") ?? false
29384
+ };
29385
+ });
29386
+ }
29387
+ function memberKeyArgument2(value, ownerClass, declaration) {
29388
+ const positional = value.arguments[0];
29389
+ const raw = argument(value, "member") ?? (positional !== void 0 && positional.name === void 0 ? positional.expression : null);
29390
+ if (raw === null) {
29391
+ throw new Error(
29392
+ `@${value.name} on ${ownerClass.name}.${declaration.name} requires a member naming a field on the list entry, such as @${value.name}(member: Name).`
29393
+ );
29394
+ }
29395
+ const expression = parseAnnotationExpression(raw);
29396
+ if (expression.kind !== "ident") {
29397
+ throw new Error(
29398
+ `@${value.name}(member: ${raw.trim()}) on ${ownerClass.name}.${declaration.name} must name a list entry field directly, such as @${value.name}(member: Name).`
29399
+ );
29400
+ }
29401
+ return expression.name;
29402
+ }
29403
+ function assertListAnnotationPlacement(ownerClass, declaration, loweredKind) {
29404
+ if (loweredKind === "list") return;
29405
+ if (hasAnnotation(declaration.annotations, "index")) {
29406
+ throw new Error(
29407
+ `@index on ${ownerClass.name}.${declaration.name} is only valid on a List member; this declaration lowers to a ${loweredKind} member.`
29408
+ );
29409
+ }
29410
+ if (hasAnnotation(declaration.annotations, "column")) {
29411
+ throw new Error(
29412
+ `@column on ${ownerClass.name}.${declaration.name} is only valid on a List member; this declaration lowers to a ${loweredKind} member.`
29413
+ );
29414
+ }
29415
+ }
29213
29416
  function lowerCollectionEntry(context, ownerClass, entryType, entryId, parentMemberId) {
29214
29417
  if (context.loweredMembers.has(entryId)) return;
29215
29418
  const base = context.baseMembers.get(entryId);
@@ -30041,13 +30244,14 @@ function* zip(left, right) {
30041
30244
  yield [left[index], right[index]];
30042
30245
  }
30043
30246
  }
30044
- var primitiveMemberKinds;
30247
+ var UNSET_LIST_COLUMN_WIDTH, primitiveMemberKinds;
30045
30248
  var init_lower_members = __esm({
30046
30249
  "src/project-source/lower-members.ts"() {
30047
30250
  "use strict";
30048
30251
  init_src();
30049
30252
  init_lower_support();
30050
30253
  init_ui_action_source();
30254
+ UNSET_LIST_COLUMN_WIDTH = -1;
30051
30255
  primitiveMemberKinds = /* @__PURE__ */ new Set([
30052
30256
  "null",
30053
30257
  "bool",
@@ -30752,6 +30956,7 @@ ${namedArguments(
30752
30956
  function emitClass(context, schemaClass2, memberIds) {
30753
30957
  const annotations = [
30754
30958
  id(schemaClass2.id).trimEnd(),
30959
+ ...schemaClass2.hiddenInMemberSelector ? ["@hidden"] : [],
30755
30960
  ...schemaClass2.allowedStorage ? [`@storage(allowed: .${enumCase(schemaClass2.allowedStorage)})`] : [],
30756
30961
  ...schemaClass2.system ? [systemAnnotation(schemaClass2.system)] : [],
30757
30962
  ...relationsAnnotations(context, schemaClass2)
@@ -30883,6 +31088,7 @@ function emitMember(context, member) {
30883
31088
  ].join(", ")})`
30884
31089
  ] : [],
30885
31090
  ...memberSettings(context, member),
31091
+ ...listAnnotations(member),
30886
31092
  ...member.system ? [systemAnnotation(member.system)] : []
30887
31093
  ];
30888
31094
  const prefix = memberModifier3(member);
@@ -31024,6 +31230,28 @@ function memberSettings(context, member) {
31024
31230
  }
31025
31231
  return settings.length ? [`@settings(${settings.join(", ")})`] : [];
31026
31232
  }
31233
+ function listAnnotations(member) {
31234
+ if (member.kind !== "list") return [];
31235
+ return [
31236
+ ...member.indexes.map((entry) => {
31237
+ assertIdentifier(entry.schemaKey, "list index member");
31238
+ return `@index(${[
31239
+ `member: ${entry.schemaKey}`,
31240
+ ...entry.unique ? ["unique: true"] : []
31241
+ ].join(", ")})`;
31242
+ }),
31243
+ ...member.columns.map((entry) => {
31244
+ assertIdentifier(entry.memberKey, "list column member");
31245
+ return `@column(${[
31246
+ `member: ${entry.memberKey}`,
31247
+ ...entry.width === null ? [] : [`width: ${entry.width}`],
31248
+ ...entry.hidden ? ["hidden: true"] : [],
31249
+ ...entry.frozen ? ["frozen: true"] : [],
31250
+ ...entry.wrapContent ? ["wrapContent: true"] : []
31251
+ ].join(", ")})`;
31252
+ })
31253
+ ];
31254
+ }
31027
31255
  function renderMemberType(context, member) {
31028
31256
  const nullable = !member.required;
31029
31257
  let value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.9.4",
3
+ "version": "0.10.0",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -196,8 +196,14 @@ abstract class InventoryItem<
196
196
  - `@settings(...)` and `@storage(...)` are context-aware typed contracts.
197
197
  Numeric limits are numeric literals. A class supports only
198
198
  `@storage(allowed: ...)`; storage keys are member-only.
199
- - Keep focused annotations such as `@locked`, `@system`, and `@relations`
200
- separate from contextual settings.
199
+ - Keep focused annotations such as `@locked`, `@hidden`, `@system`, and
200
+ `@relations` separate from contextual settings. `@hidden` is class-only and
201
+ hides the class from the member selector and classes tree; absent is visible.
202
+ - `@index(member: Field, unique: ...)` and
203
+ `@column(member: Field, width: ..., hidden: ..., frozen: ..., wrapContent: ...)`
204
+ are repeatable List annotations. `member:` is a bare field name on the entry
205
+ class — an index needs a non-localizable `string` or a single-select enum,
206
+ and a column may also target the reserved `__other__` key.
201
207
  - `.EnumCase` works only when one expected enum type is known. Use explicit
202
208
  `EnumType.Case` when context is ambiguous.
203
209
  - Defaults and settings must be statically analyzable. Project source is parsed