@entrorove/skill 0.1.69 → 0.1.71

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.71 — 2026-10-20
4
+
5
+ - 通用分离源码依赖事实与未知构建行为:动态配置、宏及加载器保留真实哈希与只读来源,不执行项目代码,不把构建未知标为完整。原审批与独立 S/M 签名仅允许已知源码的非结构修改;隐私文件、路径、资源预算、未知目标及配置漂移继续明确拒绝,实际构建与测试仍需原流程验收。
6
+ - Separate raw source dependency facts from opaque build behavior across projects. Preserve actual hashes and read-only origins for dynamic configuration, macros and loaders without executing project code or claiming compiled completeness. Original approvals and separate S/M signatures permit only known-source nonstructural modifications; private files, unsafe paths, resource budgets, unknown targets and configuration drift remain explicit refusals. Actual builds and tests still require original-flow acceptance.
7
+
8
+ ## 0.1.70 — 2026-10-20
9
+
10
+ - 云端沙箱固定中英文输出语言,界面、通知和 Blueprint 文档正文按对应语种显示,原始证据单列来源并保留旧流程精确版本。修复 uni-app 默认条件编译分支解析与长扫描后的原任务签名挑战衔接,核对真实读集并保留原审批、完整路径、快照及失败历史。
11
+ - Freeze cloud sandbox output language across presentation, notifications and Blueprint document prose; preserve original source evidence and exact historical runtimes. Fix default uni-app conditional branch inspection and renew the original signing challenge after long scans, rechecking actual reads while retaining approvals, complete paths, snapshots and failure history.
12
+
3
13
  ## 0.1.69 — 2026-10-20
4
14
 
5
15
  - 新增首次范围门禁前的构建上下文纠正:原账号审核真实配置和构建证据,原 S 签名完整路径快照,通过服务挑战追加上下文权威记录;保留原 flow、绑定、旧上下文和失败历史,不授予写入或完成权限。
package/IDE-EXECUTION.md CHANGED
@@ -33,9 +33,9 @@ On first use or after a material agreement revision, the CLI returns a Confirm P
33
33
 
34
34
  ### IDE display and cloud planning entry
35
35
 
36
- For a new engineering task, run `npx --yes --prefer-online @entrorove/skill@latest invoke` with `operation: "start"` and the actual task facts below. The CLI reads the authenticated account's sandbox preference before dispatch. An enabled account returns `status: "needs-input"` and a cloud choice, with no model call yet. This is a routing question, not an execution failure. Render it immediately with the host's question tool or native UI; if unavailable, display `interaction.chatFallback` and collect the answer. A collapsed command log is insufficient. Offer **确认** and **退出,采用本地方案**. After confirmation call the returned `cloud-task` callback with its exact original payload and `sandboxChoice: "use"`; exit repeats the original `start` with top-level `sandboxChoice: "local"`. Preserve the original goal, risk, work type and requirements. A disabled account continues through its configured local workflow; unavailable preferences stop entry explicitly. Reuse a prior explicit choice while resuming this task. Silence never authorizes cloud consumption.
36
+ For a new engineering task, run `npx --yes --prefer-online @entrorove/skill@latest invoke` with `operation: "start"` and the actual task facts below. The CLI reads the authenticated account's sandbox preference before dispatch. An enabled account returns `status: "needs-input"` and a cloud choice, with no model call yet. This is a routing question, not an execution failure. Render it immediately with the host's question tool or native UI; if unavailable, display `interaction.chatFallback` and collect the answer. A collapsed command log is insufficient. Use the returned locale-specific labels for **Use cloud sandbox** and **Continue locally**. After confirmation call the returned `cloud-task` callback with its exact original payload and `sandboxChoice: "use"`; exit repeats the original `start` with top-level `sandboxChoice: "local"`. Preserve the original goal, risk, work type and requirements. A disabled account continues through its configured local workflow; unavailable preferences stop entry explicitly. Reuse a prior explicit choice while resuming this task. Silence never authorizes cloud consumption.
37
37
 
38
- For an explicit cloud planning request without an existing start, `cloud-task` accepts `{"goal":"<actual goal>","ideTaskId":"<current IDE task ID, when available>"}` on stdin and returns the same choice. Do not start both routes for one task.
38
+ For an explicit cloud planning request without an existing start, `cloud-task` accepts `{"goal":"<actual goal>","ideTaskId":"<current IDE task ID, when available>","outputLocale":"en"}` on stdin and returns the same choice. Use `zh` for Chinese output. The creating page or explicit IDE request freezes this language; keep the original goal unchanged. A later viewer language switch changes presentation only. Cross-language historical output and raw evidence remain clearly labelled source details. New localized Blueprint documents use the frozen language for their prose and preserve original structured values in closed source appendices. Do not start both routes for one task.
39
39
 
40
40
  Images attached to a cloud planning task use `images: [{"path":"<explicit absolute image path>","detail":"original"}]` or `images: [{"url":"data:image/png;base64,<canonical bytes>","detail":"auto"}]`. For ordinary `invoke` task entry, keep `images` at the envelope's top level, outside the immutable `input.task`; the original cloud-choice callback carries them forward. Render the returned choice before sending. File paths are read only after `use`, and a local choice retains the attachment references without reading or uploading them. Never scan for images, copy a local path into a remote URL, omit attachments silently, or describe a text-only classification as image analysis.
41
41
 
@@ -100,7 +100,7 @@ First inspect the actual host configurations and the real build source that copi
100
100
 
101
101
  The SDK signs the exact proposal with the original S key. The service checks the original accepted owner, task, repository, signer, complete target set and current context revision, then issues an original-owner Confirm Protocol question showing the exact old/new mappings and observed evidence hashes. Show that unchanged question using the host question tool. Only the user's actual answer may call its original `interaction-answer` callback. Existing task authorization does not fabricate this exact context approval; silence remains pending. These configuration and build-evidence files remain read-only and hash-bound during later scope refreshes and file-role amendments; this approval cannot authorize changing their alias semantics or source bytes.
102
102
 
103
- After approval, run `scope-context` with `{ "action": "apply", "flowId": "<original flow>", "proposalId": "<returned proposal>", "proposalDigest": "<returned digest>" }`. It obtains a fresh server challenge, verifies a real copied snapshot and import graph using all original target paths and the proposed contexts, and submits the original S signature. Configuration or build-evidence drift, an unresolved actual alias, stale revision, expired/consumed nonce, foreign identity or missing approval rejects the attempt. No context approval grants writes or completes the task.
103
+ After approval, run `scope-context` with `{ "action": "apply", "flowId": "<original flow>", "proposalId": "<returned proposal>", "proposalDigest": "<returned digest>" }`. It prepares and independently inspects a real copied snapshot and import graph using all original target paths and the proposed contexts, then obtains a fresh server challenge before the original S signature. The short challenge lifetime remains unchanged. Its actual read set, source hashes, original missing resolver candidates, build evidence and snapshot are rechecked after that request; drift rejects signing. A local parser failure is archived, and retrying the identical approved input reuses its original snapshot only while the actual files still match. Unknown remote outcomes require the original GET recovery before any new challenge. Configuration or build-evidence drift, an unresolved actual alias, stale revision, expired/consumed nonce, foreign identity or missing approval rejects the attempt. No context approval grants writes or completes the task.
104
104
 
