@frontera-sdk/cli 1.52.39 → 1.52.41

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 (235) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +49 -5
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/draft-changes.ts +1 -1
  33. package/src/commands/agent/index-commands.ts +69 -7
  34. package/src/commands/agent/resolve.ts +2 -0
  35. package/src/commands/app/checks.ts +44 -0
  36. package/src/commands/app/deploy.ts +243 -60
  37. package/src/commands/app/deployments.ts +97 -0
  38. package/src/commands/app/dev.ts +103 -11
  39. package/src/commands/app/init.ts +36 -3
  40. package/src/commands/app/promote.ts +45 -19
  41. package/src/commands/app/pull.ts +3 -1
  42. package/src/commands/app/rollback.ts +46 -0
  43. package/src/commands/app/sdk.ts +54 -8
  44. package/src/commands/app/unpin.ts +27 -0
  45. package/src/commands/app/versions.ts +1 -1
  46. package/src/commands/automation/bindings.ts +4 -2
  47. package/src/commands/automation/build-entry.ts +4 -1
  48. package/src/commands/automation/index-commands.ts +3 -1
  49. package/src/commands/automation/pull.ts +6 -1
  50. package/src/commands/automation/run.ts +34 -9
  51. package/src/commands/blueprint/bind.ts +2 -1
  52. package/src/commands/blueprint/declarative.ts +332 -32
  53. package/src/commands/blueprint/editable.ts +3 -3
  54. package/src/commands/blueprint/get.ts +6 -2
  55. package/src/commands/blueprint/grants.ts +64 -10
  56. package/src/commands/blueprint/metric.ts +108 -0
  57. package/src/commands/blueprint/object-sets.ts +71 -0
  58. package/src/commands/blueprint/query.ts +155 -31
  59. package/src/commands/dataset/index-commands.ts +156 -7
  60. package/src/commands/knowledge/index-commands.ts +122 -20
  61. package/src/commands/local/check.ts +657 -44
  62. package/src/commands/local/data.ts +33 -12
  63. package/src/commands/local/dev.ts +876 -89
  64. package/src/commands/local/docs-mcp.ts +10 -18
  65. package/src/commands/local/index-commands.ts +300 -63
  66. package/src/commands/local/init.ts +18 -5
  67. package/src/commands/local/key.ts +0 -3
  68. package/src/commands/local/providers.ts +0 -7
  69. package/src/commands/local/remove.ts +116 -0
  70. package/src/commands/local/schema.ts +3 -4
  71. package/src/commands/local/sdk-refresh.ts +59 -0
  72. package/src/commands/local/snapshot.ts +100 -0
  73. package/src/commands/local/sync-request.ts +321 -14
  74. package/src/commands/local/sync.ts +103 -6
  75. package/src/commands/mcp/serve.ts +34 -0
  76. package/src/commands/registry.ts +54 -10
  77. package/src/commands/types.ts +29 -0
  78. package/src/commands/workflow/archive.ts +46 -0
  79. package/src/commands/workflow/get.ts +2 -1
  80. package/src/commands/workflow/index-commands.ts +5 -0
  81. package/src/commands/workflow/push.ts +49 -2
  82. package/src/commands/workflow/run.ts +65 -7
  83. package/src/commands/workflow/runs.ts +9 -6
  84. package/src/commands/workflow/webhook-test.ts +166 -0
  85. package/src/config.ts +43 -6
  86. package/src/dev-broker.ts +52 -0
  87. package/src/exit.ts +103 -4
  88. package/src/flag-help.ts +77 -17
  89. package/src/forge/claude-commands.ts +11 -6
  90. package/src/forge/claude-skills.ts +4 -4
  91. package/src/forge/touches.ts +15 -12
  92. package/src/function-outside-imports.ts +89 -0
  93. package/src/harness.ts +42 -24
  94. package/src/heal.ts +113 -2
  95. package/src/kit.ts +1 -1
  96. package/src/local/fix-words.ts +12 -0
  97. package/src/local/function-bundle-recovery.ts +775 -0
  98. package/src/local/local-actions.ts +103 -10
  99. package/src/local/local-agent-action-grants.ts +210 -0
  100. package/src/local/local-agent-checks.ts +232 -0
  101. package/src/local/local-agent-grants.ts +29 -7
  102. package/src/local/local-agent-lean-plugins.ts +68 -0
  103. package/src/local/local-agent-missing-tools.ts +68 -0
  104. package/src/local/local-agent-name.ts +27 -0
  105. package/src/local/local-app-code.ts +191 -0
  106. package/src/local/local-app-dev-files.ts +186 -0
  107. package/src/local/local-app-dev-session.ts +88 -0
  108. package/src/local/local-app-lock-install.ts +114 -0
  109. package/src/local/local-app-pulled.ts +167 -0
  110. package/src/local/local-app-restore.ts +111 -0
  111. package/src/local/local-app-runtime.ts +68 -0
  112. package/src/local/local-app-sdk-paths.ts +149 -0
  113. package/src/local/local-app-sdk.ts +509 -0
  114. package/src/local/local-applied.ts +212 -22
  115. package/src/local/local-apply-history.ts +84 -0
  116. package/src/local/local-apply-summary.ts +86 -0
  117. package/src/local/local-apply-watchdog.ts +95 -0
  118. package/src/local/local-apps.ts +181 -8
  119. package/src/local/local-blueprint-errors.ts +198 -0
  120. package/src/local/local-blueprint-outline.ts +31 -12
  121. package/src/local/local-blueprint-schemas.ts +582 -0
  122. package/src/local/local-build-inputs.ts +48 -0
  123. package/src/local/local-build-summary.ts +495 -28
  124. package/src/local/local-case-evidence.ts +120 -0
  125. package/src/local/local-case-not-run.ts +153 -0
  126. package/src/local/local-chat-apps.ts +93 -16
  127. package/src/local/local-check-inherited.ts +83 -0
  128. package/src/local/local-cities.ts +89 -0
  129. package/src/local/local-component-health.ts +119 -0
  130. package/src/local/local-control.ts +34 -7
  131. package/src/local/local-db-snapshot.ts +348 -0
  132. package/src/local/local-dev-references.ts +347 -36
  133. package/src/local/local-dev-sync.ts +141 -12
  134. package/src/local/local-docker.ts +35 -1
  135. package/src/local/local-docs-search.ts +1 -1
  136. package/src/local/local-docs.ts +281 -2
  137. package/src/local/local-empty-workflows.ts +92 -0
  138. package/src/local/local-env.ts +16 -1
  139. package/src/local/local-file-checks.ts +104 -0
  140. package/src/local/local-function-ids.ts +141 -0
  141. package/src/local/local-function-recovery.ts +141 -0
  142. package/src/local/local-function-state.ts +87 -0
  143. package/src/local/local-keys.ts +7 -5
  144. package/src/local/local-knowledge-documents.ts +1 -1
  145. package/src/local/local-knowledge-ingestion.ts +225 -0
  146. package/src/local/local-knowledge-uploads.ts +217 -0
  147. package/src/local/local-lessons.ts +1 -1
  148. package/src/local/local-library.ts +1 -1
  149. package/src/local/local-media-placeholders.ts +182 -0
  150. package/src/local/local-media-sets.ts +90 -10
  151. package/src/local/local-model-parameters.ts +89 -0
  152. package/src/local/local-object-sets.ts +18 -6
  153. package/src/local/local-operating.ts +35 -1
  154. package/src/local/local-org-settings.ts +206 -8
  155. package/src/local/local-orphans.ts +321 -0
  156. package/src/local/local-personas.ts +296 -0
  157. package/src/local/local-portability.ts +143 -18
  158. package/src/local/local-ports.ts +99 -7
  159. package/src/local/local-project-notices.ts +234 -0
  160. package/src/local/local-project-secrets.ts +133 -0
  161. package/src/local/local-project.ts +12 -0
  162. package/src/local/local-provider-refusal.ts +66 -0
  163. package/src/local/local-request-evidence.ts +112 -0
  164. package/src/local/local-runtime-health.ts +123 -0
  165. package/src/local/local-runtime-history.ts +50 -0
  166. package/src/local/local-runtime-target.ts +42 -0
  167. package/src/local/local-sample-data.ts +141 -0
  168. package/src/local/local-sample-remake.ts +138 -0
  169. package/src/local/local-scaffold.ts +336 -53
  170. package/src/local/local-schema-check.ts +148 -0
  171. package/src/local/local-schemas.ts +742 -25
  172. package/src/local/local-secret-carry.ts +107 -0
  173. package/src/local/local-secrets.ts +41 -11
  174. package/src/local/local-seed.ts +102 -26
  175. package/src/local/local-stand-ins.ts +3 -3
  176. package/src/local/local-state.ts +43 -2
  177. package/src/local/local-studio-server.ts +170 -7
  178. package/src/local/local-supervisor.ts +44 -6
  179. package/src/local/local-sync-merge.ts +1 -1
  180. package/src/local/local-sync-names.ts +2 -0
  181. package/src/local/local-sync-plan.ts +19 -8
  182. package/src/local/local-sync-push.ts +15 -12
  183. package/src/local/local-sync-refs.ts +49 -3
  184. package/src/local/local-sync-remote.ts +349 -38
  185. package/src/local/local-sync-snapshot.ts +18 -13
  186. package/src/local/local-synthetic-data.ts +2116 -99
  187. package/src/local/local-test-baseline.ts +113 -0
  188. package/src/local/local-test-cases.ts +76 -1
  189. package/src/local/local-test-runner.ts +582 -91
  190. package/src/local/local-test-runs.ts +13 -5
  191. package/src/local/local-vendored-sdk.ts +183 -0
  192. package/src/local/local-workflow-state.ts +16 -3
  193. package/src/local/local-workflow-tests.ts +482 -0
  194. package/src/local/local-workspace-grants.ts +81 -0
  195. package/src/main.ts +50 -9
  196. package/src/mcp/a11y-scan.ts +55 -0
  197. package/src/mcp/browser/ax-snapshot.ts +181 -0
  198. package/src/mcp/browser/credentials.ts +78 -0
  199. package/src/mcp/browser/frames.ts +336 -0
  200. package/src/mcp/browser/headless-session.ts +413 -0
  201. package/src/mcp/browser/headless-tab.ts +575 -0
  202. package/src/mcp/browser/keys.ts +70 -0
  203. package/src/mcp/browser/page-scripts.ts +129 -0
  204. package/src/mcp/browser/wait.ts +73 -0
  205. package/src/mcp/forge-link.ts +126 -0
  206. package/src/mcp/headless.ts +199 -0
  207. package/src/mcp/mcp-apps.ts +435 -0
  208. package/src/mcp/mcp-chat.ts +226 -0
  209. package/src/mcp/mcp-cli.ts +53 -0
  210. package/src/mcp/mcp-protocol.ts +122 -0
  211. package/src/mcp/mcp-runtime.ts +113 -0
  212. package/src/mcp/mcp-server.ts +108 -0
  213. package/src/mcp/mcp-traces.ts +409 -0
  214. package/src/mcp/page-observation.ts +193 -0
  215. package/src/mcp/tools/agent-tools.ts +700 -0
  216. package/src/mcp/tools/app-tools.ts +241 -0
  217. package/src/mcp/tools/browser-tools.ts +310 -0
  218. package/src/mcp/tools/docs-tools.ts +32 -0
  219. package/src/mcp/tools/project-tools.ts +205 -0
  220. package/src/mcp/tools/test-tools.ts +339 -0
  221. package/src/mcp/tools/workflow-tools.ts +611 -0
  222. package/src/output.ts +1 -1
  223. package/src/pack.ts +8 -3
  224. package/src/packaging.ts +1 -1
  225. package/src/project-bootstrap.ts +107 -4
  226. package/src/project-context.ts +18 -1
  227. package/src/project.ts +27 -2
  228. package/src/provenance.ts +25 -0
  229. package/src/render-evidence.ts +65 -7
  230. package/src/scopes.ts +1 -1
  231. package/src/template.ts +14 -8
  232. package/src/templates/next-app-files.ts +35 -6
  233. package/src/templates/next-skills.ts +61 -27
  234. package/src/vendor/kit-assets.json +6 -6
  235. package/src/vendor/sdk-sources.json +17 -14
