@adhdev/daemon-core 0.9.82-rc.21 → 0.9.82-rc.210
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/dist/boot/daemon-lifecycle.d.ts +1 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +23 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +3 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
- package/dist/cli-adapters/terminal-screen.d.ts +2 -11
- package/dist/commands/cli-manager.d.ts +2 -1
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +32 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +72 -1
- package/dist/git/git-commands.d.ts +9 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +46 -11
- package/dist/index.js +35171 -15756
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34878 -15556
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +36 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
- package/dist/mesh/mesh-events-pending.d.ts +33 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +14 -0
- package/dist/mesh/mesh-events.d.ts +5 -40
- package/dist/mesh/mesh-fast-forward.d.ts +41 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-missions.d.ts +58 -0
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-runtime-store.d.ts +324 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +137 -6
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +215 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
- package/dist/providers/acp-provider-instance.d.ts +2 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +39 -3
- package/dist/providers/contracts.d.ts +130 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +12 -0
- package/dist/providers/provider-instance.d.ts +13 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +52 -0
- package/dist/providers/spec/cli-adapter.d.ts +101 -0
- package/dist/providers/spec/driver.d.ts +225 -0
- package/dist/providers/spec/evaluator.d.ts +54 -0
- package/dist/providers/spec/loader.d.ts +15 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +2025 -0
- package/dist/providers/spec/types.d.ts +302 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +109 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +6 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +22 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +22 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1138 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +673 -1139
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +59 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
- package/src/cli-adapters/terminal-backends/types.ts +1 -3
- package/src/cli-adapters/terminal-screen.ts +11 -81
- package/src/commands/chat-commands.ts +1787 -60
- package/src/commands/cli-manager.ts +283 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +4198 -557
- package/src/config/chat-history.ts +95 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +280 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +42 -12
- package/src/git/git-diff.ts +53 -0
- package/src/git/git-status.ts +35 -6
- package/src/git/git-types.ts +2 -0
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +126 -10
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +218 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +1404 -0
- package/src/mesh/mesh-events-pending.ts +371 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +161 -0
- package/src/mesh/mesh-events.ts +24 -908
- package/src/mesh/mesh-fast-forward.ts +438 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +587 -102
- package/src/mesh/mesh-missions.ts +151 -0
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-runtime-store.ts +1305 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +490 -166
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +403 -0
- package/src/mesh/worktree-bootstrap-config.ts +326 -0
- package/src/providers/acp-provider-instance.ts +15 -1
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +833 -64
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +10 -1
- package/src/providers/provider-loader.ts +582 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +138 -0
- package/src/providers/spec/cli-adapter.ts +626 -0
- package/src/providers/spec/driver.ts +912 -0
- package/src/providers/spec/evaluator.ts +597 -0
- package/src/providers/spec/loader.ts +324 -0
- package/src/providers/spec/native-history-executor.ts +939 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +691 -0
- package/src/providers/spec/schema.json +341 -0
- package/src/providers/spec/types.ts +356 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +425 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +119 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec evaluator — pure function.
|
|
3
|
+
*
|
|
4
|
+
* Given a visible screen text and a CliSpec (v3), returns:
|
|
5
|
+
* - which sections cover which line ranges
|
|
6
|
+
* - which state matched and why
|
|
7
|
+
* - extracted modal title + buttons (if any)
|
|
8
|
+
* - which control_bar entries are visible
|
|
9
|
+
* - which notifications/delegates this evaluation activates
|
|
10
|
+
* - a trace object that explains every decision (for the inspector)
|
|
11
|
+
*
|
|
12
|
+
* No I/O, no state. Pass prevLines for delta (changed) condition support.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
CliSpec, SectionDef, SpecStateV3, Condition, RegexCondition,
|
|
18
|
+
ChangedCondition, AllCondition, AnyCondition,
|
|
19
|
+
ExtractTitle, ExtractButtons,
|
|
20
|
+
ControlAction, NotificationRule, DelegateTrigger,
|
|
21
|
+
} from './types.js';
|
|
22
|
+
|
|
23
|
+
export interface ResolvedSection {
|
|
24
|
+
id: string;
|
|
25
|
+
fromLine: number; // inclusive
|
|
26
|
+
toLine: number; // exclusive
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ModalSnapshot {
|
|
31
|
+
title: string | null;
|
|
32
|
+
buttons: { index: number; label: string; key: string }[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface VisibleControl {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
actionType: 'send_keys' | 'open_picker' | 'attach_image';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FiredNotification {
|
|
42
|
+
id: string;
|
|
43
|
+
title: string;
|
|
44
|
+
body: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface FiredDelegate {
|
|
48
|
+
id: string;
|
|
49
|
+
task: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TraceEntry {
|
|
53
|
+
kind: 'section' | 'state_match' | 'state_skip' | 'modal' | 'control' | 'notification' | 'delegate';
|
|
54
|
+
text: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SpecEvaluation {
|
|
58
|
+
state: { id: string; label: string; title: string | null };
|
|
59
|
+
modal: ModalSnapshot | null;
|
|
60
|
+
controls: VisibleControl[];
|
|
61
|
+
notifications: FiredNotification[];
|
|
62
|
+
delegates: FiredDelegate[];
|
|
63
|
+
sections: ResolvedSection[];
|
|
64
|
+
trace: TraceEntry[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Layout — v3 sections{} object
|
|
69
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function resolveSize(size: number | string | undefined, total: number): number {
|
|
72
|
+
if (size === undefined) return 0;
|
|
73
|
+
if (typeof size === 'number') return Math.max(0, Math.min(total, size));
|
|
74
|
+
const m = /^(\d+(?:\.\d+)?)%$/.exec(size);
|
|
75
|
+
if (!m) return 0;
|
|
76
|
+
const pct = Number(m[1]);
|
|
77
|
+
return Math.max(0, Math.min(total, Math.round((total * pct) / 100)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve v3 sections{} object into an ordered array of ResolvedSection.
|
|
82
|
+
* Two-pass: first anchor/positional, then apply `until` cross-references.
|
|
83
|
+
*/
|
|
84
|
+
function resolveSections(
|
|
85
|
+
sectionsObj: Record<string, SectionDef>,
|
|
86
|
+
lines: string[],
|
|
87
|
+
): ResolvedSection[] {
|
|
88
|
+
const total = lines.length;
|
|
89
|
+
const anchored = new Map<string, { fromLine: number; toLine: number }>();
|
|
90
|
+
const sectionEntries = Object.entries(sectionsObj);
|
|
91
|
+
|
|
92
|
+
for (const [id, sec] of sectionEntries) {
|
|
93
|
+
let from = 0;
|
|
94
|
+
let to = total;
|
|
95
|
+
|
|
96
|
+
if (sec.anchor !== undefined) {
|
|
97
|
+
try {
|
|
98
|
+
const re = new RegExp(sec.anchor, sec.anchor_flags ?? '');
|
|
99
|
+
const prevRe = sec.anchor_context?.prev !== undefined
|
|
100
|
+
? new RegExp(sec.anchor_context.prev, sec.anchor_context.prev_flags ?? '') : null;
|
|
101
|
+
const nextRe = sec.anchor_context?.next !== undefined
|
|
102
|
+
? new RegExp(sec.anchor_context.next, sec.anchor_context.next_flags ?? '') : null;
|
|
103
|
+
const matches = (i: number) =>
|
|
104
|
+
re.test(lines[i])
|
|
105
|
+
&& (prevRe === null || (i > 0 && prevRe.test(lines[i - 1])))
|
|
106
|
+
&& (nextRe === null || (i < total - 1 && nextRe.test(lines[i + 1])));
|
|
107
|
+
let idx = -1;
|
|
108
|
+
if (sec.anchor_last) {
|
|
109
|
+
for (let i = total - 1; i >= 0; i--) { if (matches(i)) { idx = i; break; } }
|
|
110
|
+
} else {
|
|
111
|
+
for (let i = 0; i < total; i++) { if (matches(i)) { idx = i; break; } }
|
|
112
|
+
}
|
|
113
|
+
if (idx !== -1) {
|
|
114
|
+
from = idx;
|
|
115
|
+
to = total;
|
|
116
|
+
if (sec.until_regex !== undefined) {
|
|
117
|
+
// until_regex on anchor-based sections (extension field)
|
|
118
|
+
try {
|
|
119
|
+
const ure = new RegExp(sec.until_regex, sec.until_regex_flags ?? '');
|
|
120
|
+
const end = lines.findIndex((l, i) => i > idx && ure.test(l));
|
|
121
|
+
if (end !== -1) to = end;
|
|
122
|
+
} catch { /* bad until_regex */ }
|
|
123
|
+
} else if (sec.lines !== undefined) {
|
|
124
|
+
to = Math.min(total, from + sec.lines);
|
|
125
|
+
}
|
|
126
|
+
// `until` regex check on anchor sections (starts with ^)
|
|
127
|
+
// handled in second pass
|
|
128
|
+
}
|
|
129
|
+
} catch { /* bad anchor regex */ }
|
|
130
|
+
} else if (sec.from_top !== undefined) {
|
|
131
|
+
from = resolveSize(sec.from_top, total);
|
|
132
|
+
to = total;
|
|
133
|
+
} else if (sec.from_bottom !== undefined) {
|
|
134
|
+
const sz = resolveSize(sec.from_bottom, total);
|
|
135
|
+
from = total - sz;
|
|
136
|
+
to = total;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
anchored.set(id, { fromLine: from, toLine: to });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Second pass: apply `until` references or `until` regex strings.
|
|
143
|
+
const resolved: ResolvedSection[] = [];
|
|
144
|
+
for (const [id, sec] of sectionEntries) {
|
|
145
|
+
let { fromLine, toLine } = anchored.get(id)!;
|
|
146
|
+
|
|
147
|
+
if (sec.until !== undefined) {
|
|
148
|
+
if (sec.until.startsWith('^')) {
|
|
149
|
+
// `until` is a regex: stop at the first matching line after fromLine
|
|
150
|
+
try {
|
|
151
|
+
const ure = new RegExp(sec.until);
|
|
152
|
+
const end = lines.findIndex((l, i) => i > fromLine && ure.test(l));
|
|
153
|
+
if (end !== -1) toLine = end;
|
|
154
|
+
} catch { /* bad until regex */ }
|
|
155
|
+
} else {
|
|
156
|
+
// `until` is a section id reference
|
|
157
|
+
const target = anchored.get(sec.until);
|
|
158
|
+
if (target) toLine = target.fromLine;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (toLine < fromLine) toLine = fromLine;
|
|
163
|
+
const text = lines.slice(fromLine, toLine).join('\n');
|
|
164
|
+
resolved.push({ id, fromLine, toLine, text });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return resolved;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sectionText(sections: ResolvedSection[], sectionId: string | undefined, fullScreen: string): string {
|
|
171
|
+
if (!sectionId) return fullScreen;
|
|
172
|
+
const found = sections.find(s => s.id === sectionId);
|
|
173
|
+
return found ? found.text : '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
// Condition evaluation (v3)
|
|
178
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
function isRegexCondition(c: Condition): c is RegexCondition {
|
|
181
|
+
return 'matches' in c;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isChangedCondition(c: Condition): c is ChangedCondition {
|
|
185
|
+
return 'cursor_above' in c && 'changed' in c;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isAllCondition(c: Condition): c is AllCondition {
|
|
189
|
+
return 'all' in c;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isAnyCondition(c: Condition): c is AnyCondition {
|
|
193
|
+
return 'any' in c;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function evaluateCondition(
|
|
197
|
+
cond: Condition,
|
|
198
|
+
sections: ResolvedSection[],
|
|
199
|
+
fullScreen: string,
|
|
200
|
+
cursor: { row: number; col: number } | undefined,
|
|
201
|
+
prevLines: string[] | undefined,
|
|
202
|
+
trace: TraceEntry[],
|
|
203
|
+
stateId: string,
|
|
204
|
+
): boolean {
|
|
205
|
+
if (isAllCondition(cond)) {
|
|
206
|
+
for (const child of cond.all) {
|
|
207
|
+
if (!evaluateCondition(child, sections, fullScreen, cursor, prevLines, trace, stateId)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (isAnyCondition(cond)) {
|
|
215
|
+
for (const child of cond.any) {
|
|
216
|
+
if (evaluateCondition(child, sections, fullScreen, cursor, prevLines, trace, stateId)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isChangedCondition(cond)) {
|
|
224
|
+
if (!cursor || !prevLines || prevLines.length === 0) return false;
|
|
225
|
+
const curLines = fullScreen.split('\n');
|
|
226
|
+
const startRow = Math.max(0, cursor.row - cond.cursor_above);
|
|
227
|
+
const endRow = cursor.row; // exclusive
|
|
228
|
+
const currentSlice = curLines.slice(startRow, endRow).join('\n');
|
|
229
|
+
const prevSlice = prevLines.slice(startRow, endRow).join('\n');
|
|
230
|
+
const result = currentSlice !== prevSlice;
|
|
231
|
+
trace.push({ kind: 'section', text: `state[${stateId}] changed cond cursor_above=${cond.cursor_above} rows[${startRow},${endRow}) changed=${result}` });
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (isRegexCondition(cond)) {
|
|
236
|
+
const haystack = sectionText(sections, cond.section, fullScreen);
|
|
237
|
+
let matched = false;
|
|
238
|
+
try {
|
|
239
|
+
const re = new RegExp(cond.matches, cond.flags ?? 'i');
|
|
240
|
+
matched = re.test(haystack);
|
|
241
|
+
} catch { matched = false; }
|
|
242
|
+
|
|
243
|
+
if (!matched) {
|
|
244
|
+
trace.push({ kind: 'state_skip', text: `state[${stateId}] regex cond ${cond.section ?? '*'}~/${cond.matches}/ no match` });
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Cursor-position guards
|
|
249
|
+
if (cursor !== undefined) {
|
|
250
|
+
if (cond.cursor_row_min !== undefined && cursor.row < cond.cursor_row_min) {
|
|
251
|
+
trace.push({ kind: 'state_skip', text: `state[${stateId}] cursor row ${cursor.row} < cursor_row_min ${cond.cursor_row_min}` });
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (cond.cursor_row_max !== undefined && cursor.row > cond.cursor_row_max) {
|
|
255
|
+
trace.push({ kind: 'state_skip', text: `state[${stateId}] cursor row ${cursor.row} > cursor_row_max ${cond.cursor_row_max}` });
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (cond.cursor_col_min !== undefined && cursor.col < cond.cursor_col_min) {
|
|
259
|
+
trace.push({ kind: 'state_skip', text: `state[${stateId}] cursor col ${cursor.col} < cursor_col_min ${cond.cursor_col_min}` });
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (cond.cursor_col_max !== undefined && cursor.col > cond.cursor_col_max) {
|
|
263
|
+
trace.push({ kind: 'state_skip', text: `state[${stateId}] cursor col ${cursor.col} > cursor_col_max ${cond.cursor_col_max}` });
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
trace.push({ kind: 'state_match', text: `state[${stateId}] regex cond ${cond.section ?? '*'}~/${cond.matches}/ matched${cursor !== undefined ? ` cursor=(${cursor.row},${cursor.col})` : ''}` });
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
276
|
+
// State matching
|
|
277
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
function matchState(
|
|
280
|
+
state: SpecStateV3,
|
|
281
|
+
sections: ResolvedSection[],
|
|
282
|
+
fullScreen: string,
|
|
283
|
+
trace: TraceEntry[],
|
|
284
|
+
cursor: { row: number; col: number } | undefined,
|
|
285
|
+
prevLines: string[] | undefined,
|
|
286
|
+
): { matched: boolean; title: string | null } {
|
|
287
|
+
// Support v1-shaped state.when ({ regex, section }) for backward compat
|
|
288
|
+
// with test objects that don't go through the loader.
|
|
289
|
+
let effectiveWhen = state.when;
|
|
290
|
+
const stateAny = state as any;
|
|
291
|
+
if (stateAny.when && 'regex' in stateAny.when && !('all' in stateAny.when) && !('any' in stateAny.when)) {
|
|
292
|
+
effectiveWhen = normalizeV1When(stateAny.when);
|
|
293
|
+
}
|
|
294
|
+
const condMatched = evaluateCondition(effectiveWhen, sections, fullScreen, cursor, prevLines, trace, state.id);
|
|
295
|
+
|
|
296
|
+
if (!condMatched) {
|
|
297
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] when condition not met` });
|
|
298
|
+
return { matched: false, title: null };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
trace.push({ kind: 'state_match', text: `state[${state.id}] matched` });
|
|
302
|
+
|
|
303
|
+
let title: string | null = null;
|
|
304
|
+
const stateAny2 = state as any;
|
|
305
|
+
const extract = state.extract;
|
|
306
|
+
// v1 compat: extract_title
|
|
307
|
+
const titleRule: ExtractTitle | undefined = extract?.title
|
|
308
|
+
?? (stateAny2.extract_title ? v1ExtractTitleToV3(stateAny2.extract_title) : undefined);
|
|
309
|
+
if (titleRule) {
|
|
310
|
+
title = extractTitle(titleRule, sections, fullScreen);
|
|
311
|
+
trace.push({ kind: 'state_match', text: `state[${state.id}] extract.title → ${title ?? '(none)'}` });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { matched: true, title };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function extractTitle(
|
|
318
|
+
rule: ExtractTitle,
|
|
319
|
+
sections: ResolvedSection[],
|
|
320
|
+
fullScreen: string,
|
|
321
|
+
): string | null {
|
|
322
|
+
const hay = sectionText(sections, rule.section, fullScreen);
|
|
323
|
+
if (!hay) return null;
|
|
324
|
+
|
|
325
|
+
if (rule.first_line) {
|
|
326
|
+
// Take the first non-separator, non-empty line
|
|
327
|
+
const lines = hay.split('\n');
|
|
328
|
+
for (const line of lines) {
|
|
329
|
+
const stripped = line.trim();
|
|
330
|
+
if (stripped && !/^[─╌═─\s]+$/.test(stripped)) {
|
|
331
|
+
return stripped;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (rule.regex) {
|
|
338
|
+
try {
|
|
339
|
+
const re = new RegExp(rule.regex, rule.flags ?? 'i');
|
|
340
|
+
const m = re.exec(hay);
|
|
341
|
+
if (m) return (m[1] ?? m[0]).trim();
|
|
342
|
+
} catch { /* bad regex */ }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function compilePattern(ref: { pattern: string; flags?: string }): RegExp {
|
|
349
|
+
const flags = ref.flags ?? 'gm';
|
|
350
|
+
return new RegExp(ref.pattern, flags.includes('g') ? flags : flags + 'g');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
|
|
354
|
+
const flags = (ref.flags ?? 'm').replace(/g/g, '');
|
|
355
|
+
return new RegExp(ref.pattern, flags);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function extractButtonsFromRule(
|
|
359
|
+
rule: ExtractButtons,
|
|
360
|
+
hay: string,
|
|
361
|
+
): { index: number; label: string; key: string }[] {
|
|
362
|
+
const keyTemplate = rule.key_for_index;
|
|
363
|
+
const continuationLines = rule.continuation_lines ?? false;
|
|
364
|
+
const buttons: { index: number; label: string; key: string }[] = [];
|
|
365
|
+
|
|
366
|
+
if (continuationLines) {
|
|
367
|
+
const re = compileLinePattern(rule);
|
|
368
|
+
const lines = hay.split('\n');
|
|
369
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
370
|
+
const m = re.exec(lines[i]);
|
|
371
|
+
if (!m) continue;
|
|
372
|
+
const idx = Number(m[1]);
|
|
373
|
+
let label = String(m[2] ?? '').trim();
|
|
374
|
+
if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
|
|
375
|
+
let j = i + 1;
|
|
376
|
+
while (j < lines.length) {
|
|
377
|
+
const next = lines[j];
|
|
378
|
+
if (!next.trim()) break;
|
|
379
|
+
if (re.test(next)) break;
|
|
380
|
+
if (!/^\s+/.test(next)) break;
|
|
381
|
+
label += ' ' + next.trim();
|
|
382
|
+
j += 1;
|
|
383
|
+
}
|
|
384
|
+
if (buttons.some(b => b.index === idx)) continue;
|
|
385
|
+
const key = keyTemplate.replace(/\{index\}/g, String(idx));
|
|
386
|
+
buttons.push({ index: idx, label, key });
|
|
387
|
+
i = j - 1;
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
const re = compilePattern(rule);
|
|
391
|
+
let m: RegExpExecArray | null;
|
|
392
|
+
while ((m = re.exec(hay)) !== null) {
|
|
393
|
+
const idx = Number(m[1]);
|
|
394
|
+
const label = String(m[2] ?? '').trim();
|
|
395
|
+
if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
|
|
396
|
+
if (buttons.some(b => b.index === idx)) continue;
|
|
397
|
+
const key = keyTemplate.replace(/\{index\}/g, String(idx));
|
|
398
|
+
buttons.push({ index: idx, label, key });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
buttons.sort((a, b) => a.index - b.index);
|
|
403
|
+
return buttons;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function extractModal(
|
|
407
|
+
state: SpecStateV3,
|
|
408
|
+
sections: ResolvedSection[],
|
|
409
|
+
fullScreen: string,
|
|
410
|
+
title: string | null,
|
|
411
|
+
trace: TraceEntry[],
|
|
412
|
+
): ModalSnapshot | null {
|
|
413
|
+
const stateAny = state as any;
|
|
414
|
+
// v1 compat: modal_buttons
|
|
415
|
+
const buttonsRule: ExtractButtons | undefined = state.extract?.buttons
|
|
416
|
+
?? (stateAny.modal_buttons ? v1ModalButtonsToV3(stateAny.modal_buttons) : undefined);
|
|
417
|
+
if (!buttonsRule) return null;
|
|
418
|
+
|
|
419
|
+
const hay = sectionText(sections, buttonsRule.section, fullScreen);
|
|
420
|
+
const minCount = buttonsRule.min_count ?? 2;
|
|
421
|
+
const buttons = extractButtonsFromRule(buttonsRule, hay);
|
|
422
|
+
|
|
423
|
+
if (buttons.length < minCount) {
|
|
424
|
+
trace.push({ kind: 'modal', text: `extract.buttons matched ${buttons.length}/${minCount} required — discarded` });
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
trace.push({ kind: 'modal', text: `extract.buttons matched ${buttons.length} choices` });
|
|
428
|
+
return { title, buttons };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
432
|
+
// v1 backward-compat helpers
|
|
433
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Convert a v1 sections array to a v3 sections object (for inline test specs
|
|
437
|
+
* that bypass the loader and thus the migration path).
|
|
438
|
+
*/
|
|
439
|
+
function buildSectionsMapFromV1(v1Sections: any[]): Record<string, SectionDef> {
|
|
440
|
+
const map: Record<string, SectionDef> = {};
|
|
441
|
+
for (const sec of v1Sections) {
|
|
442
|
+
const { id, anchor_regex, ...rest } = sec;
|
|
443
|
+
const def: SectionDef = { ...rest };
|
|
444
|
+
if (anchor_regex) def.anchor = anchor_regex;
|
|
445
|
+
// v1 until: { section: id } → v3 until: id
|
|
446
|
+
if (rest.until?.section) def.until = rest.until.section;
|
|
447
|
+
else if (rest.until && typeof rest.until === 'string') def.until = rest.until;
|
|
448
|
+
else delete (def as any).until;
|
|
449
|
+
map[id] = def;
|
|
450
|
+
}
|
|
451
|
+
return map;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Convert v1 SectionRegex (when.regex) to v3 AllCondition. */
|
|
455
|
+
function normalizeV1When(v1When: any): AllCondition {
|
|
456
|
+
if (v1When?.cursor_above_lines && v1When?.changed) {
|
|
457
|
+
return { all: [{ cursor_above: v1When.cursor_above_lines, changed: true as const }] };
|
|
458
|
+
}
|
|
459
|
+
const cond: any = { matches: v1When.regex };
|
|
460
|
+
if (v1When.section) cond.section = v1When.section;
|
|
461
|
+
if (v1When.flags) cond.flags = v1When.flags;
|
|
462
|
+
if (v1When.cursor_row_min !== undefined) cond.cursor_row_min = v1When.cursor_row_min;
|
|
463
|
+
if (v1When.cursor_row_max !== undefined) cond.cursor_row_max = v1When.cursor_row_max;
|
|
464
|
+
if (v1When.cursor_col_min !== undefined) cond.cursor_col_min = v1When.cursor_col_min;
|
|
465
|
+
if (v1When.cursor_col_max !== undefined) cond.cursor_col_max = v1When.cursor_col_max;
|
|
466
|
+
return { all: [cond] };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Convert v1 extract_title to v3 ExtractTitle. */
|
|
470
|
+
function v1ExtractTitleToV3(v1: any): ExtractTitle {
|
|
471
|
+
if (v1.first_line) return { section: v1.section, first_line: true };
|
|
472
|
+
return { section: v1.section, regex: v1.regex, flags: v1.flags };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Convert v1 modal_buttons to v3 ExtractButtons. */
|
|
476
|
+
function v1ModalButtonsToV3(v1: any): ExtractButtons | undefined {
|
|
477
|
+
// Use first pattern from patterns[] or single pattern
|
|
478
|
+
const pat = v1.patterns?.length ? v1.patterns[0].pattern : v1.pattern;
|
|
479
|
+
if (!pat) return undefined;
|
|
480
|
+
const flg = v1.patterns?.length ? v1.patterns[0].flags : v1.flags;
|
|
481
|
+
return {
|
|
482
|
+
section: v1.section,
|
|
483
|
+
pattern: pat,
|
|
484
|
+
flags: flg,
|
|
485
|
+
key_for_index: v1.key_for_index,
|
|
486
|
+
min_count: v1.min_count,
|
|
487
|
+
continuation_lines: v1.continuation_lines,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
492
|
+
// Public evaluator
|
|
493
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
export function evaluate(
|
|
496
|
+
spec: CliSpec,
|
|
497
|
+
screenText: string,
|
|
498
|
+
/** Optional cursor position (0-based row and col). */
|
|
499
|
+
cursor?: { row: number; col: number },
|
|
500
|
+
/** Optional previous screen lines for `changed` condition detection. */
|
|
501
|
+
prevLines?: string[],
|
|
502
|
+
): SpecEvaluation {
|
|
503
|
+
const trace: TraceEntry[] = [];
|
|
504
|
+
const lines = screenText.split('\n').map(l => l.endsWith('\r') ? l.slice(0, -1) : l);
|
|
505
|
+
const cleanScreen = lines.join('\n');
|
|
506
|
+
|
|
507
|
+
// Support both v3 (sections{}) and v1-shaped objects (layout.sections[])
|
|
508
|
+
// The v1 path exists for tests that build raw spec objects without going
|
|
509
|
+
// through the loader (which would normally migrate v1 → v3).
|
|
510
|
+
const specAny = spec as any;
|
|
511
|
+
const effectiveSectionsMap: Record<string, SectionDef> = spec.sections
|
|
512
|
+
?? buildSectionsMapFromV1(specAny.layout?.sections ?? []);
|
|
513
|
+
const sections = resolveSections(effectiveSectionsMap, lines);
|
|
514
|
+
|
|
515
|
+
for (const s of sections) {
|
|
516
|
+
trace.push({ kind: 'section', text: `section[${s.id}] lines [${s.fromLine}, ${s.toLine}) (${s.toLine - s.fromLine} lines)` });
|
|
517
|
+
}
|
|
518
|
+
if (cursor !== undefined) {
|
|
519
|
+
trace.push({ kind: 'section', text: `cursor (${cursor.row}, ${cursor.col})` });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let activeState: { id: string; label: string; title: string | null } | null = null;
|
|
523
|
+
let modal: ModalSnapshot | null = null;
|
|
524
|
+
|
|
525
|
+
for (const st of spec.states) {
|
|
526
|
+
const { matched, title } = matchState(st, sections, cleanScreen, trace, cursor, prevLines);
|
|
527
|
+
if (!matched) continue;
|
|
528
|
+
const extractedModal = extractModal(st, sections, cleanScreen, title, trace);
|
|
529
|
+
// If the state declares extract.buttons (or v1 modal_buttons) but
|
|
530
|
+
// extraction failed, don't promote the state (avoids phantom approvals).
|
|
531
|
+
const stAny = st as any;
|
|
532
|
+
if ((st.extract?.buttons || stAny.modal_buttons) && !extractedModal) {
|
|
533
|
+
trace.push({ kind: 'state_skip', text: `state[${st.id}] matched but extract.buttons failed — not promoting` });
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
activeState = { id: st.id, label: st.label, title };
|
|
537
|
+
modal = extractedModal;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!activeState) {
|
|
542
|
+
const def = spec.states.find(s => s.id === spec.default_state);
|
|
543
|
+
if (def) {
|
|
544
|
+
activeState = { id: def.id, label: def.label, title: null };
|
|
545
|
+
trace.push({ kind: 'state_match', text: `(no state matched — fallback to default_state ${def.id})` });
|
|
546
|
+
} else {
|
|
547
|
+
activeState = { id: spec.default_state, label: spec.default_state, title: null };
|
|
548
|
+
trace.push({ kind: 'state_match', text: `(no state matched, no default_state defined — using id "${spec.default_state}")` });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const controls: VisibleControl[] = [];
|
|
553
|
+
for (const c of spec.control_bar ?? []) {
|
|
554
|
+
const visible = !c.visible_when_state || c.visible_when_state.includes(activeState.id);
|
|
555
|
+
trace.push({ kind: 'control', text: `control[${c.id}] visible=${visible}${c.visible_when_state ? ` (when_state ${c.visible_when_state.join('|')})` : ''}` });
|
|
556
|
+
if (visible) controls.push({ id: c.id, label: c.label, actionType: c.action.type });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const notifications: FiredNotification[] = [];
|
|
560
|
+
for (const n of spec.notifications ?? []) {
|
|
561
|
+
if (n.when_state !== activeState.id) continue;
|
|
562
|
+
const body = interpolate(n.body ?? '', activeState, sections);
|
|
563
|
+
notifications.push({ id: n.id, title: n.title, body });
|
|
564
|
+
trace.push({ kind: 'notification', text: `notification[${n.id}] fired (when_state=${n.when_state})` });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const delegates: FiredDelegate[] = [];
|
|
568
|
+
for (const d of spec.delegate ?? []) {
|
|
569
|
+
if (d.when_state !== activeState.id) continue;
|
|
570
|
+
delegates.push({ id: d.id, task: interpolate(d.task_template, activeState, sections) });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
state: activeState,
|
|
575
|
+
modal,
|
|
576
|
+
controls,
|
|
577
|
+
notifications,
|
|
578
|
+
delegates,
|
|
579
|
+
sections,
|
|
580
|
+
trace,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function interpolate(
|
|
585
|
+
template: string,
|
|
586
|
+
state: { id: string; label: string; title: string | null },
|
|
587
|
+
sections: ResolvedSection[],
|
|
588
|
+
): string {
|
|
589
|
+
return template
|
|
590
|
+
.replace(/\{state\.label\}/g, state.label)
|
|
591
|
+
.replace(/\{state\.title\}/g, state.title ?? '')
|
|
592
|
+
.replace(/\{state\.id\}/g, state.id)
|
|
593
|
+
.replace(/\{screen\.([a-z][a-z0-9_]*)\}/g, (_, id: string) => {
|
|
594
|
+
const s = sections.find(s => s.id === id);
|
|
595
|
+
return s ? s.text : '';
|
|
596
|
+
});
|
|
597
|
+
}
|