@wcstack/typescript 1.32.0 → 2.0.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/dist/tsc-core.cjs CHANGED
@@ -44,7 +44,7 @@ function parseWcsScriptBlocks(html, stateTagName = "wcs-state") {
44
44
  pos++;
45
45
  continue;
46
46
  }
47
- const stateName = extractAttribute(wcsMatch.tagContent, "name") ?? "default";
47
+ const mountPath = extractAttribute(wcsMatch.tagContent, "mount");
48
48
  pos = wcsMatch.end;
49
49
  const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
50
50
  const wcsEnd = wcsCloseIdx === -1 ? len : wcsCloseIdx;
@@ -76,7 +76,7 @@ function parseWcsScriptBlocks(html, stateTagName = "wcs-state") {
76
76
  contentStart,
77
77
  contentEnd,
78
78
  content: html.slice(contentStart, contentEnd),
79
- stateName
79
+ mountPath
80
80
  });
81
81
  pos = html.indexOf(">", scriptCloseIdx) + 1;
82
82
  if (pos === 0) break;
@@ -354,40 +354,77 @@ function generateStateSchema(file, options = {}) {
354
354
  *
355
355
  * The manifest is a **derived artifact** (D9): the TypeScript type is the source
356
356
  * of truth, `wcs-schema emit` writes the manifest, and `wcs-schema check`
357
- * detects drift between the two in CI. `--merge` replaces exactly one
358
- * `states[name].stateSchema` and keeps everything else (other states, filters,
359
- * listContexts) a hand-written schema for the same state does not survive,
360
- * by design: there is no implicit merge in the sidecar spec (§5).
357
+ * detects drift between the two in CI.
358
+ *
359
+ * v2 (schemaVersion 2): the application namespace carries a **single**
360
+ * `stateSchema` one state tree per root, no name dimension
361
+ * (docs/state-mount-design.md D15). A volume (`<wcs-state mount="path">`)
362
+ * contributes a **subtree**: `--mount=<path>` merges the module's schema under
363
+ * that path inside the single `stateSchema`. `--merge` keeps everything else in
364
+ * an existing manifest (filters, listContexts); a hand-written schema for the
365
+ * same slot does not survive, by design: there is no implicit merge in the
366
+ * sidecar spec (§5).
361
367
  */
362
368
  const APPLICATION_MANIFEST_FILENAME = "wcstack.manifest.json";
363
- const SCHEMA_VERSION = 1;
369
+ const SCHEMA_VERSION = 2;
364
370
  const APPLICATION_NAMESPACE = "wcstack.application";
365
371
  /**
366
- * Create the manifest object for one state, or graft the state into `existing`
367
- * (a parsed manifest object; envelope fields are filled in when absent).
372
+ * Create the manifest object for the state tree, or graft into `existing`
373
+ * (a parsed manifest object; envelope fields are normalized to v2 — a v1
374
+ * manifest's `states` map does not survive, its replacement is exactly this
375
+ * regeneration path).
376
+ *
377
+ * `mountPath === null` replaces the whole `stateSchema`; a mount path merges
378
+ * the schema as a subtree at that path (intermediate object nodes are created).
368
379
  */
369
- function buildManifest(stateName, schema, existing) {
380
+ function buildManifest(mountPath, schema, existing) {
370
381
  const base = existing !== null && typeof existing === "object" && !Array.isArray(existing)
371
382
  ? { ...existing }
372
383
  : {};
373
- base.schemaVersion = typeof base.schemaVersion === "number" ? base.schemaVersion : SCHEMA_VERSION;
384
+ base.schemaVersion = SCHEMA_VERSION;
374
385
  base.kind = "application";
375
386
  const extensions = base.manifestExtensions !== null && typeof base.manifestExtensions === "object" && !Array.isArray(base.manifestExtensions)
376
387
  ? { ...base.manifestExtensions }
377
388
  : {};
378
389
  const nsRaw = extensions[APPLICATION_NAMESPACE];
379
390
  const ns = nsRaw !== null && typeof nsRaw === "object" && !Array.isArray(nsRaw) ? { ...nsRaw } : {};
380
- ns.version = typeof ns.version === "number" ? ns.version : SCHEMA_VERSION;
381
- const statesRaw = ns.states;
382
- const states = statesRaw !== null && typeof statesRaw === "object" && !Array.isArray(statesRaw) ? { ...statesRaw } : {};
383
- states[stateName] = { stateSchema: schema };
384
- ns.states = states;
391
+ ns.version = SCHEMA_VERSION;
392
+ // v1 leftovers: the name dimension is gone; regeneration does not carry it
393
+ delete ns.states;
394
+ if (mountPath === null) {
395
+ ns.stateSchema = schema;
396
+ }
397
+ else {
398
+ const rootRaw = ns.stateSchema;
399
+ const root = rootRaw !== null && typeof rootRaw === "object" && !Array.isArray(rootRaw)
400
+ ? { ...rootRaw }
401
+ : { type: "object" };
402
+ let node = root;
403
+ const segments = mountPath.split(".");
404
+ for (let i = 0; i < segments.length; i++) {
405
+ const properties = node.properties !== null && typeof node.properties === "object" && !Array.isArray(node.properties)
406
+ ? { ...node.properties }
407
+ : {};
408
+ node.properties = properties;
409
+ if (i === segments.length - 1) {
410
+ properties[segments[i]] = schema;
411
+ break;
412
+ }
413
+ const childRaw = properties[segments[i]];
414
+ const child = childRaw !== null && typeof childRaw === "object" && !Array.isArray(childRaw)
415
+ ? { ...childRaw }
416
+ : { type: "object" };
417
+ properties[segments[i]] = child;
418
+ node = child;
419
+ }
420
+ ns.stateSchema = root;
421
+ }
385
422
  extensions[APPLICATION_NAMESPACE] = ns;
386
423
  base.manifestExtensions = extensions;
387
424
  return base;
388
425
  }
389
- /** Read `states[name].stateSchema` from a parsed manifest object, or undefined. */
390
- function readStateSchema(manifest, stateName) {
426
+ /** Read the single `stateSchema` from a parsed manifest object, or undefined. */
427
+ function readStateSchema(manifest) {
391
428
  if (manifest === null || typeof manifest !== "object")
392
429
  return undefined;
393
430
  const extensions = manifest.manifestExtensions;
@@ -396,13 +433,38 @@ function readStateSchema(manifest, stateName) {
396
433
  const ns = extensions[APPLICATION_NAMESPACE];
397
434
  if (ns === null || typeof ns !== "object")
398
435
  return undefined;
399
- const states = ns.states;
400
- if (states === null || typeof states !== "object")
401
- return undefined;
402
- const entry = states[stateName];
403
- if (entry === null || typeof entry !== "object")
404
- return undefined;
405
- return entry.stateSchema;
436
+ return ns.stateSchema;
437
+ }
438
+ /**
439
+ * True for a v1-shaped manifest (`schemaVersion: 1` or a `states` map in the
440
+ * application namespace). `check` uses it to point at the regeneration path
441
+ * instead of reporting a confusing "missing stateSchema".
442
+ */
443
+ function isV1Manifest(manifest) {
444
+ if (manifest === null || typeof manifest !== "object")
445
+ return false;
446
+ if (manifest.schemaVersion === 1)
447
+ return true;
448
+ const extensions = manifest.manifestExtensions;
449
+ if (extensions === null || typeof extensions !== "object")
450
+ return false;
451
+ const ns = extensions[APPLICATION_NAMESPACE];
452
+ if (ns === null || typeof ns !== "object")
453
+ return false;
454
+ return ns.states !== undefined;
455
+ }
456
+ /** Navigate `properties` by mount-path segments; undefined when the subtree is absent. */
457
+ function subtreeAt(schema, mountPath) {
458
+ let node = schema;
459
+ for (const segment of mountPath.split(".")) {
460
+ if (node === null || typeof node !== "object")
461
+ return undefined;
462
+ const properties = node.properties;
463
+ if (properties === null || typeof properties !== "object")
464
+ return undefined;
465
+ node = properties[segment];
466
+ }
467
+ return node;
406
468
  }
407
469
  /** JSON with object keys sorted at every level — the canonical form used for comparison. */
408
470
  function stableStringify(value) {
@@ -421,11 +483,12 @@ function sortKeys(value) {
421
483
  return value;
422
484
  }
423
485
  /**
424
- * Compare the schema generated from the type with the one stored in `manifestText`.
486
+ * Compare the schema generated from the type with the one stored in `manifestText`
487
+ * (the whole tree, or the subtree at `mountPath`).
425
488
  * `changes` lists JSON pointers: `+ ptr` (only in generated), `- ptr` (only in
426
489
  * manifest), `~ ptr` (both, different value).
427
490
  */
428
- function compareStateSchema(manifestText, stateName, generated) {
491
+ function compareStateSchema(manifestText, mountPath, generated) {
429
492
  let parsed;
430
493
  try {
431
494
  parsed = JSON.parse(manifestText);
@@ -433,7 +496,12 @@ function compareStateSchema(manifestText, stateName, generated) {
433
496
  catch (e) {
434
497
  return { kind: "broken", message: e.message };
435
498
  }
436
- const stored = readStateSchema(parsed, stateName);
499
+ if (isV1Manifest(parsed))
500
+ return { kind: "v1-manifest" };
501
+ let stored = readStateSchema(parsed);
502
+ if (stored !== undefined && mountPath !== null) {
503
+ stored = subtreeAt(stored, mountPath);
504
+ }
437
505
  if (stored === undefined)
438
506
  return { kind: "missing-state" };
439
507
  if (stableStringify(stored) === stableStringify(generated))
@@ -499,7 +567,7 @@ function loadSchemaCore() {
499
567
  return cached;
500
568
  }
501
569
 
502
- var version = "1.32.0";
570
+ var version = "2.0.0";
503
571
  var pkg = {
504
572
  version: version};
505
573
 
@@ -508,26 +576,28 @@ const VERSION = pkg.version;
508
576
  /**
509
577
  * wcsSchema.ts — the `wcs-schema` command.
510
578
  *
511
- * wcs-schema emit <state.ts|state.js> [--state=default] [--out=wcstack.manifest.json] [--merge] [--tsconfig=<path>] [--max-depth=5]
512
- * wcs-schema check <state.ts|state.js> [--state=default] [--manifest=wcstack.manifest.json] [--tsconfig=<path>] [--max-depth=5]
579
+ * wcs-schema emit <state.ts|state.js> [--mount=<path>] [--out=wcstack.manifest.json] [--merge] [--tsconfig=<path>] [--max-depth=5]
580
+ * wcs-schema check <state.ts|state.js> [--mount=<path>] [--manifest=wcstack.manifest.json] [--tsconfig=<path>] [--max-depth=5]
513
581
  *
514
582
  * exit codes
515
583
  * emit : 0 written / 2 usage, unreadable file, syntax error, or the generated manifest failed its self-check
516
584
  * check: 0 manifest matches the type / 1 drift (changes listed on stderr) / 2 usage, unreadable file, broken or state-less manifest
517
585
  *
518
586
  * `--out=-` prints the manifest to stdout instead of writing a file. `--merge`
519
- * keeps everything in an existing manifest except `states[<name>].stateSchema`.
587
+ * keeps everything in an existing manifest except the slot being written —
588
+ * the whole `stateSchema`, or the subtree at `--mount=<path>` (a volume's type
589
+ * merges as a subtree of the single tree, docs/state-mount-design.md D15).
520
590
  *
521
591
  * The generated artifact is always run through the validator core's
522
592
  * `validateManifestArtifact` (dist/schema-core.cjs, built from vscode-wcs) so a
523
593
  * generator bug can never write a manifest the validator would reject.
524
594
  */
525
- const USAGE = "usage: wcs-schema emit <state.ts|state.js> [--state=default] [--out=wcstack.manifest.json] [--merge] [--tsconfig=<path>] [--max-depth=5]\n" +
526
- " wcs-schema check <state.ts|state.js> [--state=default] [--manifest=wcstack.manifest.json] [--tsconfig=<path>] [--max-depth=5]\n";
595
+ const USAGE = "usage: wcs-schema emit <state.ts|state.js> [--mount=<path>] [--out=wcstack.manifest.json] [--merge] [--tsconfig=<path>] [--max-depth=5]\n" +
596
+ " wcs-schema check <state.ts|state.js> [--mount=<path>] [--manifest=wcstack.manifest.json] [--tsconfig=<path>] [--max-depth=5]\n";
527
597
  function parseArgs(argv) {
528
598
  let command;
529
599
  let file;
530
- let state = "default";
600
+ let mount = null;
531
601
  let out = APPLICATION_MANIFEST_FILENAME;
532
602
  let manifest = APPLICATION_MANIFEST_FILENAME;
533
603
  let merge = false;
@@ -539,8 +609,10 @@ function parseArgs(argv) {
539
609
  command ??= "version";
540
610
  else if (arg === "--help" || arg === "-h")
541
611
  command ??= "help";
612
+ else if (arg.startsWith("--mount="))
613
+ mount = arg.slice("--mount=".length);
542
614
  else if (arg.startsWith("--state="))
543
- state = arg.slice("--state=".length);
615
+ unknown.push(arg); // v2: 名前次元は撤去 — --mount=<path> へ
544
616
  else if (arg.startsWith("--out="))
545
617
  out = arg.slice("--out=".length);
546
618
  else if (arg.startsWith("--manifest="))
@@ -560,7 +632,7 @@ function parseArgs(argv) {
560
632
  else
561
633
  unknown.push(arg);
562
634
  }
563
- return { command, file, state, out, manifest, merge, tsconfig, maxDepth, unknown };
635
+ return { command, file, mount, out, manifest, merge, tsconfig, maxDepth, unknown };
564
636
  }
565
637
  function main(argv, io = defaultIo()) {
566
638
  const args = parseArgs(argv);
@@ -573,8 +645,12 @@ function main(argv, io = defaultIo()) {
573
645
  return 0;
574
646
  }
575
647
  if (args.command === undefined || args.file === undefined || args.unknown.length > 0) {
576
- if (args.unknown.length > 0)
648
+ if (args.unknown.some((arg) => arg.startsWith("--state="))) {
649
+ io.stderr('--state was removed in v2 — there is a single state tree. Use --mount=<path> for a volume, or no flag for the root tree.\n');
650
+ }
651
+ else if (args.unknown.length > 0) {
577
652
  io.stderr(`unknown argument(s): ${args.unknown.join(" ")}\n`);
653
+ }
578
654
  io.stderr(USAGE);
579
655
  return 2;
580
656
  }
@@ -582,9 +658,15 @@ function main(argv, io = defaultIo()) {
582
658
  io.stderr("--max-depth must be a positive integer\n");
583
659
  return 2;
584
660
  }
585
- if (!/^[A-Za-z_$][\w$-]*$/.test(args.state)) {
586
- io.stderr(`--state must be a state name (got "${args.state}")\n`);
587
- return 2;
661
+ if (args.mount !== null) {
662
+ // runtime(state validateVolumeMountPath)と同条件: 空セグメント・ワイルドカードと
663
+ // 予約文字($ # @ — 位置を問わず)を拒否し、数字先頭などはランタイム同様に受理する。
664
+ // 旧 regex は `$` 先頭を受理し数字先頭を拒否していて runtime と両方向にずれていた
665
+ const problem = findMountPathProblem(args.mount);
666
+ if (problem !== null) {
667
+ io.stderr(`--mount ${problem} (got "${args.mount}")\n`);
668
+ return 2;
669
+ }
588
670
  }
589
671
  let generated;
590
672
  try {
@@ -598,11 +680,25 @@ function main(argv, io = defaultIo()) {
598
680
  io.stderr(`warning: ${warning}\n`);
599
681
  return args.command === "emit" ? emit(args, generated.schema, io) : check(args, generated.schema, io);
600
682
  }
683
+ /** runtime(validateVolumeMountPath)が raise する形なら理由を返す(妥当なら null)。 */
684
+ function findMountPathProblem(mountPath) {
685
+ if (mountPath.length === 0)
686
+ return "requires a non-empty tree path";
687
+ for (const segment of mountPath.split(".")) {
688
+ if (segment.length === 0)
689
+ return "path has an empty segment";
690
+ if (segment === "*")
691
+ return "path must be static (wildcards are not allowed)";
692
+ if (/[$#@]/.test(segment))
693
+ return "path must not use reserved characters ($, #, @)";
694
+ }
695
+ return null;
696
+ }
601
697
  function emit(args, schema, io) {
602
698
  const toStdout = args.out === "-";
603
699
  const outPath = toStdout ? undefined : resolve(io.cwd(), args.out);
604
700
  let existing;
605
- if (args.merge && outPath !== undefined && existsSync(outPath)) {
701
+ if ((args.merge || args.mount !== null) && outPath !== undefined && existsSync(outPath)) {
606
702
  try {
607
703
  existing = JSON.parse(readFileSync(outPath, "utf8"));
608
704
  }
@@ -611,7 +707,7 @@ function emit(args, schema, io) {
611
707
  return 2;
612
708
  }
613
709
  }
614
- const manifest = buildManifest(args.state, schema, existing);
710
+ const manifest = buildManifest(args.mount, schema, existing);
615
711
  const text = `${JSON.stringify(manifest, null, 2)}\n`;
616
712
  // Self-check: the validator must accept what the generator wrote.
617
713
  const core = loadSchemaCore();
@@ -631,7 +727,7 @@ function emit(args, schema, io) {
631
727
  return 0;
632
728
  }
633
729
  writeFileSync(outPath, text, "utf8");
634
- io.stderr(`wrote ${args.out} (state "${args.state}")\n`);
730
+ io.stderr(`wrote ${args.out} (${args.mount === null ? "state tree" : `mount "${args.mount}"`})\n`);
635
731
  return 0;
636
732
  }
637
733
  function check(args, schema, io) {
@@ -640,22 +736,28 @@ function check(args, schema, io) {
640
736
  io.stderr(`cannot read ${args.manifest}: no such file (run \`wcs-schema emit\` first)\n`);
641
737
  return 2;
642
738
  }
643
- const comparison = compareStateSchema(readFileSync(manifestPath, "utf8"), args.state, schema);
739
+ const slot = args.mount === null ? "state tree" : `mount "${args.mount}"`;
740
+ // 誘導コマンドは check と同じスロットを指すこと(--mount 抜きだとルートを差し替えてしまう)
741
+ const mountFlag = args.mount === null ? "" : ` --mount=${args.mount}`;
742
+ const comparison = compareStateSchema(readFileSync(manifestPath, "utf8"), args.mount, schema);
644
743
  switch (comparison.kind) {
645
744
  case "same":
646
- io.stderr(`${args.manifest}: state "${args.state}" is up to date\n`);
745
+ io.stderr(`${args.manifest}: ${slot} is up to date\n`);
647
746
  return 0;
648
747
  case "missing-state":
649
- io.stderr(`${args.manifest}: state "${args.state}" has no stateSchema (run \`wcs-schema emit --merge\`)\n`);
748
+ io.stderr(`${args.manifest}: ${slot} has no stateSchema (run \`wcs-schema emit --merge${mountFlag}\`)\n`);
749
+ return 2;
750
+ case "v1-manifest":
751
+ io.stderr(`${args.manifest}: schemaVersion 1 manifest (states[name]) — the name dimension was removed in v2. Regenerate with \`wcs-schema emit --out=${args.manifest}\` (single stateSchema, schemaVersion 2)\n`);
650
752
  return 2;
651
753
  case "broken":
652
754
  io.stderr(`${args.manifest}: broken JSON: ${comparison.message}\n`);
653
755
  return 2;
654
756
  case "differs":
655
- io.stderr(`${args.manifest}: state "${args.state}" is out of date (${comparison.changes.length} change(s)):\n`);
757
+ io.stderr(`${args.manifest}: ${slot} is out of date (${comparison.changes.length} change(s)):\n`);
656
758
  for (const change of comparison.changes)
657
759
  io.stderr(` ${change}\n`);
658
- io.stderr(`run \`wcs-schema emit --merge --out=${args.manifest}\` to update it\n`);
760
+ io.stderr(`run \`wcs-schema emit --merge${mountFlag} --out=${args.manifest}\` to update it\n`);
659
761
  return 1;
660
762
  }
661
763
  }