dsh-ptc-plus 0.3.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.en.md +63 -80
  2. package/README.md +63 -80
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-repair-zh.png +0 -0
  7. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  8. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  9. package/assets/ptc-plus-settings-en.png +0 -0
  10. package/assets/ptc-plus-settings-zh.png +0 -0
  11. package/client.js +30827 -742
  12. package/compiler-amaro.cjs +5 -0
  13. package/compiler-amaro.cjs.map +6 -0
  14. package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
  15. package/compiler-assets.json +3 -0
  16. package/compiler-core.cjs +556 -0
  17. package/compiler-core.cjs.map +6 -0
  18. package/compiler-platform.cjs +2 -0
  19. package/compiler-platform.cjs.map +6 -0
  20. package/compiler-typescript.cjs +388 -0
  21. package/compiler-typescript.cjs.map +6 -0
  22. package/docs/README.md +7 -0
  23. package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
  24. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
  26. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
  27. package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
  28. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
  29. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
  30. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
  32. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
  33. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  34. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
  35. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
  36. package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
  37. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  38. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
  39. package/docs/adr/0023-global-user-bindings.md +104 -0
  40. package/docs/adr/0024-repl-console-observation.md +33 -0
  41. package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
  42. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
  43. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
  44. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
  45. package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
  46. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
  47. package/docs/architecture.md +77 -21
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +95 -19
  51. package/docs/durability-design.md +28 -10
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/publishing.md +5 -3
  55. package/docs/runtime-reference.md +76 -25
  56. package/docs/semantic-validation.md +29 -0
  57. package/docs/stateful-binding-updates-proposal.md +759 -0
  58. package/docs/user-bindings.en.md +66 -0
  59. package/docs/user-bindings.md +66 -0
  60. package/docs/value-wire.md +3 -3
  61. package/docs/verification-optimization-work.md +41 -0
  62. package/docs/verification.md +71 -0
  63. package/index.js +369 -92
  64. package/internal/ast-traversal.js +5 -2
  65. package/internal/async-local-storage-intrinsics.js +38 -0
  66. package/internal/binding-declaration-contract.js +8 -0
  67. package/internal/binding-descriptors.js +1 -0
  68. package/internal/binding-pattern-traversal.js +25 -0
  69. package/internal/binding-pattern.js +26 -29
  70. package/internal/binding-source-analysis.js +708 -0
  71. package/internal/binding-update-policy.js +72 -0
  72. package/internal/bounded-worker-round.js +56 -0
  73. package/internal/callable-definition-environment.js +42 -0
  74. package/internal/callable-source-catalog.js +129 -0
  75. package/internal/callable-source-encoding.js +47 -0
  76. package/internal/callable-source-facts.js +459 -0
  77. package/internal/callable-source-format.js +2 -0
  78. package/internal/callable-source-lowering.js +123 -0
  79. package/internal/cell-analysis-contract.js +25 -0
  80. package/internal/cell-analysis.js +253 -290
  81. package/internal/cell-completion.js +15 -0
  82. package/internal/cell-error.js +7 -0
  83. package/internal/cell-parser.js +66 -0
  84. package/internal/cell-rewriter.js +3 -677
  85. package/internal/commonjs-export-evidence.js +63 -0
  86. package/internal/commonjs-source-evidence.js +72 -0
  87. package/internal/compiler-amaro.cjs +2 -0
  88. package/internal/compiler-compression.js +12 -0
  89. package/internal/compiler-data.js +58 -0
  90. package/internal/compiler-descriptors.js +54 -0
  91. package/internal/compiler-dynamic-cache.js +48 -0
  92. package/internal/compiler-entry.js +35 -0
  93. package/internal/compiler-generated-names.js +17 -0
  94. package/internal/compiler-intrinsics.js +50 -0
  95. package/internal/compiler-module-links.js +37 -0
  96. package/internal/compiler-operations.js +61 -0
  97. package/internal/compiler-platform-factory.js +84 -0
  98. package/internal/compiler-platform.js +136 -0
  99. package/internal/compiler-record-roles.js +53 -0
  100. package/internal/compiler-region-output.js +259 -0
  101. package/internal/compiler-resource-lowering.js +103 -0
  102. package/internal/compiler-scope-facts.js +115 -0
  103. package/internal/compiler-service.js +109 -0
  104. package/internal/compiler-source-cache.js +32 -0
  105. package/internal/compiler-source-pieces.js +93 -0
  106. package/internal/compiler-source-regions.js +132 -0
  107. package/internal/compiler-storage-source.js +54 -0
  108. package/internal/compiler-text.js +15 -0
  109. package/internal/compiler-typescript.cjs +2 -0
  110. package/internal/config-spec.js +83 -27
  111. package/internal/cordis-tools-owner.js +130 -21
  112. package/internal/diagnostic.js +27 -1
  113. package/internal/direct-surface-owner.js +76 -23
  114. package/internal/dynamic-binding-evidence.js +16 -0
  115. package/internal/dynamic-environment-compiler.js +779 -0
  116. package/internal/dynamic-environment-integration.js +91 -0
  117. package/internal/dynamic-environment-runtime.js +383 -0
  118. package/internal/dynamic-native-calls.js +153 -0
  119. package/internal/dynamic-native-runtime.js +262 -0
  120. package/internal/dynamic-scope-analysis.js +90 -0
  121. package/internal/edit-transport-owner.js +27 -2
  122. package/internal/execution-seam-compat.js +193 -0
  123. package/internal/failure-reporting.js +136 -28
  124. package/internal/host-rpc.js +66 -0
  125. package/internal/isolated-worker.js +555 -0
  126. package/internal/kernel-child.js +139 -0
  127. package/internal/kernel-worker.js +1179 -233
  128. package/internal/language-semantics.js +14 -0
  129. package/internal/legacy-binding-catalog.js +48 -0
  130. package/internal/legacy-cell-analysis.js +600 -0
  131. package/internal/legacy-cell-parser.js +50 -0
  132. package/internal/legacy-cell-rewriter.js +631 -0
  133. package/internal/legacy-repl-convenience.js +291 -0
  134. package/internal/managed-module-operations.js +170 -0
  135. package/internal/module-compilation.js +322 -0
  136. package/internal/module-runtime-platform.js +19 -0
  137. package/internal/module-transform-contract.js +42 -0
  138. package/internal/native-await-declarations.js +68 -0
  139. package/internal/native-root-compilation.js +54 -0
  140. package/internal/native-root-dynamic.js +67 -0
  141. package/internal/record-utils.js +24 -15
  142. package/internal/recovery-tips.js +27 -15
  143. package/internal/rejected-cell-editor.js +13 -8
  144. package/internal/repl-convenience.js +2 -289
  145. package/internal/repl-memory-projection.js +81 -12
  146. package/internal/repl-observation-interest.js +70 -0
  147. package/internal/repl-rewrite-contract.js +6 -0
  148. package/internal/repl-scope-normalizer.js +2091 -0
  149. package/internal/repl-value-observer.js +117 -0
  150. package/internal/rpc-contract.js +52 -0
  151. package/internal/runtime-bridge-owner.js +172 -28
  152. package/internal/runtime-config.js +48 -52
  153. package/internal/runtime-contexts.js +82 -10
  154. package/internal/runtime-intrinsics.js +101 -0
  155. package/internal/runtime-messages.js +111 -0
  156. package/internal/session-cell-executor.js +402 -87
  157. package/internal/session-journal-recovery.js +684 -0
  158. package/internal/session-journal-schema.js +123 -0
  159. package/internal/session-journal.js +199 -654
  160. package/internal/session-log-view.js +42 -9
  161. package/internal/session-runtime.js +257 -95
  162. package/internal/session-state.js +247 -71
  163. package/internal/source-position-map.js +230 -71
  164. package/internal/stateful-module-compiler.js +154 -0
  165. package/internal/stateful-module-runtime.js +303 -0
  166. package/internal/stateful-root-compiler.js +600 -0
  167. package/internal/stateful-root-runtime.js +340 -0
  168. package/internal/typert-codec-compat.js +28 -0
  169. package/internal/typescript-transform.js +31 -0
  170. package/internal/user-binding-authoring.js +70 -0
  171. package/internal/user-binding-console-worker.js +244 -0
  172. package/internal/user-binding-console.js +148 -0
  173. package/internal/user-binding-draft-projection.js +335 -0
  174. package/internal/user-binding-model-context.js +31 -0
  175. package/internal/user-binding-runner.js +56 -0
  176. package/internal/user-bindings-owner.js +1065 -0
  177. package/internal/user-bindings-store.js +293 -0
  178. package/internal/user-bindings.js +361 -0
  179. package/internal/value-wire-schema.js +28 -0
  180. package/internal/value-wire.js +130 -91
  181. package/internal/worker-client.js +57 -67
  182. package/internal/worker-cwd-virtualization.js +80 -53
  183. package/internal/worker-environment.js +65 -0
  184. package/internal/worker-output-relay.js +29 -0
  185. package/internal/worker-repl-realm.js +134 -0
  186. package/internal/worker-shutdown.js +14 -0
  187. package/package.json +76 -7
  188. package/src/client-activity.js +95 -53
  189. package/src/client-authoring-view.js +580 -0
  190. package/src/client-binding-review.js +230 -0
  191. package/src/client-bindings-data.js +29 -0
  192. package/src/client-catalog.js +127 -0
  193. package/src/client-code-editor.js +84 -0
  194. package/src/client-console.js +112 -0
  195. package/src/client-copy.js +370 -0
  196. package/src/client-feature-gates.js +62 -0
  197. package/src/client-host-compat.js +150 -0
  198. package/src/client-indicator-view.js +267 -0
  199. package/src/client-repl-view.js +292 -0
  200. package/src/client-rpc.js +20 -0
  201. package/src/client-settings-view.js +221 -0
  202. package/src/client-styles.js +106 -0
  203. package/src/client-tool-view.js +108 -0
  204. package/src/client-transport.js +159 -0
  205. package/src/client-workbench.js +521 -0
  206. package/src/client.js +233 -615
