projectionlab-mcp 1.0.1 → 1.2.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.
Files changed (122) hide show
  1. package/HOW-IT-WORKS.md +2 -2
  2. package/MANUAL-SETUP.md +64 -0
  3. package/README.md +34 -271
  4. package/REFERENCE.md +221 -0
  5. package/SETUP-FOR-AGENTS.md +37 -0
  6. package/dist/bootstrap.js +20 -3
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/bridge.d.ts +2 -0
  9. package/dist/clonePlan.d.ts +9 -0
  10. package/dist/clonePlan.js +15 -14
  11. package/dist/clonePlan.js.map +1 -1
  12. package/dist/config.d.ts +4 -0
  13. package/dist/config.js +41 -6
  14. package/dist/config.js.map +1 -1
  15. package/dist/currentFinances.d.ts +12 -1
  16. package/dist/currentFinances.js +60 -0
  17. package/dist/currentFinances.js.map +1 -1
  18. package/dist/dataModel.d.ts +58 -0
  19. package/dist/dataModel.js +1429 -0
  20. package/dist/dataModel.js.map +1 -0
  21. package/dist/deletePlan.d.ts +17 -0
  22. package/dist/deletePlan.js +72 -0
  23. package/dist/deletePlan.js.map +1 -0
  24. package/dist/diff.d.ts +288 -0
  25. package/dist/diff.js +744 -0
  26. package/dist/diff.js.map +1 -0
  27. package/dist/errors.d.ts +6 -0
  28. package/dist/errors.js +14 -1
  29. package/dist/errors.js.map +1 -1
  30. package/dist/extensionBridge.d.ts +16 -2
  31. package/dist/extensionBridge.js +104 -23
  32. package/dist/extensionBridge.js.map +1 -1
  33. package/dist/index.js +27 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/instructions.d.ts +1 -1
  36. package/dist/instructions.js +3 -2
  37. package/dist/instructions.js.map +1 -1
  38. package/dist/largeOutput.js +25 -0
  39. package/dist/largeOutput.js.map +1 -1
  40. package/dist/logger.js +11 -4
  41. package/dist/logger.js.map +1 -1
  42. package/dist/pathPatch.d.ts +5 -2
  43. package/dist/pathPatch.js +16 -2
  44. package/dist/pathPatch.js.map +1 -1
  45. package/dist/planSettings.d.ts +1 -1
  46. package/dist/planSettings.js +3 -3
  47. package/dist/planSettings.js.map +1 -1
  48. package/dist/plans.d.ts +73 -10
  49. package/dist/plans.js +206 -42
  50. package/dist/plans.js.map +1 -1
  51. package/dist/prompts/index.js +7 -4
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/realEstate.d.ts +22 -0
  54. package/dist/realEstate.js +118 -0
  55. package/dist/realEstate.js.map +1 -0
  56. package/dist/redact.d.ts +26 -0
  57. package/dist/redact.js +93 -1
  58. package/dist/redact.js.map +1 -1
  59. package/dist/resources/dataModel.d.ts +3 -0
  60. package/dist/resources/dataModel.js +66 -0
  61. package/dist/resources/dataModel.js.map +1 -0
  62. package/dist/resources/knowledge.js +189 -47
  63. package/dist/resources/knowledge.js.map +1 -1
  64. package/dist/resources/planData.js +3 -1
  65. package/dist/resources/planData.js.map +1 -1
  66. package/dist/resources/summary.js +4 -1
  67. package/dist/resources/summary.js.map +1 -1
  68. package/dist/snapshot.d.ts +21 -0
  69. package/dist/snapshot.js +116 -12
  70. package/dist/snapshot.js.map +1 -1
  71. package/dist/summary.js +19 -6
  72. package/dist/summary.js.map +1 -1
  73. package/dist/tools/clonePlan.js +17 -8
  74. package/dist/tools/clonePlan.js.map +1 -1
  75. package/dist/tools/connection.js +29 -4
  76. package/dist/tools/connection.js.map +1 -1
  77. package/dist/tools/deletePlan.d.ts +4 -0
  78. package/dist/tools/deletePlan.js +103 -0
  79. package/dist/tools/deletePlan.js.map +1 -0
  80. package/dist/tools/diff.d.ts +4 -0
  81. package/dist/tools/diff.js +417 -0
  82. package/dist/tools/diff.js.map +1 -0
  83. package/dist/tools/getCurrentFinances.js +27 -4
  84. package/dist/tools/getCurrentFinances.js.map +1 -1
  85. package/dist/tools/getPlan.js +4 -4
  86. package/dist/tools/getPlan.js.map +1 -1
  87. package/dist/tools/getPlanSection.js +7 -6
  88. package/dist/tools/getPlanSection.js.map +1 -1
  89. package/dist/tools/getPlanSettings.js +10 -6
  90. package/dist/tools/getPlanSettings.js.map +1 -1
  91. package/dist/tools/restore.js +18 -1
  92. package/dist/tools/restore.js.map +1 -1
  93. package/dist/tools/schema.js +28 -4
  94. package/dist/tools/schema.js.map +1 -1
  95. package/dist/tools/snapshots.d.ts +1 -1
  96. package/dist/tools/snapshots.js +53 -6
  97. package/dist/tools/snapshots.js.map +1 -1
  98. package/dist/tools/summary.js +3 -2
  99. package/dist/tools/summary.js.map +1 -1
  100. package/dist/tools/updateAccount.js +9 -7
  101. package/dist/tools/updateAccount.js.map +1 -1
  102. package/dist/tools/updatePlanEvent.js +71 -33
  103. package/dist/tools/updatePlanEvent.js.map +1 -1
  104. package/dist/tools/updatePlanSection.js +17 -32
  105. package/dist/tools/updatePlanSection.js.map +1 -1
  106. package/dist/tools/updatePlanSettings.js +11 -7
  107. package/dist/tools/updatePlanSettings.js.map +1 -1
  108. package/dist/types/projectionlab.d.ts +3 -1
  109. package/dist/types/wire.d.ts +27 -2
  110. package/dist/types/wire.js +6 -2
  111. package/dist/types/wire.js.map +1 -1
  112. package/dist/updateCheck.d.ts +22 -0
  113. package/dist/updateCheck.js +58 -0
  114. package/dist/updateCheck.js.map +1 -0
  115. package/dist/versionCompare.d.ts +5 -0
  116. package/dist/versionCompare.js +33 -0
  117. package/dist/versionCompare.js.map +1 -0
  118. package/dist/versionNotices.d.ts +9 -0
  119. package/dist/versionNotices.js +42 -0
  120. package/dist/versionNotices.js.map +1 -0
  121. package/package.json +6 -3
  122. package/skills/projectionlab/SKILL.md +329 -29
