@neocompose/cli 0.25.1 → 0.25.2

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
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.2] - 2026-08-09
4
+
5
+ ### Added
6
+
7
+ - `neo doctor` now audits the pulled document itself, not just this working
8
+ copy's authoring contracts. A project can be perfectly clean to push and
9
+ still hold malformed records, and the first audit catches value rows whose
10
+ storage partition disagrees with their placement parent. `--json` carries a
11
+ `repair` entry per finding, which is what lets the matching repair touch only
12
+ the affected records instead of scanning a project to rediscover them.
13
+
3
14
  ## [0.25.1] - 2026-08-09
4
15
 
5
16
  ### Changed
package/dist/neo.mjs CHANGED
@@ -96381,6 +96381,181 @@ var init_scaffold = __esm({
96381
96381
  }
96382
96382
  });
96383
96383
 
96384
+ // src/project-source/project-integrity.ts
96385
+ function auditProjectIntegrity(workspace) {
96386
+ const records2 = workspace.state.records;
96387
+ const values = collectValueRows(records2);
96388
+ const placements = collectPlacements(values);
96389
+ const memberIdBySchemaKey = collectClassSchemas(records2);
96390
+ const storageKeyByMemberId = collectMemberStorageKeys(records2);
96391
+ const findings = [];
96392
+ for (const value of values.values()) {
96393
+ const placement = placements.get(value.id);
96394
+ if (placement === void 0) continue;
96395
+ const declaration = childStorageKeyDeclaration({
96396
+ placement,
96397
+ memberIdBySchemaKey,
96398
+ storageKeyByMemberId
96399
+ });
96400
+ if (declaration === null) continue;
96401
+ const expected = resolveMapKeyForCreatedValue({
96402
+ declaration,
96403
+ parentMapKey: placement.parent.mapKey,
96404
+ parentClassId: placement.parent.classId ?? void 0
96405
+ });
96406
+ const normalizedExpected = normalizePartition(expected);
96407
+ const normalizedActual = normalizePartition(value.mapKey);
96408
+ if (normalizedExpected === normalizedActual) continue;
96409
+ findings.push({
96410
+ kind: "value-storage-partition",
96411
+ recordKind: "value",
96412
+ recordId: value.id,
96413
+ message: `Value "${value.id}" is stored in partition ${describePartition(normalizedActual)} but its placement parent "${placement.parent.id}" puts it in ${describePartition(normalizedExpected)}.`,
96414
+ repair: {
96415
+ valueId: value.id,
96416
+ parentValueId: placement.parent.id,
96417
+ schemaKey: placement.schemaKey,
96418
+ actualMapKey: normalizedActual,
96419
+ expectedMapKey: normalizedExpected
96420
+ }
96421
+ });
96422
+ }
96423
+ return findings.sort(
96424
+ (left, right) => left.recordId.localeCompare(right.recordId)
96425
+ );
96426
+ }
96427
+ function childStorageKeyDeclaration(args) {
96428
+ const { placement } = args;
96429
+ if (placement.schemaKey === UNORDERED_MEMBERSHIP_KEY) {
96430
+ return normalizeStorageKeyDeclaration(void 0);
96431
+ }
96432
+ const parentClassId = placement.parent.classId;
96433
+ if (parentClassId === null) return null;
96434
+ const memberId = args.memberIdBySchemaKey.get(parentClassId)?.get(placement.schemaKey);
96435
+ if (memberId === void 0) return null;
96436
+ const declaration = args.storageKeyByMemberId.get(memberId);
96437
+ if (declaration === void 0) return null;
96438
+ return declaration;
96439
+ }
96440
+ function collectValueRows(records2) {
96441
+ const values = /* @__PURE__ */ new Map();
96442
+ for (const record3 of Object.values(records2)) {
96443
+ if (record3.recordKind !== "value") continue;
96444
+ const data = record3.data;
96445
+ if (!isObjectRecord2(data)) continue;
96446
+ values.set(record3.recordId, {
96447
+ id: record3.recordId,
96448
+ classId: typeof data.classId === "string" ? data.classId : null,
96449
+ mapKey: typeof data.mapKey === "string" ? data.mapKey : null,
96450
+ containerId: typeof data.containerId === "string" ? data.containerId : null,
96451
+ value: data.value
96452
+ });
96453
+ }
96454
+ return values;
96455
+ }
96456
+ function collectPlacements(values) {
96457
+ const placements = /* @__PURE__ */ new Map();
96458
+ const claim = (childId, placement) => {
96459
+ if (values.has(childId) && !placements.has(childId)) {
96460
+ placements.set(childId, placement);
96461
+ }
96462
+ };
96463
+ for (const parent of values.values()) {
96464
+ const body = parent.value;
96465
+ if (isObjectRecord2(body)) {
96466
+ for (const [schemaKey, childId] of Object.entries(body)) {
96467
+ if (typeof childId === "string") claim(childId, { parent, schemaKey });
96468
+ }
96469
+ continue;
96470
+ }
96471
+ if (!Array.isArray(body)) continue;
96472
+ for (const childId of body) {
96473
+ if (typeof childId === "string") {
96474
+ claim(childId, { parent, schemaKey: UNORDERED_MEMBERSHIP_KEY });
96475
+ }
96476
+ }
96477
+ }
96478
+ for (const value of values.values()) {
96479
+ if (value.containerId === null) continue;
96480
+ const container = values.get(value.containerId);
96481
+ if (container === void 0) continue;
96482
+ claim(value.id, { parent: container, schemaKey: UNORDERED_MEMBERSHIP_KEY });
96483
+ }
96484
+ return placements;
96485
+ }
96486
+ function collectClassSchemas(records2) {
96487
+ const schemas = /* @__PURE__ */ new Map();
96488
+ for (const record3 of Object.values(records2)) {
96489
+ if (record3.recordKind !== "class") continue;
96490
+ const data = record3.data;
96491
+ if (!isObjectRecord2(data) || !isObjectRecord2(data.schema)) continue;
96492
+ const schema = /* @__PURE__ */ new Map();
96493
+ for (const [schemaKey, memberId] of Object.entries(data.schema)) {
96494
+ if (typeof memberId === "string") schema.set(schemaKey, memberId);
96495
+ }
96496
+ schemas.set(record3.recordId, schema);
96497
+ }
96498
+ for (const record3 of Object.values(records2)) {
96499
+ if (record3.recordKind !== "class") continue;
96500
+ const own = schemas.get(record3.recordId);
96501
+ if (own === void 0) continue;
96502
+ const seen = /* @__PURE__ */ new Set([record3.recordId]);
96503
+ let data = record3.data;
96504
+ while (isObjectRecord2(data) && typeof data.extendsClassId === "string") {
96505
+ const parentId = data.extendsClassId;
96506
+ if (seen.has(parentId)) break;
96507
+ seen.add(parentId);
96508
+ for (const [schemaKey, memberId] of schemas.get(parentId) ?? []) {
96509
+ if (!own.has(schemaKey)) own.set(schemaKey, memberId);
96510
+ }
96511
+ const parentRecord = records2[`class:${parentId}`];
96512
+ data = parentRecord?.data;
96513
+ }
96514
+ }
96515
+ return schemas;
96516
+ }
96517
+ function collectMemberStorageKeys(records2) {
96518
+ const declarations = /* @__PURE__ */ new Map();
96519
+ for (const record3 of Object.values(records2)) {
96520
+ if (record3.recordKind !== "member") continue;
96521
+ let data = record3.data;
96522
+ const seen = /* @__PURE__ */ new Set([record3.recordId]);
96523
+ let declaration;
96524
+ while (isObjectRecord2(data)) {
96525
+ if (typeof data.storageKey === "string" && data.storageKey.length > 0) {
96526
+ declaration = data.storageKey;
96527
+ break;
96528
+ }
96529
+ if (typeof data.extendsMemberId !== "string") break;
96530
+ const parentId = data.extendsMemberId;
96531
+ if (seen.has(parentId)) break;
96532
+ seen.add(parentId);
96533
+ data = records2[`member:${parentId}`]?.data;
96534
+ }
96535
+ declarations.set(
96536
+ record3.recordId,
96537
+ normalizeStorageKeyDeclaration(declaration)
96538
+ );
96539
+ }
96540
+ return declarations;
96541
+ }
96542
+ function normalizePartition(mapKey) {
96543
+ if (mapKey === null || mapKey.length === 0) return null;
96544
+ return mapKey === MAIN_STORAGE_PARTITION ? null : mapKey;
96545
+ }
96546
+ function describePartition(mapKey) {
96547
+ return mapKey === null ? `"${MAIN_STORAGE_PARTITION}"` : `"${mapKey}"`;
96548
+ }
96549
+ var UNORDERED_MEMBERSHIP_KEY;
96550
+ var init_project_integrity = __esm({
96551
+ "src/project-source/project-integrity.ts"() {
96552
+ "use strict";
96553
+ init_member_storage_key();
96554
+ init_projection();
96555
+ UNORDERED_MEMBERSHIP_KEY = "(container)";
96556
+ }
96557
+ });
96558
+
96384
96559
  // src/neoscript-language-adapter.ts