@@ -1,3 +1,5 @@
1
+ import { recoveryTipIdentity } from './runtime-messages.js'
2
+
1
3
  const PLATFORM_CAUSE_CODES = new Set(['EACCES', 'EINVAL', 'ENOTDIR', 'ENOENT', 'UNKNOWN'])
2
4
  const TIP_CONTEXT_PREFIX = 'tools:ptc-plus-tip/'
3
5
  const TIP_PREFIXES = Object.freeze({
@@ -10,16 +12,20 @@ function tipHistory(view) {
10
12
  const active = new Map()
11
13
  const seen = new Set()
12
14
  const history = []
13
- for (const snapshot of view.systemPromptSnapshots) {
15
+ const snapshots = [
16
+ ...view.systemPromptSnapshots,
17
+ ...(view.ptcMessages ?? []).filter(record => record.form === 'notice').map(record => ({
18
+ ...record, sections: [{ name: record.name, text: record.text }],
19
+ })),
20
+ ].sort((left, right) => left.index - right.index)
21
+ for (const snapshot of snapshots) {
14
22
  const next = new Map(snapshot.sections.map(section => [section.name, section.text]))
15
23
  for (const [name] of next) {
16
24
  if (active.has(name) || seen.has(name)) continue
17
- const match = /^tools:ptc-plus-tip\/(repeated-binding-failure|platform-command-failure)\/([1-9][0-9]*)$/.exec(name)
18
- if (match === null) continue
19
- const ordinal = Number(match[2])
20
- if (!Number.isSafeInteger(ordinal)) continue
25
+ const identity = recoveryTipIdentity(name)
26
+ if (identity === undefined) continue
21
27
  seen.add(name)
22
- history.push({ id: match[1], ordinal, index: snapshot.index, contextStep: snapshot.contextStep })
28
+ history.push({ ...identity, index: snapshot.index, contextStep: snapshot.contextStep })
23
29
  }
24
30
  active.clear()
25
31
  for (const entry of next) active.set(...entry)
@@ -36,7 +42,7 @@ function hasPlatformCommandDiagnostic(diagnostics) {
36
42
  const causeCode = typeof diagnostic.cause?.code === 'string' ? diagnostic.cause.code.toUpperCase() : undefined
37
43
  if (causeCode !== undefined && PLATFORM_CAUSE_CODES.has(causeCode)
38
44
  && /\b(?:spawn|exec(?:ute)?|command|executable|shell|path|file)\b/i.test(text)) return true
39
- return /(?:spawn|exec(?:ute)?|child process)\b.*(?:failed|error|not found|cannot find)/i.test(text)
45
+ return /\b(?:spawn|exec(?:ute)?|child process)\b.*\b(?:ENOENT|ENOTDIR|EACCES|EINVAL)\b/i.test(text)
40
46
  || /command not found|no such file|not recognized as (?:an )?internal|cannot find (?:the )?(?:path|file)/i.test(text)
41
47
  })
42
48
  }
@@ -45,7 +51,10 @@ function tipCandidate(view) {
45
51
  const { args, journal } = view.latestRun ?? {}
46
52
  if (args === undefined || journal === undefined) return undefined
47
53
  const codes = new Set(journal.diagnostics.map(diagnostic => diagnostic.code))
48
- if (codes.has('PTC-W001')) return { id: 'repeated-binding-failure' }
54
+ if (codes.has('PTC-W001')) return {
55
+ id: 'repeated-binding-failure',
56
+ capability: journal.diagnostics.some(item => item.code === 'PTC-W001' && item.cause?.code === 'PTC-CAPABILITY'),
57
+ }
49
58
  if (hasPlatformCommandDiagnostic(journal.diagnostics)) return { id: 'platform-command-failure' }
50
59
  return undefined
51
60
  }
@@ -56,6 +65,9 @@ function renderTip(id, detailed, candidate = {}) {
56
65
  ? `${TIP_PREFIXES[id]} Re-check the active execution world and the actual executable before retrying. Use direct argv for a normal executable; use a shell only when its syntax or resolution is required. Windows, WSL, POSIX, and package shims have different paths and launch rules.`
57
66
  : `${TIP_PREFIXES[id]} Inspect the executable or path in the current execution world and choose direct argv or a shell only when required; do not assume Windows, WSL, POSIX, or one shell.`
58
67
  }
68
+ if (!candidate.capability) {
69
+ return `${TIP_PREFIXES[id]} Inspect the visible cell source and results for the local name, scope, initialization, or declaration conflict. Correct that expression before continuing; capability discovery does not list session-local variables.${detailed ? ' Earlier statements may have run; use current expression evidence without assuming a failed declaration initialized its binding.' : ''}`
70
+ }
59
71
  return detailed
60
72
  ? `${TIP_PREFIXES[id]} Inspect the live request with \`capabilities.tree()\`, \`capabilities.find()\`, or \`capabilities.inspect()\`, then call the typed member through \`tools.*\`. Do not invent hidden bindings or repeat the same failing expression.`
61
73
  : `${TIP_PREFIXES[id]} Inspect the live request with \`capabilities.tree()\`, \`capabilities.find()\`, or \`capabilities.inspect()\`, then call the typed member through \`tools.*\`.`
@@ -65,14 +77,14 @@ export function latestRecoveryTip(view, config) {
65
77
  if (!config.enabled) return undefined
66
78
  const candidate = tipCandidate(view)
67
79
  if (candidate === undefined) return undefined
68
- const history = tipHistory(view)
69
- const lastTip = history.at(-1)
80
+ // Cooldown, escalation, and ordinal are per trigger kind: one kind's tip must
81
+ // not suppress a different kind.
82
+ const matching = tipHistory(view).filter(item => item.id === candidate.id)
83
+ const lastTip = matching.at(-1)
70
84
  if (lastTip !== undefined && view.contextStep - lastTip.contextStep < config.cooldownMessages) return undefined
71
- const unresolved = history.filter(item => item.id === candidate.id && item.index > (view.lastSuccessfulRunIndex ?? -1))
72
- const ordinal = history.reduce(
73
- (highest, item) => item.id === candidate.id ? Math.max(highest, item.ordinal) : highest,
74
- 0,
75
- ) + 1
85
+ const unresolved = matching.filter(item => item.index > (view.lastSuccessfulRunIndex ?? -1))
86
+ const ordinal = matching.reduce((highest, item) => Math.max(highest, item.ordinal), 0) + 1
87
+ if (!Number.isSafeInteger(ordinal)) return undefined
76
88
  return {
77
89
  name: `${TIP_CONTEXT_PREFIX}${candidate.id}/${ordinal}`,
78
90
  text: renderTip(candidate.id, unresolved.length >= config.escalationFailures, candidate),
@@ -270,7 +270,7 @@ function editRunCodeParameterBranch(operation, operationSchema) {
270
270
  export function editRunCodeSchema() {
271
271
  return {
272
272
  name: 'edit_run_code',
273
- description: 'Make a small exact or regular-expression change to the most recent eligible cell captured when this edit call is dispatched, then run the complete corrected cell. A successful edit becomes the next eligible cell. Use this only when replaying the whole cell is safe. If earlier code may already have caused an external effect, use a new run_code cell and its existing variables instead; this tool does not resume at the error location. Send exactly one atomic edits or regex_edits array, plus expected_target_call_seq only when a diagnostic supplies it to bind a validated repair to its rejected cell. Choose edits for a few unique literal fragments; choose regex_edits when one counted pattern covers repeated fragments. Every resolved range must be non-overlapping in the original cell.',
273
+ description: 'Edit the most recent eligible cell captured at dispatch and run the complete corrected cell. A successful edit becomes the next target. This reruns the entire cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.',
274
274
  parameters: {
275
275
  type: 'object',
276
276
  oneOf: [
@@ -288,13 +288,13 @@ export function editRunCodeSchema() {
288
288
  }),
289
289
  editRunCodeParameterBranch('regex_edits', {
290
290
  type: 'array', minItems: 1, maxItems: EDIT_LIMITS.regexEdits,
291
- description: 'Atomic regular-expression replacements, all matched against the original target cell.',
291
+ description: 'Atomic regular-expression replacements, all matched against the original target cell with JavaScript pattern and replacement semantics; each replacement is a JavaScript template (see replacement).',
292
292
  items: {
293
293
  type: 'object', additionalProperties: false,
294
294
  properties: {
295
- pattern: { type: 'string', minLength: 1, description: 'JavaScript regular-expression source. Zero-length matches are rejected.' },
295
+ pattern: { type: 'string', minLength: 1, description: 'JavaScript regular-expression source without delimiters or flags (for example \\d+px, not /\\d+px/). Zero-length matches are rejected.' },
296
296
  flags: { type: 'string', pattern: '^(?!.*(.).*\\1)[gimsu]*$', description: 'Unique JavaScript g, i, m, s, or u flags. Global matching is always applied.' },
297
- replacement: { type: 'string', description: 'JavaScript replacement template, including capture references such as $1 or $<name>.' },
297
+ replacement: { type: 'string', description: 'JavaScript replacement template. Reference captures as $1, $2, ... or $<name> (for example "<$1>" inserts capture 1); backslash escapes such as \\1 are NOT capture references and stay literal text; write a literal dollar sign as $$.' },
298
298
  expected_matches: { type: 'integer', minimum: 1, maximum: EDIT_LIMITS.regexMatches, description: 'Exact match count required before any replacement is applied.' },
299
299
  },
300
300
  required: ['pattern', 'flags', 'replacement', 'expected_matches'],
@@ -310,11 +310,16 @@ export function editRejectedCell(value, source, timeoutMs = DEFAULT_REGEX_TIMEOU
310
310
  return rejection('edit_run_code expects an object with exactly one edits or regex_edits array')
311
311
  }
312
312
  const keys = Object.keys(value)
313
+ const unsupported = keys.find(key => !['edits', 'regex_edits', EXPECTED_TARGET_CALL_SEQ].includes(key))
314
+ if (unsupported !== undefined) {
315
+ return rejection(`edit_run_code does not accept field ${JSON.stringify(unsupported)}; remove it and retry the edit`)
316
+ }
313
317
  const operationKeys = keys.filter(key => key === 'edits' || key === 'regex_edits')
314
- if (operationKeys.length !== 1 || keys.length > 2
315
- || keys.some(key => !['edits', 'regex_edits', EXPECTED_TARGET_CALL_SEQ].includes(key))
316
- || !Array.isArray(value[operationKeys[0]])) {
317
- return rejection('edit_run_code expects exactly one edits or regex_edits array')
318
+ if (operationKeys.length !== 1) {
319
+ return rejection('edit_run_code expects exactly one of edits or regex_edits; supply one operation and omit the other')
320
+ }
321
+ if (!Array.isArray(value[operationKeys[0]])) {
322
+ return rejection(`edit_run_code ${operationKeys[0]} must be an array`)
318
323
  }
319
324
  if (Object.hasOwn(value, EXPECTED_TARGET_CALL_SEQ)
320
325
  && (!Number.isSafeInteger(value[EXPECTED_TARGET_CALL_SEQ])
@@ -1,289 +1,2 @@
1
- import { bindingNodes, createGeneratedNameAllocator } from './binding-pattern.js'
2
- import {
3
- CLASS_REDECLARATION_REWRITE,
4
- FUNCTION_REDECLARATION_REWRITE,
5
- MIXED_REDECLARATION_REWRITE,
6
- redeclarationCommitTarget,
7
- } from './repl-rewrite-contract.js'
8
- import { applySourceEdits, createMappedTextBuilder, identitySourceMap } from './source-position-map.js'
9
- import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
10
-
11
- /**
12
- * Optional compatibility policy for a persistent REPL. The cell language does
13
- * not own this policy: callers may replace it when persistence uses separate
14
- * lexical frames instead of one shared REPL environment.
15
- */
16
-
17
- function containsTopLevelAwait(node) {
18
- let found = false
19
- walkAst(node, current => {
20
- if (current.type === 'AwaitExpression') found = true
21
- if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression'
22
- || current.type === 'ArrowFunctionExpression') return SKIP_AST_CHILDREN
23
- })
24
- return found
25
- }
26
-
27
- function mixedDeclaratorText(statement, declarator, bindings, code, offset, allocateName) {
28
- const fresh = bindings.filter(binding => !binding.existing)
29
- const existing = bindings.filter(binding => binding.existing)
30
- const valueName = allocateName('mixed_value')
31
- const commitName = allocateName('mixed_commit')
32
- const valuesName = allocateName('mixed_values')
33
- const positions = new Map(bindings.map((binding, index) => [binding.name, index]))
34
- const asynchronous = containsTopLevelAwait(declarator.id)
35
- const builder = createMappedTextBuilder(code, offset)
36
- const appendBindingName = binding => builder.appendMapped(binding.name, binding.start, binding.end)
37
- builder.append(statement.kind === 'var' ? 'var [' : 'let [')
38
- fresh.forEach((binding, index) => {
39
- if (index > 0) builder.append(', ')
40
- appendBindingName(binding)
41
- })
42
- builder.append(`] = ${asynchronous ? 'await ' : ''}(`)
43
- builder.append(`(${asynchronous ? 'async ' : ''}(${valueName}, ${commitName}) => { ${statement.kind} `)
44
- builder.appendSource(declarator.id.start, declarator.id.end)
45
- builder.append(` = ${valueName}; return ${commitName}([`)
46
- bindings.forEach((binding, index) => {
47
- if (index > 0) builder.append(', ')
48
- appendBindingName(binding)
49
- })
50
- builder.append(`]); })((`)
51
- if (declarator.init === null) builder.append('undefined')
52
- else builder.appendSource(declarator.init.start, declarator.init.end)
53
- builder.append(`), (${valuesName}) => { `)
54
- existing.forEach((binding, index) => {
55
- if (index > 0) builder.append(' ')
56
- appendBindingName(binding)
57
- builder.append(` = ${valuesName}[${positions.get(binding.name)}];`)
58
- })
59
- builder.append(' return [')
60
- fresh.forEach((binding, index) => {
61
- if (index > 0) builder.append(', ')
62
- builder.append(valuesName)
63
- builder.append(`[${positions.get(binding.name)}]`)
64
- })
65
- builder.append(']; }) );')
66
- return builder.result()
67
- }
68
-
69
- function existingDeclaratorText(declarator, code, offset) {
70
- const builder = createMappedTextBuilder(code, offset)
71
- builder.append(';(')
72
- builder.appendSource(declarator.id.start, declarator.id.end)
73
- builder.append(' = ')
74
- if (declarator.init === null) builder.append('undefined')
75
- else builder.appendSource(declarator.init.start, declarator.init.end)
76
- builder.append(');')
77
- return builder.result()
78
- }
79
-
80
- function freshDeclaratorText(statement, declarator, code, offset) {
81
- const builder = createMappedTextBuilder(code, offset)
82
- builder.append(statement.kind === 'var' ? 'var ' : 'let ')
83
- builder.appendSource(declarator.start, declarator.end)
84
- builder.append(';')
85
- return builder.result()
86
- }
87
-
88
- function bindingEntries(pattern, declarationSpan) {
89
- return bindingNodes(pattern).map(node => ({
90
- name: node.name,
91
- start: node.start,
92
- end: node.end,
93
- span: declarationSpan(node),
94
- }))
95
- }
96
-
97
- function existingFunctionText(statement, code, offset) {
98
- const builder = createMappedTextBuilder(code, offset)
99
- builder.append(';(')
100
- builder.appendMapped(statement.id.name, statement.id.start, statement.id.end)
101
- builder.append(' = ')
102
- builder.appendSource(statement.start, statement.id.start)
103
- builder.appendSource(statement.id.end, statement.end)
104
- builder.append(');')
105
- return builder.result()
106
- }
107
-
108
- function existingClassText(statement, code, offset) {
109
- const builder = createMappedTextBuilder(code, offset)
110
- builder.append(';(')
111
- builder.appendMapped(statement.id.name, statement.id.start, statement.id.end)
112
- builder.append(' = ')
113
- builder.appendSource(statement.start, statement.end)
114
- builder.append(');')
115
- return builder.result()
116
- }
117
-
118
- export function rewriteReplRedeclarations({
119
- code,
120
- sourceMap = identitySourceMap(code.length),
121
- body,
122
- offset,
123
- knownBindings,
124
- variableRedeclarationBindings = knownBindings,
125
- writableBindings,
126
- declarations,
127
- declarationSpan,
128
- collisionFor,
129
- bindingPolicy,
130
- autoSplitRedeclarations,
131
- commitSignal,
132
- }) {
133
- const redeclared = []
134
- const rewrites = []
135
- if (body === undefined) {
136
- return {
137
- executableCode: code,
138
- executableSourceMap: sourceMap,
139
- collisions: declarations.filter(declaration => knownBindings.has(declaration.name)).map(collisionFor),
140
- redeclared,
141
- rewrites,
142
- }
143
- }
144
- const replacements = []
145
- const rejected = []
146
- const allocateName = createGeneratedNameAllocator(body)
147
- for (const statement of body) {
148
- if (statement.type !== 'VariableDeclaration') {
149
- if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
150
- && statement.id !== null && knownBindings.has(statement.id.name)) {
151
- const kind = statement.type === 'ClassDeclaration' ? 'class' : 'function'
152
- if (!writableBindings.has(statement.id.name)) {
153
- rejected.push({
154
- name: statement.id.name,
155
- kind,
156
- reason: 'binding-not-writable',
157
- span: declarationSpan(statement.id),
158
- })
159
- } else if (!bindingPolicy.functionClassRedeclarations) {
160
- rejected.push({
161
- name: statement.id.name,
162
- kind,
163
- reason: 'function-class-redeclarations-disabled',
164
- span: declarationSpan(statement.id),
165
- })
166
- } else {
167
- const commitDependency = redeclarationCommitTarget(statement.id.name, statement.start)
168
- const replacement = kind === 'class'
169
- ? existingClassText(statement, code, offset)
170
- : existingFunctionText(statement, code, offset)
171
- replacement.text += ` void 0; this[${JSON.stringify(commitSignal)}](${JSON.stringify(commitDependency)});`
172
- replacements.push({
173
- start: statement.start - offset,
174
- end: statement.end - offset,
175
- text: replacement.text,
176
- mappings: replacement.mappings,
177
- })
178
- redeclared.push(collisionFor({
179
- name: statement.id.name,
180
- kind,
181
- commitDependency,
182
- span: declarationSpan(statement.id),
183
- }))
184
- rewrites.push({
185
- kind: 'redeclaration',
186
- description: kind === 'class'
187
- ? CLASS_REDECLARATION_REWRITE
188
- : FUNCTION_REDECLARATION_REWRITE,
189
- source: statement.id.name,
190
- })
191
- }
192
- }
193
- continue
194
- }
195
- const entries = []
196
- let statementRejected = false
197
- for (const declarator of statement.declarations) {
198
- const bindings = bindingEntries(declarator.id, declarationSpan)
199
- const existing = bindings.filter(binding => variableRedeclarationBindings.has(binding.name))
200
- const immutable = existing.filter(binding => !writableBindings.has(binding.name))
201
- if (immutable.length > 0) {
202
- rejected.push(...immutable.map(binding => ({
203
- ...binding,
204
- reason: 'binding-not-writable',
205
- })))
206
- statementRejected = true
207
- continue
208
- }
209
- if (existing.length > 0 && !bindingPolicy.variableRedeclarations) {
210
- rejected.push(...existing.map(binding => ({
211
- ...binding,
212
- reason: 'variable-redeclarations-disabled',
213
- })))
214
- statementRejected = true
215
- continue
216
- }
217
- if (existing.length > 0 && existing.length < bindings.length) {
218
- if (autoSplitRedeclarations) entries.push({ declarator, bindings, existing })
219
- else {
220
- rejected.push(...existing)
221
- statementRejected = true
222
- }
223
- continue
224
- }
225
- entries.push({ declarator, bindings, existing })
226
- }
227
- if (statementRejected) continue
228
- if (!entries.some(entry => entry.existing.length > 0)) {
229
- if (statement.kind === 'const' && bindingPolicy.variableRedeclarations) {
230
- replacements.push({
231
- start: statement.start - offset,
232
- end: statement.start - offset + statement.kind.length,
233
- text: 'let',
234
- })
235
- }
236
- continue
237
- }
238
- let replacementText = ''
239
- const replacementMappings = []
240
- const appendPart = (part) => {
241
- if (replacementText.length > 0) replacementText += '\n'
242
- const partOffset = replacementText.length
243
- replacementText += part.text
244
- part.mappings.forEach(mapping => replacementMappings.push({
245
- ...mapping,
246
- generatedStart: mapping.generatedStart + partOffset,
247
- generatedEnd: mapping.generatedEnd + partOffset,
248
- }))
249
- }
250
- for (const { declarator, bindings, existing } of entries) {
251
- if (existing.length > 0 && existing.length < bindings.length) {
252
- appendPart(mixedDeclaratorText(
253
- statement,
254
- declarator,
255
- bindings.map(binding => ({ ...binding, existing: existing.includes(binding) })),
256
- code,
257
- offset,
258
- allocateName,
259
- ))
260
- redeclared.push(...existing.map(collisionFor))
261
- rewrites.push({
262
- kind: 'redeclaration',
263
- description: MIXED_REDECLARATION_REWRITE,
264
- source: existing.map(binding => binding.name).join(', '),
265
- })
266
- } else if (existing.length === bindings.length && bindings.length > 0) {
267
- redeclared.push(...existing.map(collisionFor))
268
- appendPart(existingDeclaratorText(declarator, code, offset))
269
- } else {
270
- appendPart(freshDeclaratorText(statement, declarator, code, offset))
271
- }
272
- }
273
- replacements.push({
274
- start: statement.start - offset,
275
- end: statement.end - offset,
276
- text: replacementText,
277
- mappings: replacementMappings,
278
- })
279
- }
280
- const collisions = rejected.map(collisionFor)
281
- let executableCode = code
282
- let executableSourceMap = sourceMap
283
- if (collisions.length === 0) {
284
- const applied = applySourceEdits(code, sourceMap, replacements)
285
- executableCode = applied.code
286
- executableSourceMap = applied.sourceMap
287
- }
288
- return { executableCode, executableSourceMap, collisions, redeclared, rewrites }
289
- }
1
+ /** Historical declaration adapter; new cells use logical binding identities. */
2
+ export { rewriteReplRedeclarations } from './legacy-repl-convenience.js'
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from './record-utils.js'
2
+ import { REPL_TOOL_NAMES } from './session-journal-schema.js'
2
3
 
3
4
  export const REPL_MEMORY_KEY = 'ptcPlusRepl'
4
5
  export const REPL_MEMORY_META_KEY = 'dshPtcPlusBindings'
@@ -12,13 +13,22 @@ const MAX_PENDING_REPL_CALLS = 256
12
13
  const MAX_CALL_ID_LENGTH = 512
13
14
  const MAX_GENERATION_LENGTH = 128
14
15
  const SNAPSHOT_FIELDS = new Set(['available', 'entries', 'total', 'omitted'])
16
+ const COUNTED_SNAPSHOT_FIELDS = new Set([...SNAPSHOT_FIELDS, 'reuseTotal'])
17
+ const OBSERVED_SNAPSHOT_FIELDS = new Set([...SNAPSHOT_FIELDS, 'observation'])
18
+ const OBSERVED_COUNTED_SNAPSHOT_FIELDS = new Set([...COUNTED_SNAPSHOT_FIELDS, 'observation'])
19
+ const OBSERVATION_FIELDS = new Set(['at', 'entries'])
20
+ const PREVIEW_FIELDS = new Set(['name', 'status', 'text', 'truncated'])
15
21
  const ENTRY_FIELDS = new Set(['name', 'kind', 'definition'])
22
+ const COUNTED_ENTRY_FIELDS = new Set([...ENTRY_FIELDS, 'reuseCount'])
16
23
  const DEFINITION_FIELDS = new Set(['source', 'line', 'column'])
17
24
  const MEMORY_META_FIELDS = new Set(['version', 'generation', 'memory'])
18
25
  const PROJECTION_STATE_FIELDS = new Set(['generation', 'memory', 'pendingReplCalls'])
19
26
  const PENDING_CALL_FIELDS = new Set(['callId', 'seq'])
20
- const REPL_TOOL_NAMES = new Set(['run_code', 'edit_run_code'])
21
- const REPL_MEMORY_META_VERSION = 3
27
+ const REPL_MEMORY_META_VERSION = 5
28
+
29
+ function isReuseCount(value) {
30
+ return Number.isSafeInteger(value) && value >= 0
31
+ }
22
32
 
23
33
  function exactFields(value, fields) {
24
34
  if (!isRecord(value)) return false
@@ -34,6 +44,7 @@ const EMPTY_REPL_MEMORY = Object.freeze({
34
44
  entries: Object.freeze([]),
35
45
  total: 0,
36
46
  omitted: 0,
47
+ reuseTotal: 0,
37
48
  })
38
49
 
39
50
  export function unavailableReplMemorySnapshot() {
@@ -59,20 +70,43 @@ function normalizeDefinition(value) {
59
70
  }
60
71
 
61
72
  function normalizeBinding(value) {
62
- if (!exactFields(value, ENTRY_FIELDS) || typeof value.name !== 'string'
73
+ const counted = exactFields(value, COUNTED_ENTRY_FIELDS)
74
+ if ((!counted && !exactFields(value, ENTRY_FIELDS)) || typeof value.name !== 'string'
63
75
  || value.name.length === 0 || value.name.length > MAX_BINDING_NAME_LENGTH
64
- || !BINDING_KINDS.has(value.kind)) {
76
+ || !BINDING_KINDS.has(value.kind)
77
+ || (counted && !isReuseCount(value.reuseCount))) {
65
78
  throw new Error('invalid dsh-ptc-plus REPL memory binding')
66
79
  }
67
80
  return Object.freeze({
68
81
  name: value.name,
69
82
  kind: value.kind,
70
83
  definition: normalizeDefinition(value.definition),
84
+ reuseCount: counted ? value.reuseCount : 0,
85
+ })
86
+ }
87
+
88
+ export function normalizeReplObservation(value) {
89
+ if (!exactFields(value, OBSERVATION_FIELDS) || !Number.isSafeInteger(value.at) || value.at < 0
90
+ || !Array.isArray(value.entries) || value.entries.length > MAX_BINDINGS) {
91
+ throw new Error('invalid dsh-ptc-plus REPL observation')
92
+ }
93
+ const names = new Set()
94
+ const entries = value.entries.map(entry => {
95
+ if (!exactFields(entry, PREVIEW_FIELDS) || typeof entry.name !== 'string'
96
+ || entry.name.length === 0 || entry.name.length > MAX_BINDING_NAME_LENGTH || names.has(entry.name)
97
+ || !['readable', 'unreadable'].includes(entry.status)
98
+ || typeof entry.text !== 'string' || entry.text.length > 512 || typeof entry.truncated !== 'boolean'
99
+ || (entry.status === 'unreadable' && (entry.text !== '' || entry.truncated))) {
100
+ throw new Error('invalid dsh-ptc-plus REPL value preview')
101
+ }
102
+ names.add(entry.name)
103
+ return Object.freeze({ ...entry })
71
104
  })
105
+ return Object.freeze({ at: value.at, entries: Object.freeze(entries) })
72
106
  }
73
107
 
74
- /** Produce bounded, value-independent presentation metadata from the live binding catalog. */
75
- export function createReplMemorySnapshot(bindings) {
108
+ /** Source inventory and optional observation are UI-only; neither is recovery evidence. */
109
+ export function createReplMemorySnapshot(bindings, observation = undefined) {
76
110
  const all = Array.isArray(bindings) ? bindings : []
77
111
  const candidates = []
78
112
  const names = new Set()
@@ -92,28 +126,45 @@ export function createReplMemorySnapshot(bindings) {
92
126
  entries.push(binding)
93
127
  sourceLength += binding.definition.source.length
94
128
  }
129
+ let observed
130
+ if (observation !== undefined) {
131
+ try {
132
+ const normalized = normalizeReplObservation(observation)
133
+ const visibleNames = new Set(entries.map(entry => entry.name))
134
+ observed = Object.freeze({ ...normalized, entries: Object.freeze(normalized.entries.filter(entry => visibleNames.has(entry.name))) })
135
+ } catch {}
136
+ }
95
137
  return Object.freeze({
96
138
  available: true,
97
139
  entries: Object.freeze(entries),
98
140
  total: candidates.length,
99
141
  omitted: candidates.length - entries.length,
142
+ // Counts survive the presentation truncation: this total covers every valid
143
+ // identity, including entries omitted from the bounded list.
144
+ reuseTotal: candidates.reduce((sum, entry) => sum + entry.reuseCount, 0),
145
+ ...(observed === undefined ? {} : { observation: observed }),
100
146
  })
101
147
  }
102
148
 
103
149
  /** Validate one complete session-projection value without accepting unknown fields. */
104
150
  export function normalizeReplMemorySnapshot(value) {
105
- if (!exactFields(value, SNAPSHOT_FIELDS) || typeof value.available !== 'boolean'
151
+ const counted = exactFields(value, COUNTED_SNAPSHOT_FIELDS) || exactFields(value, OBSERVED_COUNTED_SNAPSHOT_FIELDS)
152
+ const legacy = exactFields(value, SNAPSHOT_FIELDS) || exactFields(value, OBSERVED_SNAPSHOT_FIELDS)
153
+ if ((!counted && !legacy) || typeof value.available !== 'boolean'
106
154
  || !Array.isArray(value.entries) || value.entries.length > MAX_BINDINGS
107
155
  || !Number.isSafeInteger(value.total) || value.total < 0
108
156
  || !Number.isSafeInteger(value.omitted) || value.omitted < 0
109
- || value.total !== value.entries.length + value.omitted) {
157
+ || value.total !== value.entries.length + value.omitted
158
+ || counted && !isReuseCount(value.reuseTotal)) {
110
159
  throw new Error('invalid dsh-ptc-plus REPL memory snapshot')
111
160
  }
112
- if (!value.available && (value.total !== 0 || value.entries.length !== 0)) {
161
+ const reuseTotal = counted ? value.reuseTotal : 0
162
+ if (!value.available && (value.total !== 0 || value.entries.length !== 0 || reuseTotal !== 0)) {
113
163
  throw new Error('unavailable dsh-ptc-plus REPL memory snapshot must be empty')
114
164
  }
115
165
  const names = new Set()
116
166
  let sourceLength = 0
167
+ let entryReuseTotal = 0
117
168
  const entries = value.entries.map((entry) => {
118
169
  const normalized = normalizeBinding(entry)
119
170
  if (names.has(normalized.name)) {
@@ -124,18 +175,36 @@ export function normalizeReplMemorySnapshot(value) {
124
175
  throw new Error('dsh-ptc-plus REPL binding definitions exceed the presentation budget')
125
176
  }
126
177
  names.add(normalized.name)
178
+ entryReuseTotal += normalized.reuseCount
127
179
  return normalized
128
180
  })
181
+ if (reuseTotal < entryReuseTotal) {
182
+ throw new Error('dsh-ptc-plus REPL reuse total is smaller than its entries')
183
+ }
184
+ const observation = value.observation === undefined ? undefined : normalizeReplObservation(value.observation)
185
+ if (observation !== undefined && (!value.available || observation.entries.some(entry => !names.has(entry.name)))) {
186
+ throw new Error('REPL observation requires a matching available inventory')
187
+ }
129
188
  return Object.freeze({
130
189
  available: value.available,
131
190
  entries: Object.freeze(entries),
132
191
  total: value.total,
133
192
  omitted: value.omitted,
193
+ reuseTotal,
194
+ ...(observation === undefined ? {} : { observation }),
134
195
  })
135
196
  }
136
197
 
198
+ /** Reuse fields are version 5 additions; an older metadata version cannot own them. */
199
+ function countedMemory(memory) {
200
+ return memory?.reuseTotal !== undefined
201
+ || Array.isArray(memory?.entries) && memory.entries.some(entry => entry?.reuseCount !== undefined)
202
+ }
203
+
137
204
  function normalizeReplMemoryMetadata(value) {
138
- if (!exactFields(value, MEMORY_META_FIELDS) || value.version !== REPL_MEMORY_META_VERSION) {
205
+ if (!exactFields(value, MEMORY_META_FIELDS) || ![3, 4, REPL_MEMORY_META_VERSION].includes(value.version)
206
+ || (value.version === 3 && value.memory?.observation !== undefined)
207
+ || (value.version < REPL_MEMORY_META_VERSION && countedMemory(value.memory))) {
139
208
  throw new Error('invalid dsh-ptc-plus REPL memory metadata')
140
209
  }
141
210
  return Object.freeze({
@@ -253,12 +322,12 @@ export function withReplMemorySnapshot(meta, snapshot, generation) {
253
322
  return base
254
323
  }
255
324
 
256
- /** Build the current runtime owner's value-independent session projection. */
325
+ /** Build the current runtime owner's model-invisible session projection. */
257
326
  export function createReplMemoryProjection(generation) {
258
327
  const currentGeneration = normalizeGeneration(generation)
259
328
  return Object.freeze({
260
329
  key: REPL_MEMORY_KEY,
261
- stateVersion: 3,
330
+ stateVersion: 5,
262
331
  stateSchema: Object.freeze({
263
332
  parse: value => normalizeProjectionState(value, currentGeneration),
264
333
  }),