@@ -0,0 +1,113 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { LocalProject } from './local-project'
5
+ import type { CaseResult } from './local-test-runner'
6
+
7
+ /**
8
+ * The outcome of each case at its last local run, so a run can say which
9
+ * cases its change made fail (newly failing) rather than leave the agent to
10
+ * compare two lists. Written by `frontera local test` and the MCP `test_run`,
11
+ * one entry per case, in the runtime's state folder.
12
+ */
13
+
14
+ export interface CaseOutcome {
15
+ passed: boolean
16
+ at: string
17
+ reason?: string
18
+ ms?: number
19
+ toolCalls?: number
20
+ }
21
+
22
+ export type LastRun = Record<string, CaseOutcome>
23
+
24
+ /** A case's key: its Agent, file and name. */
25
+ export function caseKey(r: Pick<CaseResult, 'agent' | 'file' | 'name'>): string {
26
+ return `${r.agent}\u0000${r.file}\u0000${r.name}`
27
+ }
28
+
29
+ function lastRunPath(project: LocalProject): string {
30
+ return join(project.stateDir, 'test-results.json')
31
+ }
32
+
33
+ export function readLastRun(project: LocalProject): LastRun {
34
+ const file = lastRunPath(project)
35
+ if (!existsSync(file)) return {}
36
+ try {
37
+ return JSON.parse(readFileSync(file, 'utf8')) as LastRun
38
+ } catch {
39
+ return {}
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Keep these results as each case's last run; cases that did not run keep
45
+ * theirs, as does a case that couldn't run (the provider refused it, the
46
+ * runtime did not answer): it says nothing about the Agent.
47
+ */
48
+ export function recordRun(project: LocalProject, results: CaseResult[], at = new Date().toISOString()): void {
49
+ const last = readLastRun(project)
50
+ for (const r of results) if (!r.notRun) last[caseKey(r)] = { passed: r.passed, at, ms: r.ms, ...(r.toolCalls !== undefined ? { toolCalls: r.toolCalls } : {}), ...(r.passed || !r.reason ? {} : { reason: r.reason.slice(0, 300) }) }
51
+ mkdirSync(project.stateDir, { recursive: true })
52
+ writeFileSync(lastRunPath(project), `${JSON.stringify(last, null, 2)}\n`)
53
+ }
54
+
55
+ export interface RunComparison {
56
+ /** Passed at their last run and fail now: what the change broke. */
57
+ newlyFailing: CaseResult[]
58
+ /** Failed at their last run and pass now. */
59
+ fixed: CaseResult[]
60
+ /** Failed both times. */
61
+ stillFailing: CaseResult[]
62
+ /** No earlier run to compare with. */
63
+ firstRun: CaseResult[]
64
+ /**
65
+ * Couldn't run (the model provider refused them, the turn timed out, the
66
+ * runtime did not answer): neither newly failing nor still failing, whatever
67
+ * their last run was.
68
+ */
69
+ couldNotRun: CaseResult[]
70
+ }
71
+
72
+ export function compareWithLastRun(results: CaseResult[], last: LastRun): RunComparison {
73
+ const out: RunComparison = { newlyFailing: [], fixed: [], stillFailing: [], firstRun: [], couldNotRun: [] }
74
+ for (const r of results) {
75
+ const before = last[caseKey(r)]
76
+ if (r.notRun) out.couldNotRun.push(r)
77
+ else if (!before) out.firstRun.push(r)
78
+ else if (before.passed && !r.passed) out.newlyFailing.push(r)
79
+ else if (!before.passed && r.passed) out.fixed.push(r)
80
+ else if (!before.passed && !r.passed) out.stillFailing.push(r)
81
+ }
82
+ return out
83
+ }
84
+
85
+ /**
86
+ * Each case's run when its Agent was last applied to the workspace by a
87
+ * change request: the baseline a request's evals are compared with (design
88
+ * 2026-10-18 §6). Kept beside the last run; an Agent never applied from here
89
+ * has none, and its evals compare with the last run instead.
90
+ */
91
+ function appliedRunPath(project: LocalProject): string {
92
+ return join(project.stateDir, 'test-results-applied.json')
93
+ }
94
+
95
+ export function readAppliedRun(project: LocalProject): LastRun {
96
+ const file = appliedRunPath(project)
97
+ if (!existsSync(file)) return {}
98
+ try {
99
+ return JSON.parse(readFileSync(file, 'utf8')) as LastRun
100
+ } catch {
101
+ return {}
102
+ }
103
+ }
104
+
105
+ /** These Agents were applied: their cases' last runs become their applied baseline. */
106
+ export function recordApplied(project: LocalProject, agents: string[]): void {
107
+ if (!agents.length) return
108
+ const last = readLastRun(project)
109
+ const applied = readAppliedRun(project)
110
+ for (const [key, outcome] of Object.entries(last)) if (agents.includes(key.split('\u0000')[0]!)) applied[key] = outcome
111
+ mkdirSync(project.stateDir, { recursive: true })
112
+ writeFileSync(appliedRunPath(project), `${JSON.stringify(applied, null, 2)}\n`)
113
+ }
@@ -31,13 +31,45 @@ function block(text: string, indent: string): string {
31
31
  return `|-\n${text.split('\n').map((line) => `${indent}${line}`).join('\n')}`
32
32
  }
33
33
 
34
- export function renderTestCase(args: { conversationId: string; messages: ChatMessage[]; savedAt: string }): string {
34
+ /** How a saved case answers the tool approvals its conversation stopped on: as the person did. */
35
+ export type SavedDecision = 'approve' | 'deny'
36
+
37
+ /** An approval card's status once someone approved it: approved, then run (executing, succeeded, failed, ambiguous). */
38
+ const APPROVED = new Set(['approved', 'executing', 'succeeded', 'failed', 'ambiguous', 'resumed'])
39
+
40
+ /**
41
+ * How the person decided the tool approvals a conversation stopped on, from
42
+ * the runtime's list of its cards (`/v1/chat/approvals?conversationId=`):
43
+ * `approve` when every decided card was approved, `deny` when every one was
44
+ * denied; nothing when none was decided (pending, expired) or the cards were
45
+ * decided both ways, which one case decision can't replay (`mixed`).
46
+ */
47
+ export function decisionOfApprovals(cards: Array<{ status?: unknown; pauseReason?: unknown }>): { decision?: SavedDecision; mixed?: true } {
48
+ const decided = cards
49
+ .filter((c) => !c.pauseReason || c.pauseReason === 'needs_approval')
50
+ .map((c) => (typeof c.status === 'string' && APPROVED.has(c.status) ? 'approve' : c.status === 'denied' ? 'deny' : null))
51
+ .filter((d): d is SavedDecision => d !== null)
52
+ const kinds = new Set(decided)
53
+ if (kinds.size > 1) return { mixed: true }
54
+ return kinds.size ? { decision: [...kinds][0]! } : {}
55
+ }
56
+
57
+ /** What saving a case says when its conversation's cards were decided both ways. */
58
+ export const MIXED_DECISIONS_LINE = 'the conversation approved some tool approvals and denied others; a case answers every one the same way, so it was saved without `decision`: add `decision: approve` or `deny` by hand, or split it into one case per turn'
59
+
60
+ export function renderTestCase(args: { conversationId: string; messages: ChatMessage[]; savedAt: string; variables?: Record<string, string | number | boolean>; decision?: SavedDecision }): string {
35
61
  const turns = args.messages.filter((m) => (m.role === 'user' || m.role === 'assistant') && textOf(m))
36
62
  const lastReply = [...turns].reverse().find((m) => m.role === 'assistant')
37
63
  const inputs = turns.slice(0, lastReply ? turns.lastIndexOf(lastReply) : turns.length)
38
64
  const lines = [
39
65
  `- name: conversation-${args.conversationId.slice(0, 8)}`,
40
66
  ` savedFrom: studio ${args.savedAt}`,
67
+ // The tool approvals it stopped on, answered as the person did, so it replays to the end.
68
+ ...(args.decision ? [` decision: ${args.decision}`] : []),
69
+ // The variables the conversation started with, so the case replays as the same customer.
70
+ ...(args.variables && Object.keys(args.variables).length
71
+ ? [' variables:', ...Object.entries(args.variables).map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`)]
72
+ : []),
41
73
  ' conversation:',
42
74
  ...inputs.map((m) => ` - ${m.role}: ${block(textOf(m), ' ')}`),
43
75
  ` expected: ${block(lastReply ? textOf(lastReply) : '', ' ')}`,
@@ -56,3 +88,46 @@ export function appendTestCase(root: string, agentSlug: string, yaml: string): s
56
88
  appendFileSync(file, yaml.replace(/^/gm, ' ').replace(/ +$/gm, ''))
57
89
  return relative(root, file)
58
90
  }
91
+
92
+ /**
93
+ * The variables a conversation's Test setup recorded (`variables` on its
94
+ * `TRY_SETUP` session event, when the runtime records them), as a case file
95
+ * holds them: plain values only.
96
+ */
97
+ export function variablesOfTimeline(timeline: Array<{ type?: string; payload?: Record<string, unknown> | null }>): Record<string, string | number | boolean> | undefined {
98
+ const setup = timeline.find((e) => e.payload?.event === 'TRY_SETUP')
99
+ const variables = setup?.payload?.variables
100
+ return variables && typeof variables === 'object' && !Array.isArray(variables) ? plainVariables(variables as Record<string, unknown>) : undefined
101
+ }
102
+
103
+ /** A conversation's variables a case can carry (strings, numbers, booleans), or undefined when none are. */
104
+ export function plainVariables(raw: Record<string, unknown> | undefined): Record<string, string | number | boolean> | undefined {
105
+ if (!raw) return undefined
106
+ const plain = Object.fromEntries(Object.entries(raw).filter((e): e is [string, string | number | boolean] => ['string', 'number', 'boolean'].includes(typeof e[1])))
107
+ return Object.keys(plain).length ? plain : undefined
108
+ }
109
+
110
+ /**
111
+ * The sensitive variables a conversation's Test setup was given
112
+ * (`redactedVariables` on its `TRY_SETUP` session event): their names only,
113
+ * since the runtime never records their values. A case saved from the
114
+ * conversation lacks them until someone fills them in by hand.
115
+ */
116
+ export function redactedVariablesOfTimeline(timeline: Array<{ type?: string; payload?: Record<string, unknown> | null }>): string[] {
117
+ const setup = timeline.find((e) => e.payload?.event === 'TRY_SETUP')
118
+ const keys = setup?.payload?.redactedVariables
119
+ return Array.isArray(keys) ? [...new Set(keys.filter((k): k is string => typeof k === 'string' && k !== ''))] : []
120
+ }
121
+
122
+ /** A case (one list item, as rendered) with a comment under its name naming the variables to fill in by hand. */
123
+ export function noteRedactedVariables(yaml: string, keys: string[]): string {
124
+ if (!keys.length) return yaml
125
+ const at = yaml.indexOf('\n')
126
+ const note = ` # Fill in by hand under variables: ${keys.join(', ')} (sensitive: the conversation's values are never recorded)`
127
+ return at < 0 ? `${yaml}\n${note}\n` : `${yaml.slice(0, at + 1)}${note}\n${yaml.slice(at + 1)}`
128
+ }
129
+
130
+ /** What saving a case says of the sensitive variables it could not keep. */
131
+ export function redactedVariablesLine(keys: string[], path: string): string {
132
+ return `not kept: ${keys.join(', ')} (sensitive: the Test setup records ${keys.length === 1 ? 'its name' : 'their names'}, never ${keys.length === 1 ? 'its value' : 'their values'}). Fill ${keys.length === 1 ? 'it' : 'them'} in by hand under the case's variables: in ${path}, or the case starts without ${keys.length === 1 ? 'it' : 'them'}.`
133
+ }