feat(cypher-executor): magic vars {{_today}} / {{_iso_week}} / {{_now}} 等
對應 LI SDD M2.x improvement,源自 Claude (我) 透過 arcrun_report_feedback
寫的 feedback block c47bf70b 提的「需要內建變數展開」需求。完整閉環:
AI 用 MCP tool 報 bug → KBDB 收 feedback → AI 自己看 → 自己修補 → deploy
新增 cypher-executor/src/lib/magic-vars.ts:
- _today / _yesterday / _now / _now_unix / _now_unix_s
- _iso_week (2026-W20) / _iso_week_num / _iso_year
- _yyyymm / _yyyymmdd
- _year / _month / _day / _hour / _minute / _second(zero-padded)
- _weekday (0-6, 0=日) / _iso_weekday (1-7, 1=一)
全部 UTC,避免 worker 跨時區誤判
GraphExecutor.execute() 入口注入:
ctxWithMagic = { ...initialContext, ...buildMagicVars() }
順序確保 magic vars 永遠 win(防 user 不小心用 _ prefix)
不違反 §2.2(cypher-executor TS 禁實作 secret/JWT 業務邏輯):
magic vars 是公開時間常數,跟既有 interpolateString 的 ctx 變數展開
同層,純 orchestrator routing 職責。
AGENTS.md §10.5 加 magic vars 完整表 + weekly archive 範例。
實測(commit 後 deploy + 觸發 weekly_review):
- KBDB 新建 roadmap-2026-W20 block 正確展開 page_name
- roadmap-latest 同步更新(雙寫 pattern)
- 證明 weekly archive 從此真能累積歷史,不再固定 latest 覆蓋
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -255,6 +255,36 @@ config:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 10.5 內建 magic vars(`_` prefix reserved)
|
||||||
|
|
||||||
|
YAML 內可直接用以下變數,cypher-executor 自動展開為當下時間(UTC):
|
||||||
|
|
||||||
|
| 變數 | 範例 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `{{_today}}` | `2026-05-16` | 日 log / page_name |
|
||||||
|
| `{{_yesterday}}` | `2026-05-15` | digest 取昨日 |
|
||||||
|
| `{{_now}}` | `2026-05-16T09:30:00.123Z` | ISO 8601 |
|
||||||
|
| `{{_now_unix}}` | `1778937000123` | unix ms |
|
||||||
|
| `{{_now_unix_s}}` | `1778937000` | unix sec |
|
||||||
|
| `{{_iso_week}}` | `2026-W20` | weekly archive (本 doc 推薦) |
|
||||||
|
| `{{_iso_week_num}}` / `{{_iso_year}}` | `20` / `2026` | 拆開用 |
|
||||||
|
| `{{_yyyymm}}` / `{{_yyyymmdd}}` | `202605` / `20260516` | 緊湊路徑 |
|
||||||
|
| `{{_year}}` / `{{_month}}` / `{{_day}}` / `{{_hour}}` / `{{_minute}}` | 各別 zero-padded | 自己拼路徑 |
|
||||||
|
| `{{_weekday}}` | `0`-`6`(0=日)| if-control |
|
||||||
|
| `{{_iso_weekday}}` | `1`-`7`(1=一)| ISO 風格 |
|
||||||
|
|
||||||
|
**rule**:`_` prefix reserved for system,**用戶自己 ctx 變數不要用 `_` 開頭**。
|
||||||
|
|
||||||
|
**範例**:weekly archive
|
||||||
|
```yaml
|
||||||
|
publish_roadmap_archive:
|
||||||
|
component: kbdb_upsert_block
|
||||||
|
page_name: "roadmap-{{_iso_week}}" # roadmap-2026-W20
|
||||||
|
tags_json: '["weekly", "week:{{_iso_week}}"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 11. 給寫 LI 的 AI 自己的 meta-規範
|
## 11. 給寫 LI 的 AI 自己的 meta-規範
|
||||||
|
|
||||||
你(AI)在寫 arcrun workflow 時,**遵守以下習慣**會少踩坑:
|
你(AI)在寫 arcrun workflow 時,**遵守以下習慣**會少踩坑:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { injectCredentials } from './actions/credential-injector';
|
|||||||
import { tryAuthDispatch } from './actions/auth-dispatcher';
|
import { tryAuthDispatch } from './actions/auth-dispatcher';
|
||||||
import { expandPromptRecipe } from './lib/recipe-expander';
|
import { expandPromptRecipe } from './lib/recipe-expander';
|
||||||
import { persistPausedRun, isResumablePending, parseRecipeOutput } from './lib/paused-runs';
|
import { persistPausedRun, isResumablePending, parseRecipeOutput } from './lib/paused-runs';
|
||||||
|
import { buildMagicVars } from './lib/magic-vars';
|
||||||
|
|
||||||
export type ComponentLoader = (componentId: string) => Promise<ComponentRunner>;
|
export type ComponentLoader = (componentId: string) => Promise<ComponentRunner>;
|
||||||
export type WorkflowLoader = (workflowId: string) => Promise<ExecutionGraph>;
|
export type WorkflowLoader = (workflowId: string) => Promise<ExecutionGraph>;
|
||||||
@@ -51,12 +52,20 @@ export class GraphExecutor {
|
|||||||
this.currentGraph = graph;
|
this.currentGraph = graph;
|
||||||
this.currentRunId = kvStore?.runId ?? `${graph.id}-${Date.now()}`;
|
this.currentRunId = kvStore?.runId ?? `${graph.id}-${Date.now()}`;
|
||||||
|
|
||||||
|
// Magic vars:注入 _today / _now / _iso_week 等系統變數(LI SDD M2.x)
|
||||||
|
// initialContext 寫前,magic vars 寫後 → magic vars 永遠 win(防 user accidentally 用 _ prefix)
|
||||||
|
// 同時保留 user 既有 ctx,magic vars 不破壞既有 workflow(_ prefix reserved)
|
||||||
|
const ctxWithMagic: Record<string, unknown> = {
|
||||||
|
...initialContext,
|
||||||
|
...buildMagicVars(),
|
||||||
|
};
|
||||||
|
|
||||||
// 找出所有起點(沒有任何邊指向的節點)
|
// 找出所有起點(沒有任何邊指向的節點)
|
||||||
const hasIncoming = new Set(graph.edges.map(e => e.to));
|
const hasIncoming = new Set(graph.edges.map(e => e.to));
|
||||||
const startNodes = graph.nodes.filter(n => !hasIncoming.has(n.id));
|
const startNodes = graph.nodes.filter(n => !hasIncoming.has(n.id));
|
||||||
|
|
||||||
if (startNodes.length === 0) {
|
if (startNodes.length === 0) {
|
||||||
return { data: initialContext, trace };
|
return { data: ctxWithMagic, trace };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建立 fan-in 狀態(入度 > 1 的節點需要等所有上游)
|
// 建立 fan-in 狀態(入度 > 1 的節點需要等所有上游)
|
||||||
@@ -64,14 +73,14 @@ export class GraphExecutor {
|
|||||||
for (const node of graph.nodes) {
|
for (const node of graph.nodes) {
|
||||||
const inDeg = graph.edges.filter(e => e.to === node.id).length;
|
const inDeg = graph.edges.filter(e => e.to === node.id).length;
|
||||||
if (inDeg > 1) {
|
if (inDeg > 1) {
|
||||||
fanIn.set(node.id, { ctx: { ...initialContext }, remaining: inDeg });
|
fanIn.set(node.id, { ctx: { ...ctxWithMagic }, remaining: inDeg });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 並行執行所有起點
|
// 並行執行所有起點
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
startNodes.map(node =>
|
startNodes.map(node =>
|
||||||
this.executeNode(node, graph, initialContext, new Set(), trace, fanIn, kvStore)
|
this.executeNode(node, graph, ctxWithMagic, new Set(), trace, fanIn, kvStore)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Magic vars — workflow YAML 內建變數
|
||||||
|
*
|
||||||
|
* 對應 LI SDD M2.x improvement(feedback block c47bf70b)。
|
||||||
|
*
|
||||||
|
* 任何以 `_` 開頭的變數名都是 reserved(system)。常見:時間、執行 metadata。
|
||||||
|
* 用於 page_name / file path / URL 等需要時間戳的場景。
|
||||||
|
*
|
||||||
|
* 範例 YAML:
|
||||||
|
* page_name: "roadmap-week-{{_iso_week}}" # roadmap-week-2026-W20
|
||||||
|
* page_name: "log-{{_today}}" # log-2026-05-16
|
||||||
|
* filename: "snapshot-{{_now_unix}}.json" # snapshot-1778940000123.json
|
||||||
|
*
|
||||||
|
* 不違反 §2.2:這是 orchestrator routing 提供的「環境變數」(像 shell 的 $DATE),
|
||||||
|
* 不涉及 secret / credential / JWT,跟既有 ctx 變數展開同層。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 算 ISO 8601 週數(W01-W53)。
|
||||||
|
* 週一為週首,W01 含當年首個週四(ISO 標準)。
|
||||||
|
* https://en.wikipedia.org/wiki/ISO_week_date
|
||||||
|
*/
|
||||||
|
function isoWeekNumber(d: Date): { year: number; week: number } {
|
||||||
|
const target = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
|
const dayNum = (target.getUTCDay() + 6) % 7; // Mon=0
|
||||||
|
target.setUTCDate(target.getUTCDate() - dayNum + 3);
|
||||||
|
const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
|
||||||
|
const weekNum = 1 + Math.round(
|
||||||
|
((target.getTime() - firstThursday.getTime()) / 86400000 -
|
||||||
|
3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7
|
||||||
|
);
|
||||||
|
return { year: target.getUTCFullYear(), week: weekNum };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad2(n: number): string {
|
||||||
|
return n.toString().padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立 magic vars。每次 workflow 觸發時呼叫一次,貫穿整個執行。
|
||||||
|
*
|
||||||
|
* 設計:UTC 為基準(避免 worker 跨時區誤判)。需要本地時區的場景,
|
||||||
|
* 用戶可自己組(例如 yaml 寫 `{{_today_utc}}` + 自己處理偏移)。
|
||||||
|
*/
|
||||||
|
export function buildMagicVars(now: Date = new Date()): Record<string, string | number> {
|
||||||
|
const iso = now.toISOString(); // 2026-05-16T09:30:00.123Z
|
||||||
|
const yyyy = now.getUTCFullYear();
|
||||||
|
const mm = pad2(now.getUTCMonth() + 1);
|
||||||
|
const dd = pad2(now.getUTCDate());
|
||||||
|
const hh = pad2(now.getUTCHours());
|
||||||
|
const mi = pad2(now.getUTCMinutes());
|
||||||
|
const ss = pad2(now.getUTCSeconds());
|
||||||
|
|
||||||
|
const yesterday = new Date(now.getTime() - 86400000);
|
||||||
|
const yMm = pad2(yesterday.getUTCMonth() + 1);
|
||||||
|
const yDd = pad2(yesterday.getUTCDate());
|
||||||
|
|
||||||
|
const { year: isoYear, week: isoWeek } = isoWeekNumber(now);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 日期 / 時間(UTC)
|
||||||
|
_today: `${yyyy}-${mm}-${dd}`, // 2026-05-16
|
||||||
|
_yesterday: `${yesterday.getUTCFullYear()}-${yMm}-${yDd}`, // 2026-05-15
|
||||||
|
_now: iso, // ISO 8601
|
||||||
|
_now_unix: now.getTime(), // unix ms
|
||||||
|
_now_unix_s: Math.floor(now.getTime() / 1000), // unix sec
|
||||||
|
|
||||||
|
// 個別欄位(給 path / page_name 拼)
|
||||||
|
_year: yyyy,
|
||||||
|
_month: mm,
|
||||||
|
_day: dd,
|
||||||
|
_hour: hh,
|
||||||
|
_minute: mi,
|
||||||
|
_second: ss,
|
||||||
|
|
||||||
|
// ISO 週(roadmap weekly archive 必備)
|
||||||
|
_iso_week: `${isoYear}-W${pad2(isoWeek)}`, // 2026-W20
|
||||||
|
_iso_week_num: isoWeek,
|
||||||
|
_iso_year: isoYear,
|
||||||
|
|
||||||
|
// 簡單時間 slot(cron-friendly)
|
||||||
|
_yyyymm: `${yyyy}${mm}`, // 202605
|
||||||
|
_yyyymmdd: `${yyyy}${mm}${dd}`, // 20260516
|
||||||
|
|
||||||
|
// 週幾(0=週日,1=週一 ... 6=週六;ISO 風格在 _iso_weekday)
|
||||||
|
_weekday: now.getUTCDay(),
|
||||||
|
_iso_weekday: ((now.getUTCDay() + 6) % 7) + 1, // 1=Mon...7=Sun
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user