@@ -1,3 +1,4 @@
1
+ import { readdir, stat, unlink } from "node:fs/promises";
1
2
  import { join } from "node:path";
2
3
  import { homedir } from "node:os";
3
4
  import { toJson } from "./json.js";
@@ -5,6 +6,28 @@ import { secureMkdir, secureWriteFile } from "./secureFs.js";
5
6
  import { log } from "./logger.js";
6
7
  export const FILE_THRESHOLD = 100_000;
7
8
  export const EXPORT_DIR = join(homedir(), ".projectionlab-mcp", "exports");
9
+ const RETENTION_DAYS = 30;
10
+ /** Delete export files whose last modification is older than the retention
11
+ * window. Only .json files this module writes are touched. */
12
+ async function prune() {
13
+ const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000;
14
+ const files = await readdir(EXPORT_DIR);
15
+ for (const file of files) {
16
+ if (!file.endsWith(".json"))
17
+ continue;
18
+ const filePath = join(EXPORT_DIR, file);
19
+ try {
20
+ const info = await stat(filePath);
21
+ if (info.mtimeMs < cutoff) {
22
+ await unlink(filePath);
23
+ log.info("largeOutput", `Pruned expired export ${file}`);
24
+ }
25
+ }
26
+ catch {
27
+ // Skip unreadable files.
28
+ }
29
+ }
30
+ }
8
31
  /**
9
32
  * Returns the text as-is when small enough; otherwise writes it to a file
10
33
  * and returns a pointer message so MCP clients don't truncate mid-JSON.
@@ -17,6 +40,8 @@ export async function textOrFile(text, label, summary = {}) {
17
40
  const filePath = join(EXPORT_DIR, `${label}-${timestamp}.json`);
18
41
  await secureWriteFile(filePath, text);
19
42
  log.info("largeOutput", `Wrote ${text.length} chars to ${filePath}`);
43
+ // Prune expired exports in the background, the way snapshots do.
44
+ prune().catch((err) => log.warn("largeOutput", `Prune failed: ${err}`));
20
45
  return toJson({
21
46
  truncated: true,
22
47
  reason: `Result is ${text.length} chars, larger than the ${FILE_THRESHOLD} char inline limit`,
@@ -1 +1 @@
1
- {"version":3,"file":"largeOutput.js","sourceRoot":"","sources":["../src/largeOutput.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AACtC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,oBAAoB,EAAE,SAAS,CAAC,CAAC;AAE3E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,KAAa,EACb,UAAmC,EAAE;IAErC,IAAI,IAAI,CAAC,MAAM,IAAI,cAAc;QAAE,OAAO,IAAI,CAAC;IAE/C,MAAM,WAAW,CAAC,UAAU,CAAC,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,SAAS,OAAO,CAAC,CAAC;IAChE,MAAM,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,IAAI,CAAC,MAAM,aAAa,QAAQ,EAAE,CAAC,CAAC;IAErE,OAAO,MAAM,CACX;QACE,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,aAAa,IAAI,CAAC,MAAM,2BAA2B,cAAc,oBAAoB;QAC7F,QAAQ;QACR,IAAI,EAAE,8DAA8D;QACpE,GAAG,OAAO;KACX,CACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"largeOutput.js","sourceRoot":"","sources":["../src/largeOutput.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AACtC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,oBAAoB,EAAE,SAAS,CAAC,CAAC;AAC3E,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B;8DAC8D;AAC9D,KAAK,UAAU,KAAK;IAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACjE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IAExC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAS;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClC,IAAI,IAAI,CAAC,OAAO,GAAG,MAAM,EAAE,CAAC;gBAC1B,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACvB,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,yBAAyB,IAAI,EAAE,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,KAAa,EACb,UAAmC,EAAE;IAErC,IAAI,IAAI,CAAC,MAAM,IAAI,cAAc;QAAE,OAAO,IAAI,CAAC;IAE/C,MAAM,WAAW,CAAC,UAAU,CAAC,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,SAAS,OAAO,CAAC,CAAC;IAChE,MAAM,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,IAAI,CAAC,MAAM,aAAa,QAAQ,EAAE,CAAC,CAAC;IAErE,iEAAiE;IACjE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,iBAAiB,GAAG,EAAE,CAAC,CAAC,CAAC;IAExE,OAAO,MAAM,CACX;QACE,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,aAAa,IAAI,CAAC,MAAM,2BAA2B,cAAc,oBAAoB;QAC7F,QAAQ;QACR,IAAI,EAAE,8DAA8D;QACpE,GAAG,OAAO;KACX,CACF,CAAC;AACJ,CAAC"}
package/dist/logger.js CHANGED
@@ -1,25 +1,32 @@
1
1
  import { config } from "./config.js";
2
+ import { redactSecrets } from "./redact.js";
2
3
  const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
3
4
  const threshold = LEVELS[config.logLevel] ?? LEVELS.info;
4
5
  function timestamp() {
5
6
  return new Date().toISOString();
6
7
  }
8
+ // Messages often carry an error from the browser executor, which can echo
9
+ // request data. Every line is scrubbed here so no call site can log the
10
+ // Plugin API key.
11
+ function write(label, op, msg) {
12
+ console.error(`[${timestamp()}] ${label} [${op}] ${redactSecrets(msg)}`);
13
+ }
7
14
  export const log = {
8
15
  error(op, msg) {
9
16
  if (threshold >= LEVELS.error)
10
- console.error(`[${timestamp()}] ERROR [${op}] ${msg}`);
17
+ write("ERROR", op, msg);
11
18
  },
12
19
  warn(op, msg) {
13
20
  if (threshold >= LEVELS.warn)
14
- console.error(`[${timestamp()}] WARN [${op}] ${msg}`);
21
+ write("WARN ", op, msg);
15
22
  },
16
23
  info(op, msg) {
17
24
  if (threshold >= LEVELS.info)
18
- console.error(`[${timestamp()}] INFO [${op}] ${msg}`);
25
+ write("INFO ", op, msg);
19
26
  },
20
27
  debug(op, msg) {
21
28
  if (threshold >= LEVELS.debug)
22
- console.error(`[${timestamp()}] DEBUG [${op}] ${msg}`);
29
+ write("DEBUG", op, msg);
23
30
  },
24
31
  };
25
32
  //# sourceMappingURL=logger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAW,CAAC;AAEjE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC;AAEzD,SAAS,SAAS;IAChB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,KAAK,CAAC,EAAU,EAAE,GAAW;QAC3B,IAAI,SAAS,IAAI,MAAM,CAAC,KAAK;YAC3B,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,EAAE,YAAY,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC,EAAU,EAAE,GAAW;QAC1B,IAAI,SAAS,IAAI,MAAM,CAAC,IAAI;YAC1B,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,EAAE,YAAY,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC,EAAU,EAAE,GAAW;QAC1B,IAAI,SAAS,IAAI,MAAM,CAAC,IAAI;YAC1B,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,EAAE,YAAY,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,KAAK,CAAC,EAAU,EAAE,GAAW;QAC3B,IAAI,SAAS,IAAI,MAAM,CAAC,KAAK;YAC3B,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,EAAE,YAAY,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC;IAC3D,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAW,CAAC;AAEjE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC;AAEzD,SAAS,SAAS;IAChB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,0EAA0E;AAC1E,wEAAwE;AACxE,kBAAkB;AAClB,SAAS,KAAK,CAAC,KAAa,EAAE,EAAU,EAAE,GAAW;IACnD,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,EAAE,KAAK,KAAK,KAAK,EAAE,KAAK,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,KAAK,CAAC,EAAU,EAAE,GAAW;QAC3B,IAAI,SAAS,IAAI,MAAM,CAAC,KAAK;YAAE,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,EAAU,EAAE,GAAW;QAC1B,IAAI,SAAS,IAAI,MAAM,CAAC,IAAI;YAAE,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,EAAU,EAAE,GAAW;QAC1B,IAAI,SAAS,IAAI,MAAM,CAAC,IAAI;YAAE,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IACxD,CAAC;IACD,KAAK,CAAC,EAAU,EAAE,GAAW;QAC3B,IAAI,SAAS,IAAI,MAAM,CAAC,KAAK;YAAE,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IACzD,CAAC;CACF,CAAC"}
@@ -24,9 +24,12 @@ export interface AddressableValueOptions {
24
24
  export declare function listAddressableValues(data: unknown, prefix?: string, options?: AddressableValueOptions): AddressableValue[];
25
25
  /**
26
26
  * Apply path-addressed changes immutably. Paths must already exist and values
27
- * must retain their JSON type.
27
+ * must retain their JSON type. When `scope` names a group in the data model
28
+ * registry (a settings section, event section, or Current Finances
29
+ * collection), a string value outside an observed enum's choices is refused
30
+ * with INVALID_INPUT.
28
31
  */
