@visual-z/xeno 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2066
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1713
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3001
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -200
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/screen.ts +0 -548
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/core/work-state.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/** O.2: identity and lifecycle are explicit; only command evidence can verify completion. */
|
|
2
|
-
import type { LogEvent } from "./events.ts";
|
|
3
|
-
import type { Evidence } from "./handoff.ts";
|
|
4
|
-
import { isCommandResult, resolveEvidence, evidenceStale } from "./evidence.ts";
|
|
5
|
-
|
|
6
|
-
export type WorkItem = {
|
|
7
|
-
id: string;
|
|
8
|
-
what: string;
|
|
9
|
-
status: "active" | "blocked" | "completed" | "cancelled";
|
|
10
|
-
verification: "not_run" | "verified" | "unverified" | "stale";
|
|
11
|
-
opened_at: string;
|
|
12
|
-
updated_at: string;
|
|
13
|
-
next_action?: string;
|
|
14
|
-
reason?: string;
|
|
15
|
-
evidence?: Evidence;
|
|
16
|
-
/** Reopening keeps provenance without carrying a second copy of old proof. */
|
|
17
|
-
previous_evidence?: string;
|
|
18
|
-
depends_on?: readonly string[];
|
|
19
|
-
owner?: string;
|
|
20
|
-
owner_event?: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type WorkReadiness = { ready: boolean; reason: "ready" | "waiting" | "blocked" | "closed" | "owned" | "missing"; waiting_for: string[] };
|
|
24
|
-
export function workReadiness(items: readonly WorkItem[], id: string, actor?: string): WorkReadiness {
|
|
25
|
-
const item = items.find(work => work.id === id);
|
|
26
|
-
if (!item) return { ready: false, reason: "missing", waiting_for: [id] };
|
|
27
|
-
if (item.status === "completed" || item.status === "cancelled") return { ready: false, reason: "closed", waiting_for: [] };
|
|
28
|
-
const waiting = (item.depends_on ?? []).filter(dep => items.find(work => work.id === dep)?.status !== "completed");
|
|
29
|
-
const reason = item.status === "blocked" ? "blocked" : waiting.length ? "waiting" : item.owner && item.owner !== actor ? "owned" : "ready";
|
|
30
|
-
return { ready: reason === "ready", reason, waiting_for: [...waiting] };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function dependencyError(items: readonly WorkItem[], id: string, dependencies: readonly string[]): string | null {
|
|
34
|
-
if (new Set(dependencies).size !== dependencies.length) return "duplicate work dependencies";
|
|
35
|
-
if (dependencies.includes(id)) return "work dependency cycle: self dependency";
|
|
36
|
-
if (dependencies.some(dep => !items.some(item => item.id === dep))) return "unknown work dependency";
|
|
37
|
-
const seen = new Set<string>(), pending = [...dependencies];
|
|
38
|
-
while (pending.length) {
|
|
39
|
-
const dep = pending.pop()!;
|
|
40
|
-
if (dep === id) return "work dependency cycle";
|
|
41
|
-
if (seen.has(dep)) continue;
|
|
42
|
-
seen.add(dep); pending.push(...items.find(item => item.id === dep)?.depends_on ?? []);
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** O.2: sibling sessions may choose the same local label without aliasing work. */
|
|
48
|
-
export function scopedWorkId(namespace: string, requested: string, items: readonly WorkItem[]): string {
|
|
49
|
-
return items.some(item => item.id === requested) || requested.startsWith(`${namespace}:`)
|
|
50
|
-
? requested : `${namespace}:${requested}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function unknownWork(id: string): string {
|
|
54
|
-
return `unknown work_id ${id}; create it with a next note (kind=next) first`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function workChangeError(items: readonly WorkItem[], event: LogEvent): string | null {
|
|
58
|
-
if (!("work_id" in event) || event.work_id === undefined) return null;
|
|
59
|
-
const item = items.find(item => item.id === event.work_id);
|
|
60
|
-
if ((event.t === "next_action" || event.t === "work_dependencies") && event.depends_on !== undefined) {
|
|
61
|
-
const error = dependencyError(items, event.work_id, event.depends_on);
|
|
62
|
-
if (error) return error;
|
|
63
|
-
}
|
|
64
|
-
// Every unknown-id rejection names the way to make the id exist, or the model retries the same call.
|
|
65
|
-
if (event.t === "work_dependencies" && !item) return unknownWork(event.work_id);
|
|
66
|
-
if (event.t === "work_owner") {
|
|
67
|
-
if (!item) return unknownWork(event.work_id);
|
|
68
|
-
if (event.operation === "release") return item.owner === event.owner ? null : "only the recorded owner can release work";
|
|
69
|
-
const ready = workReadiness(items, item.id, event.owner);
|
|
70
|
-
return ready.ready ? null : `work ${item.id} is ${ready.reason}; cannot claim it`;
|
|
71
|
-
}
|
|
72
|
-
if (event.t === "claim" && item && (item.depends_on ?? []).some(dep => items.find(work => work.id === dep)?.status !== "completed")) return "work dependencies are not completed";
|
|
73
|
-
if (event.t === "work_status") {
|
|
74
|
-
// D15 carries post-fork instructions, not their whole history. A harness-
|
|
75
|
-
// authenticated transition may seed intent in the adopted branch, never proof.
|
|
76
|
-
if (!item && !event.carried_from) return `unknown work_id ${event.work_id}; create it with a next note first`;
|
|
77
|
-
if (!item) return event.operation !== "cancel" && !event.next_action?.trim()
|
|
78
|
-
? "reactivating work requires a current next_action" : null;
|
|
79
|
-
if (event.operation === "unblock" && item.status !== "blocked") return `work ${item.id} is not blocked`;
|
|
80
|
-
if (event.operation === "reopen" && item.status !== "completed" && item.status !== "cancelled") {
|
|
81
|
-
return `work ${item.id} is not closed; update its next action or unblock it`;
|
|
82
|
-
}
|
|
83
|
-
if (event.operation !== "cancel" && !event.next_action?.trim()) return "reactivating work requires a current next_action";
|
|
84
|
-
} else if (event.t === "claim" && !item) {
|
|
85
|
-
return `unknown work_id ${event.work_id}; record a next note before doing and verifying the work`;
|
|
86
|
-
} else if (item && (item.status === "completed" || item.status === "cancelled")) {
|
|
87
|
-
return `work ${item.id} is ${item.status}; explicitly reopen it before updating it`;
|
|
88
|
-
} else if (event.t === "claim" && item?.status === "blocked") {
|
|
89
|
-
return `work ${item.id} is blocked; explicitly unblock it before completing it`;
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function deriveWorkItems(previous: readonly WorkItem[], events: readonly LogEvent[]): WorkItem[] {
|
|
95
|
-
const items = new Map(previous.map(item => [item.id, { ...item }]));
|
|
96
|
-
for (let index = 0; index < events.length; index++) {
|
|
97
|
-
const event = events[index]!;
|
|
98
|
-
if ((event.t !== "next_action" && event.t !== "blocked" && event.t !== "work_status" && event.t !== "claim" && event.t !== "work_dependencies" && event.t !== "work_owner") ||
|
|
99
|
-
event.work_id === undefined || workChangeError([...items.values()], event)) continue;
|
|
100
|
-
const old = items.get(event.work_id);
|
|
101
|
-
const item: WorkItem = old ? { ...old, what: event.t === "work_owner" || event.t === "work_dependencies" ? old.what : event.what, updated_at: event.id } : {
|
|
102
|
-
id: event.work_id, what: event.what, status: "active", verification: "not_run",
|
|
103
|
-
opened_at: event.id, updated_at: event.id,
|
|
104
|
-
};
|
|
105
|
-
if (event.t === "next_action") { item.next_action = event.next_action; if (event.depends_on !== undefined) item.depends_on = [...event.depends_on]; }
|
|
106
|
-
else if (event.t === "work_dependencies") item.depends_on = [...event.depends_on];
|
|
107
|
-
else if (event.t === "work_owner") {
|
|
108
|
-
if (event.operation === "claim") { item.owner = event.owner; item.owner_event = event.id; }
|
|
109
|
-
else { delete item.owner; delete item.owner_event; }
|
|
110
|
-
}
|
|
111
|
-
else if (event.t === "blocked") {
|
|
112
|
-
item.status = "blocked";
|
|
113
|
-
item.reason = event.reason;
|
|
114
|
-
} else if (event.t === "work_status") {
|
|
115
|
-
if (event.operation === "cancel") {
|
|
116
|
-
item.status = "cancelled";
|
|
117
|
-
item.reason = event.reason ?? "cancelled";
|
|
118
|
-
delete item.next_action;
|
|
119
|
-
} else {
|
|
120
|
-
item.status = "active";
|
|
121
|
-
item.next_action = event.next_action!;
|
|
122
|
-
delete item.reason;
|
|
123
|
-
if (event.operation === "reopen") {
|
|
124
|
-
item.opened_at = event.id;
|
|
125
|
-
item.verification = "not_run";
|
|
126
|
-
if (item.evidence) item.previous_evidence = item.evidence.event_id;
|
|
127
|
-
delete item.evidence;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
} else if (event.t === "claim") {
|
|
131
|
-
// Preserve D11's same-segment rule and require proof from this activation.
|
|
132
|
-
const resolved = resolveEvidence(events.slice(0, index), event.evidence, item.opened_at);
|
|
133
|
-
if (resolved.ok) {
|
|
134
|
-
item.status = "completed";
|
|
135
|
-
item.verification = "verified";
|
|
136
|
-
item.evidence = resolved.evidence;
|
|
137
|
-
delete item.reason;
|
|
138
|
-
delete item.next_action;
|
|
139
|
-
} else { item.verification = "unverified"; item.reason = resolved.reason; }
|
|
140
|
-
}
|
|
141
|
-
items.set(item.id, item);
|
|
142
|
-
if (item.status === "completed" || item.status === "cancelled") { delete item.owner; delete item.owner_event; }
|
|
143
|
-
}
|
|
144
|
-
return [...items.values()].map(item => {
|
|
145
|
-
if (!item.evidence) return item;
|
|
146
|
-
const proof = item.evidence;
|
|
147
|
-
const later = events.slice(events.findIndex(event => event.id === proof.event_id) + 1);
|
|
148
|
-
const latest = later.findLast(event => isCommandResult(event) &&
|
|
149
|
-
(proof.gate ? event.effect.gate === proof.gate : event.effect.cmd === proof.command));
|
|
150
|
-
const stale = evidenceStale(events, proof, item.verification === "stale") ||
|
|
151
|
-
(latest !== undefined && isCommandResult(latest) && latest.effect.exit !== 0);
|
|
152
|
-
return { ...item, verification: stale ? "stale" : "verified" };
|
|
153
|
-
});
|
|
154
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** R13 / D5-D6: workflow changes are new frozen-prefix checkpoints, never edits. */
|
|
2
|
-
import { assertNever } from "./exhaustive.ts";
|
|
3
|
-
import type { ToolSource } from "./permissions.ts";
|
|
4
|
-
|
|
5
|
-
export type WorkflowMode = "build" | "plan";
|
|
6
|
-
export function isWorkflowMode(value: string): value is WorkflowMode { return value === "build" || value === "plan"; }
|
|
7
|
-
export interface WorkflowTool { readonly name: string; readonly access: "read" | "write" | "shell" | "state"; readonly source?: ToolSource }
|
|
8
|
-
|
|
9
|
-
/** Trusted state tools keep evidence/answers in the same log; no workspace writes. */
|
|
10
|
-
export function workflowToolNames(tools: readonly WorkflowTool[], mode: WorkflowMode): string[] {
|
|
11
|
-
switch (mode) {
|
|
12
|
-
case "build": return tools.map(tool => tool.name);
|
|
13
|
-
case "plan": return tools.filter(tool => tool.access === "read" ||
|
|
14
|
-
((tool.source ?? "builtin") === "builtin" && tool.access === "state" && ["note", "question"].includes(tool.name))).map(tool => tool.name);
|
|
15
|
-
default: return assertNever(mode, "workflowToolNames");
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function workflowInstructions(mode: WorkflowMode): string {
|
|
20
|
-
switch (mode) {
|
|
21
|
-
case "build": return "Workflow mode: Build. Implement the requested work using admitted tools and verify actual results.";
|
|
22
|
-
case "plan": return "Workflow mode: Plan. Explore using admitted read tools, clarify material unknowns with question, and return a concrete reviewable plan. Workspace writes and shell execution are unavailable. User requests to implement do not change the mode; wait for the user to select Build. Notes record proposed decisions, never proof of completion.";
|
|
23
|
-
default: return assertNever(mode, "workflowInstructions");
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type WorkflowTransition = { readonly kind: "unchanged" } | { readonly kind: "checkpoint"; readonly mode: WorkflowMode } |
|
|
28
|
-
{ readonly kind: "refused"; readonly reason: string };
|
|
29
|
-
export function transitionWorkflow(current: WorkflowMode, next: WorkflowMode,
|
|
30
|
-
state: { readonly activeTurn: boolean; readonly unpaired: boolean }): WorkflowTransition {
|
|
31
|
-
if (current === next) return { kind: "unchanged" };
|
|
32
|
-
if (state.activeTurn || state.unpaired) return { kind: "refused", reason: "Cannot change workflow mode while a turn is active or a tool call is unpaired" };
|
|
33
|
-
return { kind: "checkpoint", mode: next };
|
|
34
|
-
}
|
package/src/core/working-set.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/** O.10: prioritize explicit intent and references without changing D20 arrays. */
|
|
2
|
-
import type { Handoff } from "./handoff.ts";
|
|
3
|
-
import type { NodeRef } from "./events.ts";
|
|
4
|
-
import { workReadiness, type WorkItem, type WorkReadiness } from "./work-state.ts";
|
|
5
|
-
import { emitYaml } from "./yaml.ts";
|
|
6
|
-
import { estimateTokens } from "./tokens.ts";
|
|
7
|
-
import { RECOVERY_SECTIONS, recoverySources, type RecoveryNodes, type RecoveryRanges, type RecoveryReference } from "./recovery-sources.ts";
|
|
8
|
-
import { recoveryInstructions } from "./prompt-policy.ts";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The cut format new sessions record. Live cuts, cold replay and interrupted-cut
|
|
12
|
-
* repair all select through `carriedHandoff`, so this is only the default for
|
|
13
|
-
* cuts not yet written; a recorded version is never reinterpreted.
|
|
14
|
-
*/
|
|
15
|
-
export const CURRENT_WORKING_SET_VERSION = 3;
|
|
16
|
-
|
|
17
|
-
export type WorkingSet = {
|
|
18
|
-
version: 1 | 2 | 3; source: NodeRef;
|
|
19
|
-
/** v3 omits `what`, `next_action` and `reason` when the carried `state.work_items` holds the same item. */
|
|
20
|
-
work: (Pick<WorkItem, "id" | "status" | "next_action" | "reason" | "depends_on" | "owner"> & { what?: string; readiness?: WorkReadiness })[];
|
|
21
|
-
references: ({ node?: number; event: string; kind: string; work?: string } & Partial<Omit<RecoveryReference, "node" | "event" | "kind" | "work">>)[];
|
|
22
|
-
omitted: { work: number; references: number };
|
|
23
|
-
recovery_sections?: RecoveryRanges;
|
|
24
|
-
recovery: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export function workingSet(full: Handoff, budget: number): WorkingSet | null {
|
|
28
|
-
if (!full.predecessor) return null;
|
|
29
|
-
const items = (full.state.work_items ?? []).filter(item => item.status === "blocked" || item.status === "active")
|
|
30
|
-
.toSorted((a, b) => Number(b.status === "blocked") - Number(a.status === "blocked") || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
31
|
-
const active = new Set(items.map(item => item.id));
|
|
32
|
-
const references: WorkingSet["references"] = [
|
|
33
|
-
...(full.evidence_links ?? []).filter(link => active.has(link.work) && !link.superseded_by)
|
|
34
|
-
.map(link => ({ event: link.event, kind: "evidence", work: link.work })),
|
|
35
|
-
...full.decisions.filter(item => item.work_id && active.has(item.work_id) && item.source)
|
|
36
|
-
.map(item => ({ node: item.source!.node, event: item.source!.event_id, kind: "decision", work: item.work_id! })),
|
|
37
|
-
...full.pitfalls.filter(item => item.work_id && active.has(item.work_id) && item.source)
|
|
38
|
-
.map(item => ({ node: item.source!.node, event: item.source!.event_id, kind: "pitfall", work: item.work_id! })),
|
|
39
|
-
...(full.progress_evidence ?? []).toReversed().map(item => ({ node: item.source.node, event: item.source.event_id, kind: item.kind })),
|
|
40
|
-
];
|
|
41
|
-
const view: WorkingSet = { version: 1, source: full.predecessor, work: [], references: [],
|
|
42
|
-
omitted: { work: items.length, references: references.length }, recovery: "history(work_id=ID), history(event=ID), or history(node=source.node, section=...)" };
|
|
43
|
-
const fits = () => budget === Infinity || estimateTokens.count([], emitYaml(view)) <= budget;
|
|
44
|
-
if (!fits()) return null;
|
|
45
|
-
for (const item of items) {
|
|
46
|
-
const { id, what, status, next_action, reason, depends_on, owner } = item;
|
|
47
|
-
view.work.push({ id, what, status, ...(next_action ? { next_action } : {}), ...(reason ? { reason } : {}),
|
|
48
|
-
...(depends_on ? { depends_on, readiness: workReadiness(full.state.work_items ?? [], id) } : {}), ...(owner ? { owner } : {}) });
|
|
49
|
-
view.omitted.work--;
|
|
50
|
-
if (!fits()) { view.work.pop(); view.omitted.work++; }
|
|
51
|
-
}
|
|
52
|
-
for (const reference of references) {
|
|
53
|
-
view.references.push(reference); view.omitted.references--;
|
|
54
|
-
if (!fits()) { view.references.pop(); view.omitted.references++; }
|
|
55
|
-
}
|
|
56
|
-
return view;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** R9: a new projection version keeps the original v1 ordering byte-stable. */
|
|
60
|
-
export function recoveryWorkingSet(full: Handoff, nodes: RecoveryNodes, budget: number, tools?: readonly string[]): WorkingSet | null {
|
|
61
|
-
return sourceView(full, nodes, { version: 2, users: true,
|
|
62
|
-
fits: view => estimateTokens.count([], emitYaml(view)) <= budget,
|
|
63
|
-
recovery: tools === undefined ? "Read original user messages before continuing: history(node=source.node,recovery=users,offset=0). Other pages: failures, environment, next_actions; event=ID reads one original. Abandoned messages are cancelled; annotations are not user instructions. Observations are historical: recheck resource, host and current permissions before acting. history(work_id=ID) recovers work." : recoveryInstructions(tools) });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* v3 shares v2's source ordering. Two things differ, both decided by the caller
|
|
68
|
-
* that owns the whole carried handoff: `fits` measures the view inside that
|
|
69
|
-
* handoff rather than standalone, and `users: false` leaves user sources out
|
|
70
|
-
* because the carried `user_turns` already hold every one verbatim.
|
|
71
|
-
*/
|
|
72
|
-
export function recoveryWorkingSetV3(full: Handoff, nodes: RecoveryNodes, options: {
|
|
73
|
-
users: boolean; recovery: string; fits: (view: WorkingSet) => boolean;
|
|
74
|
-
}): WorkingSet | null {
|
|
75
|
-
return sourceView(full, nodes, { version: 3, ...options });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function sourceView(full: Handoff, nodes: RecoveryNodes, options: {
|
|
79
|
-
version: 2 | 3; users: boolean; recovery: string; fits: (view: WorkingSet) => boolean;
|
|
80
|
-
}): WorkingSet | null {
|
|
81
|
-
const legacy = workingSet(full, Infinity);
|
|
82
|
-
if (!legacy) return null;
|
|
83
|
-
const sources = recoverySources(nodes);
|
|
84
|
-
const view: WorkingSet = { version: options.version, source: legacy.source, work: [], references: [],
|
|
85
|
-
omitted: { work: legacy.work.length, references: legacy.references.length + RECOVERY_SECTIONS.reduce((sum, section) => sum + sources[section].length, 0) },
|
|
86
|
-
recovery_sections: Object.fromEntries(RECOVERY_SECTIONS.map(section => [section, { total: sources[section].length, omitted: sources[section].length }])) as RecoveryRanges,
|
|
87
|
-
recovery: options.recovery };
|
|
88
|
-
const fits = () => options.fits(view);
|
|
89
|
-
if (!fits()) return null;
|
|
90
|
-
const queues = RECOVERY_SECTIONS.map(section => ({ section, items: [...sources[section]], at: 0 }));
|
|
91
|
-
// The latest and first live user messages anchor correction and initial scope;
|
|
92
|
-
// remaining user sources are newest first. Cancelled turns stay pageable.
|
|
93
|
-
const live = sources.users.filter(ref => !ref.abandoned), first = live.at(-1);
|
|
94
|
-
queues[0]!.items = !options.users ? [] : [...live.slice(0, 1), ...(first && first !== live[0] ? [first] : []),
|
|
95
|
-
...live.slice(1, -1), ...sources.users.filter(ref => ref.abandoned)];
|
|
96
|
-
const takeReference = (reference: WorkingSet["references"][number], section?: keyof RecoveryRanges) => {
|
|
97
|
-
view.references.push(reference); view.omitted.references--;
|
|
98
|
-
if (section) view.recovery_sections![section].omitted--;
|
|
99
|
-
if (!fits()) {
|
|
100
|
-
view.references.pop(); view.omitted.references++;
|
|
101
|
-
if (section) view.recovery_sections![section].omitted++;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
// One source per role precedes work prose; round-robin growth thereafter
|
|
105
|
-
// prevents a long task list or repeated failures starving other categories.
|
|
106
|
-
for (const queue of queues) { const reference = queue.items[queue.at++]; if (reference) takeReference(reference, queue.section); }
|
|
107
|
-
let at = 0;
|
|
108
|
-
while (at < legacy.work.length || at < legacy.references.length || queues.some(queue => queue.at < queue.items.length)) {
|
|
109
|
-
const work = legacy.work[at];
|
|
110
|
-
if (work) {
|
|
111
|
-
view.work.push(work); view.omitted.work--;
|
|
112
|
-
if (!fits()) { view.work.pop(); view.omitted.work++; }
|
|
113
|
-
}
|
|
114
|
-
for (const queue of queues) { const reference = queue.items[queue.at++]; if (reference) takeReference(reference, queue.section); }
|
|
115
|
-
const reference = legacy.references[at++];
|
|
116
|
-
if (reference) takeReference(reference);
|
|
117
|
-
}
|
|
118
|
-
return view;
|
|
119
|
-
}
|
package/src/core/yaml.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Restricted, deterministic YAML emitter.
|
|
3
|
-
*
|
|
4
|
-
* Why hand-written instead of a dependency: YAML emitters differ in indentation,
|
|
5
|
-
* quoting and line-wrapping defaults, so the usual advice is "pick one writer and
|
|
6
|
-
* pin its version". A restricted emitter removes the version-drift risk entirely --
|
|
7
|
-
* the format is fixed by construction, not by someone else's defaults.
|
|
8
|
-
*
|
|
9
|
-
* Guarantees:
|
|
10
|
-
* - Block style only (the sole exception is `[]` / `{}` for empty collections).
|
|
11
|
-
* - Indent 2, LF endings, exactly one trailing newline, no trailing whitespace.
|
|
12
|
-
* - No anchors or aliases, so no topological-ordering hazard.
|
|
13
|
-
* - Key order comes from an explicit `keyOrder` resolver; unlisted keys are
|
|
14
|
-
* appended in sorted order so a new field can never silently vanish.
|
|
15
|
-
* - Byte-identical output for equal input: emit(x) === emit(structuredClone(x)).
|
|
16
|
-
*
|
|
17
|
-
* Supported value types: string, finite number, boolean, null, array, plain object.
|
|
18
|
-
* `undefined` values are omitted (that is how optional fields disappear); `null` is
|
|
19
|
-
* emitted explicitly, so "absent" and "present but empty" stay distinguishable.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export type YamlValue =
|
|
23
|
-
| string
|
|
24
|
-
| number
|
|
25
|
-
| boolean
|
|
26
|
-
| null
|
|
27
|
-
| undefined
|
|
28
|
-
| readonly YamlValue[]
|
|
29
|
-
| { readonly [key: string]: YamlValue };
|
|
30
|
-
|
|
31
|
-
export interface EmitOptions {
|
|
32
|
-
/**
|
|
33
|
-
* Returns the preferred key order for the object at `path`, or undefined to
|
|
34
|
-
* sort alphabetically. Keys returned but absent from the object are skipped;
|
|
35
|
-
* keys present but unlisted are appended in sorted order.
|
|
36
|
-
*/
|
|
37
|
-
keyOrder?: (path: readonly string[]) => readonly string[] | undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const INDENT = " ";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Short all-scalar arrays render in flow style (`[1, 120]`). Nested block
|
|
44
|
-
* sequences are legal but read badly -- a line range would otherwise print as
|
|
45
|
-
* "- - 1" / " - 120" -- and this file is meant to be reviewed by a person.
|
|
46
|
-
* The rule is a fixed predicate, so output stays deterministic.
|
|
47
|
-
*/
|
|
48
|
-
const FLOW_MAX_WIDTH = 60;
|
|
49
|
-
|
|
50
|
-
/** YAML 1.1-era tokens that a bare scalar must not collide with. */
|
|
51
|
-
const RESERVED_PLAIN =
|
|
52
|
-
/^(|~|null|Null|NULL|true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF|y|Y|n|N)$/;
|
|
53
|
-
|
|
54
|
-
const NUMBER_LIKE =
|
|
55
|
-
/^[+-]?(\d[\d_]*\.?[\d_]*|\.[\d_]+)([eE][+-]?\d+)?$|^[+-]?0[xXoObB][0-9a-fA-F_]+$|^[+-]?\.(inf|Inf|INF)$|^\.(nan|NaN|NAN)$/;
|
|
56
|
-
|
|
57
|
-
/** Leading characters that carry structural meaning in YAML. */
|
|
58
|
-
const LEADING_INDICATOR = new Set([
|
|
59
|
-
"-", "?", ":", ",", "[", "]", "{", "}", "#", "&", "*", "!",
|
|
60
|
-
"|", ">", "'", '"', "%", "@", "`",
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
function hasControlChars(s: string): boolean {
|
|
64
|
-
for (const ch of s) {
|
|
65
|
-
const c = ch.codePointAt(0)!;
|
|
66
|
-
// Allow \n (handled by the block-scalar path); reject every other control char.
|
|
67
|
-
if ((c < 0x20 && c !== 0x0a) || c === 0x7f) return true;
|
|
68
|
-
}
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isPlainSafe(s: string): boolean {
|
|
73
|
-
if (s.length === 0) return false;
|
|
74
|
-
if (s !== s.trim()) return false;
|
|
75
|
-
if (s.includes("\n") || hasControlChars(s)) return false;
|
|
76
|
-
if (LEADING_INDICATOR.has(s[0]!)) return false;
|
|
77
|
-
if (RESERVED_PLAIN.test(s) || NUMBER_LIKE.test(s)) return false;
|
|
78
|
-
// ": " opens a mapping; " #" opens a comment; a trailing ":" also reads as a key.
|
|
79
|
-
if (s.includes(": ") || s.includes(" #") || s.endsWith(":")) return false;
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function quoteDouble(s: string): string {
|
|
84
|
-
let out = '"';
|
|
85
|
-
for (const ch of s) {
|
|
86
|
-
const c = ch.codePointAt(0)!;
|
|
87
|
-
if (ch === '"') out += '\\"';
|
|
88
|
-
else if (ch === "\\") out += "\\\\";
|
|
89
|
-
else if (ch === "\n") out += "\\n";
|
|
90
|
-
else if (ch === "\t") out += "\\t";
|
|
91
|
-
else if (ch === "\r") out += "\\r";
|
|
92
|
-
else if (c < 0x20 || c === 0x7f) out += "\\x" + c.toString(16).padStart(2, "0");
|
|
93
|
-
else out += ch;
|
|
94
|
-
}
|
|
95
|
-
return out + '"';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* A multi-line string is only emitted as a block scalar when every line is clean.
|
|
100
|
-
* Leading whitespace on the first line would need an explicit indentation
|
|
101
|
-
* indicator, and trailing whitespace on any line would violate the
|
|
102
|
-
* no-trailing-whitespace guarantee -- both fall back to a double-quoted scalar,
|
|
103
|
-
* which is always correct if less pretty.
|
|
104
|
-
*/
|
|
105
|
-
function canUseBlockScalar(s: string): boolean {
|
|
106
|
-
if (!s.includes("\n")) return false;
|
|
107
|
-
if (hasControlChars(s)) return false;
|
|
108
|
-
// `|+` (keep) is the most ambiguous corner of the spec -- parsers disagree about
|
|
109
|
-
// trailing blank lines at EOF. Two or more trailing newlines therefore fall back
|
|
110
|
-
// to a quoted scalar rather than relying on that behaviour.
|
|
111
|
-
if (/\n\n$/.test(s)) return false;
|
|
112
|
-
const lines = s.split("\n");
|
|
113
|
-
const firstNonEmpty = lines.find((l) => l.length > 0);
|
|
114
|
-
if (firstNonEmpty !== undefined && /^\s/.test(firstNonEmpty)) return false;
|
|
115
|
-
for (const line of lines) {
|
|
116
|
-
if (line.length > 0 && line !== line.replace(/\s+$/, "")) return false;
|
|
117
|
-
}
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function blockScalar(s: string, indent: string): string {
|
|
122
|
-
// canUseBlockScalar() guarantees at most one trailing newline here.
|
|
123
|
-
const trailing = /\n*$/.exec(s)![0].length;
|
|
124
|
-
const header = trailing === 0 ? "|-" : "|";
|
|
125
|
-
const body = trailing === 0 ? s : s.slice(0, -1);
|
|
126
|
-
const inner = indent + INDENT;
|
|
127
|
-
const lines = body.split("\n").map((l) => (l.length === 0 ? "" : inner + l));
|
|
128
|
-
return header + "\n" + lines.join("\n");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function scalar(v: string | number | boolean | null, indent: string): string {
|
|
132
|
-
if (v === null) return "null";
|
|
133
|
-
if (typeof v === "boolean") return v ? "true" : "false";
|
|
134
|
-
if (typeof v === "number") {
|
|
135
|
-
if (!Number.isFinite(v)) {
|
|
136
|
-
throw new TypeError(`yaml: non-finite number is not emittable: ${v}`);
|
|
137
|
-
}
|
|
138
|
-
// Number#toString is the shortest round-trippable form, hence deterministic.
|
|
139
|
-
// String(-0) drops the sign, so spell it out.
|
|
140
|
-
return Object.is(v, -0) ? "-0" : String(v);
|
|
141
|
-
}
|
|
142
|
-
if (canUseBlockScalar(v)) return blockScalar(v, indent);
|
|
143
|
-
if (isPlainSafe(v)) return v;
|
|
144
|
-
return quoteDouble(v);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function isPlainObject(v: unknown): v is { readonly [key: string]: YamlValue } {
|
|
148
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
type Scalar = string | number | boolean | null;
|
|
152
|
-
|
|
153
|
-
function isScalar(v: YamlValue): v is Scalar {
|
|
154
|
-
return v === null || ["string", "number", "boolean"].includes(typeof v);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Returns the flow rendering (`[a, b]`) when every element is a scalar that needs
|
|
159
|
-
* no block form and the result fits FLOW_MAX_WIDTH; otherwise undefined, and the
|
|
160
|
-
* caller falls back to block style.
|
|
161
|
-
*/
|
|
162
|
-
function tryFlow(items: readonly YamlValue[]): string | undefined {
|
|
163
|
-
if (items.length === 0) return undefined;
|
|
164
|
-
if (!items.every(isScalar)) return undefined;
|
|
165
|
-
const parts: string[] = [];
|
|
166
|
-
for (const item of items as readonly Scalar[]) {
|
|
167
|
-
if (typeof item === "string" && item.includes("\n")) return undefined;
|
|
168
|
-
parts.push(scalar(item, ""));
|
|
169
|
-
}
|
|
170
|
-
const text = "[" + parts.join(", ") + "]";
|
|
171
|
-
return text.length <= FLOW_MAX_WIDTH ? text : undefined;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function orderedKeys(
|
|
175
|
-
obj: { readonly [key: string]: YamlValue },
|
|
176
|
-
path: readonly string[],
|
|
177
|
-
opts: EmitOptions,
|
|
178
|
-
): string[] {
|
|
179
|
-
const present = Object.keys(obj).filter((k) => obj[k] !== undefined);
|
|
180
|
-
const preferred = opts.keyOrder?.(path);
|
|
181
|
-
if (!preferred) return present.sort();
|
|
182
|
-
const set = new Set(present);
|
|
183
|
-
const ordered = preferred.filter((k) => set.has(k));
|
|
184
|
-
const rest = present.filter((k) => !preferred.includes(k)).sort();
|
|
185
|
-
return [...ordered, ...rest];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function emitKey(k: string): string {
|
|
189
|
-
return isPlainSafe(k) ? k : quoteDouble(k);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function emitNode(
|
|
193
|
-
v: YamlValue,
|
|
194
|
-
indent: string,
|
|
195
|
-
path: readonly string[],
|
|
196
|
-
opts: EmitOptions,
|
|
197
|
-
lines: string[],
|
|
198
|
-
): void {
|
|
199
|
-
if (Array.isArray(v)) {
|
|
200
|
-
const items = v.filter((x) => x !== undefined);
|
|
201
|
-
if (items.length === 0) {
|
|
202
|
-
lines[lines.length - 1] += " []";
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
for (const item of items) {
|
|
206
|
-
if (Array.isArray(item)) {
|
|
207
|
-
const flow = tryFlow(item);
|
|
208
|
-
if (flow !== undefined) {
|
|
209
|
-
lines.push(indent + "- " + flow);
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (Array.isArray(item) || isPlainObject(item)) {
|
|
214
|
-
const before = lines.length;
|
|
215
|
-
lines.push(indent + "-");
|
|
216
|
-
emitNode(item, indent + INDENT, path, opts, lines);
|
|
217
|
-
// Hoist the first child onto the dash line: "- key: value".
|
|
218
|
-
const dashLine = lines[before]!;
|
|
219
|
-
const firstChild = lines[before + 1];
|
|
220
|
-
if (firstChild !== undefined && dashLine === indent + "-") {
|
|
221
|
-
lines.splice(before, 2, indent + "- " + firstChild.slice(indent.length + INDENT.length));
|
|
222
|
-
}
|
|
223
|
-
} else {
|
|
224
|
-
lines.push(indent + "- " + scalar(item as string | number | boolean | null, indent));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (isPlainObject(v)) {
|
|
231
|
-
const keys = orderedKeys(v, path, opts);
|
|
232
|
-
if (keys.length === 0) {
|
|
233
|
-
lines[lines.length - 1] += " {}";
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
for (const k of keys) {
|
|
237
|
-
const child = v[k]!;
|
|
238
|
-
const childPath = [...path, k];
|
|
239
|
-
if (Array.isArray(child)) {
|
|
240
|
-
const items = child.filter((x) => x !== undefined);
|
|
241
|
-
const flow = tryFlow(items);
|
|
242
|
-
if (items.length === 0) {
|
|
243
|
-
lines.push(indent + emitKey(k) + ": []");
|
|
244
|
-
} else if (flow !== undefined) {
|
|
245
|
-
lines.push(indent + emitKey(k) + ": " + flow);
|
|
246
|
-
} else {
|
|
247
|
-
lines.push(indent + emitKey(k) + ":");
|
|
248
|
-
// Indent sequences under their key; valid either way, but readable.
|
|
249
|
-
emitNode(child, indent + INDENT, childPath, opts, lines);
|
|
250
|
-
}
|
|
251
|
-
} else if (isPlainObject(child)) {
|
|
252
|
-
if (orderedKeys(child, childPath, opts).length === 0) {
|
|
253
|
-
lines.push(indent + emitKey(k) + ": {}");
|
|
254
|
-
} else {
|
|
255
|
-
lines.push(indent + emitKey(k) + ":");
|
|
256
|
-
emitNode(child, indent + INDENT, childPath, opts, lines);
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
lines.push(
|
|
260
|
-
indent + emitKey(k) + ": " +
|
|
261
|
-
scalar(child as string | number | boolean | null, indent),
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
lines.push(indent + scalar(v as string | number | boolean | null, indent));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Emit `value` as deterministic block-style YAML with a single trailing newline. */
|
|
272
|
-
export function emitYaml(value: YamlValue, opts: EmitOptions = {}): string {
|
|
273
|
-
if (value === undefined) return "";
|
|
274
|
-
const lines: string[] = [];
|
|
275
|
-
if (Array.isArray(value) || isPlainObject(value)) {
|
|
276
|
-
emitNode(value, "", [], opts, lines);
|
|
277
|
-
} else {
|
|
278
|
-
lines.push(scalar(value as string | number | boolean | null, ""));
|
|
279
|
-
}
|
|
280
|
-
if (lines.length === 0) return "{}\n";
|
|
281
|
-
return lines.join("\n") + "\n";
|
|
282
|
-
}
|