@ssobig/writer-cli 0.3.0 → 0.3.3

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.
Files changed (30) hide show
  1. package/README.md +11 -4
  2. package/asset-repository.js +7 -2
  3. package/package.json +1 -1
  4. package/templates/mystery-v1/authoring-view-preference.js +25 -2
  5. package/templates/mystery-v1/codemirror6-runtime.min.js +1 -1
  6. package/templates/mystery-v1/component-asset-operations.js +19 -6
  7. package/templates/mystery-v1/component-catalog-contract.js +2 -2
  8. package/templates/mystery-v1/component-field-contracts.js +98 -2
  9. package/templates/mystery-v1/component-navigation-counts.js +6 -1
  10. package/templates/mystery-v1/component-renderers.js +1 -1
  11. package/templates/mystery-v1/component-storage-contract.js +28 -9
  12. package/templates/mystery-v1/markdown-document-model.js +458 -0
  13. package/templates/mystery-v1/markdown-image-editor.js +336 -0
  14. package/templates/mystery-v1/markdown-live-editor.js +1246 -110
  15. package/templates/mystery-v1/markdown-toolbar.js +514 -0
  16. package/templates/mystery-v1/timeline-model.js +235 -0
  17. package/tools/writer-cli/package-lock.json +2 -2
  18. package/tools/writer-cli/package.json +1 -1
  19. package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +12 -13
  20. package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -1
  21. package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +4 -1
  22. package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +3 -3
  23. package/tools/writer-cli/skills/ssobig-writer-cli/references/investigation-board.md +9 -0
  24. package/tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md +130 -0
  25. package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -1
  26. package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
  27. package/tools/writer-cli/src/command-registry.cjs +24 -23
  28. package/tools/writer-cli/src/commands.cjs +22 -1
  29. package/tools/writer-cli/src/domain.cjs +46 -0
  30. package/tools/writer-cli/src/project-import.cjs +15 -1