29
- export declare function applyExistingPathChanges(data: unknown, incoming: Record<string, unknown>, context: PathPatchContext): {
32
+ export declare function applyExistingPathChanges(data: unknown, incoming: Record<string, unknown>, context: PathPatchContext, scope?: string): {
30
33
  updated: PlanObj;
31
34
  changes: Record<string, PathValueChange>;
32
35
  };
package/dist/pathPatch.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { enumRefusalError } from "./dataModel.js";
1
2
  import { ProjectionLabError } from "./errors.js";
2
3
  import { isSecretFieldName } from "./redact.js";
3
4
  const UNSAFE_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
@@ -97,6 +98,11 @@ function assertCompatibleType(path, current, incoming, context) {
97
98
  }
98
99
  function replaceAtPath(root, parts, incoming, fullPath, context) {
99
100
  const [head, ...tail] = parts;
101
+ // "length" is an own property of arrays, so hasOwnProperty alone would let
102
+ // a leaf patch resize the array through copy[Number("length")].
103
+ if (Array.isArray(root) && !/^(0|[1-9]\d*)$/.test(head)) {
104
+ throw new ProjectionLabError("INVALID_INPUT", `Array segment "${head}" in ${context.label} path "${fullPath}" is not an index`, `Use a numeric array index returned by ${context.discoveryTool}`);
105
+ }
100
106
  if (!Object.prototype.hasOwnProperty.call(root, head)) {
101
107
  throw new ProjectionLabError("INVALID_INPUT", `Unknown ${context.label} path "${fullPath}"`, `Only existing paths returned by ${context.discoveryTool} can be changed`);
102
108
  }
@@ -127,9 +133,12 @@ function replaceAtPath(root, parts, incoming, fullPath, context) {
127
133
  }
128
134
  /**
129
135
  * Apply path-addressed changes immutably. Paths must already exist and values
130
- * must retain their JSON type.
136
+ * must retain their JSON type. When `scope` names a group in the data model
137
+ * registry (a settings section, event section, or Current Finances
138
+ * collection), a string value outside an observed enum's choices is refused
139
+ * with INVALID_INPUT.
131
140
  */
132
- export function applyExistingPathChanges(data, incoming, context) {
141
+ export function applyExistingPathChanges(data, incoming, context, scope) {
133
142
  if (!isPlainObject(data)) {
134
143
  throw new ProjectionLabError("INVALID_INPUT", `The selected ${context.label} container is not an object`, `Inspect it with ${context.discoveryTool}`);
135
144
  }
@@ -148,6 +157,11 @@ export function applyExistingPathChanges(data, incoming, context) {
148
157
  const changes = {};
149
158
  for (const [path, after] of Object.entries(incoming)) {
150
159
  const parts = parsePath(path, context);
160
+ if (scope !== undefined) {
161
+ const refusal = enumRefusalError(scope, path, after);
162
+ if (refusal)
163
+ throw refusal;
164
+ }
151
165
  let before = data;
152
166
  for (const part of parts) {
153
167
  if (!isContainer(before) ||
@@ -1 +1 @@
1
- {"version":3,"file":"pathPatch.js","sourceRoot":"","sources":["../src/pathPatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pathPatch.js","sourceRoot":"","sources":["../src/pathPatch.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ export type PlanSettingSection = (typeof PLAN_SETTING_SECTIONS)[number];
4
4
  export type PlanSetting = AddressableValue;
5
5
  export type PlanSettingChange = PathValueChange;
6
6
  export declare function listPlanSettings(sectionData: unknown, section?: PlanSettingSection): PlanSetting[];
7
- export declare function applyPlanSettingChanges(sectionData: unknown, incoming: Record<string, unknown>): {
7
+ export declare function applyPlanSettingChanges(sectionData: unknown, incoming: Record<string, unknown>, section?: PlanSettingSection): {
8
8
  updated: Record<string, unknown>;
9
9
  changes: Record<string, PlanSettingChange>;
10
10
  };
@@ -14,15 +14,15 @@ const ADVANCED_RATE_SCHEDULES = new Set([
14
14
  const PLAN_SETTING_CONTEXT = {
15
15
  label: "plan setting",
16
16
  discoveryTool: "projectionlab_get_plan_settings",
17
- example: '{ "inflation": 0.03 }',
17
+ example: '{ "inflation": 3 }',
18
18
  };
19
19
  export function listPlanSettings(sectionData, section) {
20
20
  return listAddressableValues(sectionData, "", section === "variables"
21
21
  ? { expandArrayPaths: ADVANCED_RATE_SCHEDULES }
22
22
  : {});
23
23
  }
24
- export function applyPlanSettingChanges(sectionData, incoming) {
25
- return applyExistingPathChanges(sectionData, incoming, PLAN_SETTING_CONTEXT);
24
+ export function applyPlanSettingChanges(sectionData, incoming, section) {
25
+ return applyExistingPathChanges(sectionData, incoming, PLAN_SETTING_CONTEXT, section);
26
26
  }
27
27
  export function planSettingChangesApplied(sectionData, expected) {
28
28
  return pathChangesApplied(sectionData, expected, PLAN_SETTING_CONTEXT);
@@ -1 +1 @@
1
- {"version":3,"file":"planSettings.js","sourceRoot":"","sources":["../src/planSettings.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,GAGnB,MAAM,gBAAgB,CAAC;AAExB,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,WAAW;IACX,YAAY;IACZ,oBAAoB;CACZ,CAAC;AAMX,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC;IACtC,iBAAiB;IACjB,wBAAwB;IACxB,oBAAoB;IACpB,4BAA4B;IAC5B,wBAAwB;CACzB,CAAC,CAAC;AAEH,MAAM,oBAAoB,GAAG;IAC3B,KAAK,EAAE,cAAc;IACrB,aAAa,EAAE,iCAAiC;IAChD,OAAO,EAAE,uBAAuB;CACxB,CAAC;AAEX,MAAM,UAAU,gBAAgB,CAC9B,WAAoB,EACpB,OAA4B;IAE5B,OAAO,qBAAqB,CAC1B,WAAW,EACX,EAAE,EACF,OAAO,KAAK,WAAW;QACrB,CAAC,CAAC,EAAE,gBAAgB,EAAE,uBAAuB,EAAE;QAC/C,CAAC,CAAC,EAAE,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,WAAoB,EACpB,QAAiC;IAKjC,OAAO,wBAAwB,CAC7B,WAAW,EACX,QAAQ,EACR,oBAAoB,CACrB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,WAAoB,EACpB,QAA2C;IAE3C,OAAO,kBAAkB,CAAC,WAAW,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AACzE,CAAC"}
1
+ {"version":3,"file":"planSettings.js","sourceRoot":"","sources":["../src/planSettings.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,GAGnB,MAAM,gBAAgB,CAAC;AAExB,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,WAAW;IACX,YAAY;IACZ,oBAAoB;CACZ,CAAC;AAMX,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC;IACtC,iBAAiB;IACjB,wBAAwB;IACxB,oBAAoB;IACpB,4BAA4B;IAC5B,wBAAwB;CACzB,CAAC,CAAC;AAEH,MAAM,oBAAoB,GAAG;IAC3B,KAAK,EAAE,cAAc;IACrB,aAAa,EAAE,iCAAiC;IAChD,OAAO,EAAE,oBAAoB;CACrB,CAAC;AAEX,MAAM,UAAU,gBAAgB,CAC9B,WAAoB,EACpB,OAA4B;IAE5B,OAAO,qBAAqB,CAC1B,WAAW,EACX,EAAE,EACF,OAAO,KAAK,WAAW;QACrB,CAAC,CAAC,EAAE,gBAAgB,EAAE,uBAAuB,EAAE;QAC/C,CAAC,CAAC,EAAE,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,WAAoB,EACpB,QAAiC,EACjC,OAA4B;IAK5B,OAAO,wBAAwB,CAC7B,WAAW,EACX,QAAQ,EACR,oBAAoB,EACpB,OAAO,CACR,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,WAAoB,EACpB,QAA2C;IAE3C,OAAO,kBAAkB,CAAC,WAAW,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AACzE,CAAC"}
package/dist/plans.d.ts CHANGED
@@ -2,13 +2,34 @@ import { ProjectionLabError } from "./errors.js";
2
2
  export type PlanObj = Record<string, unknown>;
3
3
  export declare const PLAN_SECTIONS: readonly ["income", "expenses", "accounts", "assets", "milestones", "variables", "withdrawalStrategy", "montecarlo", "priorities"];
4
4
  export type PlanSection = (typeof PLAN_SECTIONS)[number];
5
+ /** Sections whose members are events addressed by id or name; the
6
+ * settings-like sections use projectionlab_update_plan_settings instead. */
7
+ export declare const EVENT_SECTIONS: readonly ["income", "expenses", "accounts", "assets", "milestones", "priorities"];
8
+ /** The container each section holds, known from its name alone and not from
9
+ * what a plan happens to store: milestones is a direct array, the event
10
+ * sections are objects with an events array, and the settings-like sections
11
+ * are plain objects. Per DATA-MODEL.md sections 5.1 to 5.6 and the
12
+ * `variables`, `montecarlo` and `withdrawalStrategy` field trees. */
13
+ export declare const SECTION_CONTAINERS: {
14
+ readonly income: "events";
15
+ readonly expenses: "events";
16
+ readonly accounts: "events";
17
+ readonly assets: "events";
18
+ readonly milestones: "array";
19
+ readonly variables: "object";
20
+ readonly withdrawalStrategy: "object";
21
+ readonly montecarlo: "object";
22
+ readonly priorities: "events";
23
+ };
5
24
  /**
6
- * Find a plan by identifier. Match priority: exact ID, exact name
7
- * (case-insensitive), then partial name. Exact matches win over partial so
8
- * plans with shared name prefixes can't be silently confused on write paths.
25
+ * Index of the plan the identifier names, -1 when nothing matches. Throws
26
+ * INVALID_INPUT when several plans match the same way, so a read and the
27
+ * following write can never target different plans.
9
28
  */
10
- export declare function findPlanIndex(plans: unknown[], identifier: string): number;
11
- export declare function findPlan(plans: unknown[], identifier: string): PlanObj | null;
29
+ export declare function resolvePlanIndex(plans: unknown[], identifier: string): number;
30
+ /** The plan the identifier names, or null when nothing matches. Refuses
31
+ * ambiguous identifiers the way resolvePlanIndex does. */
32
+ export declare function resolvePlan(plans: unknown[], identifier: string): PlanObj | null;
12
33
  export declare function planNotFoundError(plans: unknown[], identifier: string): ProjectionLabError;
13
34
  export declare function requirePlans(data: {
14
35
  plans?: unknown[];
@@ -24,14 +45,38 @@ export declare function planEventCounts(plan: unknown): {
24
45
  expenseCount: number;
25
46
  milestoneCount: number;
26
47
  };
27
- /** Index of the event matching `identifier`: exact id, then exact name (case-insensitive), then name substring. -1 if none. */
28
- export declare function findEventIndex(events: unknown[], identifier: string): number;
48
+ /**
49
+ * Index of the event the identifier names: exact id, then exact name
50
+ * (case-insensitive), then name substring. -1 when nothing matches. Throws
51
+ * INVALID_INPUT when several events match the same way.
52
+ */
53
+ export declare function resolveEventIndex(events: unknown[], identifier: string): number;
29
54
  export declare function eventLabel(event: unknown): string;
30
55
  /**
31
- * True when every leaf value in `expected` is present and equal in `actual`.
32
- * Tolerates fields ProjectionLab adds on its own (e.g. generated IDs) but
33
- * catches dropped or changed values. Arrays must match in length and order.
56
+ * Key events for diffing by label plus occurrence number, so events sharing a
57
+ * name each keep their own entry. Ids are not used because a replacement
58
+ * section may omit them. The display name shows the occurrence only when the
59
+ * label repeats.
60
+ */
61
+ export declare function keyEvents(events: unknown[]): Map<string, {
62
+ name: string;
63
+ event: unknown;
64
+ }>;
65
+ /** The container kind of a section value, or null when it is not a usable
66
+ * container (null, undefined, a primitive, or a non-plain object). */
67
+ export declare function sectionContainer(value: unknown): "array" | "events" | "object" | null;
68
+ /**
69
+ * Refuse sectionData whose container shape does not match the section named.
70
+ * The expected container comes from the section's name, not from what the
71
+ * plan currently stores, so an event section held as {} still accepts the
72
+ * correct { events: [...] } replacement. One exception: when the stored
73
+ * section is itself a usable container that contradicts the table, a
74
+ * replacement in the stored shape is accepted too, so the tool never refuses
75
+ * to write back what it just read. An omitted value would delete the
76
+ * section, and a container of the wrong kind would store data ProjectionLab
77
+ * cannot read.
34
78
  */
79
+ export declare function assertSectionShape(section: string, planName: string, current: unknown, incoming: unknown): void;
35
80
  /**
36
81
  * True when every value in `expected` is present and equal in `actual`.
37
82
  * Objects may carry extra keys; arrays must match in length and order.
@@ -49,5 +94,23 @@ export declare function verifySectionApplied(submitted: unknown, actual: unknown
49
94
  ok: boolean;
50
95
  reason?: string;
51
96
  };
97
+ /**
98
+ * True when `after` still holds every input value of `before`.
99
+ * `lastUpdated`, `simKey`, and `meta` are left out: ProjectionLab saves every
100
+ * plan on array replacement and re-runs simulations, which may re-stamp
101
+ * lastUpdated and update computed fields (simKey, meta.runKey,
102
+ * meta.yearlySummary) on untouched plans.
103
+ */
104
+ export declare function samePlanContent(before: PlanObj, after: PlanObj): boolean;
105
+ /**
106
+ * Checks on a plan a write did not target, beyond its input content: its
107
+ * `lastUpdated` stamp may move forward (restorePlans saves every plan) but may
108
+ * not go backwards or disappear, and a `simKey` it carried may change value
109
+ * (ProjectionLab re-runs the plan open in the tab) but may not disappear. A
110
+ * plan that carried no simKey, or a stamp that is not a number on either side,
111
+ * is not reported, so absent fields never read as a change. `label` names the
112
+ * plan in each problem, for example `original plan p1`.
113
+ */
114
+ export declare function keptPlanIdentityProblems(label: string, before: PlanObj, after: PlanObj): string[];
52
115
  /** Field-by-field diff of two objects; values rendered as "from → to". */
53
116
  export declare function diffFields(current: unknown, incoming: unknown): Record<string, string>;
package/dist/plans.js CHANGED
@@ -10,34 +10,93 @@ export const PLAN_SECTIONS = [
10
10
  "montecarlo",
11
11
  "priorities",
12
12
  ];
13
+ /** Sections whose members are events addressed by id or name; the
14
+ * settings-like sections use projectionlab_update_plan_settings instead. */
15
+ export const EVENT_SECTIONS = [
16
+ "income",
17
+ "expenses",
18
+ "accounts",
19
+ "assets",
20
+ "milestones",
21
+ "priorities",
22
+ ];
23
+ /** The container each section holds, known from its name alone and not from
24
+ * what a plan happens to store: milestones is a direct array, the event
25
+ * sections are objects with an events array, and the settings-like sections
26
+ * are plain objects. Per DATA-MODEL.md sections 5.1 to 5.6 and the
27
+ * `variables`, `montecarlo` and `withdrawalStrategy` field trees. */
28
+ export const SECTION_CONTAINERS = {
29
+ income: "events",
30
+ expenses: "events",
31
+ accounts: "events",
32
+ assets: "events",
33
+ milestones: "array",
34
+ variables: "object",
35
+ withdrawalStrategy: "object",
36
+ montecarlo: "object",
37
+ priorities: "events",
38
+ };
13
39
  /**
14
- * Find a plan by identifier. Match priority: exact ID, exact name
15
- * (case-insensitive), then partial name. Exact matches win over partial so
16
- * plans with shared name prefixes can't be silently confused on write paths.
40
+ * Resolve one identifier against a list of { id, name } items. Match
41
+ * priority: exact ID, then exact name (case-insensitive), then name
42
+ * substring. Anything beyond a single match at the level reached is
43
+ * ambiguous: picking the first would silently target whichever item happens
44
+ * to come first, so the caller must refuse and ask for an exact name or ID.
17
45
  */
18
- export function findPlanIndex(plans, identifier) {
46
+ function matchOne(items, identifier) {
19
47
  if (!identifier.trim())
20
- return -1;
48
+ return { kind: "none" };
21
49
  const lower = identifier.toLowerCase();
22
- let exactName = -1;
23
- let partial = -1;
24
- for (let i = 0; i < plans.length; i++) {
25
- const plan = plans[i];
26
- if (!plan || typeof plan !== "object")
50
+ const exact = [];
51
+ const partial = [];
52
+ for (let i = 0; i < items.length; i++) {
53
+ const item = items[i];
54
+ if (!item || typeof item !== "object")
27
55
  continue;
28
- const obj = plan;
56
+ const obj = item;
29
57
  if (String(obj.id ?? "") === identifier)
30
- return i;
58
+ return { kind: "found", index: i };
31
59
  const name = String(obj.name ?? "").toLowerCase();
32
- if (name === lower && exactName === -1)
33
- exactName = i;
34
- else if (name.includes(lower) && partial === -1)
35
- partial = i;
60
+ if (name === lower)
61
+ exact.push(i);
62
+ else if (name.includes(lower))
63
+ partial.push(i);
64
+ }
65
+ if (exact.length === 1)
66
+ return { kind: "found", index: exact[0] };
67
+ if (exact.length > 1)
68
+ return { kind: "ambiguous", indexes: exact };
69
+ if (partial.length === 1)
70
+ return { kind: "found", index: partial[0] };
71
+ if (partial.length > 1)
72
+ return { kind: "ambiguous", indexes: partial };
73
+ return { kind: "none" };
74
+ }
75
+ function ambiguousMatchError(kind, identifier, items, indexes) {
76
+ const candidates = indexes
77
+ .map((i) => {
78
+ const obj = items[i];
79
+ return ` - "${obj.name ?? "(unnamed)"}" (ID: ${obj.id ?? "?"})`;
80
+ })
81
+ .join("\n");
82
+ return new ProjectionLabError("INVALID_INPUT", `More than one ${kind} matches "${identifier}"`, `Candidates:\n${candidates}\nSupply an exact name or one of the IDs`);
83
+ }
84
+ /**
85
+ * Index of the plan the identifier names, -1 when nothing matches. Throws
86
+ * INVALID_INPUT when several plans match the same way, so a read and the
87
+ * following write can never target different plans.
88
+ */
89
+ export function resolvePlanIndex(plans, identifier) {
90
+ const outcome = matchOne(plans, identifier);
91
+ if (outcome.kind === "ambiguous") {
92
+ throw ambiguousMatchError("plan", identifier, plans, outcome.indexes);
36
93
  }
37
- return exactName !== -1 ? exactName : partial;
94
+ return outcome.kind === "found" ? outcome.index : -1;
38
95
  }
39
- export function findPlan(plans, identifier) {
40
- const i = findPlanIndex(plans, identifier);
96
+ /** The plan the identifier names, or null when nothing matches. Refuses
97
+ * ambiguous identifiers the way resolvePlanIndex does. */
98
+ export function resolvePlan(plans, identifier) {
99
+ const i = resolvePlanIndex(plans, identifier);
41
100
  return i === -1 ? null : plans[i];
42
101
  }
43
102
  export function planNotFoundError(plans, identifier) {
@@ -87,27 +146,17 @@ function eventId(event) {
87
146
  }
88
147
  return undefined;
89
148
  }
90
- /** Index of the event matching `identifier`: exact id, then exact name (case-insensitive), then name substring. -1 if none. */
91
- export function findEventIndex(events, identifier) {
92
- if (!identifier.trim())
93
- return -1;
94
- const lower = identifier.toLowerCase();
95
- let exactName = -1;
96
- let partial = -1;
97
- for (let i = 0; i < events.length; i++) {
98
- const e = events[i];
99
- if (!e || typeof e !== "object")
100
- continue;
101
- const obj = e;
102
- if (String(obj.id ?? "") === identifier)
103
- return i;
104
- const name = String(obj.name ?? "").toLowerCase();
105
- if (name === lower && exactName === -1)
106
- exactName = i;
107
- else if (name.includes(lower) && partial === -1)
108
- partial = i;
149
+ /**
150
+ * Index of the event the identifier names: exact id, then exact name
151
+ * (case-insensitive), then name substring. -1 when nothing matches. Throws
152
+ * INVALID_INPUT when several events match the same way.
153
+ */
154
+ export function resolveEventIndex(events, identifier) {
155
+ const outcome = matchOne(events, identifier);
156
+ if (outcome.kind === "ambiguous") {
157
+ throw ambiguousMatchError("event", identifier, events, outcome.indexes);
109
158
  }
110
- return exactName !== -1 ? exactName : partial;
159
+ return outcome.kind === "found" ? outcome.index : -1;
111
160
  }
112
161
  export function eventLabel(event) {
113
162
  if (event && typeof event === "object") {
@@ -116,6 +165,30 @@ export function eventLabel(event) {
116
165
  }
117
166
  return "unnamed";
118
167
  }
168
+ /**
169
+ * Key events for diffing by label plus occurrence number, so events sharing a
170
+ * name each keep their own entry. Ids are not used because a replacement
171
+ * section may omit them. The display name shows the occurrence only when the
172
+ * label repeats.
173
+ */
174
+ export function keyEvents(events) {
175
+ const labelCounts = new Map();
176
+ for (const e of events) {
177
+ const label = eventLabel(e);
178
+ labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
179
+ }
180
+ const seen = new Map();
181
+ const keyed = new Map();
182
+ for (const e of events) {
183
+ const label = eventLabel(e);
184
+ const n = (seen.get(label) ?? 0) + 1;
185
+ seen.set(label, n);
186
+ const key = `${label}#${n}`;
187
+ const name = (labelCounts.get(label) ?? 0) > 1 ? `${label} #${n}` : label;
188
+ keyed.set(key, { name, event: e });
189
+ }
190
+ return keyed;
191
+ }
119
192
  function sectionIsEventBased(section) {
120
193
  if (Array.isArray(section))
121
194
  return true;
@@ -123,11 +196,59 @@ function sectionIsEventBased(section) {
123
196
  typeof section === "object" &&
124
197
  Array.isArray(section.events));
125
198
  }
199
+ function isPlainSectionObject(value) {
200
+ if (!value || typeof value !== "object" || Array.isArray(value))
201
+ return false;
202
+ const prototype = Object.getPrototypeOf(value);
203
+ return prototype === Object.prototype || prototype === null;
204
+ }
205
+ /** The container kind of a section value, or null when it is not a usable
206
+ * container (null, undefined, a primitive, or a non-plain object). */
207
+ export function sectionContainer(value) {
208
+ if (Array.isArray(value))
209
+ return "array";
210
+ if (isPlainSectionObject(value)) {
211
+ return Array.isArray(value.events) ? "events" : "object";
212
+ }
213
+ return null;
214
+ }
215
+ const CONTAINER_WORDS = {
216
+ array: "an array of events",
217
+ events: "an object with an events array",
218
+ object: "an object",
219
+ };
126
220
  /**
127
- * True when every leaf value in `expected` is present and equal in `actual`.
128
- * Tolerates fields ProjectionLab adds on its own (e.g. generated IDs) but
129
- * catches dropped or changed values. Arrays must match in length and order.
221
+ * Refuse sectionData whose container shape does not match the section named.
222
+ * The expected container comes from the section's name, not from what the
223
+ * plan currently stores, so an event section held as {} still accepts the
224
+ * correct { events: [...] } replacement. One exception: when the stored
225
+ * section is itself a usable container that contradicts the table, a
226
+ * replacement in the stored shape is accepted too, so the tool never refuses
227
+ * to write back what it just read. An omitted value would delete the
228
+ * section, and a container of the wrong kind would store data ProjectionLab
229
+ * cannot read.
130
230
  */
231
+ export function assertSectionShape(section, planName, current, incoming) {
232
+ const incomingShape = sectionContainer(incoming);
233
+ if (incomingShape === null) {
234
+ const got = incoming === undefined
235
+ ? "missing"
236
+ : incoming === null
237
+ ? "null"
238
+ : Array.isArray(incoming)
239
+ ? "an array"
240
+ : typeof incoming;
241
+ throw new ProjectionLabError("INVALID_INPUT", `sectionData for "${section}" of plan "${planName}" is ${got}; a section is ${CONTAINER_WORDS.array} or ${CONTAINER_WORDS.object}`, "Send the complete section in the same shape projectionlab_get_plan_section returns: an array for milestones, an object with an events array for event sections, an object for settings-like sections");
242
+ }
243
+ const expected = SECTION_CONTAINERS[section];
244
+ const accepted = new Set([expected]);
245
+ const currentShape = sectionContainer(current);
246
+ if (currentShape !== null && currentShape !== expected)
247
+ accepted.add(currentShape);
248
+ if (!accepted.has(incomingShape)) {
249
+ throw new ProjectionLabError("INVALID_INPUT", `sectionData for "${section}" of plan "${planName}" must be ${CONTAINER_WORDS[expected]}, which is how this section is stored; received ${CONTAINER_WORDS[incomingShape]}`, `Read the section with projectionlab_get_plan_section and send the replacement in the container shape the section name calls for`);
250
+ }
251
+ }
131
252
  /**
132
253
  * True when every value in `expected` is present and equal in `actual`.
133
254
  * Objects may carry extra keys; arrays must match in length and order.
@@ -191,6 +312,49 @@ export function verifySectionApplied(submitted, actual) {
191
312
  }
192
313
  return { ok: true };
193
314
  }
315
+ /**
316
+ * True when `after` still holds every input value of `before`.
317
+ * `lastUpdated`, `simKey`, and `meta` are left out: ProjectionLab saves every
318
+ * plan on array replacement and re-runs simulations, which may re-stamp
319
+ * lastUpdated and update computed fields (simKey, meta.runKey,
320
+ * meta.yearlySummary) on untouched plans.
321
+ */
322
+ export function samePlanContent(before, after) {
323
+ const { lastUpdated: _stamp, simKey: _key, meta: _meta, ...content } = before;
324
+ return containsSubset(content, after);
325
+ }
326
+ /**
327
+ * Checks on a plan a write did not target, beyond its input content: its
328
+ * `lastUpdated` stamp may move forward (restorePlans saves every plan) but may
329
+ * not go backwards or disappear, and a `simKey` it carried may change value
330
+ * (ProjectionLab re-runs the plan open in the tab) but may not disappear. A
331
+ * plan that carried no simKey, or a stamp that is not a number on either side,
332
+ * is not reported, so absent fields never read as a change. `label` names the
333
+ * plan in each problem, for example `original plan p1`.
334
+ */
335
+ export function keptPlanIdentityProblems(label, before, after) {
336
+ const problems = [];
337
+ const simKey = before.simKey;
338
+ if (simKey !== undefined && simKey !== null) {
339
+ const current = after.simKey;
340
+ if (current === undefined || current === null) {
341
+ problems.push(`${label} lost its simKey`);
342
+ }
343
+ }
344
+ const was = before.lastUpdated;
345
+ const now = after.lastUpdated;
346
+ if (was !== undefined && was !== null) {
347
+ if (now === undefined || now === null) {
348
+ problems.push(`${label} lost its lastUpdated`);
349
+ }
350
+ else if (typeof was === "number" &&
351
+ typeof now === "number" &&
352
+ now < was) {
353
+ problems.push(`${label} lastUpdated moved backwards from ${was} to ${now}`);
354
+ }
355
+ }
356
+ return problems;
357
+ }
194
358
  /** Field-by-field diff of two objects; values rendered as "from → to". */
195
359
  export function diffFields(current, incoming) {
196
360
  const cur = (current ?? {});