105
105
  Successful application appends a server-signed context authority and local history while preserving the original binding bytes. Retry `scope-connect` with its returned complete `nextInput`, then resume the original gate request. Later ordinary scope refreshes and `scope-amend` use that effective context authority; their signed facts bind its digest and exact mappings. M snapshot preparation and independent guarded writes recover the same context authority from the original private history and snapshot without a caller-selected mapping or S private-key read; their separate original M approval remains required. Status uses `{ "action": "status", "flowId": "<original flow>" }`. An unknown POST must recover its original request with GET through the saved journal; never repeat a POST to guess its outcome. This recovery is unavailable after the first accepted scope facts, successful gate, frozen plan or execution; preserve that later flow's original authority instead. The existing strict handoff protocol does not transfer a corrected context head; do not choose another receiver to bypass this binding.
106
106
 
@@ -211,3 +211,5 @@ Feedback command example (replace the request ID with the original successful in
211
211
  Ordinary Windows scope inspection resolves an absolute Git for Windows `git.exe` from its installation or absolute PATH directories, including paths containing spaces. It never invokes `/usr/bin/git` on Windows, a shell wrapper or a Git executable inside the inspected repository. Missing Git gives `ENTROROVE_GIT_UNAVAILABLE` and requires installing Git for Windows before reconnecting the original scope.
212
212
 
213
213
  The fixed Git byte-metering adapter ships hash-pinned precompiled libraries for Darwin and glibc Linux on arm64/x64. It extracts the selected library on first use; it never invokes a C compiler on the user machine. Darwin requires Apple’s trusted Git (Xcode or Command Line Tools), xcrun and codesign; Linux requires the trusted `/usr/bin/git` and glibc. Native comparisons with enforced source-read budgets remain restricted to these platforms. An unsupported native budget adapter returns `ENTROROVE_GIT_READER_UNSUPPORTED`; do not bypass its budget or claim an ordinary Windows inspection verifies that separate capability.
214
+
215
+ Conditional uni-app Vue sources are inspected across every supported feasible default platform and version branch, retaining raw source hashes and physical diagnostic locations. Unknown macros, unsupported expressions, custom build overrides or environment files that could change conditional semantics are rejected explicitly. A passing fixture does not prove that another project’s full scope passed; retry that project’s original approved operation and retain its actual result.
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: Execute a task selected for Entrorove through explicit SDK operatio
5
5
 
6
6
  # entrorove · 熵巡
7
7
 
8
- Version: 0.1.69.
8
+ Version: 0.1.71.
9
9
 
10
10
  Entrorove applies only to the task for which it is selected; reading these instructions, discovering capabilities or installing the package never selects or governs another task. Hooks are optional. Reading this document or calling an entry once does not execute the task. On every later turn, including after compaction, continue the original task and its required next operation before dependent work. Keep the original goal, scope, account, flow, cloud task and failure records. Never describe planning, a reported step or model output as verified delivery.
11
11
 
@@ -19,10 +19,10 @@ Entrorove applies only to the task for which it is selected; reading these instr
19
19
  3. Discover `capabilities`, then invoke `start` with the actual goal, task type, risk and scope facts. An enabled account returns a cloud choice before charging a model. Show that choice visibly using the host question tool. Confirmation calls its original callback; exit adopts the local route for this task. Never treat silence as permission, invent acceptance, or create both routes. Existing explicit authorization for this task persists.
20
20
  With cloud sandbox disabled, the IDE performs its own planning and continues the original API flow. Do not require a cloud model call or create a sandbox task to perform local work. The service freezes eligible member plans with `verificationMode: member-local` and `runnerKeyId: null`; successful original IDE execution and tests satisfy local acceptance. This mode comes from the stored account preference and enrolled scope, never a caller flag or an unavailable validator key. An already frozen independent plan keeps its original acceptance contract.
21
21
  When the model produces a task contract, preserve its `workType`, `risk`, `difficulty`, `crossModule`, `parallel`, `unclear`, `architectureContract`, optional `baselineRef`, and optional `interaction` as returned. Valid work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision`, and `diagnosis`. A `merge` requires a concrete `baselineRef` identifying the branch, tag, commit, or review baseline; keep that baseline unchanged through independent verification. A `decision` requires `interaction`, including a risk description for medium or high risk. Do not fill missing required fields by guessing.
22
- 4. After real cloud acceptance, immediately display **云端任务 <taskNumber> · 已进入**. Preserve `taskNumber` verbatim: it is the homepage sandbox tab number. Keep its UUID `taskId` separately. Run the returned `cloud-watch`, show each real public event and its cursor, and continue the original accepted flow from status. A command log alone is insufficient. No accepted cloud task means no cloud ID or model-execution claim.
22
+ 4. After real cloud acceptance, immediately display the returned entering status in the task’s selected language, including its **taskNumber**. Preserve `taskNumber` verbatim: it is the homepage sandbox tab number. Keep its UUID `taskId` separately. Run the returned `cloud-watch`, show each real public event and its cursor, and continue the original accepted flow from status. A command log alone is insufficient. No accepted cloud task means no cloud ID or model-execution claim.
23
23
  5. For an accepted `flowId`, execute the returned `nextStep` with its required payload. Discover the schema before constructing input. An empty payload is valid only when that operation's schema allows it. Reconnect and refresh only the original flow. A changed snapshot is a binding failure to investigate; do not invent a fresh successful baseline, rebuild a flow to evade it, or erase the old failure. If a fresh scope proof returns `ENTROROVE_SCOPE_PLANNING_REASSESSMENT_REQUIRED` before plan freeze, follow its explicit `workflow-scope-reassess` recovery and retry the original complete request; a local reassessment or snapshot refresh alone does not renew the accepted gate. If the terminal flow truly owns a stale task slot, use the explicit `flow-abandon` lifecycle operation described below before routing the original task again.
24
24
  6. All governed mutations use the original authority: `local guarded-write`, `local guarded-delete`, `local guarded-move`, or the original frozen approved runner plan. Native patches, scripts, terminal writes, database mutations and unknown MCP operations must not substitute for the controlled entry. User-authorized task messages use the host's message tool and remain subject to host approval; sending a report does not change the flow or grant execution authority. **A failed gate or unknown outcome must never be replaced with another executor or reported as local success.** Query the original request read-only, repair the cause, and obtain the original flow's valid authority before proceeding. If the original scope challenge preparation is absent from authenticated status, `scope-amend` may use the discovered `scope-challenge-retire` lifecycle operation. The server validates the same approved proposal and receiver, atomically fences only an unadmitted challenge, and records a signed retirement without executing that challenge. Recover and acknowledge the original retirement result with GET before a fresh challenge attempt; retain both requests and all failures. An admitted request, issued nonce, scope application or mutation has no missing-challenge retirement path. An uncertain retirement itself permits only its original GET.
25
- For a member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass; it keeps unresolved loader files and project configuration read only. After actual writes, refresh the original scope and run `mutation-settle`. Use the returned `previousPassId` for further edits. In a signed member-local plan, `workflow-observe-step` runs the original approved command and tests without a validator private key. A server-signed `localAcceptance` advances local dependencies; a failed test does not. Preserve the raw observation and its absent independent receipt. See [IDE-EXECUTION.md](IDE-EXECUTION.md#local-ide-acceptance).
25
+ For a member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass. Its non-structural modify role requires known target source imports, preserves source imports and public contracts, and keeps unresolved origins, other observed dependencies and project configuration read only. Build scripts, macros and generated sources can remain opaque; source observations and this editing role never prove a complete compiled dependency graph or successful tests. After actual writes, refresh the original scope and run `mutation-settle`. Use the returned `previousPassId` for further edits. In a signed member-local plan, `workflow-observe-step` runs the original frozen command and tests without a validator private key. A server-signed `localAcceptance` advances local dependencies; a failed test does not. A frozen independent plan retains its independent validation requirement. Preserve the raw observation and its absent independent receipt. See [IDE-EXECUTION.md](IDE-EXECUTION.md#local-ide-acceptance).
26
26
  7. Read [REUSE-DISCIPLINE.md](REUSE-DISCIPLINE.md) when modifying an existing system. The comparison baseline is the existing implementation in that same repository. It never means another project, another account, or copying external systems.
27
27
  Never overwrite a newer package with older bytes, relabel an old archive as new, or roll back because the current state is uncertain. Package identity/version checks and immutable historical byte observations run in the guarded entries. A refusal requires preserved inputs and diagnosis; a `force`, `rollback`, timestamp or filename is not evidence. Intentional rollback uses the original independently signed merge rollback authorization, transaction journal, exact current/restore hashes and fresh original mutation pass. Preserve old failures, receipts and unknown effects; a successful rollback never makes the failed operation successful.
28
28
  **Before preparing or writing any UI implementation, read [DESIGN-GATES.md](DESIGN-GATES.md).** Record the whole-site flow first, fetch current mainstream layout references and the specified framework's official rules, create actual mockups or prototypes using the host image/design tool, and show them to the user. Respect their choice of visual format; a prototype is a valid approval baseline. Obtain the original persisted design confirmation before UI code preparation. Carry that exact design approval through the original scope, plan and mutation gates. Neither a caller flag nor a text-only description substitutes for this sequence. Finish real functional tests, actual browser interaction/layout/style checks and original user visual acceptance before delivery. Keep Entrorove unchanged in every language.
@@ -0,0 +1,7 @@
1
+ export type CloudOutputLocale = 'zh' | 'en'
2
+ export function cloudOutputLocale(value: unknown): CloudOutputLocale | undefined
3
+ export function cloudMessage(key: string, locale?: CloudOutputLocale): string
4
+ export function cloudProseMatches(text: unknown, locale: CloudOutputLocale): boolean
5
+ export function cloudEventDisplay(value: { content: string; kind?: string; metadata?: Record<string, unknown> }, locale: CloudOutputLocale): {
6
+ content: string; summary?: string; sourceContent?: string; sourceSummary?: string; sourceLocale: CloudOutputLocale | 'unknown'
7
+ }
@@ -0,0 +1,59 @@
1
+ const copy = {
2
+ choice: ['是否启用云端沙箱执行思考规划?', 'Use the cloud sandbox to plan this task?'],
3
+ use: ['确认', 'Use cloud sandbox'], local: ['退出,采用本地方案', 'Continue locally'],
4
+ useHint: ['为当前任务创建云端思考规划并显示真实进度', 'Plan this task in the cloud and show actual progress'],
5
+ localHint: ['继续当前任务,不改变账号开关', 'Continue this task without changing the account preference'],
6
+ risk: ['这是当前任务的执行位置选择,不改变账号级云端沙箱开关。', 'This chooses the execution location for this task without changing the account preference.'],
7
+ fallback: ['是否启用云端沙箱执行思考规划?\n1. 确认:当前任务使用云端沙箱\n2. 退出:自动采用本地方案,继续当前任务',
8
+ 'Use the cloud sandbox to plan this task?\n1. Use cloud sandbox for this task\n2. Continue this task locally'],
9
+ skipped: ['已退出云端沙箱,采用本地方案继续当前任务。', 'Continue this task locally without entering the cloud sandbox.'],
10
+ enter: ['正在进入云端沙箱', 'Entering the cloud sandbox'], accepted: ['已进入', 'Accepted'],
11
+ task: ['云端任务', 'Cloud task'], output: ['云端沙箱输出完毕;实际验证状态以任务回执为准。', 'Cloud sandbox output is available; verification remains determined by the actual task receipt.'],
12
+ exit: ['云端沙箱正在退出本次进度订阅', 'Closing this cloud progress subscription'],
13
+ waiting: ['云端沙箱仍在执行,下次从当前进度继续读取', 'Cloud execution continues; resume reading from the current progress cursor'],
14
+ source: ['原始输出(保留来源,未作翻译)', 'Original output (source preserved; not translated)'],
15
+ sourceDetails: ['原始详情(未翻译)', 'Original details (not translated)'],
16
+ event: ['云端进度已更新;原文保留在来源详情。', 'Cloud progress updated; the original text is preserved in source details.'],
17
+ failed: ['云端任务失败;原始诊断保留在来源详情。', 'Cloud task failed; the original diagnostic is preserved in source details.'],
18
+ keepAlive: ['智能执行中,请勿关闭!', 'Execution in progress. Please keep this window open.'],
19
+ }
20
+ const events = {
21
+ 'Cloud task accepted.': ['任务已进入', 'Task accepted'],
22
+ 'Cloud workflow started.': ['云端沙箱任务已分配', 'Cloud sandbox task allocated'],
23
+ 'Cloud model classification started.': ['云端沙箱正在分析任务', 'Cloud sandbox is analyzing the task'],
24
+ 'Task classification completed.': ['云端沙箱分析已返回', 'Cloud sandbox analysis returned'],
25
+ 'Workflow completed with verified evidence.': ['任务已核验', 'Task verified'],
26
+ 'Workflow is blocked.': ['流程已阻断', 'Workflow blocked'],
27
+ 'Workflow is incomplete.': ['流程待补全', 'Workflow incomplete'],
28
+ 'Workflow failed.': ['流程执行失败', 'Workflow failed'],
29
+ 'Workflow reported its result; verification is pending.': ['结果已返回,仍待实际核验', 'Result reported; actual verification remains pending'],
30
+ 'Workflow is waiting for an explicit decision.': ['等待你明确确认', 'Awaiting your explicit decision'],
31
+ 'Original cloud task stopped.': ['原云端任务已停止', 'Original cloud task stopped'],
32
+ 'Model returned prose outside the frozen cloud output language': ['模型未按本任务固定语种返回说明', 'Model prose did not match the frozen task output language'],
33
+ }
34
+ export function cloudOutputLocale(value) {
35
+ if (value === undefined || value === null) return undefined;
36
+ if (!['zh', 'en'].includes(value)) throw new Error('Invalid frozen cloud output locale');
37
+ return value;
38
+ }
39
+ export function cloudMessage(key, locale) {
40
+ if (!Object.hasOwn(copy, key)) throw new Error('Unknown cloud presentation message');
41
+ return copy[key][locale === 'en' ? 1 : 0];
42
+ }
43
+ export function cloudProseMatches(text, locale) {
44
+ return typeof text === 'string' && (locale === 'en' ? !/\p{Script=Han}/u.test(text) : /\p{Script=Han}/u.test(text));
45
+ }
46
+ export function cloudEventDisplay(value, locale) {
47
+ const metadata = value.metadata ?? {}, content = value.content ?? '';
48
+ const fixed = Object.hasOwn(events, content) ? events[content][locale === 'en' ? 1 : 0]
49
+ : content.trim() === copy.keepAlive[0] ? cloudMessage('keepAlive', locale) : undefined;
50
+ const sourceContent = !fixed && !cloudProseMatches(content, locale) ? content : undefined;
51
+ const displayed = fixed ?? (sourceContent ? cloudMessage(value.kind === 'error' ? 'failed' : 'event', locale) : content);
52
+ const originalSummary = typeof metadata.summary === 'string' ? metadata.summary : undefined;
53
+ const translated = metadata.summaryTranslations?.[locale];
54
+ const candidate = typeof translated === 'string' && translated.trim() ? translated : originalSummary;
55
+ const summary = candidate && cloudProseMatches(candidate, locale) ? candidate : undefined;
56
+ return { content: displayed, ...(summary ? { summary } : {}),
57
+ ...(sourceContent ? { sourceContent } : {}), ...(!summary && originalSummary ? { sourceSummary: originalSummary } : {}),
58
+ sourceLocale: cloudOutputLocale(metadata.outputLocale) ?? 'unknown' };
59
+ }
@@ -5,6 +5,7 @@ import { displayIdeEvent, ideClientHeaders, ideTaskIdentity } from './ide-presen
5
5
  import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
6
6
  import { sandboxTaskNumber } from './task-identity.mjs';
7
7
  import { prepareCloudImages } from './cloud-task-images.mjs';
8
+ import { cloudEventDisplay, cloudMessage, cloudOutputLocale } from './cloud-locale.mjs';
8
9
 
9
10
  const CLOUD_TASK_SCHEMA = 'entrorove.cloud-task/1.0';
10
11
  const CLOUD_STATUS_SCHEMA = 'entrorove.cloud-status/1.0';
@@ -42,9 +43,11 @@ function validateInput(input) {
42
43
  if (input.images !== undefined && (!Array.isArray(input.images) || input.images.length > 4)) {
43
44
  throw new Error('cloud-task images must be an array of at most four images');
44
45
  }
46
+ if (input.outputLocale !== undefined && !['zh', 'en'].includes(input.outputLocale)) throw new Error('cloud-task outputLocale must be zh or en');
47
+ const outputLocale = cloudOutputLocale(input.outputLocale);
45
48
  return { goal: input.goal, ...(input.ideTaskId === undefined ? {} : { ideTaskId: input.ideTaskId }),
46
49
  ...(input.images === undefined ? {} : { images: input.images }),
47
- ...(input.task === undefined ? {} : { task: input.task }) };
50
+ ...(input.task === undefined ? {} : { task: input.task }), ...(outputLocale ? { outputLocale } : {}) };
48
51
  }
49
52
 
50
53
  function validateDependencies(dependencies) {
@@ -64,18 +67,19 @@ function authenticatedHeaders(token, headers = {}) {
64
67
  }
65
68
 
66
69
  export function sandboxChoiceInteraction(id, task = {}) {
70
+ const locale = cloudOutputLocale(task.outputLocale);
67
71
  return {
68
72
  schemaVersion: 'confirm.interaction/1.0', requestId: id, type: 'choice',
69
- question: '是否启用云端沙箱执行思考规划?',
73
+ question: cloudMessage('choice', locale),
70
74
  options: [
71
- { id: 'use', label: '确认', hint: '为当前任务创建云端思考规划并显示真实进度' },
72
- { id: 'local', label: '退出,采用本地方案', hint: '继续当前任务,不改变账号开关' },
75
+ { id: 'use', label: cloudMessage('use', locale), hint: cloudMessage('useHint', locale) },
76
+ { id: 'local', label: cloudMessage('local', locale), hint: cloudMessage('localHint', locale) },
73
77
  ],
74
78
  default: 'local', timeout: null, timeoutAction: 'local', risk: 'low',
75
- riskDescription: '这是当前任务的执行位置选择,不改变账号级云端沙箱开关。',
79
+ riskDescription: cloudMessage('risk', locale),
76
80
  rememberable: false, memoryKey: '',
77
81
  callback: { operation: 'cloud-task', payload: { ...task } },
78
- chatFallback: '是否启用云端沙箱执行思考规划?\n1. 确认:当前任务使用云端沙箱\n2. 退出:自动采用本地方案,继续当前任务',
82
+ chatFallback: cloudMessage('fallback', locale),
79
83
  };
80
84
  }
81
85
 
@@ -103,9 +107,9 @@ export async function runCloudTask(input, dependencies) {
103
107
  task, interaction: sandboxChoiceInteraction(id,task),
104
108
  });
105
109
  if (choice === 'local') return envelope(task, 'skipped', {
106
- reason: 'task', executionIsolation: 'local', message: '已退出云端沙箱,采用本地方案继续当前任务。',
110
+ reason: 'task', executionIsolation: 'local', message: cloudMessage('skipped', task.outputLocale),
107
111
  continuation: { goal: task.goal,...(task.ideTaskId ? { ideTaskId: task.ideTaskId } : {}),
108
- ...(task.images === undefined ? {} : { images: task.images }) },
112
+ ...(task.images === undefined ? {} : { images: task.images }), ...(task.outputLocale ? { outputLocale: task.outputLocale } : {}) },
109
113
  });
110
114
 
111
115
  if (task.images !== undefined) task.images = await prepareCloudImages(task.images);
@@ -115,7 +119,7 @@ export async function runCloudTask(input, dependencies) {
115
119
  const metadata = { kind: 'remote', operation: 'cloud-task', requestId: id };
116
120
  await appendInvocation({ ...metadata, event: 'started' }, dependencies.home);
117
121
  displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',requestId: id,kind: 'status',
118
- phase: 'enter',message: '正在进入云端沙箱',separateLine: true },dependencies.onIdeEvent,{home:dependencies.home});
122
+ phase: 'enter',message: cloudMessage('enter', task.outputLocale),separateLine: true },dependencies.onIdeEvent,{home:dependencies.home});
119
123
  let response;
120
124
  try {
121
125
  response = await requestTransport(origin, '/api/cloud-task', {
@@ -142,9 +146,12 @@ export async function runCloudTask(input, dependencies) {
142
146
  if (!response.ok) {
143
147
  await appendInvocation({ ...metadata, event: response.status >= 500 ? 'outcome-unknown' : 'rejected' }, dependencies.home);
144
148
  const code = typeof result?.error === 'string' ? result.error : 'cloud-task-rejected';
145
- return envelope(task, response.status === 403 ? 'blocked' : 'failed', { code, message: result?.message ?? code, httpStatus: response.status });
149
+ const rawMessage = typeof result?.message === 'string' ? result.message : code;
150
+ const display = task.outputLocale ? cloudEventDisplay({ kind: 'error',content: rawMessage },task.outputLocale) : undefined;
151
+ return envelope(task, response.status === 403 ? 'blocked' : 'failed', { code, message: display?.content ?? rawMessage,
152
+ ...(display?.sourceContent ? { source: { label: cloudMessage('source',task.outputLocale),content: display.sourceContent } } : {}), httpStatus: response.status });
146
153
  }
147
- if (!result?.ok || typeof result.taskId !== 'string' || !UUID.test(result.taskId)
154
+ if (!result?.ok || result.outputLocale !== task.outputLocale || typeof result.taskId !== 'string' || !UUID.test(result.taskId)
148
155
  || result.taskNumber !== undefined && result.taskNumber !== sandboxTaskNumber(result.taskId)
149
156
  || result.afterId !== undefined && (typeof result.afterId !== 'string' || !/^[1-9][0-9]{0,18}$/u.test(result.afterId) || BigInt(result.afterId)>9223372036854775807n)) {
150
157
  await appendInvocation({ ...metadata, event: 'outcome-unknown' }, dependencies.home);
@@ -153,7 +160,7 @@ export async function runCloudTask(input, dependencies) {
153
160
  await appendInvocation({ ...metadata, event: 'completed' }, dependencies.home);
154
161
  const taskNumber = sandboxTaskNumber(result.taskId);
155
162
  const event = { schemaVersion: 'entrorove.ide-event/1.0',requestId: id,taskId: result.taskId,taskNumber,
156
- kind: 'status',phase: 'accepted',message: `云端任务 ${taskNumber} · 已进入`,separateLine: true };
163
+ kind: 'status',phase: 'accepted',message: `${cloudMessage('task', task.outputLocale)} ${taskNumber} · ${cloudMessage('accepted', task.outputLocale)}`,separateLine: true };
157
164
  displayIdeEvent(event,dependencies.onIdeEvent,{home:dependencies.home});
158
165
  return envelope(task, 'started', { taskId: result.taskId,taskNumber,ideMessages: [event.message],
159
166
  taskStatus: typeof result.status === 'string' ? result.status : 'running',
@@ -164,7 +171,7 @@ export async function runCloudTask(input, dependencies) {
164
171
  function publicTask(task) {
165
172
  if (!task || typeof task !== 'object' || Array.isArray(task)) return null;
166
173
  const allowed = ['id', 'status', 'displayTitle', 'titleStatus', 'titleError', 'summary', 'ideTaskId',
167
- 'createdAt', 'updatedAt', 'completedAt'];
174
+ 'createdAt', 'updatedAt', 'completedAt', 'outputLocale'];
168
175
  return Object.fromEntries(allowed.filter(key => task[key] !== undefined).map(key => [key, task[key]]));
169
176
  }
170
177
 
@@ -196,6 +203,7 @@ export async function queryCloudTask(taskId, dependencies) {
196
203
  code: typeof taskBody?.error === 'string' ? taskBody.error : taskResponse.status === 404 ? 'cloud-task-not-found' : 'cloud-status-unavailable', httpStatus: taskResponse.status };
197
204
  }
198
205
  const task = taskBody.task;
206
+ cloudOutputLocale(task.outputLocale);
199
207
  const feedback = taskBody.feedback ? verifyAutomaticFeedback(taskBody.feedback,taskBody.feedback.requestId) : undefined;
200
208
  let usage = [];
201
209
  let usageError;
@@ -4,6 +4,7 @@ import { automaticFeedbackIdeEvent, cloudIdeEvent, displayIdeEvent, ideClientHea
4
4
  import { queryCloudTask } from './cloud-task-client.mjs';
5
5
  import { acknowledgeAutomaticFeedback } from './automatic-feedback-client.mjs';
6
6
  import { sandboxTaskNumber } from './task-identity.mjs';
7
+ import { cloudMessage, cloudOutputLocale } from './cloud-locale.mjs';
7
8
 
8
9
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
10
  function inputOptions(input) {
@@ -24,6 +25,7 @@ export async function watchCloudTask(input, dependencies) {
24
25
  const controller = new AbortController(), deadline = setTimeout(() => controller.abort(),waitMs);
25
26
  const events = [], emit = event => { events.push(event); displayIdeEvent(event,dependencies.onIdeEvent,{home:dependencies.home}); };
26
27
  let afterId = options.afterId, taskStatus = 'running', outputAvailable = false, settled = false, flowId;
28
+ let outputLocale;
27
29
  try {
28
30
  const response = await fetch(`${trustedOrigin(dependencies.apiOrigin())}/api/cloud-sandbox/tasks/${taskId}/events`, {
29
31
  redirect: 'error',signal: controller.signal,headers: { authorization: `Bearer ${token}`,
@@ -38,6 +40,9 @@ export async function watchCloudTask(input, dependencies) {
38
40
  }
39
41
  if (!/^[1-9][0-9]{0,18}$/u.test(frame.id ?? '') || frame.data?.id !== frame.id) throw new Error('Invalid cloud event cursor');
40
42
  if (BigInt(frame.id) <= BigInt(afterId)) return;
43
+ const eventLocale = cloudOutputLocale(frame.data.metadata?.outputLocale);
44
+ if (outputLocale && eventLocale !== outputLocale) throw new Error('Cloud stream changed its original output language');
45
+ if (eventLocale) outputLocale = eventLocale;
41
46
  const event = cloudIdeEvent(frame.data,taskId); afterId = frame.id; emit(event);
42
47
  if (typeof frame.data.metadata?.flowId === 'string') flowId = frame.data.metadata.flowId;
43
48
  settled = event.phase === 'workflow' && ['reported','verified','blocked','failed'].includes(event.state);
@@ -48,15 +53,18 @@ export async function watchCloudTask(input, dependencies) {
48
53
  if (!controller.signal.aborted) throw cause;
49
54
  } finally { clearTimeout(deadline); controller.abort(); }
50
55
  if (outputAvailable) emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'output',
51
- message: '云端沙箱输出完毕;实际验证状态以任务回执为准。',separateLine: true });
56
+ message: cloudMessage('output', outputLocale),separateLine: true });
52
57
  const current = await queryCloudTask(taskId,dependencies);
53
58
  if (current.status === 'observed') taskStatus = current.taskStatus;
59
+ const currentLocale = cloudOutputLocale(current.task?.outputLocale);
60
+ if (outputLocale && currentLocale !== outputLocale) throw new Error('Cloud status changed its original output language');
61
+ outputLocale = currentLocale ?? outputLocale;
54
62
  if (current.feedback) {
55
63
  current.feedback = await acknowledgeAutomaticFeedback(current.feedback,dependencies.apiOrigin(),token,ideClientHeaders());
56
- emit({ ...automaticFeedbackIdeEvent(current.feedback),taskId,taskNumber:sandboxTaskNumber(taskId) });
64
+ emit({ ...automaticFeedbackIdeEvent(current.feedback, outputLocale),taskId,taskNumber:sandboxTaskNumber(taskId) });
57
65
  }
58
66
  emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'exit',
59
- message: settled || taskStatus !== 'running' ? '云端沙箱正在退出本次进度订阅' : '云端沙箱仍在执行,下次从当前进度继续读取',separateLine: true });
67
+ message: cloudMessage(settled || taskStatus !== 'running' ? 'exit' : 'waiting', outputLocale),separateLine: true });
60
68
  return { schemaVersion: 'entrorove.cloud-watch/1.0',status: current.status,taskId,taskNumber: sandboxTaskNumber(taskId),taskStatus,afterId,outputAvailable,
61
69
  events,...(flowId ? { flowId } : {}),task: current.task,usage: current.usage,...(current.feedback ? { feedback: current.feedback } : {}),
62
70
  nextAction: taskStatus === 'running' && !settled ? { operation: 'cloud-watch',input: { taskId,afterId,waitMs } } : null };
package/design-lint.mjs CHANGED
@@ -3,6 +3,7 @@ import postcss from 'postcss';
3
3
  import { parse as parseVue } from '@vue/compiler-sfc';
4
4
  import { designFail, validateDesignPolicy } from './design-policy.mjs';
5
5
  import { designUiAsset } from './design-ui.mjs';
6
+ import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
6
7
 
7
8
  export { designUiCandidate } from './design-ui.mjs';
8
9
  const issue = (rule, detail) => ({ rule, detail });
@@ -65,7 +66,7 @@ function scriptIssues(source, name, rules) {
65
66
  walk(tree);
66
67
  return { found, imports };
67
68
  }
68
- export function designLint(name, source, rules) {
69
+ function lintSource(name, source, rules) {
69
70
  if (designUiAsset(name)) return [];
70
71
  if (typeof source !== 'string' || Buffer.byteLength(source) > 2_097_152) designFail('LINT_LIMIT', 'UI lint requires bounded UTF-8 source');
71
72
  if (/\.(?:scss|sass|less|styl|wxml|wxss|axml|acss)$/i.test(name)) return [issue('unsupported-ui-language', name)];
@@ -85,10 +86,27 @@ export function designLint(name, source, rules) {
85
86
  if (styles.length) found.push(issue('central-style-source-required', name));
86
87
  return found;
87
88
  }
89
+ export function designLint(name, source, rules) {
90
+ if (!/\.vue$/i.test(name)) return lintSource(name, source, rules);
91
+ if (typeof source !== 'string' || Buffer.byteLength(source) > 2_097_152) designFail('LINT_LIMIT', 'UI lint requires bounded UTF-8 source');
92
+ try {
93
+ const conditional = uniConditionalVariants(source, name);
94
+ if (!conditional.directives.length) return lintSource(name, source, rules);
95
+ return conditional.variants.flatMap(variant => variant.source.trim()
96
+ ? lintSource(name, variant.source, rules).map(problem => ({ ...problem, conditionalBranch: variant.key })) : []);
97
+ } catch (error) {
98
+ if (!error.code?.startsWith('ENTROROVE_')) throw error;
99
+ return [issue('vue-conditional', error.message)];
100
+ }
101
+ }
88
102
  export function assertDesignLint(policy, name, before, after, approvedBefore = before) {
89
103
  validateDesignPolicy(policy);
90
104
  const retained = new Map();
91
- const inspect = source => [...designLint(name, source, policy.rules), ...privateFieldIssues(source, name, policy)];
105
+ const inspect = source => {
106
+ const problems = [...designLint(name, source, policy.rules), ...privateFieldIssues(source, name, policy)];
107
+ if (problems.some(problem => problem.rule === 'vue-conditional')) designFail('LINT', 'Conditional UI source requires complete supported branch inspection');
108
+ return problems;
109
+ };
92
110
  const approved = new Map();
93
111
  for (const problem of inspect(approvedBefore)) { const key = JSON.stringify(problem); approved.set(key, (approved.get(key) ?? 0) + 1); }
94
112
  for (const problem of inspect(before)) { const key = JSON.stringify(problem); retained.set(key, Math.min((retained.get(key) ?? 0) + 1, approved.get(key) ?? 0)); }
@@ -1,6 +1,7 @@
1
1
  const UUID = /^[0-9a-f-]{36}$/i;
2
2
  import { notifyIdeEvent } from './native-notification.mjs';
3
3
  import { sandboxTaskNumber } from './task-identity.mjs';
4
+ import { cloudEventDisplay, cloudMessage, cloudOutputLocale } from './cloud-locale.mjs';
4
5
  export function ideClientHeaders(environment = process.env) {
5
6
  const ide = environment.ENTROROVE_IDE ?? (environment.CODEX_THREAD_ID ? 'Codex'
6
7
  : environment.CURSOR_TRACE_ID ? 'Cursor' : environment.CLAUDE_CODE_ENTRYPOINT ? 'Claude Code' : 'Entrorove CLI');
@@ -27,17 +28,23 @@ export function cloudIdeEvent(value, taskId) {
27
28
  throw new Error('Cloud event lost its original task identity');
28
29
  }
29
30
  const metadata = value.metadata ?? {};
31
+ const locale = cloudOutputLocale(metadata.outputLocale);
32
+ const displayed = locale ? cloudEventDisplay(value, locale) : undefined;
30
33
  let message = text(value.content), phase = String(metadata.stepId ?? 'workflow');
31
- if (message === 'Cloud task accepted.') { message = `云端任务 ${sandboxTaskNumber(taskId)} · 已进入`; phase = 'accepted'; }
34
+ if (message === 'Cloud task accepted.') { message = `${cloudMessage('task', locale)} ${sandboxTaskNumber(taskId)} · ${cloudMessage('accepted', locale)}`; phase = 'accepted'; }
32
35
  else if (message === 'Cloud workflow started.') message = '云端沙箱任务已分配';
33
36
  else if (message === 'Cloud model classification started.') message = '云端沙箱正在思考中';
34
37
  else if (message === 'Task classification completed.') message = '云端沙箱思考正在输出';
38
+ if (displayed && phase !== 'accepted') message = displayed.content;
35
39
  const event = { schemaVersion: 'entrorove.ide-event/1.0',taskId,taskNumber: sandboxTaskNumber(taskId),eventId: value.id,kind: value.kind,
36
40
  state: metadata.state ?? 'running',phase,message,separateLine: true };
37
41
  if (metadata.requiresUserInput === true || phase === 'confirm' && event.state === 'pending') event.requiresConfirmation = true;
38
42
  if (metadata.errorCode === 'model-balance-insufficient') event.errorCode = metadata.errorCode;
39
43
  if (typeof metadata.summary === 'string' && metadata.summary !== value.content
40
- && value.content !== 'Cloud model classification started.') event.detail = text(metadata.summary);
44
+ && value.content !== 'Cloud model classification started.') event.detail = text(displayed?.summary ?? (locale ? '' : metadata.summary));
45
+ if (displayed?.sourceContent || displayed?.sourceSummary) event.source = {
46
+ label: cloudMessage('source', locale), ...(displayed.sourceContent ? { content: text(displayed.sourceContent) } : {}),
47
+ ...(displayed.sourceSummary ? { summary: text(displayed.sourceSummary) } : {}), locale: displayed.sourceLocale };
41
48
  if (value.kind === 'notice' && metadata.url !== undefined) {
42
49
  let url;
43
50
  try { url = new URL(metadata.url); } catch { throw new Error('Cloud promotion link is invalid'); }
@@ -63,11 +70,15 @@ export function presentInvocationNotices(result, sink, notifications) {
63
70
  }
64
71
  return messages.length ? { ...result,ideMessages: messages } : result;
65
72
  }
66
- export function automaticFeedbackIdeEvent(feedback) {
73
+ export function automaticFeedbackIdeEvent(feedback, outputLocale) {
67
74
  const disposition = feedback.evaluation.disposition;
68
75
  const status = disposition === 'good' ? '当前调用验证通过' : disposition === 'bad'
69
76
  ? '当前调用被阻断或验证失败' : '交付验证尚未完成,暂不评分';
77
+ const english = outputLocale === 'en';
78
+ const englishStatus = disposition === 'good' ? 'this invocation passed verification' : disposition === 'bad'
79
+ ? 'this invocation was blocked or failed verification' : 'delivery verification remains incomplete; no score is assigned';
70
80
  return { schemaVersion: 'entrorove.ide-event/1.0',requestId: feedback.requestId,receiptId: feedback.receiptId,
71
81
  kind: 'evaluation',phase: 'feedback',disposition,
72
- message: `${feedback.acknowledgement?.stored === true ? '本次调用评价已回传至服务器' : '服务器已保存本次调用评价'}:${status}。`,separateLine: true };
82
+ message: english ? `${feedback.acknowledgement?.stored === true ? 'The server acknowledged this invocation evaluation' : 'The server saved this invocation evaluation'}: ${englishStatus}.`
83
+ : `${feedback.acknowledgement?.stored === true ? '本次调用评价已回传至服务器' : '服务器已保存本次调用评价'}:${status}。`,separateLine: true };
73
84
  }
@@ -1,6 +1,7 @@
1
- import { parse as parseVue, compileTemplate } from '@vue/compiler-sfc';
1
+ import { parse as parseVue, parseCache, compileTemplate } from '@vue/compiler-sfc';
2
2
  import { parse as parseSvelte } from 'svelte/compiler';
3
3
  import { analyzeArchguardLanguage } from './local-archguard-language-ast.mjs';
4
+ import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
4
5
 
5
6
  export function archguardComponentLanguage(path) {
6
7
  return new Map([['vue', 'vue'], ['svelte', 'svelte'], ['wxml', 'wxml']]).get(path.split('.').at(-1)) ?? null;
@@ -70,6 +71,7 @@ function addStyle(target, source, rules, content, base, lineOffsets, property =
70
71
  function vueSections(file) {
71
72
  const parsed = parseVue(file.content, { filename: file.path, sourceMap: false,
72
73
  templateParseOptions: { expressionPlugins: ['typescript'] } });
74
+ parseCache.clear();
73
75
  if (parsed.errors.length) return { errors: parsed.errors };
74
76
  const descriptor = parsed.descriptor;
75
77
  if (descriptor.customBlocks.length || descriptor.template?.lang) throw new Error('ARCH-AST-EMBEDDED-LANGUAGE-UNSUPPORTED');
@@ -137,11 +139,13 @@ function wxmlSections(file) {
137
139
  transformAssetUrls: false, compilerOptions: { comments: true } });
138
140
  return { template: compiled.ast, scripts: [], styles: [], errors: compiled.errors };
139
141
  }
140
- export function analyzeArchguardComponent(file, rules, analyzeScript) {
141
- const language = archguardComponentLanguage(file.path); const lineOffsets = offsets(file.content);
142
- const target = { selected: new Map(rules.filter(rule => rule.engine === 'ast').map(rule => [rule.id, { rule, locations: [] }])),
142
+ function componentResult(rules) {
143
+ return { selected: new Map(rules.filter(rule => rule.engine === 'ast').map(rule => [rule.id, { rule, locations: [] }])),
143
144
  builtin: new Map(['no-eval', 'no-debug', 'no-inline-style', 'no-hardcoded-color', 'no-magic-number'].map(id => [id, []])),
144
145
  dependencies: [], maxFunctionLines: 0, maxNestingDepth: 0, animations: 0, transitions: 0, diagnostics: [] };
146
+ }
147
+ function analyzeComponentVariant(file, rules, analyzeScript) {
148
+ const language = archguardComponentLanguage(file.path), lineOffsets = offsets(file.content), target = componentResult(rules);
145
149
  try {
146
150
  let sections; let ranges;
147
151
  if (language === 'svelte') {
@@ -175,3 +179,26 @@ export function analyzeArchguardComponent(file, rules, analyzeScript) {
175
179
  target.diagnostics.push(syntaxError(error, language, lineOffsets)); return target;
176
180
  }
177
181
  }
182
+ function unionLocations(left, right) {
183
+ return [...new Map([...left, ...right].map(item => [JSON.stringify(item), item])).values()];
184
+ }
185
+ export function analyzeArchguardComponent(file, rules, analyzeScript) {
186
+ if (archguardComponentLanguage(file.path) !== 'vue') return analyzeComponentVariant(file, rules, analyzeScript);
187
+ const target = componentResult(rules);
188
+ try {
189
+ const conditional = uniConditionalVariants(file.content, file.path);
190
+ if (!conditional.directives.length) return analyzeComponentVariant(file, rules, analyzeScript);
191
+ const inspected = new Set();
192
+ for (const variant of conditional.variants) {
193
+ if (!variant.source.trim() || inspected.has(variant.source)) continue;
194
+ inspected.add(variant.source);
195
+ const part = analyzeComponentVariant({ ...file, content: variant.source }, rules, analyzeScript);
196
+ for (const [id, locations] of part.builtin) target.builtin.set(id, unionLocations(target.builtin.get(id), locations));
197
+ for (const [id, selected] of part.selected) target.selected.get(id).locations = unionLocations(target.selected.get(id).locations, selected.locations);
198
+ for (const field of ['dependencies', 'diagnostics']) target[field] = unionLocations(target[field], part[field]);
199
+ for (const field of ['maxFunctionLines', 'maxNestingDepth', 'animations', 'transitions']) target[field] = Math.max(target[field], part[field]);
200
+ }
201
+ if (!inspected.size) throw new Error('Conditional component has no inspectable branch');
202
+ } catch (error) { target.diagnostics.push(syntaxError(error, 'uni-conditional', offsets(file.content))); }
203
+ return target;
204
+ }
@@ -102,7 +102,7 @@ export async function readBudgetedProjectFile(input, target, options = {}) {
102
102
  const root = await fs.realpath(input.repositoryRoot);
103
103
  const sourcePath = options.sourcePath ?? path.relative(root, target).split(path.sep).join('/');
104
104
  const before = await fs.lstat(target, { bigint: true });
105
- const result = await budgetReadBuffer({ ...binding, path: sourcePath }, { target });
105
+ const result = await budgetReadBuffer({ ...binding, path: sourcePath }, { target, verifyReadTarget: options.verifyReadTarget });
106
106
  const after = await fs.lstat(target, { bigint: true });
107
107
  if (!sameSourceStat(before, after)) fail('ENTROROVE_SCOPE_CHANGED', 'budgeted project source changed during its bound observation');
108
108
  projectBuffers.set(result.content, Object.freeze({ home: await fs.realpath(binding.home ?? os.homedir()),
@@ -110,7 +110,12 @@ export async function readBudgetedProjectFile(input, target, options = {}) {
110
110
  return options.encoding ? result.content.toString(options.encoding) : result.content;
111
111
  }
112
112
  const handle = await fs.open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
113
- try { if (!(await handle.stat()).isFile()) fail('ENTROROVE_FILE_INVALID', 'source must be a regular file'); return await handle.readFile(options.encoding ? { encoding: options.encoding } : undefined); }
113
+ try {
114
+ const descriptor = await handle.stat();
115
+ if (!descriptor.isFile()) fail('ENTROROVE_FILE_INVALID', 'source must be a regular file');
116
+ if (options.verifyReadTarget) await options.verifyReadTarget({ target: await fs.realpath(target), descriptor });
117
+ return await handle.readFile(options.encoding ? { encoding: options.encoding } : undefined);
118
+ }
114
119
  finally { await handle.close(); }
115
120
  }
116
121
 
package/local-budget.mjs CHANGED
@@ -91,7 +91,7 @@ function assertReadAllowed(state, relative, tokens, now = Date.now()) {
91
91
  if (tokens > state.maxTokenEstimate - state.tokenEstimate) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read token budget exhausted');
92
92
  if (now - Date.parse(state.startedAt) >= state.maxDurationMs) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read duration budget exhausted');
93
93
  }
94
- async function readSource(location, state, relative, target, received) {
94
+ async function readSource(location, state, relative, target, received, verifyReadTarget) {
95
95
  const binding = { home: location.home, repositoryRoot: location.root, flowId: location.flowId, path: relative };
96
96
  await assertBudgetHandoffReadAllowed(binding, state);
97
97
  const actualTarget = await budgetSourceTarget(location, relative, target);
@@ -101,6 +101,7 @@ async function readSource(location, state, relative, target, received) {
101
101
  try {
102
102
  const stat = await handle.stat(); if (!stat.isFile()) fail('ENTROROVE_FILE_INVALID', 'budget source must be a regular file');
103
103
  await assertDescriptorTarget(target, actualTarget, stat);
104
+ if (verifyReadTarget) await verifyReadTarget({ target: actualTarget, descriptor: stat });
104
105
  assertReadAllowed(state, relative, Math.ceil(stat.size / 4));
105
106
  const cap = state.enforced ? Math.max(0, (state.maxTokenEstimate - state.tokenEstimate) * 4) : Number.MAX_SAFE_INTEGER;
106
107
  const chunks = []; let size = 0;
@@ -131,7 +132,7 @@ export async function budgetReadBuffer(input, options = {}) {
131
132
  if (state.grant?.purpose === 'handoff-read') requireLiveBudget(budgetHandoffReadValidationState(state));
132
133
  else state = consumeBudgetRenewal({ state });
133
134
  const target = options.target ?? path.join(location.root, relative);
134
- const content = await readSource(location, state, relative, target, received); const tokens = Math.ceil(content.length / 4);
135
+ const content = await readSource(location, state, relative, target, received, options.verifyReadTarget); const tokens = Math.ceil(content.length / 4);
135
136
  const updated = { ...state, files: state.files.includes(relative) ? state.files : [...state.files, relative], readCalls: state.readCalls + 1, tokenEstimate: state.tokenEstimate + tokens };
136
137
  const committed = await commitBudget(location, loaded, updated, 'budget-read', { path: relative, tokens, bytes: content.length,
137
138
  readId, renewedIntervals: (state.autoRenew?.renewalsUsed ?? 0) - (loaded.state.autoRenew?.renewalsUsed ?? 0), reason: state.autoRenew?.lastRenewalReason ?? null });
@@ -3,6 +3,7 @@ import { parse as parseVue, parseCache } from '@vue/compiler-sfc';
3
3
  import { canonical } from './local-integrity.mjs';
4
4
  import { createScopeImportResolver } from './local-scope-import-expressions.mjs';
5
5
  import { isDynamicFunctionReference, isTypeOnlyReference } from './local-typescript-function-reference.mjs';
6
+ import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
6
7
 
7
8
  const fail = () => { throw Object.assign(new Error('Preserved modification changed an existing definition, public contract, dependency, or unsupported source boundary'),
8
9
  { code: 'ENTROROVE_MUTATION_DELEGATION_CHANGE' }); };
@@ -216,17 +217,25 @@ function parts(path, content) {
216
217
  function retained(before, after) {
217
218
  for (const [key, count] of before) if ((after.get(key) ?? 0) < count) fail();
218
219
  }
220
+ function compareSource(path, before, after) {
221
+ if (!before.trim() && !after.trim()) return;
222
+ const original = parts(path, before), candidate = parts(path, after);
223
+ if (canonical(original.metadata) !== canonical(candidate.metadata) || original.scripts.length !== candidate.scripts.length) fail();
224
+ for (let index = 0; index < original.scripts.length; index++) {
225
+ const a = scriptProjection(original.scripts[index]), b = scriptProjection(candidate.scripts[index]);
226
+ if (canonical(a.dependencies) !== canonical(b.dependencies) || canonical(a.public) !== canonical(b.public)) fail();
227
+ retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
228
+ }
229
+ }
219
230
  export function assertDelegatedSourceModification(input) {
220
231
  const { path, before, after } = input ?? {};
221
232
  if (typeof path !== 'string' || typeof before !== 'string' || typeof after !== 'string'
222
233
  || Buffer.byteLength(before) > 1_048_576 || Buffer.byteLength(after) > 1_048_576) fail();
223
234
  try {
224
- const original = parts(path, before), candidate = parts(path, after);
225
- if (canonical(original.metadata) !== canonical(candidate.metadata) || original.scripts.length !== candidate.scripts.length) fail();
226
- for (let index = 0; index < original.scripts.length; index++) {
227
- const a = scriptProjection(original.scripts[index]), b = scriptProjection(candidate.scripts[index]);
228
- if (canonical(a.dependencies) !== canonical(b.dependencies) || canonical(a.public) !== canonical(b.public)) fail();
229
- retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
230
- }
235
+ if (!/\.vue$/i.test(path)) return compareSource(path, before, after);
236
+ const original = uniConditionalVariants(before, path), candidate = uniConditionalVariants(after, path);
237
+ if (canonical(original.directives) !== canonical(candidate.directives)
238
+ || canonical(original.variants.map(value => value.key)) !== canonical(candidate.variants.map(value => value.key))) fail();
239
+ for (let index = 0; index < original.variants.length; index++) compareSource(path, original.variants[index].source, candidate.variants[index].source);
231
240
  } catch { fail(); }
232
241
  }