96385
96560
  function createCliNeoScriptContext(options) {
96386
96561
  const members = [...options.documents.members];
@@ -103262,7 +103437,7 @@ var init_registry2 = __esm({
103262
103437
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
103263
103438
  formatVersion: 3,
103264
103439
  contractVersion: "3.9",
103265
- cliVersion: "0.25.1",
103440
+ cliVersion: "0.25.2",
103266
103441
  projectFileUploadBatchSize: 32,
103267
103442
  documentRecords: {
103268
103443
  member: {
@@ -106044,8 +106219,10 @@ function inspectNeoDoctor(workspace) {
106044
106219
  errors: [],
106045
106220
  compatible: true
106046
106221
  };
106222
+ const findings = auditProjectIntegrity(workspace);
106047
106223
  return {
106048
106224
  ok: formatCompatible && compiler.compatible && extension.compatible && source.compatible && files.compatible,
106225
+ document: { findings, consistent: findings.length === 0 },
106049
106226
  format: {
106050
106227
  current: workspace.config.formatVersion,
106051
106228
  expected: CURRENT_FORMAT_VERSION,
@@ -106304,6 +106481,12 @@ async function runDoctor(workspace, json) {
106304
106481
  console.log(
106305
106482
  ` Files: contract v${String(report.files.contractVersion)}, SHA-256, ${String(report.files.trackedBinaryCount)} tracked binar${report.files.trackedBinaryCount === 1 ? "y" : "ies"}, workspace ${report.files.workspaceReadable ? "readable" : "not readable"}/${report.files.workspaceWritable ? "writable" : "not writable"}`
106306
106483
  );
106484
+ console.log(
106485
+ ` Document: ${report.document.consistent ? "no malformed records" : `${String(report.document.findings.length)} malformed record${report.document.findings.length === 1 ? "" : "s"} (from the last pull)`}`
106486
+ );
106487
+ for (const finding of report.document.findings) {
106488
+ console.log(` Document defect (${finding.kind}): ${finding.message}`);
106489
+ }
106307
106490
  if (report.compiler.error)
106308
106491
  console.log(` Compiler error: ${report.compiler.error}`);
106309
106492
  if (report.extension.error)
@@ -106315,6 +106498,11 @@ async function runDoctor(workspace, json) {
106315
106498
  console.log(
106316
106499
  report.ok ? "Project authoring contracts are compatible." : "Project authoring contracts need attention."
106317
106500
  );
106501
+ if (!report.document.consistent) {
106502
+ console.log(
106503
+ `${String(report.document.findings.length)} malformed record${report.document.findings.length === 1 ? "" : "s"} in the pulled document \u2014 see \`--json\` for the repair entries.`
106504
+ );
106505
+ }
106318
106506
  if (!report.ok) process.exitCode = 1;
106319
106507
  }
106320
106508
  var NEO_COMPILER_CONTRACT, NEO_VSCODE_EXTENSION_ID, NEO_VSCODE_EXTENSION_CONTRACT_VERSION, NEO_SOURCE_CONTRACT_VERSION, NEO_FILE_CONTRACT_VERSION, SOURCE_EXTENSIONS, FILE_CAPABILITIES;
@@ -106326,6 +106514,7 @@ var init_doctor = __esm({
106326
106514
  init_workspace_status();
106327
106515
  init_source_diagnostics();
106328
106516
  init_project_documents();
106517
+ init_project_integrity();
106329
106518
  NEO_COMPILER_CONTRACT = "ProjectSourceAnalysisV4";
106330
106519
  NEO_VSCODE_EXTENSION_ID = "neocompose.neo-compose-neoscript";
106331
106520
  NEO_VSCODE_EXTENSION_CONTRACT_VERSION = "0.3.0";
@@ -109302,7 +109491,7 @@ ${h("Working copy")}
109302
109491
  test ${d("[file-or-dir ...] [-t <pattern>] [--reporter=json] [--outputFile <path>]")}
109303
109492
  status ${d("[--json]")} diff ${d("[--json]")}
109304
109493
  dev ${d("[--push]")} resolve ${d("[--mine|--theirs]")}
109305
- doctor ${d("[--json]")} ${d("validate format/compiler/editor/source/file contracts")}
109494
+ doctor ${d("[--json]")} ${d("validate contracts and audit the pulled document")}
109306
109495
 
109307
109496
  ${h("Pending source scaffolds")}
109308
109497
  class ${d("new <Name> [--abstract]")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.25.1 -->
12
+ <!-- reviewed-through-cli: 0.25.2 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -83,7 +83,7 @@ wrappers.
83
83
  The marker near the top of `SKILL.md` must exactly match the package version:
84
84
 
85
85
  ```html
86
- <!-- reviewed-through-cli: 0.25.1 -->
86
+ <!-- reviewed-through-cli: 0.25.2 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -146,6 +146,13 @@ neo login [--api <url>] [--profile editor|release] [--save-project <id>]
146
146
  neo doctor
147
147
  ```
148
148
 
149
+ `neo doctor` reports two different things. The authoring contracts (format,
150
+ compiler, extension cache, source, files) decide its exit code; the document
151
+ audit below them reports malformed records in the last pull and deliberately
152
+ does not, so a project with records still to repair does not fail the command.
153
+ `neo doctor --json` carries a `repair` entry per finding, which is what a
154
+ targeted repair is handed instead of scanning a project to rediscover them.
155
+
149
156
  The Node CLI talks directly to authenticated Convex APIs through the
150
157
  session-gated CAS boundary. Tokens use the OS credential store when available
151
158
  and a protected file only as fallback. Use `NEO_COMPOSE_TOKEN` or