@@ -41,44 +41,45 @@ const COMMANDS = Object.freeze([
41
41
  command("version", "Show installed CLI version and integrity.", [option("check", null, "Check npm latest.", { type: "boolean" })], ["ssobig-writer version --check"]),
42
42
  command("doctor", "Run read-only installation and connectivity diagnostics.", [], ["ssobig-writer doctor"]),
43
43
  command("command-schema", "Discover groups, leaves, or exact machine-readable command metadata.", [positional("scope", "Optional group or group.command scope.", { required: false })], ["ssobig-writer command-schema component.get --compact"]),
44
- command("auth.login", "Sign in through the staff authentication flow.", [option("no-open", null, "Print the login URL without opening a browser.", { type: "boolean" })], ["ssobig-writer auth login --no-open"], SERVER),
45
- command("auth.whoami", "Show the current authenticated staff identity.", [], ["ssobig-writer auth whoami"], SERVER),
44
+ command("auth.login", "Sign in through the Writer member Google sign-in flow.", [option("no-open", null, "Print the login URL without opening a browser.", { type: "boolean" })], ["ssobig-writer auth login --no-open"], SERVER),
45
+ command("auth.whoami", "Show the current authenticated Writer member identity.", [], ["ssobig-writer auth whoami"], SERVER),
46
46
  command("auth.logout", "Clear local credentials and cached agent state.", [], ["ssobig-writer auth logout"]),
47
47
  command("agent.start", "Start the private local Writer daemon.", [], ["ssobig-writer agent start"], SERVER),
48
48
  command("agent.status", "Show local daemon status.", [], ["ssobig-writer agent status"]),
49
49
  command("agent.stop", "Stop the local daemon.", [], ["ssobig-writer agent stop"]),
50
- command("agent.get", "Read one indexed match by identifier.", [option("match", "<match-id>", "Search match identifier.", { required: true }), option("max-characters", "<100-20000>", "Maximum returned characters.")], ["ssobig-writer agent get --match <match-id>"], SERVER),
51
- command("agent.apply", "Apply an agent-produced reviewed plan.", [option("plan", "<plan-file>", "Reviewed plan path.", { required: true }), option("receipt", "<receipt-file>", "Receipt output path.", { required: true })], ["ssobig-writer agent apply --plan plan.json --receipt receipt.json"], { ...SERVER, ...MUTATE }),
50
+ command("agent.get", "Read a matched scalar from the authoritative server by search match identifier.", [option("match", "<match-id>", "Search match identifier.", { required: true }), option("max-characters", "<100-20000>", "Maximum returned characters.")], ["ssobig-writer agent get --match <match-id>"], SERVER),
51
+ command("agent.apply", "Apply an agent-produced reviewed plan.", [option("plan", "<plan-file>", "Reviewed plan path.", { required: true }), option("receipt", "<receipt-file>", "Receipt output path.", { required: true })], ["ssobig-writer agent apply --plan .ssobig-writer/plan.json --receipt .ssobig-writer/receipt.json"], { ...SERVER, ...MUTATE }),
52
52
  command("agent.purge", "Purge local manuscript indexes.", [], ["ssobig-writer agent purge"]),
53
53
  command("sync", "Refresh the local manuscript index.", [], ["ssobig-writer sync"], SERVER),
54
- command("read", "Read indexed or authoritative Component data.", [option("project", "<slug|uuid>", "Limit to a project."), version, option("component", "<id|template-id>", "Limit to a Component."), option("pointer", "<json-pointer>", "Read a JSON pointer."), option("include-assets", null, "Include asset metadata.", { type: "boolean" }), option("fresh", null, "Prefer an authoritative refresh.", { type: "boolean" }), option("require-fresh", null, "Fail unless authoritative data is available.", { type: "boolean" })], ["ssobig-writer read --project <slug> --component <template-id> --fresh"], SERVER),
55
- command("search", "Search Component values and paths.", [option("query", "<text>", "Text query."), option("value-json", "<json-scalar>", "Typed exact value."), option("mode", "<all|exact|substring|regex>", "Search mode."), option("project", "<slug|uuid>", "Limit to a project."), version, option("component", "<id|template-id>", "Limit to a Component."), option("path", "<json-pointer>", "Exact JSON pointer."), option("path-prefix", "<json-pointer>", "JSON pointer prefix."), option("path-glob", "<glob>", "JSON pointer glob."), option("field", "<field>", "Field selector."), option("value-type", "<type>", "Value type."), option("limit", "<n>", "Maximum matches."), option("flags", "<regex-flags>", "Regular-expression flags."), option("case-sensitive", null, "Use case-sensitive matching.", { type: "boolean" }), option("include-assets", null, "Include asset metadata.", { type: "boolean" }), option("fresh", null, "Prefer an authoritative refresh.", { type: "boolean" }), option("require-fresh", null, "Fail unless authoritative data is available.", { type: "boolean" })], ["ssobig-writer search --query <text> --project <slug>", "ssobig-writer search --mode all --path-prefix /characters"], SERVER),
54
+ command("read", "Read local indexed Component data (not authoritative).", [option("project", "<slug|uuid>", "Limit to a project."), version, option("component", "<id|template-id>", "Limit to a Component."), option("pointer", "<json-pointer>", "Read a JSON pointer."), option("include-assets", null, "Include asset metadata.", { type: "boolean" }), option("fresh", null, "Sync the corpus before reading the local index; output stays non-authoritative.", { type: "boolean" }), option("require-fresh", null, "Refresh a missing or known-stale index; not a server read on every call.", { type: "boolean" })], ["ssobig-writer read --project <slug> --component <template-id> --fresh"], SERVER),
55
+ command("search", "Search Component values and paths.", [option("query", "<text>", "Text query."), option("value-json", "<json-scalar>", "Typed exact value."), option("mode", "<all|exact|substring|regex>", "Search mode."), option("project", "<slug|uuid>", "Limit to a project."), version, option("component", "<id|template-id>", "Limit to a Component."), option("path", "<json-pointer>", "Exact JSON pointer."), option("path-prefix", "<json-pointer>", "JSON pointer prefix."), option("path-glob", "<glob>", "JSON pointer glob."), option("field", "<field>", "Field selector."), option("value-type", "<type>", "Value type."), option("limit", "<n>", "Maximum matches."), option("flags", "<regex-flags>", "Regular-expression flags."), option("case-sensitive", null, "Use case-sensitive matching.", { type: "boolean" }), option("include-assets", null, "Include asset metadata.", { type: "boolean" }), option("fresh", null, "Sync the corpus before reading the local index; output stays non-authoritative.", { type: "boolean" }), option("require-fresh", null, "Refresh a missing or known-stale index; not a server read on every call.", { type: "boolean" })], ["ssobig-writer search --query <text> --project <slug>", "ssobig-writer search --mode all --path-prefix /characters"], SERVER),
56
56
  command("project.list", "List visible Writer projects.", [option("status", "<active|archived>", "Project status.")], ["ssobig-writer project list --status active"], SERVER),
57
57
  command("project.show", "Show project identity and versions.", [project, option("status", "<active|archived>", "Project status.")], ["ssobig-writer project show --project <slug>"], SERVER),
58
- command("project.plan-create", "Create a reviewed project creation plan.", [option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-create --slug new-case --title 'New Case' --out plan.json"], { ...SERVER, ...PLAN }),
59
- command("project.plan-import", "Create a reviewed import plan from a supported case export.", [option("source-root", "<case-directory>", "Source case root.", { required: true }), option("source-json", "<room-export.json>", "Source room export.", { required: true }), option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-import --source-root case --source-json room.json --slug new-case --title 'New Case' --out plan.json"], { ...SERVER, ...PLAN }),
60
- command("project.plan-archive", "Create a reviewed project archive plan.", [project, out], ["ssobig-writer project plan-archive --project <slug> --out plan.json"], { ...SERVER, ...PLAN }),
61
- command("project.plan-restore", "Create a reviewed project restore plan.", [project, out], ["ssobig-writer project plan-restore --project <slug> --out plan.json"], { ...SERVER, ...PLAN }),
62
- command("project.plan-set-catalog-demo", "Create a reviewed catalog-demo status plan.", [project, option("enabled", "<true|false>", "Catalog demo state.", { required: true }), out], ["ssobig-writer project plan-set-catalog-demo --project <slug> --enabled true --out plan.json"], { ...SERVER, ...PLAN }),
58
+ command("project.plan-create", "Create a reviewed project creation plan.", [option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-create --slug new-case --title 'New Case' --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
59
+ command("project.plan-import", "Create a reviewed import plan from a supported case export.", [option("source-root", "<case-directory>", "Source case root.", { required: true }), option("source-json", "<room-export.json>", "Source room export.", { required: true }), option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-import --source-root case --source-json room.json --slug new-case --title 'New Case' --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
60
+ command("project.plan-archive", "Create a reviewed project archive plan.", [project, out], ["ssobig-writer project plan-archive --project <slug> --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
61
+ command("project.plan-restore", "Create a reviewed project restore plan.", [project, out], ["ssobig-writer project plan-restore --project <slug> --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
62
+ command("project.plan-set-catalog-demo", "Create a reviewed catalog-demo status plan.", [project, option("enabled", "<true|false>", "Catalog demo state.", { required: true }), out], ["ssobig-writer project plan-set-catalog-demo --project <slug> --enabled true --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
63
63
  command("component.list", "List Components in a workspace.", [project, version], ["ssobig-writer component list --project <slug>"], SERVER),
64
64
  command("component.get", "Read one authoritative Component Instance.", [project, version, option("instance", "<id|template-id>", "Component Instance or Template identifier.", { required: true })], ["ssobig-writer component get --project <slug> --instance <template-id>"], SERVER),
65
- command("component.plan-add", "Create a reviewed plan to add one registered optional Component.", [project, version, option("template", "<template-id>", "Registered optional Data Component Template.", { required: true }), option("instance", "<instance-id>", "Instance identifier; required for multi-instance Templates."), option("label", "<tab-label>", "User-facing Component label."), out], ["ssobig-writer component plan-add --project <slug> --template ssobig.timeline --out plan.json"], { ...SERVER, ...PLAN }),
66
- command("component.plan-set-active", "Create a reviewed plan to toggle one optional Component.", [project, version, option("instance", "<instance-id>", "Stored optional Component Instance.", { required: true }), option("active", "<true|false>", "Use or archive the Component.", { required: true }), out], ["ssobig-writer component plan-set-active --project <slug> --instance clues --active false --out plan.json"], { ...SERVER, ...PLAN }),
67
- command("component.plan-set-field", "Create a reviewed plan to toggle one optional Component field.", [project, version, option("instance", "<id|template-id>", "Active Component Instance or Template.", { required: true }), option("field", "<field-key>", "Registered optional field key.", { required: true }), option("enabled", "<true|false>", "Show or hide the optional field.", { required: true }), out, message], ["ssobig-writer component plan-set-field --project <slug> --instance clues --field image --enabled false --out plan.json"], { ...SERVER, ...PLAN }),
68
- command("component.plan-patch", "Create a reviewed Component data patch plan.", [project, version, option("instance", "<id|template-id>", "Component Instance or Template identifier.", { required: true }), option("patch", "<file>", "JSON patch document.", { required: true }), out, message], ["ssobig-writer component plan-patch --project <slug> --instance <id> --patch patch.json --out plan.json"], { ...SERVER, ...PLAN }),
69
- command("component.plan-layout-investigation-board", "Create a deterministic investigation-board layout plan.", [project, version, option("instance", "<id|template-id>", "Investigation board Component."), option("spec", "<layout-spec.json>", "Validated layout spec.", { required: true }), out, message], ["ssobig-writer component plan-layout-investigation-board --project <slug> --spec layout.json --out plan.json"], { ...SERVER, ...PLAN }),
70
- command("component.plan-replace", "Create a reviewed indexed text replacement plan.", [option("match", "<match-id>", "Search match identifier.", { required: true }), option("old", "<text>", "Expected old text.", { required: true }), option("new", "<text>", "Replacement text.", { required: true }), out, option("occurrence", "<zero-based-index>", "Occurrence index."), message], ["ssobig-writer component plan-replace --match <id> --old old --new new --out plan.json"], { ...SERVER, ...PLAN }),
65
+ command("component.plan-add", "Create a reviewed plan to add one registered optional Component.", [project, version, option("template", "<template-id>", "Registered optional Data Component Template.", { required: true }), option("instance", "<instance-id>", "Instance identifier; required for multi-instance Templates."), option("label", "<tab-label>", "User-facing Component label."), out], ["ssobig-writer component plan-add --project <slug> --template ssobig.timeline --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
66
+ command("component.plan-set-active", "Create a reviewed plan to toggle one optional Component.", [project, version, option("instance", "<instance-id>", "Stored optional Component Instance.", { required: true }), option("active", "<true|false>", "Use or archive the Component.", { required: true }), out], ["ssobig-writer component plan-set-active --project <slug> --instance clues --active false --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
67
+ command("component.plan-set-field", "Create a reviewed plan to toggle one optional Component field.", [project, version, option("instance", "<id|template-id>", "Active Component Instance or Template.", { required: true }), option("field", "<field-key>", "Registered optional field key.", { required: true }), option("enabled", "<true|false>", "Show or hide the optional field.", { required: true }), out, message], ["ssobig-writer component plan-set-field --project <slug> --instance clues --field image --enabled false --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
68
+ command("component.plan-patch", "Create a reviewed Component data patch plan.", [project, version, option("instance", "<id|template-id>", "Component Instance or Template identifier.", { required: true }), option("patch", "<file>", "JSON patch document.", { required: true }), out, message], ["ssobig-writer component plan-patch --project <slug> --instance <id> --patch .ssobig-writer/patch.json --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
69
+ command("component.plan-layout-investigation-board", "Create a deterministic investigation-board layout plan.", [project, version, option("instance", "<id|template-id>", "Investigation board Component."), option("spec", "<layout-spec.json>", "Semantic layout spec; see the installed ssobig-writer-cli skill references/layout-spec.md (bundled at tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md).", { required: true }), out, message], ["ssobig-writer component plan-layout-investigation-board --project <slug> --spec .ssobig-writer/layout.json --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
70
+ command("component.plan-migrate-timeline", "Create a reviewed timeline v2 migration plan.", [project, version, option("instance", "<id|template-id>", "Timeline Component."), out, message], ["ssobig-writer component plan-migrate-timeline --project <slug> --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
71
+ command("component.plan-replace", "Create a reviewed indexed text replacement plan.", [option("match", "<match-id>", "Search match identifier.", { required: true }), option("old", "<text>", "Expected old text.", { required: true }), option("new", "<text>", "Replacement text.", { required: true }), out, option("occurrence", "<zero-based-index>", "Occurrence index."), message], ["ssobig-writer component plan-replace --match <id> --old old --new new --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
71
72
  command("asset.list", "List immutable asset pointers.", [project, version], ["ssobig-writer asset list --project <slug>"], SERVER),
72
- command("asset.plan-upload", "Create a reviewed asset upload plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), option("file", "<path>", "JPEG, PNG, WebP, GIF, or JSON file.", { required: true }), out, message], ["ssobig-writer asset plan-upload --project <slug> --asset-id cover --file cover.webp --out plan.json"], { ...SERVER, ...PLAN }),
73
- command("asset.plan-delete", "Create a reviewed asset delete plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), out, message], ["ssobig-writer asset plan-delete --project <slug> --asset-id cover --out plan.json"], { ...SERVER, ...PLAN }),
73
+ command("asset.plan-upload", "Create a reviewed asset upload plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), option("file", "<path>", "JPEG, PNG, WebP, GIF, or JSON file.", { required: true }), out, message], ["ssobig-writer asset plan-upload --project <slug> --asset-id cover --file cover.webp --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
74
+ command("asset.plan-delete", "Create a reviewed asset delete plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), out, message], ["ssobig-writer asset plan-delete --project <slug> --asset-id cover --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
74
75
  command("checkpoint.list", "List workspace checkpoints.", [project, version, option("before", "<checkpoint-number>", "Pagination cursor."), option("limit", "<1-100>", "Maximum rows.")], ["ssobig-writer checkpoint list --project <slug> --limit 20"], SERVER),
75
76
  command("checkpoint.status", "Show the current checkpoint head.", [project, version], ["ssobig-writer checkpoint status --project <slug>"], SERVER),
76
77
  command("checkpoint.show", "Show one checkpoint snapshot.", [project, version, option("checkpoint", "<id|number>", "Checkpoint selector.", { required: true }), option("include-data", null, "Include manuscript data.", { type: "boolean" })], ["ssobig-writer checkpoint show --project <slug> --checkpoint 3"], SERVER),
77
78
  command("checkpoint.diff", "Diff two checkpoints or Latest.", [project, version, option("from", "<id|number|latest>", "Diff base.", { required: true }), option("to", "<id|number|latest>", "Diff target.", { required: true }), option("include-values", null, "Include changed values.", { type: "boolean" })], ["ssobig-writer checkpoint diff --project <slug> --from 2 --to latest"], SERVER),
78
- command("checkpoint.plan-create", "Create a reviewed checkpoint creation plan.", [project, version, option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-create --project <slug> --message review --out plan.json"], { ...SERVER, ...PLAN }),
79
- command("checkpoint.plan-restore", "Create a reviewed checkpoint restore plan.", [project, version, option("checkpoint", "<id|number>", "Checkpoint selector.", { required: true }), option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-restore --project <slug> --checkpoint 3 --message restore --out plan.json"], { ...SERVER, ...PLAN }),
79
+ command("checkpoint.plan-create", "Create a reviewed checkpoint creation plan.", [project, version, option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-create --project <slug> --message review --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
80
+ command("checkpoint.plan-restore", "Create a reviewed checkpoint restore plan.", [project, version, option("checkpoint", "<id|number>", "Checkpoint selector.", { required: true }), option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-restore --project <slug> --checkpoint 3 --message restore --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
80
81
  command("validate", "Validate the authoritative Component composition.", [project, version], ["ssobig-writer validate --project <slug>"], SERVER),
81
- command("apply", "Apply one reviewed plan and optionally write a receipt.", [positional("plan-file", "Reviewed plan file."), option("receipt", "<receipt-file>", "Private receipt output path.")], ["ssobig-writer apply plan.json --receipt receipt.json"], { ...SERVER, ...MUTATE }),
82
+ command("apply", "Apply one reviewed plan and optionally write a receipt.", [positional("plan-file", "Reviewed plan file."), option("receipt", "<receipt-file>", "Private receipt output path.")], ["ssobig-writer apply .ssobig-writer/plan.json --receipt .ssobig-writer/receipt.json"], { ...SERVER, ...MUTATE }),
82
83
  command("skills.install", "Install the bundled project-local agent skill.", [option("target", "<workspace>", "Workspace root.", { required: true }), option("codex", null, "Install only to .agents/skills.", { type: "boolean" }), option("claude", null, "Install only to .claude/skills.", { type: "boolean" }), option("dry-run", null, "Report without changing files.", { type: "boolean" })], ["ssobig-writer skills install --target . --dry-run"]),
83
84
  command("skills.status", "Inspect bundled skill installation state.", [option("target", "<workspace>", "Workspace root.", { required: true })], ["ssobig-writer skills status --target ."]),
84
85
  command("skills.update", "Safely update managed skill files.", [option("target", "<workspace>", "Workspace root.", { required: true }), option("codex", null, "Update only .agents/skills.", { type: "boolean" }), option("claude", null, "Update only .claude/skills.", { type: "boolean" }), option("dry-run", null, "Report without changing files.", { type: "boolean" })], ["ssobig-writer skills update --target . --dry-run"]),
@@ -89,7 +90,7 @@ const BY_PATH = new Map(COMMANDS.map(item => [item.path, item]));
89
90
  if (BY_PATH.size !== COMMANDS.length) throw new Error("Writer CLI command registry contains duplicate paths.");
90
91
 
91
92
  const GROUP_DESCRIPTIONS = Object.freeze({
92
- version: "installation version", doctor: "installation diagnostics", "command-schema": "targeted machine metadata", auth: "staff authentication", agent: "local agent backend",
93
+ version: "installation version", doctor: "installation diagnostics", "command-schema": "targeted machine metadata", auth: "Writer member authentication", agent: "local agent backend",
93
94
  sync: "local index refresh", read: "targeted manuscript read", search: "targeted manuscript search", project: "project identity and plans",
94
95
  component: "Component data and plans", asset: "asset pointers and plans", checkpoint: "workspace history", validate: "composition validation",
95
96
  apply: "reviewed plan mutation", skills: "project-local agent skills"
@@ -20,6 +20,7 @@ const {
20
20
  createComponentFieldPlan,
21
21
  createComponentPlan,
22
22
  createInvestigationBoardLayoutPlan,
23
+ createTimelineMigrationPlan,
23
24
  createAssetPlan,
24
25
  createProjectCreationPlan,
25
26
  createProjectImportPlan,
@@ -648,7 +649,7 @@ async function execute(argv, deps) {
648
649
  return { command: "project.plan-set-catalog-demo", data: { planFile: outputPath, planId: plan.planId, digest: plan.digest, operation: plan.operation, project: plan.project, target: plan.target } };
649
650
  }
650
651
 
651
- if (command === "component" && ["list", "get", "plan-add", "plan-set-active", "plan-set-field", "plan-patch", "plan-layout-investigation-board"].includes(subcommand)) {
652
+ if (command === "component" && ["list", "get", "plan-add", "plan-set-active", "plan-set-field", "plan-patch", "plan-layout-investigation-board", "plan-migrate-timeline"].includes(subcommand)) {
652
653
  assertAllowedOptions(options, `component.${subcommand}`);
653
654
  const projectRef = requireOption(options, "project");
654
655
  const validated = validateLoadedVersion(await gateway.loadLatestVersion(projectRef, optionalVersionOption(options)));
@@ -725,6 +726,26 @@ async function execute(argv, deps) {
725
726
  }
726
727
  };
727
728
  }
729
+ if (subcommand === "plan-migrate-timeline") {
730
+ const timelinePlanOptions = planOptions(deps);
731
+ if (options.message !== undefined) timelinePlanOptions.checkpointMessage = String(options.message);
732
+ const plan = createTimelineMigrationPlan(validated, options.instance || "timeline", user.email, timelinePlanOptions);
733
+ const outputPath = writePrivateJson(requireOption(options, "out"), plan, deps.fs);
734
+ return {
735
+ command: "component.plan-migrate-timeline",
736
+ data: {
737
+ planFile: outputPath,
738
+ planId: plan.planId,
739
+ digest: plan.digest,
740
+ operation: plan.operation,
741
+ checkpointMessage: plan.checkpointMessage,
742
+ target: plan.target,
743
+ migration: plan.migration,
744
+ beforeHash: plan.beforeHash,
745
+ afterHash: plan.afterHash
746
+ }
747
+ };
748
+ }
728
749
  const selector = requireOption(options, "instance");
729
750
  if (subcommand === "get") {
730
751
  return { command: "component.get", data: { project: publicProject(validated.project, validated.instances), versionNumber: version, component: publicInstance(findInstance(validated, selector), true) } };
@@ -5,6 +5,7 @@ const componentContract = require("../../../templates/mystery-v1/component-stora
5
5
  const componentCatalog = require("../../../templates/mystery-v1/component-catalog-contract.js");
6
6
  const componentRegistry = require("../../../templates/mystery-v1/component-registry.js").createDefaultRegistry();
7
7
  const viewContract = require("../../../templates/mystery-v1/view-component-contract.js");
8
+ const timelineModel = require("../../../templates/mystery-v1/timeline-model.js");
8
9
  const projectRuntime = require("../../../project-runtime.js");
9
10
  const { applyPatch } = require("./json-patch.cjs");
10
11
  const { clone, equalJson, sha256 } = require("./json.cjs");
@@ -399,6 +400,50 @@ function createInvestigationBoardLayoutPlan(validated, selector, spec, actorEmai
399
400
  return finalizePlan(plan);
400
401
  }
401
402
 
403
+ function timelineEntryCountsByCharacter(entries) {
404
+ const counts = new Map();
405
+ for (const entry of entries) {
406
+ if (entry.lane?.kind !== "character") continue;
407
+ const characterId = String(entry.lane.characterId || "");
408
+ counts.set(characterId, (counts.get(characterId) || 0) + 1);
409
+ }
410
+ return Object.fromEntries([...counts.keys()].sort().map(characterId => [characterId, counts.get(characterId)]));
411
+ }
412
+
413
+ function createTimelineMigrationPlan(validated, selector, actorEmail, options = {}) {
414
+ const instance = findInstance(validated, selector || "timeline");
415
+ if (instance.templateId !== "ssobig.timeline") {
416
+ throw cliError("E_VALIDATION", "타임라인 v2 이관은 ssobig.timeline Component에만 사용할 수 있습니다.");
417
+ }
418
+ const shape = timelineModel.detectShape(instance.data);
419
+ if (shape === "v2") throw cliError("E_NO_CHANGES", "타임라인 Component가 이미 v2 모양입니다.");
420
+ if (shape !== "legacy") throw cliError("E_VALIDATION", "타임라인 Component data가 legacy 또는 v2 모양이 아닙니다.");
421
+ const converted = timelineModel.convertLegacyTimeline(instance.data, { idFactory: options.idFactory });
422
+ const review = timelineModel.compareLegacyToV2(instance.data, converted);
423
+ if (!review.ok) throw cliError("E_VALIDATION", `타임라인 v2 변환이 원본과 일치하지 않습니다: ${review.mismatches.join(" / ")}`);
424
+ const patch = [
425
+ { op: "test", path: "/events", value: clone(instance.data.events) },
426
+ { op: "remove", path: "/events" },
427
+ { op: "add", path: "/timeLevels", value: clone(converted.timeLevels) },
428
+ { op: "add", path: "/entries", value: clone(converted.entries) }
429
+ ];
430
+ const componentPlan = createComponentPlan(validated, instance.instanceId, patch, actorEmail, {
431
+ ...options,
432
+ checkpointMessage: options.checkpointMessage || "CLI: 타임라인 v2 이관"
433
+ });
434
+ const plan = clone(componentPlan);
435
+ delete plan.digest;
436
+ plan.migration = {
437
+ kind: "timeline-v2",
438
+ counts: clone(review.counts),
439
+ mismatches: clone(review.mismatches),
440
+ levelCount: converted.timeLevels.length,
441
+ entryCount: converted.entries.length,
442
+ entryCountByCharacter: timelineEntryCountsByCharacter(converted.entries)
443
+ };
444
+ return finalizePlan(plan);
445
+ }
446
+
402
447
  function createAssetPlan(validated, operation, assetId, actorEmail, options = {}) {
403
448
  const assetInstance = componentContract.assetInstance(validated.instances);
404
449
  const manifest = componentContract.assetManifest(validated.instances);
@@ -959,6 +1004,7 @@ module.exports = Object.freeze({
959
1004
  createComponentPlan,
960
1005
  investigationBoardReferences,
961
1006
  createInvestigationBoardLayoutPlan,
1007
+ createTimelineMigrationPlan,
962
1008
  createAssetPlan,
963
1009
  createProjectCreationPlan,
964
1010
  createProjectImportPlan,
@@ -6,6 +6,7 @@ const path = require("node:path");
6
6
  const catalogContract = require("../../../templates/mystery-v1/component-catalog-contract.js");
7
7
  const componentContract = require("../../../templates/mystery-v1/component-storage-contract.js");
8
8
  const viewContract = require("../../../templates/mystery-v1/view-component-contract.js");
9
+ const timelineModel = require("../../../templates/mystery-v1/timeline-model.js");
9
10
  const { inspectAssetFile } = require("./asset-policy.cjs");
10
11
  const { clone, equalJson, sha256 } = require("./json.cjs");
11
12
  const { cliError } = require("./errors.cjs");
@@ -142,6 +143,15 @@ function parseTimeline(source) {
142
143
  return events;
143
144
  }
144
145
 
146
+ // 원본 표는 사건이 시간을 소유하는 legacy 행으로 읽고, 등록된 v2 모양으로만 Component data를 만든다.
147
+ function timelineComponentData(events) {
148
+ const legacy = { events };
149
+ const converted = timelineModel.convertLegacyTimeline(legacy);
150
+ const review = timelineModel.compareLegacyToV2(legacy, converted);
151
+ if (!review.ok) throw cliError("E_VALIDATION", `타임라인 v2 변환이 원본과 일치하지 않습니다: ${review.mismatches.join(" / ")}`);
152
+ return { data: converted, review };
153
+ }
154
+
145
155
  function characterComponent(characterSource) {
146
156
  const characters = {};
147
157
  const names = {};
@@ -354,6 +364,7 @@ function loadTrueWriterCase(input, options = {}) {
354
364
  const progress = progressComponent(jsonData.game_set.flow);
355
365
  const ending = endingComponent(jsonData.game_set.ending);
356
366
  const timelineEvents = parseTimeline(docs["03-timeline.md"]);
367
+ const timeline = timelineComponentData(timelineEvents);
357
368
  const resources = Object.keys(jsonData.resource).length;
358
369
  const components = normalizeImportComponents([
359
370
  component("basic", {
@@ -380,7 +391,7 @@ function loadTrueWriterCase(input, options = {}) {
380
391
  component("characters", characterComponent(jsonData.game_set.character)),
381
392
  component("assets", { assets: {} }),
382
393
  component("clues", cluesComponent(jsonData.resource)),
383
- component("timeline", { events: timelineEvents }),
394
+ component("timeline", timeline.data),
384
395
  component("ending", ending),
385
396
  component("author-notes", {
386
397
  notes: [
@@ -417,6 +428,9 @@ function loadTrueWriterCase(input, options = {}) {
417
428
  endingGroups: Object.keys(jsonData.game_set.ending || {}).length,
418
429
  endingOutcomes: ending.outcomes.length,
419
430
  timelineEvents: timelineEvents.length,
431
+ timelineLevels: timeline.data.timeLevels.length,
432
+ timelineEntries: timeline.data.entries.length,
433
+ timelineConversion: { shape: "v2", lossless: timeline.review.ok, ...timeline.review.counts },
420
434
  components: components.length,
421
435
  assets: assetFiles.length,
422
436
  unexplained: 0