promptex-examples 0.0.0-stage → 1.4.0-alpha.2

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.
@@ -0,0 +1,2327 @@
1
+ {
2
+ "package": "promptex-examples",
3
+ "version": "1.4.0-alpha.2",
4
+ "groups": [
5
+ {
6
+ "id": "first-build",
7
+ "name": "第一次建置",
8
+ "situation": "我要從零跑出第一份產物",
9
+ "claim": "起步的負擔和手寫 SKILL.md 一樣,多換到的是 id 治理與多平台分發。產物只有三種形態:獨佔檔、合併型、區塊型。",
10
+ "scenarios": [
11
+ {
12
+ "title": "純散文字串就是一個 skill",
13
+ "situation": "只想寫一段話就有一個 skill,不想先學任何建構函式。",
14
+ "code": [
15
+ {
16
+ "caption": "prompts/skill.ts",
17
+ "lang": "ts",
18
+ "source": "import { defineSkill } from 'promptex-js'\n\nexport const reviewHelper = defineSkill({\n id: 'review-helper',\n name: '程式碼審查協助',\n config: { whenToUse: '當使用者要求 review 程式碼、檢查 PR 時使用' },\n content: '依序檢查正確性、安全性、可讀性、測試覆蓋、一致性、效能六個面向。',\n})\n\n// 多段散文用字串陣列,段落間自動空行\nexport const commitHelper = defineSkill({\n id: 'commit-helper',\n name: 'Commit 訊息協助',\n config: { whenToUse: '當使用者要求撰寫 commit 訊息時使用' },\n content: ['依 Conventional Commits 撰寫:type(scope): subject。', 'subject 用繁體中文、50 字內。'],\n})"
19
+ },
20
+ {
21
+ "caption": "promptex.config.ts",
22
+ "lang": "ts",
23
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'minimal-prose',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude()],\n})"
24
+ }
25
+ ],
26
+ "outcome": [
27
+ "`.claude/skills/review-helper/SKILL.md` 的 frontmatter 帶 name 與 when_to_use(取自 whenToUse),內文就是那段散文。",
28
+ "`.claude/skills/commit-helper/SKILL.md` 的 frontmatter 同樣只有 name 與 when_to_use,兩段散文以空行分隔。",
29
+ "兩個 skill 都沒宣告 description,所以產物不帶 description,安裝報告記提醒 2 項(每個 skill 一筆)。"
30
+ ],
31
+ "points": [
32
+ "必填欄位只有 id、name、config.whenToUse、content。",
33
+ "content 收字串或字串陣列,不需要任何建構函式。"
34
+ ],
35
+ "facts": [
36
+ "promptex.sdk.define-skill",
37
+ "promptex.sdk.define-config",
38
+ "promptex.cli.build"
39
+ ]
40
+ },
41
+ {
42
+ "title": "七種能力類型的安裝位置與產物形態",
43
+ "situation": "想一次看到 skill、rule、agent、instruction、hook、mcp、permission 各自寫進哪個檔案。",
44
+ "code": [
45
+ {
46
+ "caption": "prompts/nodes.ts",
47
+ "lang": "ts",
48
+ "source": "import {\n defineAgent,\n defineHook,\n defineInstruction,\n defineMcp,\n definePermission,\n defineResource,\n defineRule,\n defineSkill,\n useResource,\n} from 'promptex-js'\n\ndefineResource({\n id: 'commit-style',\n name: 'Commit 慣例',\n content: 'type(scope): subject',\n})\n\ndefineSkill({\n id: 'change-flow',\n name: '變更流程',\n config: { whenToUse: '推進變更時使用' },\n content: ['依階段執行。', useResource('commit-style')],\n})\n\ndefineRule({\n id: 'ts-style',\n name: 'TS 風格',\n config: { appliesTo: ['**/*.ts'] },\n content: '優先使用 interface。',\n})\n\ndefineAgent({\n id: 'reviewer',\n name: '審查代理',\n config: { whenToUse: '需要逐項核對規範時委派' },\n content: '逐項核對並附出處。',\n})\n\ndefineInstruction({ id: 'lang', name: '語言', content: '一律使用繁體中文。' })\ndefineInstruction({ id: 'pm', name: '套件管理', content: '使用 pnpm。' })\n\ndefineHook({\n id: 'fmt',\n name: '格式化',\n config: { event: 'tool:after', matcher: 'Edit' },\n actions: [{ type: 'command', command: 'prettier --write' }],\n})\n\ndefineMcp({\n id: 'ctx7',\n name: 'Context7',\n config: { transport: { type: 'stdio', command: 'npx', args: ['-y', 'ctx7'] } },\n})\n\ndefinePermission({\n id: 'no-recursive-delete',\n name: '禁止遞迴刪除',\n config: { tool: 'Bash', match: ['rm -rf'], decision: 'deny' },\n})"
49
+ },
50
+ {
51
+ "caption": "promptex.config.ts",
52
+ "lang": "ts",
53
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'all-seven-kinds',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude()],\n})"
54
+ }
55
+ ],
56
+ "outcome": [
57
+ "獨佔檔:`.claude/skills/change-flow/SKILL.md`、`.claude/rules/ts-style.md`、`.claude/agents/reviewer.md`,一個節點一個檔。",
58
+ "合併型:hook 與 permission 都寫進 `.claude/settings.json`,分別放在 `hooks` 與 `permissions` 兩個頂層鍵,互不覆蓋。mcp 併入 `.mcp.json` 的 `mcpServers`。",
59
+ "區塊型:兩份 instruction 各用 `<!-- promptex:begin {id} -->` 與 end 標記包住,寫進同一份 `CLAUDE.md`。",
60
+ "被引用的素材放在引用它的節點目錄下:`.claude/skills/change-flow/commit-style.md`,SKILL.md 內是指向它的相對連結。",
61
+ "所有權登記 `.promptex/all-seven-kinds/claude.json` 把這三種形態記成 `text`、`merge`、`section` 三種 kind。"
62
+ ],
63
+ "points": [
64
+ "落點是各平台自己的知識。框架要處理的結構差異是產物形態:獨佔檔、合併型、區塊型。",
65
+ "model、effort、allowedTools 是中性詞彙,各平台各自對映。這和產物形態是兩件不同的事。"
66
+ ],
67
+ "facts": [
68
+ "promptex.sdk.kinds",
69
+ "promptex.sdk.define-skill",
70
+ "promptex.sdk.define-rule",
71
+ "promptex.sdk.define-agent",
72
+ "promptex.sdk.define-instruction",
73
+ "promptex.sdk.define-hook",
74
+ "promptex.sdk.define-mcp",
75
+ "promptex.sdk.define-permission",
76
+ "promptex.sdk.define-resource",
77
+ "promptex.formats.ownership-registry"
78
+ ]
79
+ },
80
+ {
81
+ "title": "用 create 建立或初始化專案",
82
+ "situation": "手上是一個空目錄,或一個已經有程式的資料夾,想立刻有一份可以建置、可以安裝的 promptex 專案。",
83
+ "code": [
84
+ {
85
+ "caption": "建立新專案(目標目錄不存在)並安裝",
86
+ "lang": "console",
87
+ "source": "$ create-promptex-js my-prompts --yes --no-git --locale en\nCreate mode: new project in <project>/my-prompts\nWrote 6 files → <project>/my-prompts (template source: bundled templates (v<version>))\n create .gitignore\n create README.md\n create package.json\n create promptex.config.ts\n create prompts/example-skill.ts\n create prompts/example.ts\nNext steps:\n 1. cd my-prompts\n 2. pnpm install\n 3. pnpm run install:prompts\n\n$ promptex build --install\n✓ my-prompts: metadata written (graph + content)\n── my-prompts → claude (<project>/my-prompts) ──\n wrote .claude/rules/example-rule.md\n wrote .claude/skills/example-skill/SKILL.md\nInstall report (claude)\n Projected artifacts, 2: .claude/rules/example-rule.md, .claude/skills/example-skill/SKILL.md"
88
+ },
89
+ {
90
+ "caption": "variants/package.json",
91
+ "lang": "json",
92
+ "source": "{\n \"name\": \"existing-app\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"type\": \"module\"\n}"
93
+ },
94
+ {
95
+ "caption": "在原處初始化(既有資料夾)",
96
+ "lang": "console",
97
+ "source": "$ create-promptex-js existing --yes --no-git --locale en\nInit mode: adding promptex to the existing directory <project>/existing (it already holds package.json)\nWrote 4 files → <project>/existing (template source: bundled templates (v<version>))\n create .gitignore\n create promptex.config.ts\n create prompts/example-skill.ts\n create prompts/example.ts\n skip README.md (only written in create mode)\n skip package.json (declaration file left as is, add the promptex dependency yourself)\nNext steps:\n 1. cd existing\n 2. pnpm add promptex-js\n 3. pnpm exec promptex build --install"
98
+ },
99
+ {
100
+ "caption": "目錄已有 promptex.config.ts 時再跑一次",
101
+ "lang": "console",
102
+ "source": "$ create-promptex-js ./my-prompts --yes --no-git --locale en\nInit mode: adding promptex to the existing directory <project>/my-prompts (it already holds .promptex, package.json, promptex.config.ts, prompts)\nAlready initialized: promptex.config.ts exists, so no file was written."
103
+ },
104
+ {
105
+ "caption": "只預覽、不寫檔",
106
+ "lang": "console",
107
+ "source": "$ create-promptex-js preview --dry-run --json --no-git\n{\"formatVersion\":\"1.2\",\"package\":{\"name\":\"create-promptex-js\",\"version\":\"<version>\"},\"language\":\"ts\",\"mode\":\"create\",\"alreadyInitialized\":false,\"existingConfig\":null,\"target\":\"<project>/preview\",\"template\":\"project\",\"name\":\"main\",\"files\":{\"create\":[\".gitignore\",\"README.md\",\"package.json\",\"promptex.config.ts\",\"prompts/example-skill.ts\",\"prompts/example.ts\"],\"overwrite\":[],\"append\":[],\"skip\":[]},\"git\":{\"init\":false,\"reason\":\"disabled\"}}"
108
+ }
109
+ ],
110
+ "outcome": [
111
+ "建立新專案時寫出 6 個檔:`my-prompts/.gitignore`、`my-prompts/README.md`、`my-prompts/package.json`、`my-prompts/promptex.config.ts`、`my-prompts/prompts/example-skill.ts`、`my-prompts/prompts/example.ts`。`my-prompts/package.json` 已宣告 promptex-js,以及 build 與 install:prompts 兩支 script。`my-prompts/promptex.config.ts` 的 name 取目錄名、srcDir 為 ./prompts、outDir 為 .,targets 是 claude()。`my-prompts/prompts/example.ts` 是範例規則,`my-prompts/prompts/example-skill.ts` 是範例 skill。",
112
+ "在原處初始化時只補 promptex 自己的 4 個檔,輸出把 README.md 與 package.json 記成 skip。`existing/package.json` 一字未動,輸出提示主套件要自己用 `pnpm add promptex-js` 加。",
113
+ "`my-prompts/.gitignore` 第一行是 create 用來認得自己寫過的標記行。",
114
+ "這次的暫存目錄不在 git 工作樹內,加 `--no-git` 略過 git init,所以輸出沒有 git 那一行。`--dry-run --json` 的輸出記 `git` 為 `{\"init\":false,\"reason\":\"disabled\"}`。",
115
+ "在 `my-prompts` 裡接著跑 `build --install`,寫出 `my-prompts/.claude/rules/example-rule.md`,frontmatter 的 `paths` 是清單 `- prompts/**`。所有權登記寫在 `my-prompts/.promptex/my-prompts/claude.json`。",
116
+ "對已有 promptex 設定檔的目錄再跑一次,輸出「Already initialized」,沒有寫任何檔。",
117
+ "`--dry-run --json` 只印出會建立的 6 個檔與 `alreadyInitialized: false`,沒有建出 `preview` 目錄。"
118
+ ],
119
+ "points": [
120
+ "模式由目標目錄決定。目錄不存在,或只有系統、版控、編輯器的檔,就建立新專案。其餘目錄在原處初始化。",
121
+ "目錄裡已經有 `promptex.config.*` 時,create 視為已初始化,不寫任何檔就結束。",
122
+ "既有檔案與模板同名時不覆寫。加 `--force` 才覆寫,但仍不清空資料夾,也不動宣告檔。",
123
+ "在終端機互動執行時,create 會依序問目標目錄(沒給時)、模板(只在建立新專案時)、單元名,以及要不要 git init。git 提問只在目標不在既有工作樹內、又沒給 `--git` 或 `--no-git` 時出現。加 `--yes` 全部取預設。在原處初始化只支援 project 模板。",
124
+ "目標不在既有的 git 工作樹內時,兩種模式預設都會執行 git init,加 `--no-git` 可略過。",
125
+ "Python 用 `uvx create-promptex-py <目錄>`。Rust 先跑一次 `cargo install create-promptex-rs`,再跑 `cargo create-promptex-rs <目錄>`。",
126
+ "`promptex init` 已棄用。1.x 仍可執行,會在標準錯誤提示改用 create,2.0 移除。",
127
+ "`build` 只更新框架中繼,`build --install` 才寫平台產物。骨架的兩支 script 各對應一種。",
128
+ "範例資源刻意宣告 appliesTo。因為 appliesTo 與 alwaysApply 都不宣告的 rule 建置得過,卻永遠不會進入 context。"
129
+ ],
130
+ "facts": [
131
+ "promptex.cli.create-config-files",
132
+ "promptex.cli.create-gitignore-marker",
133
+ "promptex.formats.create-dry-run"
134
+ ]
135
+ }
136
+ ]
137
+ },
138
+ {
139
+ "id": "writing-content",
140
+ "name": "寫內容",
141
+ "situation": "我要把內容寫出來",
142
+ "claim": "結構留在中介表示才能被投影與擴充改寫;動態性在求值時固化,散文與引用各走各的通道。",
143
+ "scenarios": [
144
+ {
145
+ "title": "結構型內容用建構函式",
146
+ "situation": "手寫表格容易欄數對不齊、標題容易跳級,想讓結構在源碼就成為資料。",
147
+ "code": [
148
+ {
149
+ "caption": "prompts/resources.ts",
150
+ "lang": "ts",
151
+ "source": "import { check, defineResource, link, list, section, table } from 'promptex-js'\n\n// builder 家族以具名函式直接使用,不需任何包裹函式。\n// 只承載「結構」型內容(標題階層、表格、清單、勾選項、外部連結),\n// 長散文仍走字串或 file(),不強制函式化。\nexport const releaseGuide = defineResource({\n id: 'release-guide',\n name: '發布指引',\n content: [\n // section:標題與其下內容,可巢狀(子層標題自動遞增一級)\n section(\n '版號決策',\n '依語意化版本判定:破壞性變更進 major、新增功能進 minor、修正進 patch。',\n table(\n ['變更類型', '版號位'],\n [\n ['破壞相容性', 'major'],\n ['新增功能', 'minor'],\n ['缺陷修正', 'patch'],\n ],\n ),\n ),\n\n // list:無序清單\n section('發布前置', list(['CI 全綠', 'CHANGELOG 已更新', '版號已決定'])),\n\n // check:待辦勾選項,供執行時逐項核對\n section('發布核對', check(['tag 已推送', '套件已發佈', '發布公告已張貼', '下游專案已通知'])),\n\n // link:外部 URL;跨節點引用一律走 use*,不用 link()\n section('參考', link('語意化版本規範', 'https://github.com/semver/semver')),\n ],\n})"
152
+ },
153
+ {
154
+ "caption": "prompts/skill.ts",
155
+ "lang": "ts",
156
+ "source": "import { defineSkill, useResource } from 'promptex-js'\n\nexport const releaseFlow = defineSkill({\n id: 'release-flow',\n name: '發布流程',\n config: { whenToUse: '當使用者要求發版、打 tag、publish 時使用' },\n content: ['依指引逐步執行,核對項全數通過才宣告完成:', useResource('release-guide')],\n})"
157
+ }
158
+ ],
159
+ "outcome": [
160
+ "`.claude/skills/release-flow/SKILL.md`:一段散文加一行指向資源的連結。",
161
+ "`.claude/skills/release-flow/release-guide.md`:四個 section 依序渲染為 `##` 標題,內含表格、無序清單、勾選清單與外部連結。"
162
+ ],
163
+ "points": [
164
+ "section、table、list、check、link 五個足以起步;section 巢狀時子層標題自動降一級,不必自己管 `#` 數量。",
165
+ "散文除了字串,也可以用 file() 掛外部檔案。",
166
+ "link() 是站外 URL;跨節點引用一律走 use*,前者渲染站外連結、後者渲染指向產物的相對連結。"
167
+ ],
168
+ "facts": [
169
+ "promptex.sdk.define-resource",
170
+ "promptex.sdk.define-skill",
171
+ "promptex.sdk.use-refs"
172
+ ]
173
+ },
174
+ {
175
+ "title": "十二種區塊級加五種行內建構函式",
176
+ "situation": "需要完整的 markdown 表達力,含程式碼區塊、引用、摺疊、圖片、修飾包住引用、清單項目與表格儲存格連到某份資源。",
177
+ "code": [
178
+ {
179
+ "caption": "prompts/nodes.ts",
180
+ "lang": "ts",
181
+ "source": "import {\n bold,\n check,\n code,\n defineResource,\n defineSkill,\n details,\n divider,\n file,\n image,\n inline,\n inlineCode,\n inlineLink,\n italic,\n link,\n list,\n ordered,\n quote,\n section,\n table,\n useResource,\n} from 'promptex-js'\n\n// 區塊級 builder:各自對應一種 markdown 結構,涵蓋十二種常用情境。\nexport const releaseGuide = defineResource({\n id: 'release-guide',\n name: '發布指引',\n content: [\n section(\n '步驟',\n // 有序清單:步驟有先後\n ordered(['決定版號', '打 tag', 'publish', '發布後核對']),\n ),\n\n section(\n '指令範例',\n // 程式碼區塊:lang 標語言;純文字用 text、終端輸出用 console\n code('bash', 'pnpm version minor\\ngit push --follow-tags'),\n code('console', '$ pnpm publish\\n+ promptex@2.1.0'),\n ),\n\n section(\n '注意事項',\n // 引用區塊:可含其他結構\n quote('publish 後無法撤回,只能發新版覆蓋。', list(['先確認 CI 全綠', '先確認 CHANGELOG 已更新'])),\n ),\n\n // 分隔線:段落分界\n divider(),\n\n section(\n '核對清單',\n check(['tag 已推送', '套件已發佈', '公告已張貼']),\n // 摺疊區塊:長附錄收合,不干擾主要內容\n details(\n '完整核對項(展開)',\n table(\n ['項目', '負責人'],\n [\n ['tag', '發布者'],\n ['公告', '維護者'],\n ],\n ),\n ),\n ),\n\n section('參考', link('語意化版本', 'https://github.com/semver/semver'), image('發布流程圖', 'assets/flow.png')),\n\n // file():既有附錄原文掛入,第十二種常用情境\n section('附錄', file('docs/appendix.md')),\n ],\n})\n\n// 純文字的行內修飾直接寫 markdown 標記:那段話在中介表示裡就是一段散文,\n// 包成建構函式救不回任何結構,而標記比等價的函式呼叫短。\nexport const releaseFlow = defineSkill({\n id: 'release-flow',\n name: '發布流程',\n config: { whenToUse: '當使用者要求發版時使用' },\n content: [\n '發布前**必須**確認 CI 全綠;版號依 `pnpm version` 決定,' +\n '_不可_手改 package.json。規範見 [語意化版本](https://github.com/semver/semver)。',\n // 修飾包住引用:字面標記表達不了這件事。引用是符號,拼進模板字串會在\n // 轉換當下固定為文字而失去解析能力,框架於當下攔截(ADR-003),因此\n // `**${useResource('release-guide')}**` 不成立,只有片段形態的 bold 接得住。\n bold(useResource('release-guide')),\n // 行內容器的子項不夾分隔符號併接、且可疊加:以下四個子項渲染為單一行。\n italic('指令是 ', inlineCode('pnpm version'), ',規範見 ', inlineLink('語意化版本', 'https://github.com/semver/semver')),\n // 清單項目收的是與行內容器子項同一個集合,因此第二個步驟直接連到它依據的\n // 那份資源。項目只收字串時,這個引用得拆到清單之外另寫一行,一則步驟因此\n // 被切成兩段。\n ordered(['決定版號', useResource('release-guide'), '公告已張貼']),\n // 表頭與儲存格同一形態,兩處都收引用:框架不對表頭另設限制。\n table(['項目', '依據'], [['tag', useResource('release-guide')]]),\n // inline 是不帶樣式的行內容器:整句(文字、引用、文字)結構化而不憑空帶上\n // 粗體或斜體。與下面兩個陣列元素相比,差別是整句留在同一行;陣列並列會在\n // 引用前後各斷一段。\n inline('完整步驟見 ', useResource('release-guide'), ',其餘依團隊慣例。'),\n // 陣列並列的寫法保留作對照:它仍然成立,只是句子被斷成兩段。\n '完整步驟見:',\n useResource('release-guide'),\n ],\n})"
182
+ },
183
+ {
184
+ "caption": "docs/appendix.md",
185
+ "lang": "md",
186
+ "source": "既有附錄,未經改寫即掛入:舊版發布腳本的參數說明留存於此供查閱。"
187
+ }
188
+ ],
189
+ "outcome": [
190
+ "產物中 `bold(useResource(...))` 渲染為 `**[發布指引](release-guide.md)**`;italic 的四個子項併接成單一行。",
191
+ "清單第二項渲染為 `2. [發布指引](release-guide.md)`、儲存格渲染為 `| tag | [發布指引](release-guide.md) |`,與夾在文句中的引用同形。",
192
+ "inline 那段渲染為單一行「完整步驟見 [發布指引](release-guide.md),其餘依團隊慣例。」,不帶任何樣式標記。檔尾用陣列並列的對照寫法,則在引用前後各斷一段。",
193
+ "code 的圍欄一律帶語言標籤,`.claude/skills/release-flow/release-guide.md` 裡是 `bash` 與 `console`;details 產出 `<details>` 與 `<summary>` HTML 標籤,各平台皆可渲染。",
194
+ "link 與 inlineLink 渲染出的 markdown 逐字相同,差別只在前者獨立成段、後者夾在行內。"
195
+ ],
196
+ "points": [
197
+ "行內建構函式解決的是字串接不住引用這件事,各自只收一種形態。bold、italic 包單一引用;inline 接多段且不加任何樣式標記。",
198
+ "凡收子項處一律收同一個集合,清單項目與表格儲存格也在內:「這個步驟依據哪一份資源」寫得進項目本身,不必拆到清單之外另寫一行。",
199
+ "一個項目要放多段內容時以 inline 包起;清單因此不收巢狀陣列,最常見的純文字清單不必多一層括號。",
200
+ "inlineCode 只收文字,因為 markdown 程式碼片段內不解析標記。"
201
+ ],
202
+ "facts": [
203
+ "promptex.sdk.define-resource",
204
+ "promptex.sdk.define-skill",
205
+ "promptex.sdk.use-refs"
206
+ ]
207
+ },
208
+ {
209
+ "title": "散文、建構函式、file()、引用四形態混排",
210
+ "situation": "一個節點同時要說明散文、對照表格、直接掛既有文件、又要引用共用內容。",
211
+ "code": [
212
+ {
213
+ "caption": "prompts/skill.ts",
214
+ "lang": "ts",
215
+ "source": "import { check, defineSkill, file, list, section, useResource } from 'promptex-js'\nimport { APP_NAME } from '../src/index.ts'\n\n// 四種內容形態混排於同一個 content 陣列,順序原樣保留:\n// 散文字串(含原生模板插值)、builder、file() 既有檔、use* 引用。\nexport const changeFlow = defineSkill({\n id: 'change-flow',\n name: '變更流程',\n config: { whenToUse: '當使用者要求推進變更、開分支、提 PR 時使用' },\n content: [\n // ① 散文字串 + 原生模板插值(純資料,求值即固化)\n `在 ${APP_NAME} 推進變更時,依下列階段執行。`,\n\n // ② builder:結構型內容\n section('階段', list(['開分支', '提交變更', '提 PR', '合併後清理'])),\n\n // ③ file():既有文件原文掛入\n file('docs/team-convention.md'),\n\n // ④ use* 引用:符號保留到投影,渲染為連結\n '分支命名細則見:',\n useResource('branch-naming'),\n\n // ⑤ 再回到散文與 builder,驗證順序不因形態切換而重排\n '完成後核對:',\n check(['分支已刪除', 'issue 已關閉']),\n ],\n})"
216
+ },
217
+ {
218
+ "caption": "prompts/resources.ts",
219
+ "lang": "ts",
220
+ "source": "import { defineResource, table } from 'promptex-js'\n\nexport const branchNaming = defineResource({\n id: 'branch-naming',\n name: '分支命名慣例',\n content: table(\n ['類型', '格式'],\n [\n ['功能', 'feature/{issue}'],\n ['修正', 'fix/{issue}'],\n ],\n ),\n})"
221
+ },
222
+ {
223
+ "caption": "docs/team-convention.md",
224
+ "lang": "md",
225
+ "source": "## 團隊既有慣例\n\n沿用自舊版手冊,未改寫即掛入:分支命名 `feature/{issue}`,PR 標題與 issue 同名。"
226
+ },
227
+ {
228
+ "caption": "src/index.ts",
229
+ "lang": "ts",
230
+ "source": "// 模擬專案自身源碼(與提示詞無關)。\nexport const APP_NAME = 'mixed-content-demo'"
231
+ }
232
+ ],
233
+ "outcome": [
234
+ "`.claude/skills/change-flow/SKILL.md` 依宣告順序排出七個片段,片段間以空行分隔;插值已固化為字串常數。",
235
+ "`.claude/skills/change-flow/branch-naming.md`:被引用的資源落在引用者目錄下。"
236
+ ],
237
+ "points": [
238
+ "四形態在中介表示都是內容片段(prose、section、ref 等),content 就是片段的有序陣列。",
239
+ "不需要任何包裹函式或形態轉換宣告,順序不因形態切換而重排。"
240
+ ],
241
+ "facts": [
242
+ "promptex.sdk.define-skill",
243
+ "promptex.sdk.define-resource",
244
+ "promptex.sdk.use-refs"
245
+ ]
246
+ },
247
+ {
248
+ "title": "以 file() 掛入既有 markdown,零改寫遷移",
249
+ "situation": "手上已有一批手寫的 SKILL.md 或指引,想先納入治理再逐步結構化。",
250
+ "code": [
251
+ {
252
+ "caption": "prompts/nodes.ts",
253
+ "lang": "ts",
254
+ "source": "import { defineResource, defineSkill, file, useResource } from 'promptex-js'\n\n// file() 掛入既有 .md:檔案原文於求值期讀入、固化為內容片段,來源檔一字未改\n// (零改寫遷移路徑)。\nexport const reviewHelper = defineSkill({\n id: 'review-helper',\n name: '程式碼審查協助',\n config: { whenToUse: '當使用者要求 review 程式碼、檢查 PR 時使用' },\n content: file('docs/review-guide.md'),\n})\n\n// 既有檔案也可作為資源本體,讓多個節點共用同一份來源檔。\nexport const glossary = defineResource({\n id: 'review-glossary',\n name: '審查術語',\n content: file('docs/glossary.md'),\n})\n\n// 混用:既有檔案 + 新寫散文 + 引用,遷移可逐步進行而非一次到位。\nexport const prReview = defineSkill({\n id: 'pr-review',\n name: 'PR 審查',\n config: { whenToUse: '當使用者要求審查 PR 時使用' },\n content: [\n file('docs/review-guide.md'),\n '本專案額外要求:一個 PR 不超過 400 行(不含自動產生檔)。',\n '用語見:',\n useResource('review-glossary'),\n ],\n})"
255
+ },
256
+ {
257
+ "caption": "docs/review-guide.md",
258
+ "lang": "md",
259
+ "source": "既有的審查指引文件,未經任何改寫即被提示詞掛入。\n\n## 檢查順序\n\n1. 正確性:邏輯與邊界情境\n2. 安全性:注入、權限、敏感資料\n3. 可讀性:命名與結構\n4. 測試:關鍵邏輯是否覆蓋\n\n## 回覆慣例\n\n區分 blocking 與 nit;肯定好設計,不只挑問題。"
260
+ },
261
+ {
262
+ "caption": "docs/glossary.md",
263
+ "lang": "md",
264
+ "source": "- blocking:必須修改才可合併\n- nit:建議事項,可不改"
265
+ },
266
+ {
267
+ "caption": "掛入的檔不存在,或不是 UTF-8 文字",
268
+ "lang": "console",
269
+ "source": "$ promptex build\n✗ E103 Mount source does not exist:file(\"docs/not-there.md\") has no such file\n at <project>/prompts/nodes.ts:7\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing).\n\n$ promptex build .\n✗ E106 Mount source is not UTF-8 text:the content of file(\"docs/binary.md\") cannot be read as UTF-8\n at <project>/prompts/nodes.ts:7\n Mounted content is merged into the artifact verbatim:the source must be text; declare a binary file as an asset instead, whose artifact is a copy of the original file\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
270
+ }
271
+ ],
272
+ "outcome": [
273
+ "`.claude/skills/review-helper/SKILL.md` 內文即 `docs/review-guide.md` 全文,來源檔一字未改。",
274
+ "`.claude/skills/pr-review/review-glossary.md` 頂端的 `# 審查術語` 來自 defineResource 的渲染規則(資源以 name 為一級標題),掛入的來源檔從 `##` 起算即可正確嵌入。",
275
+ "把 file() 指向不存在的檔,build 以結束碼 1 失敗,印出 E103 與出事的那一行 `prompts/nodes.ts:7`。指向不是 UTF-8 的檔則印出 E106。兩種都是「0 artifacts written (all or nothing)」,不寫任何產物。"
276
+ ],
277
+ "points": [
278
+ "三階段遷移:完全不動(content: file(...))、逐步增補([file(...), 新散文])、完全內化([section(...), ...] 後來源檔退場)。",
279
+ "file() 在求值期讀入並固化為散文片段,投影不做任何檔案讀寫;路徑相對專案根,與工作目錄無關。",
280
+ "file()(內容進 context、會被渲染)與 defineAsset(原檔拷貝、不進 context)分工清楚。",
281
+ "來源檔不存在記 E103、非 UTF-8 記 E106、讀不出記 E107,三者都讓內容留空並一次報齊。"
282
+ ],
283
+ "facts": [
284
+ "promptex.sdk.define-skill",
285
+ "promptex.sdk.define-resource",
286
+ "promptex.diagnostics.e103",
287
+ "promptex.diagnostics.e106"
288
+ ]
289
+ },
290
+ {
291
+ "title": "原生模板字串插值,框架零介入",
292
+ "situation": "想在內文插入版本號、套件管理器名稱等專案常數,且與程式碼共用單一真相。",
293
+ "code": [
294
+ {
295
+ "caption": "src/index.ts",
296
+ "lang": "ts",
297
+ "source": "// 模擬專案自身源碼:提示詞可直接讀取專案常數,形成單一真相。\nexport const SUPPORTED_NODE = '22'\nexport const PKG_MANAGER = 'pnpm'"
298
+ },
299
+ {
300
+ "caption": "prompts/resources.ts",
301
+ "lang": "ts",
302
+ "source": "import { defineResource, table } from 'promptex-js'\nimport { PKG_MANAGER, SUPPORTED_NODE } from '../src/index.ts'\n\n// 原生模板字串直接可用:宿主語言的一般語法,框架不介入也不需要特殊 API。\n// 插值於求值當下固化為字串常數。\nconst minNodeMajor = Number(SUPPORTED_NODE)\n\nexport const envSetup = defineResource({\n id: 'env-setup',\n name: '環境需求',\n content: [\n // 單行插值:讀專案源碼常數,提示詞與程式碼共用單一真相\n `本專案需 Node.js ${SUPPORTED_NODE} 以上、套件管理器使用 ${PKG_MANAGER}。`,\n\n // 多行模板:跨行文字原樣保留,適合段落級散文\n `安裝步驟:\n\n1. 確認 node 版本不低於 ${minNodeMajor}\n2. 執行 ${PKG_MANAGER} install\n3. 執行 ${PKG_MANAGER} run dev`,\n\n // 插值可為任意運算式(三元、函式呼叫、陣列 join 等)\n table(\n ['項目', '要求'],\n [\n ['Node.js', `>= ${minNodeMajor}`],\n ['套件管理器', PKG_MANAGER],\n ['Lockfile', `${PKG_MANAGER}-lock.yaml`],\n ],\n ),\n ],\n})"
303
+ },
304
+ {
305
+ "caption": "prompts/skill.ts",
306
+ "lang": "ts",
307
+ "source": "import { defineSkill, useResource } from 'promptex-js'\nimport { PKG_MANAGER } from '../src/index.ts'\n\nexport const onboarding = defineSkill({\n id: 'onboarding',\n name: '新人上手',\n config: { whenToUse: '當使用者詢問專案環境設定、如何開始開發時使用' },\n content: [\n // 純資料插值:原生模板,求值即固化\n `協助新人設定開發環境;本專案一律使用 ${PKG_MANAGER},不混用其他套件管理器。`,\n\n // 引用不進模板:Ref 是符號、要保留到投影才解析為連結,故以陣列元素形態\n // 承載。誤寫成 `${useResource('env-setup')}` 會在求值當下報錯,不會\n // 靜默產出 [object Object]。\n '環境需求詳見:',\n useResource('env-setup'),\n ],\n})"
308
+ },
309
+ {
310
+ "caption": "引用放進模板字串",
311
+ "lang": "console",
312
+ "source": "$ promptex build\n✗ Load failed: <project>/prompts/skill.ts\n Ref \"env-setup\" (resource) cannot go into a template literal or be added to a string:the symbol is fixed as text at conversion time and can no longer be resolved into an artifact link.\n A reference is carried as an array element: content: [\"before\", useResource(\"id\"), \"after\"]\nBuild failed: source loading failed. 0 artifacts written (all or nothing)."
313
+ }
314
+ ],
315
+ "outcome": [
316
+ "`.claude/skills/onboarding/env-setup.md` 不留任何插值語法痕跡,`${PKG_MANAGER}` 已是固化的 `pnpm`;改 `src/index.ts` 的常數重建即同步。",
317
+ "把 `useResource(...)` 放進模板字串,build 在載入 `prompts/skill.ts` 時失敗,結束碼 1,訊息指出引用不能進模板字串,並示範以陣列元素承載。不會靜默產出 `[object Object]`,也不寫任何產物。"
318
+ ],
319
+ "points": [
320
+ "純資料(版本號、路徑、計算結果)用原生模板字串。",
321
+ "單行插值、多行模板、運算式插值都是宿主語言原生行為,框架不提供也不需要模板機制。"
322
+ ],
323
+ "facts": [
324
+ "promptex.sdk.define-resource",
325
+ "promptex.sdk.use-refs",
326
+ "promptex.cli.errors-run"
327
+ ]
328
+ }
329
+ ]
330
+ },
331
+ {
332
+ "id": "splitting-linking",
333
+ "name": "拆分與引用",
334
+ "situation": "我要拆成多份並互相引用",
335
+ "claim": "引用是符號不是內容展開,路徑與份數是投影期的決定;要不要有自己的落點,寫在引用它的那個節點身上。",
336
+ "scenarios": [
337
+ {
338
+ "title": "資源是一宣告一檔,meta.path 決定子目錄",
339
+ "situation": "想把一段內容獨立成檔,並決定它落在引用者目錄下的哪一層。",
340
+ "code": [
341
+ {
342
+ "caption": "prompts/resources.ts",
343
+ "lang": "ts",
344
+ "source": "import { check, defineResource, table } from 'promptex-js'\n\n// 資源是 file-level 單位:每個 defineResource 產出一個獨立檔案。\n// meta.path 宣告產物子目錄;落點根由引用方節點的 meta.resources 決定(預設 bundled,\n// 即落引用方自己的隨附目錄;兩態的差異見同群的 bundled-vs-shared 情境)。\nexport const commitStyle = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n description: 'Conventional Commits 的 type 對照與 subject 規則',\n meta: { path: 'git' },\n content: [\n table(\n ['type', '用途'],\n [\n ['feat', '新功能'],\n ['fix', '缺陷修正'],\n ],\n ),\n 'subject 用繁體中文、50 字內,不以句號結尾。',\n ],\n})\n\n// 省略 meta.path 即落落點根本身,不進子目錄。\nexport const prChecklist = defineResource({\n id: 'pr-checklist',\n name: 'PR 自查清單',\n content: check(['變更動機已寫明', '影響範圍已列出', '測試方式已說明']),\n})"
345
+ },
346
+ {
347
+ "caption": "prompts/skill.ts",
348
+ "lang": "ts",
349
+ "source": "import { defineSkill, useResource } from 'promptex-js'\n\nexport const prFlow = defineSkill({\n id: 'pr-flow',\n name: '提交流程',\n config: { whenToUse: '當使用者要求提 PR、整理提交時使用' },\n content: [\n 'commit 訊息慣例見:',\n useResource('commit-style'),\n '送出前逐項自查:',\n useResource('pr-checklist'),\n ],\n})"
350
+ }
351
+ ],
352
+ "outcome": [
353
+ "`.claude/skills/pr-flow/git/commit-style.md`(有 meta.path,落子目錄)與 `.claude/skills/pr-flow/pr-checklist.md`(無 meta.path,落根本身)。",
354
+ "SKILL.md 內兩行連結分別是 `[Commit 訊息慣例](git/commit-style.md)` 與 `[PR 自查清單](pr-checklist.md)`,依落點算出的相對路徑。",
355
+ "資源的 description 不寫進任何產物檔,只出現在 `.promptex/define-resource/graph.json` 的 commit-style 節點。"
356
+ ],
357
+ "points": [
358
+ "id 是產物檔名 `{id}.md` 也是引用鍵;name 是產物一級標題與連結文字。",
359
+ "落點根由引用方的 meta.resources 決定(預設 bundled,即引用方自己的隨附目錄;claude 的 rule 預設 shared,因為 .claude/rules/ 下的 .md 會被遞迴載入),資源自身只決定子目錄。"
360
+ ],
361
+ "facts": [
362
+ "promptex.sdk.define-resource",
363
+ "promptex.sdk.define-skill",
364
+ "promptex.sdk.use-refs",
365
+ "promptex.formats.graph"
366
+ ]
367
+ },
368
+ {
369
+ "title": "asset 是原檔拷貝並保留權限",
370
+ "situation": "要分發腳本或二進位檔,內容不該被框架解析改寫,也不該進模型 context。",
371
+ "code": [
372
+ {
373
+ "caption": "prompts/assets.ts",
374
+ "lang": "ts",
375
+ "source": "import { defineAsset } from 'promptex-js'\n\n// asset 的本體是來源檔(file 欄位),不是文字內容:\n// 產物為原檔拷貝、沿用來源檔名,內容不進 context(模型讀不到二進位)。\nexport const brandLogo = defineAsset({\n id: 'brand-logo',\n name: '品牌識別圖',\n description: '產出視覺稿時作為對照的品牌主圖',\n file: 'assets/logo.png',\n meta: { path: 'brand' },\n})\n\n// 腳本同屬 asset:分發時保留執行權限,可被 skill 內文或 hook 的 command 引用。\nexport const lintScript = defineAsset({\n id: 'doc-lint-script',\n name: '文件檢查腳本',\n file: 'scripts/lint.sh',\n})"
376
+ },
377
+ {
378
+ "caption": "prompts/skill.ts",
379
+ "lang": "ts",
380
+ "source": "import { defineSkill, useAsset } from 'promptex-js'\n\nexport const brandReview = defineSkill({\n id: 'brand-review',\n name: '品牌稿審查',\n config: { whenToUse: '當使用者要求審查視覺稿是否符合品牌規範時使用' },\n content: [\n '對照品牌主圖檢查色彩與比例:',\n useAsset('brand-logo'), // 二進位:產物只留連結,內容不進 context\n '文字部分跑檢查腳本:',\n useAsset('doc-lint-script'),\n ],\n})"
381
+ }
382
+ ],
383
+ "outcome": [
384
+ "`.claude/skills/brand-review/brand/logo.png`(有 meta.path)與 `.claude/skills/brand-review/lint.sh`(無 meta.path),檔名沿用來源檔。",
385
+ "產物與來源檔逐位元組相同;來源檔留在原地,是輸入不是產物。",
386
+ "SKILL.md 內只留兩行連結 `[品牌識別圖](brand/logo.png)` 與 `[文件檢查腳本](lint.sh)`,檔案內容不進 SKILL.md。"
387
+ ],
388
+ "points": [
389
+ "asset 的本體是 file(來源檔路徑)或 content 加 fileName(求值期給定的內容),二擇一,型別層擋下兩者皆缺或並存。",
390
+ "素材與資源共用同一套落點規則(bundled 或 shared 乘上 meta.path),差別只在本體形態與檔名來源。",
391
+ "拷貝時保留來源檔的權限位元,可執行的腳本分發後仍可執行。"
392
+ ],
393
+ "facts": [
394
+ "promptex.sdk.define-asset",
395
+ "promptex.sdk.define-skill",
396
+ "promptex.sdk.use-refs"
397
+ ]
398
+ },
399
+ {
400
+ "title": "dir 整棵目錄樹原樣分發",
401
+ "situation": "模板組、樣板專案這類一批不可分割的檔案要整包分發,不想逐檔宣告。",
402
+ "code": [
403
+ {
404
+ "caption": "prompts/dirs.ts",
405
+ "lang": "ts",
406
+ "source": "import { defineDir } from 'promptex-js'\n\n// dir 的本體是來源目錄(dir 欄位):產物為整棵目錄樹的拷貝,\n// 內部結構原樣保留,適合模板組、樣板專案、範例集這類「一批檔案是一個單位」的資源。\nexport const docTemplates = defineDir({\n id: 'doc-templates',\n name: '文件模板組',\n description: 'ADR 與 PRD 的空白模板,撰寫時複製後填寫',\n dir: 'templates',\n})"
407
+ },
408
+ {
409
+ "caption": "prompts/skill.ts",
410
+ "lang": "ts",
411
+ "source": "import { defineSkill, useDir } from 'promptex-js'\n\nexport const docAuthoring = defineSkill({\n id: 'doc-authoring',\n name: '設計文件撰寫',\n config: { whenToUse: '當使用者要求撰寫 ADR、PRD 等設計文件時使用' },\n content: [\n '依文件類型取對應模板,複製後填寫,不改動模板本體:',\n useDir('doc-templates'), // 連結指向目錄,樹內結構原樣保留\n ],\n})"
412
+ }
413
+ ],
414
+ "outcome": [
415
+ "`.claude/skills/doc-authoring/templates/adr/decision.md`、`.claude/skills/doc-authoring/templates/prd/product.md` 等,樹內層級原樣保留、不攤平也不重排。",
416
+ "產物登記檔逐檔列出(登記逐檔一筆),SKILL.md 內一行連結指向目錄 `templates`。"
417
+ ],
418
+ "points": [
419
+ "逐檔宣告 asset 會讓「新增一個模板檔」變成兩處改動;dir 自動涵蓋樹內全部檔案。",
420
+ "攤平會讓不同子目錄的同名檔互相覆蓋,重排會讓來源目錄內的相對引用失效。"
421
+ ],
422
+ "facts": [
423
+ "promptex.sdk.define-dir",
424
+ "promptex.sdk.define-skill",
425
+ "promptex.sdk.use-refs",
426
+ "promptex.formats.ownership-registry"
427
+ ]
428
+ },
429
+ {
430
+ "title": "引用一律渲染為連結,路徑由投影算",
431
+ "situation": "不想在源碼寫任何相對路徑,也不想因為搬檔而改連結。",
432
+ "code": [
433
+ {
434
+ "caption": "prompts/authoring.ts",
435
+ "lang": "ts",
436
+ "source": "import { defineSkill, useResource, useRule } from 'promptex-js'\n\n// 一個 skill 引用兩個目標:kind(rule)與資源(resource,預設 bundled)。\nexport const docAuthoring = defineSkill({\n id: 'doc-authoring',\n name: '文件撰寫',\n config: { whenToUse: '撰寫任何技術文件時使用' },\n content: [\n '陳述技術事實前先讀:',\n useRule('citation-required'),\n '。行文風格見:',\n useResource('style-guide'),\n ],\n})"
437
+ },
438
+ {
439
+ "caption": "prompts/rules.ts",
440
+ "lang": "ts",
441
+ "source": "import { defineRule } from 'promptex-js'\n\nexport const citationRequired = defineRule({\n id: 'citation-required',\n name: '引用格式規範',\n config: { alwaysApply: true },\n content: '陳述具體技術事實時必須附來源。',\n})"
442
+ },
443
+ {
444
+ "caption": "prompts/resources.ts",
445
+ "lang": "ts",
446
+ "source": "import { defineResource } from 'promptex-js'\n\nexport const styleGuide = defineResource({\n id: 'style-guide',\n name: '撰寫風格指南',\n content: '標題用祈使語氣,段落先給結論再補細節。',\n})"
447
+ }
448
+ ],
449
+ "outcome": [
450
+ "`.claude/skills/doc-authoring/SKILL.md` 內 `[引用格式規範](../../rules/citation-required.md)`(能力類型指向其主檔)與 `[撰寫風格指南](style-guide.md)`(素材隨附在引用者目錄)。",
451
+ "依賴閉包成立:skill 被安裝時,它引用的 rule 檔 `.claude/rules/citation-required.md` 與資源檔 `.claude/skills/doc-authoring/style-guide.md` 必隨之產出。"
452
+ ],
453
+ "points": [
454
+ "連結顯示文字取自目標的 name,name 因此必填。",
455
+ "同一份 content 內的引用各自算出正確深度的相對路徑,作者側完全不寫路徑。"
456
+ ],
457
+ "facts": [
458
+ "promptex.sdk.use-refs",
459
+ "promptex.sdk.define-rule",
460
+ "promptex.sdk.define-skill",
461
+ "promptex.sdk.define-resource"
462
+ ]
463
+ },
464
+ {
465
+ "title": "完整引用矩陣",
466
+ "situation": "想知道 rule 能不能引用 agent、hook 能不能引用 asset、instruction 被引用時連到哪。",
467
+ "code": [
468
+ {
469
+ "caption": "prompts/referrers.ts",
470
+ "lang": "ts",
471
+ "source": "import {\n defineAgent,\n defineHook,\n defineInstruction,\n defineResource,\n defineRule,\n defineSkill,\n useAgent,\n useAsset,\n useDir,\n useHook,\n useInstruction,\n useMcp,\n usePermission,\n useResource,\n useRule,\n useSkill,\n} from 'promptex-js'\n\n// 六種引用方之五(skill/rule/agent/instruction/resource)在 content\n// (markdown 語境)引用全部十種目標;建一次共用陣列,五個引用方各自複用\n// 同一組符號引用(Ref 只是 { id, kind } 資料,插入多份 content 互不影響)。\nconst targetRefs = () => [\n useSkill('target-skill'),\n useRule('target-rule'),\n useAgent('target-agent'),\n useHook('target-hook'),\n useMcp('target-mcp'),\n useInstruction('target-instruction'),\n usePermission('target-permission'),\n useResource('target-resource'),\n useAsset('target-asset'),\n useDir('target-dir'),\n]\n\ndefineSkill({\n id: 'referrer-skill',\n name: '引用方技能',\n config: { whenToUse: '示範引用矩陣,不實際使用' },\n content: ['引用全部十種目標:', ...targetRefs(), '另引用一份會再引用同十種目標的資源:', useResource('referrer-resource')],\n})\n\ndefineRule({\n id: 'referrer-rule',\n name: '引用方規則',\n content: ['引用全部十種目標:', ...targetRefs()],\n})\n\ndefineAgent({\n id: 'referrer-agent',\n name: '引用方代理',\n config: { whenToUse: '示範引用矩陣,不實際使用' },\n content: ['引用全部十種目標:', ...targetRefs()],\n})\n\ndefineInstruction({\n id: 'referrer-instruction',\n name: '引用方指令',\n content: ['引用全部十種目標:', ...targetRefs()],\n})\n\n// 第六種引用方:resource 也能在 content 引用其他目標,與能力類型的引用同型。\n// 本身要被 referrer-skill 引用才會進入某個能力節點的依賴閉包而被安裝\n// (素材未被引用即不產出;素材再引用素材時的 bundle 歸屬見同群的\n// cyclic-references 情境)。\ndefineResource({\n id: 'referrer-resource',\n name: '引用方資源',\n content: ['資源本身也能引用全部十種目標:', ...targetRefs()],\n})\n\n// hook 的引用發生在 shell 語境(actions[].command),只引用一個目標\n// (asset),且解析為產物的絕對路徑而非 markdown 連結。\ndefineHook({\n id: 'referrer-hook',\n name: '引用方掛鉤',\n config: { event: 'tool:after', matcher: 'Write|Edit' },\n actions: [{ type: 'command', command: ['zsh ', useAsset('target-asset')] }],\n})"
472
+ },
473
+ {
474
+ "caption": "prompts/targets.ts",
475
+ "lang": "ts",
476
+ "source": "import { defineAgent, defineAsset, defineDir, defineHook, defineInstruction, defineMcp, definePermission, defineResource, defineRule, defineSkill } from 'promptex-js'\n\n// 十種被引用目標,各一個節點:七個能力類型 + resource/asset/dir 三種素材。\ndefineSkill({\n id: 'target-skill',\n name: '目標技能',\n config: { whenToUse: '被引用示範,不實際使用' },\n content: '被引用的技能本體。',\n})\n\ndefineRule({\n id: 'target-rule',\n name: '目標規則',\n content: '被引用的規則本體。',\n})\n\ndefineAgent({\n id: 'target-agent',\n name: '目標代理',\n config: { whenToUse: '被引用示範,不實際使用' },\n content: '被引用的代理本體。',\n})\n\ndefineHook({\n id: 'target-hook',\n name: '目標掛鉤',\n config: { event: 'stop' },\n actions: [{ type: 'command', command: 'echo done' }],\n})\n\ndefineMcp({\n id: 'target-mcp',\n name: '目標服務',\n config: { transport: { type: 'stdio', command: 'npx', args: ['-y', 'target-tool'] } },\n})\n\ndefineInstruction({\n id: 'target-instruction',\n name: '目標指令',\n content: '被引用的指令本體。',\n})\n\ndefinePermission({\n id: 'target-permission',\n name: '目標權限',\n config: { tool: 'bash', match: ['ls'], decision: 'allow' },\n})\n\ndefineResource({\n id: 'target-resource',\n name: '目標資源',\n content: '被引用的資源本體。',\n})\n\ndefineAsset({\n id: 'target-asset',\n name: '目標素材',\n content: '被引用的素材本體',\n fileName: 'target-asset.txt',\n})\n\ndefineDir({\n id: 'target-dir',\n name: '目標目錄',\n dir: 'templates',\n})"
477
+ }
478
+ ],
479
+ "outcome": [
480
+ "有獨立檔的 kind(skill、rule、agent)連到主檔,如 `.claude/rules/referrer-rule.md` 內的 `[目標規則](target-rule.md)` 與 `[目標技能](../skills/target-skill/SKILL.md)`。",
481
+ "無獨立檔的 kind(hook、mcp、permission、instruction)連到放內容的檔案:`.claude/rules/referrer-rule.md` 內 `[目標掛鉤](../settings.json)`、`[目標服務](../../.mcp.json)`,instruction 另帶標題錨點 `[目標指令](../../CLAUDE.md#目標指令)`。",
482
+ "hook 的 command 渲染為 `zsh $CLAUDE_PROJECT_DIR/.claude/hooks/referrer-hook/target-asset.txt`,見 `.claude/settings.json`。",
483
+ "同一組十個目標,依引用方不同落在不同位置:skill 引用時資源、素材與目錄隨附在 `.claude/skills/referrer-skill/`,agent 引用時在 `.claude/agents/referrer-agent/`,instruction 引用時在 `.claude/instructions/referrer-instruction/`,rule 引用時(claude 的 rule 預設 shared)在 `.claude/resources/`。"
484
+ ],
485
+ "points": [
486
+ "具內容的節點(skill、rule、agent、instruction、resource)都能引用全部十種目標;hook 只在 actions[].command 引用素材。",
487
+ "mcp、permission、asset、dir 無 content 欄位,不作為引用方。",
488
+ "素材引用素材時仍會併進同一根節點的 bundle;未被引用的素材不產出。"
489
+ ],
490
+ "facts": [
491
+ "promptex.sdk.use-refs",
492
+ "promptex.sdk.kinds",
493
+ "promptex.sdk.define-skill",
494
+ "promptex.sdk.define-rule",
495
+ "promptex.sdk.define-instruction",
496
+ "promptex.sdk.define-hook",
497
+ "promptex.sdk.define-asset",
498
+ "promptex.sdk.define-dir"
499
+ ]
500
+ },
501
+ {
502
+ "title": "跨檔前向引用與載入順序無關",
503
+ "situation": "擔心引用寫在定義之前、或聚合檔載入順序影響結果。",
504
+ "code": [
505
+ {
506
+ "caption": "prompts/flow.ts",
507
+ "lang": "ts",
508
+ "source": "import { defineSkill, useResource, useSkill } from 'promptex-js'\n\n// 檔名字母序 flow < helper < style:這裡引用的兩個目標都定義在後載入的檔案,\n// 驗證跨檔前向引用合法、且是兩層鏈式的前向引用(flow → helper → style)。\nexport const prFlow = defineSkill({\n id: 'pr-flow',\n name: '提交流程',\n config: { whenToUse: '要提 PR 時使用' },\n content: [\n '先跑一次 commit 檢查:',\n useSkill('commit-helper'),\n ',訊息慣例見:',\n useResource('commit-style'),\n ],\n})"
509
+ },
510
+ {
511
+ "caption": "prompts/helper.ts",
512
+ "lang": "ts",
513
+ "source": "import { defineSkill, useResource } from 'promptex-js'\n\n// 載入順序在 flow.ts 之後、style.ts 之前,本身也向後引用尚未載入的 style.ts。\nexport const commitHelper = defineSkill({\n id: 'commit-helper',\n name: 'Commit 助手',\n config: { whenToUse: '整理提交訊息時使用' },\n content: ['依慣例撰寫:', useResource('commit-style')],\n})"
514
+ },
515
+ {
516
+ "caption": "prompts/style.ts",
517
+ "lang": "ts",
518
+ "source": "import { defineResource } from 'promptex-js'\n\n// 載入順序最後:flow.ts 與 helper.ts 都在收集遍先登記了對本資源的符號引用,\n// 到解析遍才驗證它確實存在。define* 登記與載入順序無關,本檔排最後不影響\n// 產物內容或結構。\nexport const commitStyle = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n content: 'type(scope): subject;subject 用繁體中文,50 字內。',\n})"
519
+ },
520
+ {
521
+ "caption": "把資源的 id 改掉再建置,引用就找不到目標",
522
+ "lang": "console",
523
+ "source": "$ promptex build\n✗ E101 Broken reference:useResource(\"commit-style\") has no definition\n at <project>/prompts/flow.ts:13\n Close candidate:commit-styles(defined at <project>/prompts/style.ts:6)\n✗ E101 Broken reference:useResource(\"commit-style\") has no definition\n at <project>/prompts/helper.ts:8\n Close candidate:commit-styles(defined at <project>/prompts/style.ts:6)\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
524
+ }
525
+ ],
526
+ "outcome": [
527
+ "第一次建置結束碼 0,沒有錯誤或警告,安裝報告只有「skill 未宣告 description」的提醒;`useSkill('commit-helper')` 渲染為 `.claude/skills/pr-flow/SKILL.md` 內的 `[Commit 助手](../commit-helper/SKILL.md)`。",
528
+ "`commit-style` 依預設 bundled,分別隨附在 `.claude/skills/pr-flow/commit-style.md` 與 `.claude/skills/commit-helper/commit-style.md`。",
529
+ "把 `prompts/style.ts` 的資源 id 改成 `commit-styles` 再建置,結束碼 1,兩處引用各報一筆 E101 並指出相近的 id,不寫出任何產物。"
530
+ ],
531
+ "points": [
532
+ "收集遍只登記宣告與符號引用,解析遍才驗存在性,因此先 use 後 define 跨檔成立。",
533
+ "同一份源碼換檔名或載入順序,產出仍逐字相同;中介表示的節點依 id 排序而非宣告順序。"
534
+ ],
535
+ "facts": [
536
+ "promptex.sdk.use-refs",
537
+ "promptex.sdk.define-skill",
538
+ "promptex.sdk.define-resource",
539
+ "promptex.diagnostics.e101"
540
+ ]
541
+ },
542
+ {
543
+ "title": "互引與自引都合法,最後都併成一份",
544
+ "situation": "兩個 skill 互相連結、兩份資源互相引用、資源指向自己,會不會爆。",
545
+ "code": [
546
+ {
547
+ "caption": "prompts/nodes.ts",
548
+ "lang": "ts",
549
+ "source": "import { defineResource, defineSkill, useResource, useSkill } from 'promptex-js'\n\n// 場景一:能力互引:skill-x 與 skill-y 互相連結,不構成循環(link 引用不\n// 展開內容)。\ndefineSkill({\n id: 'skill-x',\n name: '技能 X',\n config: { whenToUse: '示範互引,不實際使用' },\n content: ['另一半見:', useSkill('skill-y')],\n})\n\ndefineSkill({\n id: 'skill-y',\n name: '技能 Y',\n config: { whenToUse: '示範互引,不實際使用' },\n content: ['另一半見:', useSkill('skill-x')],\n})\n\n// 場景二:素材互引:res-a 與 res-b 互相連結。\ndefineResource({\n id: 'res-a',\n name: '資源 A',\n content: ['參照:', useResource('res-b')],\n})\n\ndefineResource({\n id: 'res-b',\n name: '資源 B',\n content: ['參照:', useResource('res-a')],\n})\n\n// 場景三:自我引用:連結指向自己。\ndefineResource({\n id: 'res-self',\n name: '自引資源',\n content: ['指向自己:', useResource('res-self')],\n})\n\n// entry 引用三個場景的根,讓 res-a/res-b/res-self 進入 entry 的依賴閉包\n// 而被安裝;三份資源因此都應歸屬 entry 的 bundle,而非各自的中間節點。\ndefineSkill({\n id: 'entry',\n name: '入口技能',\n config: { whenToUse: '彙整三個循環場景,不實際使用' },\n content: [\n '資源互引:',\n useResource('res-a'),\n '、',\n useResource('res-b'),\n '。自我引用:',\n useResource('res-self'),\n '。',\n ],\n})"
550
+ }
551
+ ],
552
+ "outcome": [
553
+ "建置成功,結束碼 0。`.claude/skills/skill-x/SKILL.md` 連到 `[技能 Y](../skill-y/SKILL.md)`,`.claude/skills/skill-y/SKILL.md` 連回 `[技能 X](../skill-x/SKILL.md)`。",
554
+ "`.claude/skills/entry/` 下同時有 res-a.md、res-b.md、res-self.md 三個檔:資源引用資源時繼承根能力節點的 bundle,互引的兩份因此同目錄、連結是最短相對路徑(res-a.md 內是 `[資源 B](res-b.md)`,res-b.md 內是 `[資源 A](res-a.md)`,res-self.md 內是連到自己的 `[自引資源](res-self.md)`)。"
555
+ ],
556
+ "points": [
557
+ "兩份文件互相連結,在文件世界完全正常。",
558
+ "依賴閉包以「根 owner 加目標」為鍵,重複的鍵不產生新項,自我引用一次就併成同一項。"
559
+ ],
560
+ "facts": [
561
+ "promptex.sdk.use-refs",
562
+ "promptex.sdk.define-skill",
563
+ "promptex.sdk.define-resource"
564
+ ]
565
+ },
566
+ {
567
+ "title": "原生值組合與 use* 引用兩條路",
568
+ "situation": "同一段內容要進多個節點,猶豫該展開進內文還是獨立成檔加連結。",
569
+ "code": [
570
+ {
571
+ "caption": "src/commit-style.ts",
572
+ "lang": "ts",
573
+ "source": "import { table } from 'promptex-js'\n\n// 路徑一的素材:宿主語言的一般常數與函式,住在專案自身源碼下、不進 prompts/。\n// 它們沒有 ID、不進註冊表,任何節點的 content 都能直接消費。框架不提供 inline\n// 函式,內容組合就用宿主語言原生機制。\nexport const COMMIT_INTRO = 'commit 訊息依 Conventional Commits:type(scope): subject。'\n\nexport function commitTable() {\n return table(\n ['type', '用途'],\n [\n ['feat', '新功能'],\n ['fix', '缺陷修正'],\n ],\n )\n}"
574
+ },
575
+ {
576
+ "caption": "prompts/skills.ts",
577
+ "lang": "ts",
578
+ "source": "import { defineSkill, useResource } from 'promptex-js'\nimport { COMMIT_INTRO, commitTable } from '../src/commit-style.ts'\n\n// 路徑一:原生值組合。內容直接展開進 SKILL.md,不產生獨立檔案、產物無連結。\n// 適用內容只有本節點使用、或本來就是計算產物的情境。\nexport const composeSkill = defineSkill({\n id: 'compose-flow',\n name: '原生組合示範',\n config: { whenToUse: '示範以宿主語言值組合內容' },\n content: ['提交前確認:', COMMIT_INTRO, commitTable()],\n})\n\n// 路徑二:use* 引用。內容落獨立檔案,產物只留一行連結。\n// 適用內容被多節點共用、或需要獨立閱讀與維護的情境。\nexport const linkSkill = defineSkill({\n id: 'link-flow',\n name: '引用示範',\n config: { whenToUse: '示範以 use* 引用資源' },\n content: ['提交前確認:', useResource('commit-style')],\n})"
579
+ },
580
+ {
581
+ "caption": "prompts/resources.ts",
582
+ "lang": "ts",
583
+ "source": "import { defineResource } from 'promptex-js'\nimport { COMMIT_INTRO, commitTable } from '../src/commit-style.ts'\n\n// 路徑二:定義為資源,被 use* 引用時產出獨立檔案 + 連結。\n// 資源內容本身仍以原生值組成,兩條路徑可疊用。\nexport const commitStyleResource = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n content: [COMMIT_INTRO, commitTable()],\n})"
584
+ }
585
+ ],
586
+ "outcome": [
587
+ "`.claude/skills/compose-flow/SKILL.md` 內含表格全文,目錄下無其他檔;`.claude/skills/link-flow/SKILL.md` 只留一行連結 `[Commit 訊息慣例](commit-style.md)`,內容住同目錄的 `.claude/skills/link-flow/commit-style.md`。"
588
+ ],
589
+ "points": [
590
+ "原生值組合:無 id、各節點各展開一份、適合只有本節點使用或本來就是計算產物。",
591
+ "use* 引用:有 id、可跨檔引用、一份內容多處連結、適合需獨立閱讀維護或被多節點共用。",
592
+ "兩條路可疊用;循環風險只存在於宿主語言的 import 圖,由建置期偵測。"
593
+ ],
594
+ "facts": [
595
+ "promptex.sdk.use-refs",
596
+ "promptex.sdk.define-skill",
597
+ "promptex.sdk.define-resource"
598
+ ]
599
+ },
600
+ {
601
+ "title": "素材落點兩態:bundled 隨附、shared 共用",
602
+ "situation": "同一份素材被多個節點引用時,想決定各帶一份還是共指一份。",
603
+ "code": [
604
+ {
605
+ "caption": "prompts/skills.ts",
606
+ "lang": "ts",
607
+ "source": "import { defineSkill, useAsset, useResource } from 'promptex-js'\n\n// bundled(顯式):素材落本節點目錄,節點自帶完整內容、可獨立安裝。\nexport const authoringSkill = defineSkill({\n id: 'spec-authoring',\n name: '規格撰寫',\n meta: { resources: 'bundled' },\n config: { whenToUse: '當使用者要求撰寫領域規格時使用' },\n content: [\n '用詞依術語表:',\n useResource('domain-glossary'),\n '命名依對照卡:',\n useAsset('naming-cheatsheet'),\n ],\n})\n\n// shared:素材落共用 .claude/resources/,多節點共指同一份檔案。\nexport const reviewSkill = defineSkill({\n id: 'spec-review',\n name: '規格審查',\n meta: { resources: 'shared' },\n config: { whenToUse: '當使用者要求審查領域規格時使用' },\n content: [\n '核對用詞是否符合術語表:',\n useResource('domain-glossary'),\n '核對命名是否符合對照卡:',\n useAsset('naming-cheatsheet'),\n ],\n})\n\n// 未宣告 meta.resources 即取預設 bundled,與 spec-authoring 同樣自帶一份。\nexport const migrateSkill = defineSkill({\n id: 'spec-migrate',\n name: '規格遷移',\n config: { whenToUse: '當使用者要求遷移舊版規格時使用' },\n content: ['對照新舊用詞:', useResource('domain-glossary')],\n})"
608
+ },
609
+ {
610
+ "caption": "prompts/resources.ts",
611
+ "lang": "ts",
612
+ "source": "import { defineAsset, defineResource } from 'promptex-js'\n\n// 同一份資源,被三個落點宣告不同的 skill 引用。\nexport const glossary = defineResource({\n id: 'domain-glossary',\n name: '領域術語表',\n description: '訂單網域的共用術語定義',\n content: '聚合(Aggregate):一致性邊界;事件(Event):已發生事實的紀錄。',\n})\n\n// 第二種本體形態:asset 的本體是來源檔、產物是原檔拷貝,資源的本體是內容、\n// 產物是渲染出的 markdown。落點兩態的分派與本體形態無關,兩者走同一條規則,\n// 因此同一組 bundled/shared 宣告對它們得出同構的結果。\nexport const namingCheatsheet = defineAsset({\n id: 'naming-cheatsheet',\n name: '命名對照卡',\n description: '撰寫與審查共用的命名速查',\n file: 'assets/naming-cheatsheet.txt',\n})"
613
+ },
614
+ {
615
+ "caption": "meta.resources 寫成 bundled、shared 以外的值",
616
+ "lang": "console",
617
+ "source": "$ promptex build\n✗ E207 The asset placement declaration meta.resources received \"Shared\", which is not a usable placement mode: only bundled, shared are available\n at meta.resources of skill \"spec-review\"\n Omitting this field means bundled (the asset ships inside the directory of the node that references it), except a claude rule, which defaults to shared; write shared only when several nodes must point at one and the same file\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
618
+ }
619
+ ],
620
+ "outcome": [
621
+ "`.claude/skills/spec-authoring/domain-glossary.md` 與 `.claude/skills/spec-authoring/naming-cheatsheet.txt` 各一份;`.claude/resources/domain-glossary.md` 與 `.claude/resources/naming-cheatsheet.txt` 只有一份,而 `.claude/skills/spec-review/` 底下只有 SKILL.md,無同名檔。",
622
+ "連結依落點算:`.claude/skills/spec-authoring/SKILL.md` 是同目錄相對路徑 `[領域術語表](domain-glossary.md)`,`.claude/skills/spec-review/SKILL.md` 是 `[領域術語表](../../resources/domain-glossary.md)`。",
623
+ "沒有宣告 meta.resources 的 `spec-migrate` 與顯式 bundled 的 `spec-authoring` 一樣自帶一份 `.claude/skills/spec-migrate/domain-glossary.md`。",
624
+ "把 `spec-review` 的 meta.resources 改成 `Shared` 再建置,結束碼 1,報 E207 並指出只接受 bundled 與 shared,不寫出任何產物。"
625
+ ],
626
+ "points": [
627
+ "bundled 產物重複但安裝閉包乾淨;shared 無重複但節點安裝需連帶共用檔。"
628
+ ],
629
+ "facts": [
630
+ "promptex.sdk.define-skill",
631
+ "promptex.sdk.use-refs",
632
+ "promptex.sdk.define-resource",
633
+ "promptex.sdk.define-asset",
634
+ "promptex.diagnostics.e207"
635
+ ]
636
+ }
637
+ ]
638
+ },
639
+ {
640
+ "id": "targeting-platforms",
641
+ "name": "多平台",
642
+ "situation": "我要裝到不只一個平台",
643
+ "claim": "源碼寫意圖、適配寫落點;一切落差走報告而非靜默丟棄,退路涵蓋全部類型,但作用範圍不會外溢。",
644
+ "scenarios": [
645
+ {
646
+ "title": "中性宣告在兩個平台的產物",
647
+ "situation": "想寫一次就裝到 claude 與 codex,源碼不出現任何平台語彙。",
648
+ "code": [
649
+ {
650
+ "caption": "prompts/nodes.ts",
651
+ "lang": "ts",
652
+ "source": "import { defineRule, defineSkill, list, section } from 'promptex-js'\n\n// 節點只寫中性欄位,不出現任何平台語彙:model/effort 是框架定義的中性詞彙,\n// 各平台適配自行對映;appliesTo 是中性載入範圍,平台有範圍載入機制就用、沒有\n// 就降級(降級的完整形態見同群的 capability-degrade 情境)。\n//\n// 不含 allowedTools:claude 的 skill frontmatter 沒有工具白名單欄位(同名的\n// allowed-tools 是免核准清單,語意相反),兩平台的 skill 因此都只能降級。本\n// 情境要示範的是乾淨對映,帶降級的那一面住同群的 capability-degrade 情境。\nexport const specAuthoring = defineSkill({\n id: 'spec-authoring',\n name: '規格撰寫',\n config: {\n whenToUse: '當使用者要求撰寫或修改服務規格時使用',\n model: 'standard',\n effort: 'medium',\n },\n content: [section('步驟', list(['盤點既有規格', '草擬變更', '核對上下游引用']))],\n})\n\nexport const citationRule = defineRule({\n id: 'citation-required',\n name: '引用格式規範',\n config: { appliesTo: ['spec/**/*.mdx'] },\n content: '約束類條目必附出處:來源類別加識別符。',\n})"
653
+ },
654
+ {
655
+ "caption": "promptex.config.ts",
656
+ "lang": "ts",
657
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'neutral-config',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n})"
658
+ }
659
+ ],
660
+ "outcome": [
661
+ "claude 時,`.claude/skills/spec-authoring/SKILL.md` 的 frontmatter 含 `when_to_use`(取自 whenToUse)、`model: sonnet` 與 `effort: medium`,不含 description;`.claude/rules/citation-required.md` 含 `paths`。",
662
+ "codex 時,`.agents/skills/spec-authoring/SKILL.md` 只有 name 與 description(description 取自 whenToUse,model 與 effort 降級略過);rule 併入根層 `AGENTS.md`,內文標註「適用範圍:spec/**/*.mdx」。",
663
+ "安裝報告逐平台印出,claude 側一筆提醒(spec-authoring 沒寫 description),codex 側兩筆降級(perSkillModel、globScopedLoading)。"
664
+ ],
665
+ "points": [
666
+ "詞彙表(model、effort 的中性值)住核心、對映表住各平台適配;同一個 standard 在兩個平台指同一層級。",
667
+ "平台無對應能力時降級進安裝報告,不靜默丟棄;報告印到 stdout,不寫進安裝目錄。",
668
+ "`.promptex/{單元}/` 底下每個平台各一份所有權登記。"
669
+ ],
670
+ "facts": [
671
+ "promptex.sdk.platform-claude",
672
+ "promptex.sdk.platform-codex",
673
+ "promptex.sdk.model-tiers",
674
+ "promptex.sdk.effort-levels",
675
+ "promptex.sdk.define-skill",
676
+ "promptex.sdk.define-rule"
677
+ ]
678
+ },
679
+ {
680
+ "title": "config.platforms 是唯一可寫平台語彙的地方",
681
+ "situation": "要用某平台專有的 frontmatter 鍵,或覆寫框架算出的某個值。",
682
+ "code": [
683
+ {
684
+ "caption": "prompts/nodes.ts",
685
+ "lang": "ts",
686
+ "source": "import { defineSkill, list, section } from 'promptex-js'\n\n// 中性欄位打底、platforms 覆寫加值:config.platforms 是唯一可出現平台語彙\n// 的地方,且刪除任一平台的覆寫,其他平台的產物必須逐字不變。\nexport const deployFlow = defineSkill({\n id: 'deploy-flow',\n name: '部署流程',\n config: {\n whenToUse: '當使用者要求部署到正式環境時使用',\n model: 'standard',\n platforms: {\n // 平台原生參數直寫:框架不解析語意,原樣併入該平台產物。model 與中性\n // 欄位同名,覆寫勝出、中性對映不介入該欄位,產物只出現一行 model。\n claude: { model: 'opus', 'disable-model-invocation': true, 'argument-hint': '<環境名>' },\n // 兩個平台各自消費自己那一格:codex 的 skill frontmatter 同樣是鍵值面。\n codex: { profile: 'release' },\n },\n },\n content: [section('前置', list(['CI 全綠', '回退方案已備妥']))],\n})\n\n// 未設 platforms 的節點:兩平台皆取中性配置,作為對照組。\nexport const rollbackFlow = defineSkill({\n id: 'rollback-flow',\n name: '回退流程',\n config: { whenToUse: '當部署後發現問題需回退時使用', model: 'fast' },\n content: '確認回退目標版本後執行,回退完成即通知相關人員。',\n})\n\n// 平台名寫錯的節點:cluade 與 claude 只差一次字元對調,沒有任何平台會消費它。\n// 逐平台忽略他人的覆寫是對的(覆寫不外溢),因此沒有任何一層擋得下這個名字,\n// 少了手誤核對的話作者寫了等於沒寫,而產物與報告都沒有痕跡。同一個節點另宣告\n// 一個合法的第三方平台名,可見這道核對不誤傷它。\nexport const releaseNotes = defineSkill({\n id: 'release-notes',\n name: '發布說明',\n config: {\n whenToUse: '當要整理發布說明時使用',\n platforms: {\n cluade: { 'argument-hint': '<版本號>' },\n acme: { 'x-owner': 'release-team' },\n },\n },\n content: '依 commit 分類整理,破壞性變更置頂。',\n})"
687
+ },
688
+ {
689
+ "caption": "安裝報告",
690
+ "lang": "console",
691
+ "source": "$ promptex build --install\n✓ platform-override: metadata written (graph + content)\n── platform-override → claude (<project>) ──\n wrote .claude/skills/deploy-flow/SKILL.md\n wrote .claude/skills/release-notes/SKILL.md\n wrote .claude/skills/rollback-flow/SKILL.md\nInstall report (claude)\n Projected artifacts, 3: .claude/skills/deploy-flow/SKILL.md, .claude/skills/release-notes/SKILL.md, .claude/skills/rollback-flow/SKILL.md\n Unsupported, 1:\n • skill \"release-notes\": platformOverride:cluade has no matching mechanism and was not emitted\n The override is declared under the platform \"cluade\" while this platform is \"claude\", so no platform consumes it; the two are one typo apart, and if this platform was the intent, rename the key to \"claude\"\n Notices, 3:\n • skill \"deploy-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n • skill \"release-notes\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n • skill \"rollback-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n── platform-override → codex (<project>) ──\n wrote .agents/skills/deploy-flow/SKILL.md\n wrote .agents/skills/release-notes/SKILL.md\n wrote .agents/skills/rollback-flow/SKILL.md\nInstall report (codex)\n Projected artifacts, 3: .agents/skills/deploy-flow/SKILL.md, .agents/skills/release-notes/SKILL.md, .agents/skills/rollback-flow/SKILL.md\n Degraded, 2:\n • skill \"deploy-flow\": perSkillModel, enforcement processGate → wording\n The platform's skill has no model or thinking-effort field (those belong to the subagent layer), so that configuration is skipped on this platform\n • skill \"rollback-flow\": perSkillModel, enforcement processGate → wording\n The platform's skill has no model or thinking-effort field (those belong to the subagent layer), so that configuration is skipped on this platform"
692
+ }
693
+ ],
694
+ "outcome": [
695
+ "claude 的 `.claude/skills/deploy-flow/SKILL.md` 只有一行 `model: opus`(覆寫勝出,中性對映的 sonnet 不寫入),加上兩個 claude 原生鍵 `disable-model-invocation` 與 `argument-hint`;codex 的 `.agents/skills/deploy-flow/SKILL.md` 多出 `profile: release`,不含任何 claude 那格的鍵。沒設 platforms 的 rollback-flow 兩平台都取中性配置。",
696
+ "平台名手誤(`cluade`)不攔產物,建置照常成功、結束碼 0,也沒有診斷碼:release-notes 兩個平台的產物都不含該覆寫,claude 的安裝報告在不支援清單記 `platformOverride:cluade`,說明它與 claude 只差一次字元對調並指名正確拼法。同一個節點的合法第三方平台名 `acme` 不誤報,codex 的報告也沒有 release-notes。"
697
+ ],
698
+ "points": [
699
+ "覆寫的粒度是鍵:寫下的鍵蓋掉框架為同一個鍵算出的值,其餘鍵照常產出。",
700
+ "框架不解析覆寫內容的語意、不做值域檢查,原樣併入該平台產物;覆寫不外溢到其他平台。"
701
+ ],
702
+ "facts": [
703
+ "promptex.sdk.define-skill",
704
+ "promptex.sdk.platform-claude",
705
+ "promptex.sdk.platform-codex"
706
+ ]
707
+ },
708
+ {
709
+ "title": "退路的全面覆蓋:十種類型逐一表態",
710
+ "situation": "想確認每種 kind 的覆寫都有出口,沒有靜默丟棄的死角。",
711
+ "code": [
712
+ {
713
+ "caption": "prompts/nodes.ts",
714
+ "lang": "ts",
715
+ "source": "import {\n defineAgent,\n defineAsset,\n defineDir,\n defineHook,\n defineInstruction,\n defineMcp,\n definePermission,\n defineResource,\n defineRule,\n defineSkill,\n useAsset,\n useDir,\n useResource,\n} from 'promptex-js'\n\n// 逃生口的覆蓋是全面的:十種類型逐一表態。產物含鍵值面者由該平台的適配把\n// 覆寫寫進產物,不含者列入安裝報告的不支援清單並說明原因,沒有第三種狀態。\n\n// ── 產物含鍵值面:覆寫寫進產物 ──\n\nexport const releaseFlow = defineSkill({\n id: 'release-flow',\n name: '發布流程',\n config: {\n whenToUse: '當使用者要求發布版本時使用',\n platforms: {\n claude: { 'argument-hint': '<版本號>' },\n codex: { profile: 'release' },\n },\n },\n content: [\n '發布前逐項核對:',\n useResource('pr-checklist'),\n useAsset('release-badge'),\n useDir('release-templates'),\n ],\n})\n\nexport const verifyAgent = defineAgent({\n id: 'verify-agent',\n name: '產出核對代理',\n config: {\n whenToUse: '需要獨立核對發布產出時委派',\n model: 'standard',\n // 覆寫與中性欄位同名:兩平台各自以覆寫為準,中性對映不介入該欄位。\n platforms: {\n claude: { model: 'opus' },\n codex: { model: 'gpt-5.6-sol' },\n },\n },\n content: '逐項核對產出與發布清單是否相符,只回報落差、不修改檔案。',\n})\n\nexport const commitStyle = defineRule({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n config: {\n appliesTo: ['prompts/**'],\n platforms: {\n // claude 的 rule 產物有 frontmatter,覆寫蓋掉框架由三態算出的 paths。\n claude: { paths: ['docs/**/*.mdx'] },\n // codex 的 rule 落 AGENTS.md 的區塊,沒有承載鍵的位置,本筆進不支援報告。\n codex: { scope: 'repo' },\n },\n },\n content: 'subject 用繁體中文、50 字內,不以句號結尾。',\n})\n\n// 同一個 kind 的兩個載入態落點不同,鍵值面的有無因此相反:本節點既未宣告\n// appliesTo 也未宣告 alwaysApply,是三態的第三態(安裝但不自動進上下文),\n// 兩個平台都落自己的獨佔檔,開頭欄位是它專屬的,因此兩邊都消費覆寫。\nexport const legacyGlossary = defineRule({\n id: 'legacy-glossary',\n name: '舊術語對照',\n config: {\n platforms: {\n claude: { 'x-owner': 'docs-team' },\n codex: { 'x-owner': 'docs-team' },\n },\n },\n content: '舊版術語與現行術語的對照,需要時顯式引用。',\n})\n\nexport const prChecklist = defineResource({\n id: 'pr-checklist',\n name: 'PR 自查清單',\n config: {\n platforms: {\n // 兩個平台的 resource 產物都是獨佔的 markdown,只在宣告了覆寫時才生出 frontmatter。\n claude: { 'x-review-stage': 'pre-merge' },\n // 判準看寫不寫得進去,不看平台此刻讀不讀,兩邊因此同一個處置。\n codex: { 'x-review-stage': 'pre-merge' },\n },\n },\n content: '變更動機已寫明、影響範圍已列出、測試方式已說明。',\n})\n\nexport const fmtOnWrite = defineHook({\n id: 'fmt-on-write',\n name: '寫入後格式化',\n config: {\n event: 'tool:after',\n matcher: 'Write',\n // 合併型設定檔的覆寫作用在本節點貢獻的那一段,不是整份檔。\n platforms: {\n claude: { 'x-owner': 'platform-team' },\n codex: { 'x-owner': 'platform-team' },\n },\n },\n // 動作層另有自己的一格:節點層的覆寫落在事件底下的項目物件,構不到陣列內\n // 的處理器物件,而兩個平台近年新增的處理器鍵(claude 的 async/if/\n // statusMessage,codex 的 statusMessage/additionalContextLimit)都住那一層。\n // 沒有這一格時唯一的出路是整個蓋掉 hooks 陣列,粒度變成整段而非鍵。\n actions: [\n {\n type: 'command',\n command: 'pnpm format',\n platforms: {\n claude: { async: true, statusMessage: '格式化中' },\n codex: { statusMessage: '格式化中' },\n },\n },\n ],\n})\n\nexport const localDocs = defineMcp({\n id: 'local-docs',\n name: '本地文件查詢',\n config: {\n transport: { type: 'stdio', command: 'npx', args: ['-y', 'docs-mcp'] },\n platforms: {\n claude: { 'x-scope': 'project' },\n codex: { startup_timeout_sec: 30 },\n },\n },\n})\n\nexport const noForcePush = definePermission({\n id: 'no-force-push',\n name: '禁止強制推送',\n config: {\n tool: 'bash',\n match: ['git push --force'],\n decision: 'deny',\n platforms: {\n claude: { defaultMode: 'acceptEdits' },\n codex: { reason_required: true },\n },\n },\n})\n\n// ── 產物不含鍵值面:覆寫進不支援報告,不靜默丟棄 ──\n\nexport const houseRules = defineInstruction({\n id: 'house-rules',\n name: '倉庫慣例',\n config: {\n platforms: {\n claude: { 'x-owner': 'platform-team' },\n codex: { 'x-owner': 'platform-team' },\n },\n },\n content: '本倉庫的產出一律使用繁體中文。',\n})\n\n// 只宣告一個平台:另一個平台既不消費、也不記一筆不支援,因為那筆宣告本來\n// 就不是給它的。asset 與 dir 各取一邊,兩份報告因此互為對照。\nexport const releaseBadge = defineAsset({\n id: 'release-badge',\n name: '發布徽章',\n file: 'assets/badge.svg',\n platforms: { claude: { 'x-owner': 'platform-team' } },\n})\n\nexport const releaseTemplates = defineDir({\n id: 'release-templates',\n name: '發布公告模板組',\n dir: 'templates',\n platforms: { codex: { 'x-owner': 'platform-team' } },\n})"
716
+ },
717
+ {
718
+ "caption": "promptex.config.ts",
719
+ "lang": "ts",
720
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'override-full-coverage',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n})"
721
+ }
722
+ ],
723
+ "outcome": [
724
+ "claude 有鍵值面的類型:skill、agent、rule、resource(frontmatter)、hook(事件項)、mcp(mcpServers.{id})、permission(permissions 物件)。無鍵值面:instruction(共用檔區塊)、asset、dir(原檔拷貝),宣告了覆寫即進不支援清單並說明產物形態。",
725
+ "codex 的常駐態與範圍態 rule 併入 AGENTS.md 因此無鍵值面;第三態 rule 落獨佔檔而有 frontmatter,判定粒度是節點不是類型。"
726
+ ],
727
+ "points": [
728
+ "hook 的覆寫有兩層:config.platforms 落事件項物件,actions[].platforms 落陣列內的處理器物件,兩層不可互相替代。",
729
+ "只宣告某平台的覆寫在另一個平台既不消費也不記不支援。"
730
+ ],
731
+ "facts": [
732
+ "promptex.sdk.define-skill",
733
+ "promptex.sdk.define-agent",
734
+ "promptex.sdk.define-rule",
735
+ "promptex.sdk.define-hook",
736
+ "promptex.sdk.define-permission",
737
+ "promptex.sdk.define-instruction",
738
+ "promptex.sdk.define-asset",
739
+ "promptex.sdk.define-dir",
740
+ "promptex.sdk.define-mcp",
741
+ "promptex.sdk.define-resource"
742
+ ]
743
+ },
744
+ {
745
+ "title": "平台原生設定住 target,鍵值直通",
746
+ "situation": "要寫 claude 的 settings.json 或 codex 的 config.toml 內容,框架不認得的新鍵也要能寫。",
747
+ "code": [
748
+ {
749
+ "caption": "promptex.config.ts",
750
+ "lang": "ts",
751
+ "source": "// 平台原生設定住目標:各平台的設定鍵完全不同、沒有共通形態,因此跟著平台走,\n// 而不是住源碼或住單元。落點(outDir)與選取(select)這類跨平台共通的決策才\n// 住單元。\n//\n// 住源碼會抵觸「源碼不讀配置」:宣告期沒有「當次單元」這件事,同一份源碼被多\n// 個單元求值時,源碼側的設定宣告會外溢到每一個單元。設定住目標之後,「這份設\n// 定屬於哪個單元」由它寫在哪個 target 裡直接回答。\n//\n// 一個單元、兩個目標、單一 outDir:兩個平台的產物合流在同一棵樹下,claude 的\n// 設定寫進 .claude/settings.json、codex 的寫進 .codex/config.toml 的框架管理\n// 區段。單元名為 app 而非情境目錄名,因為本情境要示範的正是「設定屬於哪個單\n// 元」,單元名在產物路徑 .promptex/app/ 上看得見。\n\nimport { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'app',\n srcDir: './prompts',\n outDir: '.',\n targets: [\n claude({\n settings: {\n model: 'opus',\n env: { PROJECT_TIER: 'internal' },\n cleanupPeriodDays: 45,\n // 框架未知的鍵:型別的索引簽名收得下,投影原樣寫入,不做白名單比對。\n someNewClaudeOption: { enabled: true },\n },\n }),\n codex({\n settings: {\n model_reasoning_effort: 'high',\n approval_policy: 'on-request',\n },\n }),\n ],\n})"
752
+ },
753
+ {
754
+ "caption": "prompts/nodes.ts",
755
+ "lang": "ts",
756
+ "source": "import { definePermission } from 'promptex-js'\n\n// permission 投影為 claude 的 settings.json 之 permissions 鍵、codex 的\n// .codex/rules/promptex.rules;這兩個鍵(claude 的 hooks/permissions、\n// codex 的 mcp_servers)由 kind 節點投影產生,不可在 target 的 settings\n// 重複宣告:重複時於配置解析當下即報 E201 並一次列出全部衝突。\ndefinePermission({\n id: 'deny-force-push',\n name: '禁止強制推送',\n config: {\n tool: 'bash',\n match: ['git', 'push', '--force'],\n decision: 'deny',\n justification: '強制推送會覆寫遠端歷史,且無法回復',\n },\n})"
757
+ },
758
+ {
759
+ "caption": "variants/duplicate-settings.config.ts",
760
+ "lang": "ts",
761
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'app',\n srcDir: './prompts',\n outDir: '.',\n targets: [\n claude({\n settings: {\n model: 'opus',\n // hooks 與 permissions 由節點投影產生,不可在這裡再宣告一次\n hooks: { PreToolUse: [] },\n permissions: { deny: ['Bash(rm -rf:*)'] },\n },\n }),\n codex({ settings: { mcp_servers: { docs: { command: 'npx' } } } }),\n ],\n})"
762
+ },
763
+ {
764
+ "caption": "在 settings 重複宣告 hooks、permissions、mcp_servers",
765
+ "lang": "console",
766
+ "source": "$ promptex build --install --unit app\n✗ E201 The settings of the claude target must not declare hooks, permissions: those keys are produced by kind node projection, so use the matching define* declaration instead\n at the claude target settings of unit \"app\"\n✗ E201 The settings of the codex target must not declare mcp_servers: those keys are produced by kind node projection, so use the matching define* declaration instead\n at the codex target settings of unit \"app\"\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
767
+ }
768
+ ],
769
+ "outcome": [
770
+ "`.claude/settings.json` 同時含直通的 model、env、cleanupPeriodDays、someNewClaudeOption 與節點投影的 permissions,兩者不互相覆蓋。",
771
+ "`.codex/config.toml` 的兩個鍵寫在 `# promptex:begin` 區段內,下次安裝精確替換。",
772
+ "在 settings 重複宣告 hooks、permissions(claude)或 mcp_servers(codex)時,配置解析當下即報 E201 並一次列出全部衝突,結束碼 1、不寫任何產物。"
773
+ ],
774
+ "points": [
775
+ "源碼側沒有「當次單元」的概念,這是設定要住目標而非源碼的原因。",
776
+ "框架不維護鍵白名單:平台參數演進快於框架,作者不必等框架跟進。"
777
+ ],
778
+ "facts": [
779
+ "promptex.sdk.platform-claude",
780
+ "promptex.sdk.platform-codex",
781
+ "promptex.sdk.define-config",
782
+ "promptex.diagnostics.e201"
783
+ ]
784
+ },
785
+ {
786
+ "title": "平台設定的生效範圍即宣告它的那個單元",
787
+ "situation": "多個單元共用同一個 srcDir,想確認某單元的設定不會流到別的單元。",
788
+ "code": [
789
+ {
790
+ "caption": "promptex.config.ts",
791
+ "lang": "ts",
792
+ "source": "// 平台原生設定的生效範圍即宣告它的那個配置單元:三個單元共用同一個 srcDir,\n// 差別只在 select 與各自 target 上的 settings。共用 srcDir 是 monorepo 的既有\n// 形態(同一份源碼裝到多個落點),共用時每個單元都會重新求值同一批源碼。\n//\n// 設定住目標之後,「這份設定屬於哪個單元」由它寫在哪個 target 裡直接回答:\n// 一個單元的設定沒有任何路徑可以抵達另一個單元。設定若住源碼,共用 srcDir 的\n// 每個單元都會收到同一批宣告,作者得在源碼側把單元的標籤再抄一次才擋得住外\n// 溢,而抄漏的症狀是別的單元多出一份它從沒宣告過的設定檔。\n//\n// 三個單元的目標平台完全相同(claude 與 codex),因此產物差異只可能來自各自\n// 的宣告:這讓「哪些設定該落在哪個單元」成為可逐檔比對的觀察點。單元名沿用\n// global/web/api 而非情境目錄名,各單元的安裝根即同名子目錄。\n\nimport { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig([\n {\n // 兩個平台都宣告了設定:安裝根同時寫出兩份設定檔。\n name: 'global',\n srcDir: './prompts',\n outDir: './global',\n targets: [\n claude({ settings: { model: 'opus', env: { PROJECT_TIER: 'internal' } } }),\n codex({ settings: { model_reasoning_effort: 'high' } }),\n ],\n },\n {\n // 只有 codex 宣告設定:claude 側不該憑空多出一份 settings.json。\n name: 'web',\n srcDir: './prompts',\n select: { tags: ['domain/frontend'] },\n outDir: './web',\n targets: [claude(), codex({ settings: { model_reasoning_effort: 'high' } })],\n },\n {\n // 兩個平台都不宣告設定:兩份設定檔都不該存在。\n name: 'api',\n srcDir: './prompts',\n select: { tags: ['domain/backend'] },\n outDir: './api',\n targets: [claude(), codex()],\n },\n])"
793
+ },
794
+ {
795
+ "caption": "prompts/nodes.ts",
796
+ "lang": "ts",
797
+ "source": "// 三個配置單元共用這一份源碼(見 ../promptex.config.ts):每個單元都會重新\n// 求值本檔,因此都會收到同一批節點宣告;哪個單元真的收下哪一個,由單元的\n// select 與節點的 tags 決定。\n//\n// 本檔不含任何平台設定的宣告:設定住配置的 target,源碼因此不必也不能為\n// 「這份設定屬於哪個單元」再宣告一次範圍。\nimport { defineRule } from 'promptex-js'\n\ndefineRule({\n id: 'component-naming',\n name: '元件命名',\n meta: { tags: ['domain/frontend'] },\n content: '元件檔名採 PascalCase,與匯出的元件同名。',\n})\n\ndefineRule({\n id: 'endpoint-naming',\n name: '端點命名',\n meta: { tags: ['domain/backend'] },\n content: '端點路徑一律小寫,多字以連字號分隔。',\n})"
798
+ }
799
+ ],
800
+ "outcome": [
801
+ "`global/.claude/settings.json` 存在;`web/` 與 `api/` 底下查無此檔。`global/.codex/config.toml` 與 `web/.codex/config.toml` 存在;`api/` 底下沒有 codex 的設定檔。",
802
+ "節點依各單元的 select 各自篩過:web 只有前端規範、api 只有後端規範。"
803
+ ],
804
+ "points": [
805
+ "一個單元的設定沒有任何路徑可以抵達另一個單元;select 只決定節點。"
806
+ ],
807
+ "facts": [
808
+ "promptex.sdk.config",
809
+ "promptex.sdk.define-config",
810
+ "promptex.sdk.platform-claude",
811
+ "promptex.sdk.platform-codex"
812
+ ]
813
+ },
814
+ {
815
+ "title": "meta.targets 是作者側的分發開關",
816
+ "situation": "某節點只該給一個平台、或還是草稿暫不分發。",
817
+ "code": [
818
+ {
819
+ "caption": "prompts/nodes.ts",
820
+ "lang": "ts",
821
+ "source": "import { defineInstruction, defineRule, defineSkill } from 'promptex-js'\n\n// meta.targets 控制節點分發到哪些平台:省略或 'all'(預設)全部啟用、\n// 'none' 全部停用、陣列則僅指定平台。決定「產不產出」屬框架行為,故住\n// meta 而非 config。\n\n// 預設:兩平台皆分發。\nexport const changeFlow = defineSkill({\n id: 'change-flow',\n name: '變更流程',\n config: { whenToUse: '當使用者要求推進變更時使用' },\n content: '開分支、提交、提 PR、合併後清理。',\n})\n\n// 指定平台:僅 claude。用於依賴該平台特有能力、在他處無意義的節點。\nexport const claudeOnlyRule = defineRule({\n id: 'mdx-scoped-format',\n name: 'MDX 格式規範(glob 範圍)',\n meta: { targets: ['claude'] },\n config: { appliesTo: ['docs/**/*.mdx'] },\n content: '標題層級不跳級;code fence 一律標語言。',\n})\n\n// 指定平台:僅 codex。\nexport const codexOnlyInstruction = defineInstruction({\n id: 'codex-sandbox-note',\n name: 'codex 沙箱注意事項',\n meta: { targets: ['codex'], tags: ['platform/codex'] },\n content: '本專案在 codex 下以 read-only sandbox 執行,寫檔前先請求權限。',\n})\n\n// 全部停用:保留宣告但暫不分發(草稿、或暫時停用的節點)。\nexport const draftMigrationFlow = defineSkill({\n id: 'draft-migration-flow',\n name: '遷移流程(草稿)',\n meta: { targets: 'none' },\n config: { whenToUse: '尚未定案,暫不分發' },\n content: '草稿內容,待補。',\n})"
822
+ },
823
+ {
824
+ "caption": "promptex.config.ts",
825
+ "lang": "ts",
826
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'distribution-targets',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n})"
827
+ }
828
+ ],
829
+ "outcome": [
830
+ "專案根同時有 `.claude/rules/mdx-scoped-format.md`(僅 claude)與 `AGENTS.md`(僅 codex 的 instruction 區塊);claude 側沒有 CLAUDE.md,因為沒有內容可寫時不產出空檔。",
831
+ "`draft-migration-flow` 兩邊都查無,但 `.promptex/distribution-targets/graph.json` 仍含它:分發過濾發生在投影階段,中繼不丟宣告。"
832
+ ],
833
+ "points": [
834
+ "三種宣告:省略或 `all`(預設全部)、`none`(全部停用、保留宣告)、平台陣列(僅指定平台)。",
835
+ "targets 決定「產不產出」,本身不進產物內容,因此住 meta 而非 config。",
836
+ "作者側排除與能力降級是兩件事:被排除的節點根本不進分發,該平台的報告不會出現它的降級項。"
837
+ ],
838
+ "facts": [
839
+ "promptex.sdk.define-skill",
840
+ "promptex.sdk.define-rule",
841
+ "promptex.sdk.define-instruction",
842
+ "promptex.formats.graph"
843
+ ]
844
+ },
845
+ {
846
+ "title": "雙向能力落差:降級或不支援,一律進報告",
847
+ "situation": "想知道平台缺某能力時框架怎麼處理,以及該去哪裡看。",
848
+ "code": [
849
+ {
850
+ "caption": "prompts/nodes.ts",
851
+ "lang": "ts",
852
+ "source": "import {\n defineAgent,\n defineAsset,\n defineHook,\n defineInstruction,\n defineMcp,\n definePermission,\n defineRule,\n defineSkill,\n useAsset,\n useRule,\n} from 'promptex-js'\n\n// 本情境涵蓋兩平台各自的能力缺口,觀察雙向降級與不支援的分類。六種 kind\n// 在 claude 與 codex 都有原生機制,缺口只在細部能力與個別事件。\n\nexport const formatGate = defineAsset({\n id: 'format-gate-script',\n name: '格式把關腳本',\n file: 'scripts/format-gate.sh',\n})\n\n// 兩平台皆支援的事件:完整落地,無降級。\nexport const writeGate = defineHook({\n id: 'write-gate',\n name: '寫入把關',\n config: { event: 'tool:after', matcher: 'Write|Edit' },\n actions: [{ type: 'command', command: ['zsh ', useAsset('format-gate-script')] }],\n})\n\n// 兩平台皆有的事件:完整落地,不進任何一側的報告。留在本情境是為了讓「原生\n// 支援者不進報告」與下方「缺能力者進報告」同時可見。\nexport const permissionAudit = defineHook({\n id: 'permission-audit',\n name: '權限請求稽核',\n config: { event: 'permission:request' },\n actions: [{ type: 'command', command: 'cat >> tmp/permission-audit.log' }],\n})\n\n// claude 專有事件:codex 無 notification 對應機制 → codex 側不支援。\nexport const notifyRelay = defineHook({\n id: 'notify-relay',\n name: '通知轉送',\n config: { event: 'notification' },\n actions: [{ type: 'command', command: 'cat >> tmp/notify.log' }],\n})\n\n// 兩平台皆有 subagent;工具白名單只有 agent 這一格 claude 有逐項機制 →\n// codex 側降級。model 填 'frontier':中性值域中 claude 認得而 codex 不認得的\n// 那一檔,codex 側因此不寫出該欄位並列入不支援清單,claude 側照常寫出\n// 。這個方向不掛在 effort: 'max',因為 codex 的 custom agent TOML 一直認得該\n// 值,掛上去等於把「本來能用的值」誤標為不支援。\nexport const verifyAgent = defineAgent({\n id: 'verify-agent',\n name: '驗證代理',\n config: {\n whenToUse: '需要獨立核對產出時委派',\n allowedTools: ['read', 'grep'],\n model: 'frontier',\n },\n content: '逐項核對委派方下發的判準,不自創判準。',\n})\n\n// 值域缺口的另一個方向:'minimal' 是 codex 認得而 claude 不認得的那一檔。\n// 與 verify-agent 對照可見中性值域取兩平台的聯集而非交集,缺口逐平台判定。\nexport const triageAgent = defineAgent({\n id: 'triage-agent',\n name: '分流代理',\n config: {\n whenToUse: '需要快速判斷議題歸屬時委派',\n model: 'fast',\n effort: 'minimal',\n },\n content: '只判斷歸屬與優先序,不進入實作。',\n})\n\n// glob 範圍載入僅 claude 有 → codex 側降級為常駐 + 內文標註。\nexport const mdxRule = defineRule({\n id: 'mdx-format',\n name: 'MDX 格式規範',\n config: { appliesTo: ['docs/**/*.mdx'] },\n content: '標題層級不跳級;code fence 一律標語言。',\n})\n\n// 載入宣告的第三態(appliesTo 與 alwaysApply 皆不宣告):安裝但不自動載入,\n// 僅供顯式引用。claude 以永不匹配的佔位模式表達、codex 落 AGENTS.md 以外的\n// 獨佔檔,兩側皆有原生形態可表達,故不降級。\nexport const legacyGlossary = defineRule({\n id: 'legacy-glossary',\n name: '舊術語對照',\n content: '舊稱「工單」對應現稱「工作項」。',\n})\n\n// skill 層的 model/effort 僅 claude 有 → codex 側降級(屬其 subagent 層設定)。\n// allowedTools 則是兩平台的 skill 都缺:codex 沒有逐項工具白名單,claude 的\n// 同名 allowed-tools 是免核准清單、方向相反。同一個能力鍵因此帶資源類型後綴\n// (skill 與 agent 各判一次),與 verify-agent 那筆對照可見分岔。\nexport const docFlow = defineSkill({\n id: 'doc-flow',\n name: '文件撰寫流程',\n config: {\n whenToUse: '當使用者要求撰寫文件時使用',\n model: 'standard',\n allowedTools: ['read', 'grep'],\n },\n content: ['先盤點既有文件,再草擬變更。遇到舊稱時查:', useRule('legacy-glossary')],\n})\n\n// 兩平台皆缺對應機制的權限工具 → 兩側皆不支援。claude 的檔案權限只比對\n// Edit 與 Read 的路徑規則,Write 的規則會被收下卻永不套用;codex 的權限規則\n// 只涵蓋命令執行。缺口不必然是單一平台獨有,這是雙側同缺的形態。\nexport const denyDocWrites = definePermission({\n id: 'deny-doc-writes',\n name: '禁止直接寫入文件目錄',\n config: { tool: 'write', match: ['docs/**'], decision: 'deny' },\n})\n\n// 兩平台皆原生支援:完整落地。\nexport const context7 = defineMcp({\n id: 'context7',\n name: 'Context7 文件查詢',\n config: { transport: { type: 'stdio', command: 'npx', args: ['-y', '@upstash/context7-mcp'] } },\n})\n\n// 指令的目錄範圍(meta.path)在兩平台皆為原生機制:claude 與 codex 都由\n// 工作目錄向上尋找指令檔,故無降級。\nexport const apiConventions = defineInstruction({\n id: 'api-conventions',\n name: 'API 套件慣例',\n meta: { path: 'packages/api' },\n content: '端點命名用 kebab-case 複數;錯誤回應一律含 error code 與 traceId。',\n})"
853
+ },
854
+ {
855
+ "caption": "安裝報告",
856
+ "lang": "console",
857
+ "source": "$ promptex build --install\n✓ capability-degrade: metadata written (graph + content)\n── capability-degrade → claude (<project>) ──\n wrote .claude/agents/triage-agent.md\n wrote .claude/agents/verify-agent.md\n wrote .claude/hooks/write-gate/format-gate.sh\n wrote .claude/rules/legacy-glossary.md\n wrote .claude/rules/mdx-format.md\n wrote .claude/settings.json\n wrote .claude/skills/doc-flow/SKILL.md\n wrote .mcp.json\n wrote packages/api/CLAUDE.md\nInstall report (claude)\n Projected artifacts, 9: .claude/agents/triage-agent.md, .claude/agents/verify-agent.md, .claude/hooks/write-gate/format-gate.sh, .claude/rules/legacy-glossary.md, .claude/rules/mdx-format.md, .claude/settings.json, .claude/skills/doc-flow/SKILL.md, .mcp.json, packages/api/CLAUDE.md\n Degraded, 1:\n • skill \"doc-flow\": toolAllowlist:skill, enforcement processGate → wording\n The allowed-tools of a skill on this platform is a pre-approval list rather than a tool restriction (per the official docs it does not restrict which tools are available, and every tool can still be called), so mapping onto it would invert a narrowing declaration into an authorization expansion and it is not written into the artifact; express tool restrictions in the instruction text, or name this platform's native key directly at config.platforms.claude (write allowed-tools to pre-approve, disallowed-tools to remove tools)\n Unsupported, 2:\n • agent \"triage-agent\": effort:minimal has no matching mechanism and was not emitted\n The neutral value \"minimal\" of reasoning effort has no matching native value on this platform, so that field was not written into the artifact; use one of the values this platform supports, low, medium, high, xhigh, max, or name this platform's native value directly at config.platforms.claude\n • permission \"deny-doc-writes\": toolPermission:write has no matching mechanism and was not emitted (mechanical protection is not in place; the alternative path reaches wording at best)\n The platform's file permissions are matched against the path rules of Edit and Read only, so a rule for this tool is accepted but never applied and warns at startup; to restrict it, declare an Edit or Read rule instead, or intercept it with a hook\n Notices, 1:\n • skill \"doc-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n── capability-degrade → codex (<project>) ──\n wrote .agents/hooks/write-gate/format-gate.sh\n wrote .agents/rules/legacy-glossary.md\n wrote .agents/skills/doc-flow/SKILL.md\n wrote .codex/agents/triage-agent.toml\n wrote .codex/agents/verify-agent.toml\n wrote .codex/config.toml\n wrote .codex/hooks.json\n wrote AGENTS.md\n wrote packages/api/AGENTS.md\nInstall report (codex)\n Projected artifacts, 9: .agents/hooks/write-gate/format-gate.sh, .agents/rules/legacy-glossary.md, .agents/skills/doc-flow/SKILL.md, .codex/agents/triage-agent.toml, .codex/agents/verify-agent.toml, .codex/config.toml, .codex/hooks.json, AGENTS.md, packages/api/AGENTS.md\n Degraded, 4:\n • skill \"doc-flow\": perSkillModel, enforcement processGate → wording\n The platform's skill has no model or thinking-effort field (those belong to the subagent layer), so that configuration is skipped on this platform\n • skill \"doc-flow\": toolAllowlist:skill, enforcement processGate → wording\n The platform governs this through sandbox_mode and MCP tool switches and has no per-tool allowlist; express tool restrictions in the instruction text\n • agent \"verify-agent\": toolAllowlist:agent, enforcement processGate → wording\n The platform governs this through sandbox_mode and MCP tool switches and has no per-tool allowlist; express tool restrictions in the instruction text\n • rule \"mdx-format\": globScopedLoading, enforcement processGate → wording\n The platform supports directory-level AGENTS.md only and has no glob-scoped loading, so the rule becomes always-on and its scope is noted in the body (docs/**/*.mdx)\n Unsupported, 3:\n • agent \"verify-agent\": model:frontier has no matching mechanism and was not emitted\n The neutral value \"frontier\" of model tier has no matching native value on this platform, so that field was not written into the artifact; use one of the values this platform supports, fast, standard, max, or name this platform's native value directly at config.platforms.codex\n • hook \"notify-relay\": event:notification has no matching mechanism and was not emitted (mechanical protection is not in place; the alternative path reaches wording at best)\n The platform has no matching event, so this hook was not emitted; for equivalent protection, move it to another event, carry the constraint at the wording layer with a rule, or name this platform's native event directly at config.platforms.codex with the reserved key promptex:event\n • permission \"deny-doc-writes\": toolPermission:write has no matching mechanism and was not emitted (mechanical protection is not in place; the alternative path reaches wording at best)\n The platform's permission rules cover command execution only and there is no matching mechanism for non-command tools; to restrict it, express the restriction with sandbox_mode or an instruction instead"
858
+ }
859
+ ],
860
+ "outcome": [
861
+ "兩個平台都產出九項(claude 的 `.claude/` 加 `.mcp.json` 與 `packages/api/CLAUDE.md`,codex 的 `.agents/`、`.codex/`、`AGENTS.md` 與 `packages/api/AGENTS.md`),落差全數進各自的報告。claude 側報 effort:minimal 與 permission write 不支援、skill 的 allowedTools 降級;codex 側報 model:frontier、notification 事件、permission 不支援,與 model、appliesTo、allowedTools 降級(allowedTools 在 skill 與 agent 各一筆)。",
862
+ "第三態 rule 兩側皆原生(claude 以永不匹配的 paths 佔位,見 `.claude/rules/legacy-glossary.md`;codex 落 AGENTS.md 以外的獨佔檔,見 `.agents/rules/legacy-glossary.md`),不進任何報告。"
863
+ ],
864
+ "points": [
865
+ "強制力分三階,機械閘 > 流程閘 > 只改字的層級;能用較低強制力表達相同意圖的算降級,連能表達的地方都沒有的算不支援。",
866
+ "中性詞彙的值域取兩平台聯集,缺口逐平台判定並不寫入該欄位,不把平台會拒絕的值原樣透傳。",
867
+ "claude 的 skill 沒有工具限制欄位(同名 allowed-tools 是免核准清單,方向相反),allowedTools 在 skill 層兩平台都降級;agent 層 claude 有逐項機制。",
868
+ "報告另有第三類「提醒」:產物照常寫出、能力也都在,只是宣告少了什麼而平台會自行補上。例如 claude 的 skill 沒寫 description 時,產物不寫 description,平台改取內文第一個非空行。這時報告印「Notices」一節,--json 的結果帶 notices 欄位。"
869
+ ],
870
+ "facts": [
871
+ "promptex.sdk.model-tiers",
872
+ "promptex.sdk.effort-levels",
873
+ "promptex.sdk.platform-codex",
874
+ "promptex.sdk.define-hook",
875
+ "promptex.sdk.define-agent",
876
+ "promptex.sdk.define-permission"
877
+ ]
878
+ },
879
+ {
880
+ "title": "引用的目標未在本平台產出:連結退回裸識別,進報告不中止",
881
+ "situation": "被引用的節點以 meta.targets 排除了某平台,引用方在那個平台會怎樣。",
882
+ "code": [
883
+ {
884
+ "caption": "prompts/nodes.ts",
885
+ "lang": "ts",
886
+ "source": "import { defineInstruction, defineRule, defineSkill, useInstruction, useRule } from 'promptex-js'\n\n// 引用方本身兩平台皆分發,它引用的兩個目標各自被作者側排除:一個限定單一\n// 平台、一個整個停用。求值期的斷鏈診斷(E101)管的是「查無定義」,這兩個\n// 目標都定義得好好的,過得了那一關;到投影階段才發現本次產物裡沒有它們。\nexport const specGuard = defineSkill({\n id: 'spec-guard',\n name: '規格把關',\n config: { whenToUse: '當使用者要求把關規格文件時使用' },\n content: [\n '格式依規範核對:',\n useRule('mdx-scoped-format'),\n '草稿階段的補充說明:',\n useInstruction('draft-only-note'),\n ],\n})\n\n// 僅 claude:codex 建置時本節點不產出,spec-guard 對它的引用在該平台成為斷鏈。\nexport const claudeOnlyRule = defineRule({\n id: 'mdx-scoped-format',\n name: 'MDX 格式規範',\n meta: { targets: ['claude'] },\n config: { appliesTo: ['docs/**/*.mdx'] },\n content: '標題層級不跳級;code fence 一律標語言。',\n})\n\n// 全部停用:兩個平台都不產出,兩份報告因此都要記到這一筆。\nexport const draftOnlyNote = defineInstruction({\n id: 'draft-only-note',\n name: '草稿補充說明',\n meta: { targets: 'none' },\n content: '本節內容尚未定案,暫不分發。',\n})"
887
+ },
888
+ {
889
+ "caption": "promptex.config.ts",
890
+ "lang": "ts",
891
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'broken-reference-report',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n})"
892
+ },
893
+ {
894
+ "caption": "安裝報告",
895
+ "lang": "console",
896
+ "source": "$ promptex build --install\n✓ broken-reference-report: metadata written (graph + content)\n── broken-reference-report → claude (<project>) ──\n wrote .claude/rules/mdx-scoped-format.md\n wrote .claude/skills/spec-guard/SKILL.md\nInstall report (claude)\n Projected artifacts, 2: .claude/rules/mdx-scoped-format.md, .claude/skills/spec-guard/SKILL.md\n Unsupported, 1:\n • skill \"spec-guard\": ref:instruction:draft-only-note has no matching mechanism and was not emitted\n 引用目標未在本平台產出(作者側以 meta.targets 排除,或不屬本次投影的範圍),連結因此渲染為裸識別「draft-only-note」而指不到任何檔案;請把目標加回本平台的分發範圍,或以外部引用登記給出它已安裝的落點\n Notices, 1:\n • skill \"spec-guard\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n── broken-reference-report → codex (<project>) ──\n wrote .agents/skills/spec-guard/SKILL.md\nInstall report (codex)\n Projected artifacts, 1: .agents/skills/spec-guard/SKILL.md\n Unsupported, 2:\n • skill \"spec-guard\": ref:instruction:draft-only-note has no matching mechanism and was not emitted\n 引用目標未在本平台產出(作者側以 meta.targets 排除,或不屬本次投影的範圍),連結因此渲染為裸識別「draft-only-note」而指不到任何檔案;請把目標加回本平台的分發範圍,或以外部引用登記給出它已安裝的落點\n • skill \"spec-guard\": ref:rule:mdx-scoped-format has no matching mechanism and was not emitted\n 引用目標未在本平台產出(作者側以 meta.targets 排除,或不屬本次投影的範圍),連結因此渲染為裸識別「mdx-scoped-format」而指不到任何檔案;請把目標加回本平台的分發範圍,或以外部引用登記給出它已安裝的落點"
897
+ }
898
+ ],
899
+ "outcome": [
900
+ "claude 的 `.claude/skills/spec-guard/SKILL.md` 第一則引用解析為相對路徑,第二則退回 `[draft-only-note](draft-only-note)`;codex 的 `.agents/skills/spec-guard/SKILL.md` 兩則都退回裸識別。",
901
+ "安裝報告的不支援清單記 `ref:instruction:draft-only-note has no matching mechanism and was not emitted`(codex 另記 `ref:rule:mdx-scoped-format`),並給兩條修法:把目標加回分發範圍,或以外部引用登記給出落點。建置結束碼 0。"
902
+ ],
903
+ "points": [
904
+ "兩道斷鏈核對分工,求值期 E101 攔「查無定義」(零產物);投影期攔「有定義但本次不產出」(照常產出、進報告)。",
905
+ "不判為錯誤是刻意的:否則為了排除一個節點得改寫全部引用方。"
906
+ ],
907
+ "facts": [
908
+ "promptex.sdk.use-refs",
909
+ "promptex.sdk.define-skill",
910
+ "promptex.sdk.define-rule",
911
+ "promptex.sdk.define-instruction"
912
+ ]
913
+ },
914
+ {
915
+ "title": "mcp 的傳輸形態對映與 meta.enabled 停用",
916
+ "situation": "要宣告 stdio 與 http 兩種 MCP 伺服器,並暫時停用其中一個。",
917
+ "code": [
918
+ {
919
+ "caption": "prompts/nodes.ts",
920
+ "lang": "ts",
921
+ "source": "import { defineMcp } from 'promptex-js'\n\n// mcp 的兩個 transport 形態與作者側停用開關,同一份宣告投影成兩平台各自的\n// 原生形態:欄位名稱由適配翻譯(headers 對 http_headers),停用與否則兩平台\n// 得出同一個結論。\n\n// stdio:兩平台的鍵名與結構相同,作為對照組。\nexport const localDocs = defineMcp({\n id: 'local-docs',\n name: '本地文件查詢',\n config: { transport: { type: 'stdio', command: 'npx', args: ['-y', '@upstash/context7-mcp'] } },\n})\n\n// http:自訂標頭在 claude 是 headers、在 codex 是 http_headers,欄位名稱由\n// 適配翻譯,作者不寫平台語彙。\nexport const remoteDocs = defineMcp({\n id: 'remote-docs',\n name: '遠端文件查詢',\n config: {\n transport: {\n type: 'http',\n url: 'https://github.com/mcp',\n headers: { 'X-Team': 'platform' },\n },\n },\n})\n\n// meta.enabled 為 false:保留宣告但不進產物,兩平台皆不產出這一筆。\nexport const retiredDocs = defineMcp({\n id: 'retired-docs',\n name: '已停用的舊文件服務',\n meta: { enabled: false },\n config: { transport: { type: 'stdio', command: 'legacy-docs-server' } },\n})"
922
+ },
923
+ {
924
+ "caption": "promptex.config.ts",
925
+ "lang": "ts",
926
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'mcp-transport-and-toggle',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n})"
927
+ }
928
+ ],
929
+ "outcome": [
930
+ "`.mcp.json` 的 remote-docs 有 `headers`,`.codex/config.toml` 的同一筆是 `http_headers`:欄位名由適配翻譯。",
931
+ "兩份產物都查無 retired-docs,但 `.promptex/mcp-transport-and-toggle/graph.json` 仍含它:停用不等於刪除。"
932
+ ],
933
+ "points": [
934
+ "transport 欄位對映兩平台可以不同(欄位名本就是平台機制名),停用與否兩平台必須一致。",
935
+ "sse 與 ws 為端點形態的另兩種 type,codex 不支援時該節點記不支援、不產出。"
936
+ ],
937
+ "facts": [
938
+ "promptex.sdk.define-mcp",
939
+ "promptex.formats.graph"
940
+ ]
941
+ },
942
+ {
943
+ "title": "作者欄位與產物的對應",
944
+ "situation": "想確認未填的選填欄位不會被代填,以及 permission 的集合元素怎麼展開。",
945
+ "code": [
946
+ {
947
+ "caption": "prompts/nodes.ts",
948
+ "lang": "ts",
949
+ "source": "import { defineHook, definePermission } from 'promptex-js'\n\n// 兩個軸,都是「作者填了什麼、產物就有什麼」的邊界:選填欄位未填時框架代不代\n// 填,以及集合元素的展開由誰決定。\n\n// hook 動作的逾時上限是選填:填了原樣進產物,未填時產物不出現該欄位,框架不\n// 代填任何數值(預設值屬平台語意,不屬框架)。同一個 hook 掛兩個動作,一個填\n// 一個不填,兩種形態在同一份產物裡並列可比。\nexport const writeGate = defineHook({\n id: 'write-gate',\n name: '寫入前把關',\n config: { event: 'tool:before', matcher: 'Write' },\n actions: [\n { type: 'command', command: 'scripts/lint-staged.sh', timeout: 30 },\n { type: 'command', command: 'scripts/notify.sh' },\n ],\n})\n\n// 裁決的詢問態:不是允許也不是拒絕,語意是把決定權交還使用者。claude 收進\n// settings.json 的 ask 鍵,codex 譯為 prefix_rule 的 prompt。\n//\n// match 的第二個元素是陣列(PermissionMatchElement 的集合形態),語意是「這個\n// 位置任一即命中」。展開方式刻意交由投影階段依平台決定:claude 的比對式是逐條\n// 字串,因此展開成笛卡兒積的兩條;codex 的 prefix_rule 原生接受巢狀清單,因此\n// 原樣保留為一條。同一份宣告在兩平台的產物條數不同,這是預期而非缺陷。\nexport const reviewPushes = definePermission({\n id: 'review-pushes',\n name: '推送前先問過',\n config: {\n tool: 'bash',\n match: ['git', ['push', 'push --force'], 'origin'],\n decision: 'ask',\n },\n})"
950
+ },
951
+ {
952
+ "caption": "promptex.config.ts",
953
+ "lang": "ts",
954
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'optional-fields-and-set-elements',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n})"
955
+ }
956
+ ],
957
+ "outcome": [
958
+ "第一個動作帶 `timeout: 30`,第二個動作整個沒有 timeout 鍵(不填 0、不填平台預設值),兩平台同一結論(見 `.claude/settings.json` 與 `.codex/hooks.json`)。",
959
+ "claude 的 `ask` 鍵展開成 `Bash(git push origin:*)` 與 `Bash(git push --force origin:*)` 兩條;codex 的 prefix_rule(`.codex/rules/review-pushes.rules`)原樣保留巢狀清單一條、decision 為 prompt。"
960
+ ],
961
+ "points": [
962
+ "match 元素可為 string 或 string[](任一即命中),展開方式交給適配依平台決定,兩平台條數不同是預期。"
963
+ ],
964
+ "facts": [
965
+ "promptex.sdk.define-hook",
966
+ "promptex.sdk.define-permission"
967
+ ]
968
+ },
969
+ {
970
+ "title": "事件鍵的退路:保留鍵 promptex:event",
971
+ "situation": "平台新增了中性事件詞彙表沒有的事件,或某平台不支援中性事件。",
972
+ "code": [
973
+ {
974
+ "caption": "prompts/nodes.ts",
975
+ "lang": "ts",
976
+ "source": "import { defineHook } from 'promptex-js'\n\n// 事件鍵的逃生口:中性事件詞彙表涵蓋不到的平台事件,由保留鍵 promptex:event\n// 直接指名。事件鍵是唯一構不到的鍵 ── 它決定本節點落在產物的哪一棵子樹,而\n// 一般覆寫作用在那棵子樹之內。\n\n// 中性事件 notification 在 codex 的對照表裡沒有對應,未指名時這個 hook 在\n// codex 完全不產出、報告記一筆不支援。作者指名平台此刻的原生事件名之後,\n// hook 落在該事件下,中性對映整條讓開,也不再記那一筆。\nexport const usageDigest = defineHook({\n id: 'usage-digest',\n name: '用量摘要',\n config: {\n event: 'notification',\n platforms: {\n codex: { 'promptex:event': 'Notification' },\n },\n },\n actions: [{ type: 'command', command: 'node scripts/digest.mjs' }],\n})\n\n// 保留鍵與一般原生鍵並存於同一個覆寫物件:前者由框架消費、不寫進產物,後者\n// 原樣併入本節點貢獻的那一段。matcher 一併驗到:prompt:submitted 在兩個平台\n// 都不吃 matcher,claude 側指名原生事件後照樣寫出(吃不吃是平台對中性事件的\n// 知識,目標事件已經不是它),codex 側維持既有處置、該筆進不支援報告。\nexport const deployGuard = defineHook({\n id: 'deploy-guard',\n name: '部署提示攔檢',\n config: {\n event: 'prompt:submitted',\n matcher: 'deploy',\n platforms: {\n claude: { 'promptex:event': 'UserPromptSubmitV2', 'x-owner': 'platform-team' },\n },\n },\n actions: [{ type: 'command', command: 'node scripts/guard.mjs' }],\n})"
977
+ },
978
+ {
979
+ "caption": "variants/reserved-key-on-rule.nodes.ts",
980
+ "lang": "ts",
981
+ "source": "import { defineRule } from 'promptex-js'\n\n// promptex:event 只在 hook 合法,用在 rule 上建置會被阻斷\nexport const styleRule = defineRule({\n id: 'style-rule',\n name: '風格規範',\n config: { platforms: { claude: { 'promptex:event': 'Notification' } } },\n content: '函式名稱用動詞開頭。',\n})"
982
+ },
983
+ {
984
+ "caption": "把保留鍵用在 rule 上",
985
+ "lang": "console",
986
+ "source": "$ promptex build --install --unit native-event-escape-hatch\n✗ E203 The framework reserved key \"promptex:event\" may only be declared in the override of a hook node, and this node is a rule\n at config.platforms.claude of rule \"style-rule\"\n Current reserved keys: promptex:event (for hook nodes only); to name any other platform native key, write that key name directly inside the override object\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
987
+ }
988
+ ],
989
+ "outcome": [
990
+ "`.claude/settings.json` 的事件鍵是 `UserPromptSubmitV2`,事件項內有 matcher 與 x-owner,查無 promptex:event。",
991
+ "`.codex/hooks.json` 有 Notification 事件鍵,codex 報告不再記 usage-digest 的不支援(仍記 deploy-guard 的 matcher,因為 codex 的 prompt:submitted 不吃 matcher)。",
992
+ "把 promptex:event 寫在 rule 的覆寫上,建置以 E203 阻斷,結束碼 1。"
993
+ ],
994
+ "points": [
995
+ "事件鍵是一般覆寫構不到的唯一鍵(它決定節點落在哪棵子樹),保留鍵補上這一格;由框架消費、不寫進產物。",
996
+ "保留鍵清單封閉,現行只有 promptex:event 且只在 hook 合法;清單外的 promptex: 鍵、用在其他類型、值非非空字串,皆以 E203 阻斷。",
997
+ "指名原生事件後,matcher 照樣寫出(吃不吃 matcher 是平台對中性事件的知識,目標事件已不是它)。"
998
+ ],
999
+ "facts": [
1000
+ "promptex.sdk.define-hook",
1001
+ "promptex.diagnostics.e203"
1002
+ ]
1003
+ },
1004
+ {
1005
+ "title": "同一份隨附宣告,兩平台不同落點",
1006
+ "situation": "想知道 skill 隨附的資源、素材、目錄在 codex 會落在哪個子目錄。",
1007
+ "code": [
1008
+ {
1009
+ "caption": "prompts/nodes.ts",
1010
+ "lang": "ts",
1011
+ "source": "import { defineAsset, defineDir, defineResource, defineSkill, useAsset, useDir, useResource } from 'promptex-js'\n\n// 同一份宣告,兩平台算出不同落點:referrer-skill 未宣告 meta.resources,\n// 三份隨附素材皆取預設 bundled;宣告本身不出現任何平台語彙,落點差異完全\n// 由各平台的適配決定(claude 平鋪、codex 依原生的 skill 資源桶慣例分流)。\nexport const referrerSkill = defineSkill({\n id: 'referrer-skill',\n name: '桶規則示範技能',\n config: { whenToUse: '示範 skill 隨附素材在 claude 與 codex 兩平台的桶落點差異,不實際使用' },\n content: ['引用一份資源、一份素材、一個目錄:', useResource('target-resource'), useAsset('target-asset'), useDir('target-dir')],\n})\n\nexport const targetResource = defineResource({\n id: 'target-resource',\n name: '目標資源',\n content: '被引用的資源本體。',\n})\n\nexport const targetAsset = defineAsset({\n id: 'target-asset',\n name: '目標素材',\n content: '被引用的素材本體',\n fileName: 'target-asset.txt',\n})\n\nexport const targetDir = defineDir({\n id: 'target-dir',\n name: '目標目錄',\n dir: 'templates',\n})"
1012
+ },
1013
+ {
1014
+ "caption": "promptex.config.ts",
1015
+ "lang": "ts",
1016
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'codex-skill-buckets',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n})"
1017
+ }
1018
+ ],
1019
+ "outcome": [
1020
+ "claude:`.claude/skills/referrer-skill/target-resource.md`、`.claude/skills/referrer-skill/target-asset.txt`、`.claude/skills/referrer-skill/templates/` 平鋪。",
1021
+ "codex 時,asset 進 `.agents/skills/referrer-skill/assets/`,resource 與 dir 進 `.agents/skills/referrer-skill/references/`;連結跟著落點走。"
1022
+ ],
1023
+ "points": [
1024
+ "桶規則只在「bundled 且引用方是 skill」時套用,住 codex 適配、不住共用層;落點差異不是能力落差,不進報告。"
1025
+ ],
1026
+ "facts": [
1027
+ "promptex.sdk.define-skill",
1028
+ "promptex.sdk.define-resource",
1029
+ "promptex.sdk.define-asset",
1030
+ "promptex.sdk.define-dir",
1031
+ "promptex.sdk.use-refs"
1032
+ ]
1033
+ },
1034
+ {
1035
+ "title": "用標籤分組打包平台 plugin",
1036
+ "situation": "想把一組節點打包成 claude 與 codex 各自原生的 plugin,而不是逐個節點散裝。",
1037
+ "code": [
1038
+ {
1039
+ "caption": "prompts/nodes.ts",
1040
+ "lang": "ts",
1041
+ "source": "import { defineDir, defineHook, defineRule, defineSkill } from 'promptex-js'\n\nexport const reviewFlow = defineSkill({\n id: 'review-flow',\n name: '審查流程',\n meta: { tags: ['plugin/review'] },\n config: { whenToUse: '當使用者要求審查變更時使用' },\n content: '逐項核對變更的動機、影響範圍與測試方式。',\n})\n\nexport const reviewMods = defineDir({\n id: 'review-mods',\n name: '審查用的 mod',\n meta: { tags: ['plugin/review'], emit: 'always' },\n dir: 'src-mods',\n})\n\nexport const reviewGuard = defineHook({\n id: 'review-guard',\n name: '寫入前審查提示',\n meta: { tags: ['plugin/review'] },\n config: { event: 'tool:before', matcher: 'Write' },\n actions: [{ type: 'command', command: 'echo review' }],\n})\n\nexport const styleRule = defineRule({\n id: 'style-rule',\n name: '風格規範',\n config: { appliesTo: ['src/**'] },\n content: '函式名稱用動詞開頭。',\n})"
1042
+ },
1043
+ {
1044
+ "caption": "promptex.config.ts",
1045
+ "lang": "ts",
1046
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'platform-plugin-by-tags',\n srcDir: './prompts',\n outDir: '.',\n targets: [\n claude({\n plugins: [\n {\n name: 'review-tools',\n version: '1.0.0',\n description: '審查工具組',\n select: { tags: ['plugin/review'] },\n modules: 'src-mods/mods/main.ts',\n },\n ],\n }),\n codex({ plugins: [{ name: 'review-tools', select: { tags: ['plugin/review'] } }] }),\n ],\n})"
1047
+ },
1048
+ {
1049
+ "caption": "variants/codex-modules.config.ts",
1050
+ "lang": "ts",
1051
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'platform-plugin-by-tags',\n srcDir: './prompts',\n outDir: '.',\n targets: [\n claude({\n plugins: [\n {\n name: 'review-tools',\n version: '1.0.0',\n description: '審查工具組',\n select: { tags: ['plugin/review'] },\n modules: 'src-mods/mods/main.ts',\n },\n ],\n }),\n // codex 的 plugin 不收 modules。型別檢查會先擋下,這裡用 as never 略過\n codex({\n plugins: [{ name: 'review-tools', select: { tags: ['plugin/review'] }, modules: 'src-mods/mods/main.ts' }],\n } as never),\n ],\n})"
1052
+ },
1053
+ {
1054
+ "caption": "variants/overlapping-plugins.config.ts",
1055
+ "lang": "ts",
1056
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'platform-plugin-by-tags',\n srcDir: './prompts',\n outDir: '.',\n targets: [\n claude({\n plugins: [\n { name: 'a-tools', select: { tags: ['plugin/review'] } },\n { name: 'b-tools', select: { tags: ['plugin'] } },\n ],\n }),\n ],\n})"
1057
+ },
1058
+ {
1059
+ "caption": "variants/third-party-plugins.config.ts",
1060
+ "lang": "ts",
1061
+ "source": "import { defineConfig, defineTarget } from 'promptex-js'\n\nconst acme = defineTarget('acme', () => new Map())\n\nexport default defineConfig({\n name: 'platform-plugin-by-tags',\n srcDir: './prompts',\n outDir: '.',\n targets: [{ ...acme, plugins: [{ name: 'review-tools', select: { tags: ['plugin/review'] } }] } as never],\n})"
1062
+ },
1063
+ {
1064
+ "caption": "三個反例的建置輸出",
1065
+ "lang": "console",
1066
+ "source": "$ promptex build --install .\n✓ platform-plugin-by-tags: metadata written (graph + content)\n── platform-plugin-by-tags → claude (<project>) ──\n wrote .claude/rules/style-rule.md\n wrote .claude/skills/review-tools/.claude-plugin/plugin.json\n wrote .claude/skills/review-tools/hooks/hooks.json\n wrote .claude/skills/review-tools/skills/review-flow/SKILL.md\n wrote .claude/skills/review-tools/src-mods/mods/main.ts\nInstall report (claude)\n Projected artifacts, 5: .claude/rules/style-rule.md, .claude/skills/review-tools/.claude-plugin/plugin.json, .claude/skills/review-tools/hooks/hooks.json, .claude/skills/review-tools/skills/review-flow/SKILL.md, .claude/skills/review-tools/src-mods\n Notices, 1:\n • skill \"review-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n── platform-plugin-by-tags → codex (<project>) ──\n✗ E214 The platform plugin \"review-tools\" declares modules on the codex target; modules is only allowed on the claude target, because mods exist only in Claude Code\n at plugins[0] (review-tools) of the codex target\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing).\n\n$ promptex build --install --unit platform-plugin-by-tags\n✓ platform-plugin-by-tags: metadata written (graph + content)\n── platform-plugin-by-tags → claude (<project>) ──\n✗ E215 skill:review-flow is selected by more than one platform plugin of the claude target: a-tools, b-tools; a node belongs to at most one plugin per target\n at skill \"review-flow\"\n Narrow the select of one plugin with exclude, or change the node's tags; the framework does not guess which plugin wins\n✗ E215 hook:review-guard is selected by more than one platform plugin of the claude target: a-tools, b-tools; a node belongs to at most one plugin per target\n at hook \"review-guard\"\n Narrow the select of one plugin with exclude, or change the node's tags; the framework does not guess which plugin wins\n✗ E215 dir:review-mods is selected by more than one platform plugin of the claude target: a-tools, b-tools; a node belongs to at most one plugin per target\n at dir \"review-mods\"\n Narrow the select of one plugin with exclude, or change the node's tags; the framework does not guess which plugin wins\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing).\n\n$ promptex build --install --unit platform-plugin-by-tags .\n✗ E219 The acme target declares plugins; platform plugins are an option of the builtin claude() and codex() targets only, and a third-party adapter does not receive them\n at the acme target settings of unit \"platform-plugin-by-tags\"\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1067
+ }
1068
+ ],
1069
+ "outcome": [
1070
+ "claude 時,review-flow 與 review-mods 放進 `.claude/skills/review-tools/`,專案範圍與使用者範圍都是這個相對位置。plugin 的 manifest 寫在 `.claude/skills/review-tools/.claude-plugin/plugin.json`,帶 name、version、description。",
1071
+ "`.claude/skills/review-tools/hooks/hooks.json` 的 modules 寫成 `[\"../src-mods/mods/main.ts\"]`。路徑相對 hooks.json 所在的 hooks/ 目錄。",
1072
+ "style-rule 沒被選到,照常各自安裝為 `.claude/rules/style-rule.md`。",
1073
+ "codex 時,同兩個節點放進 `plugins/review-tools/`,並寫 `.agents/plugins/marketplace.json`。codex 的宣告沒有 modules。",
1074
+ "反例一的 codex 宣告 modules,以 E214 阻斷。反例二的 review-flow 同時被 a-tools 與 b-tools 選到,以 E215 阻斷,框架不推測哪個優先(review-guard 與 review-mods 也各報一筆)。反例三的第三方目標帶 plugins,以 E219 阻斷。三個反例的結束碼都是 1。"
1075
+ ],
1076
+ "points": [
1077
+ "目標的 plugins[].select 只在單元 select 已選出的節點內分組。一個節點在同一目標最多屬於一個 plugin。被選進 plugin 的節點只裝在 plugin 裡,不再另外各自安裝。沒被選到的節點照常各自安裝。",
1078
+ "instruction、rule、permission 不能進 plugin,選到時以 E216 阻斷,用 select 的 exclude 排除。plugin 名稱和同單元各自安裝的 skill 同名時,以 E217 阻斷。",
1079
+ "codex 的 plugin 沒有 agent 元件。選進 plugin 的 agent 改照 codex 的安裝位置各自安裝,安裝報告的 degraded 列出這個節點,feature 是 `pluginComponent:agent`。",
1080
+ "select 一個節點都沒選到時照樣建置成功,產出只有 manifest 的空 plugin。",
1081
+ "modules 只屬於 claude,因為 mod 只存在於 Claude Code。路徑相對 plugin 根目錄,指向同一個 plugin 內 dir 節點的檔案。",
1082
+ "沒有節點引用的 dir 要宣告 `emit: 'always'` 才會隨 plugin 輸出,modules 才找得到檔案,否則建置以 E218 停下。範例的 review-mods 因此落在 plugin 根的 `src-mods/` 底下。",
1083
+ "目標層的 plugins 產出平台的 plugin 目錄,與單元層的 plugins(解析期擴充)是兩件事。",
1084
+ "defineTarget 的選項沒有 plugins。以物件字面值傳 plugins 會被 tsc 擋下(TS2353),用變數傳時不一定報錯。略過型別檢查或用變數傳時,plugins 會被直接丟掉,不會得 E219。第三方目標物件本身帶 plugins(例如在 defineTarget 的回傳值上展開再加)時,會在配置解析當下得 E219。"
1085
+ ],
1086
+ "facts": [
1087
+ "promptex.sdk.platform-claude",
1088
+ "promptex.sdk.platform-codex",
1089
+ "promptex.diagnostics.e214",
1090
+ "promptex.diagnostics.e215",
1091
+ "promptex.diagnostics.e219"
1092
+ ]
1093
+ }
1094
+ ]
1095
+ },
1096
+ {
1097
+ "id": "when-it-fails",
1098
+ "name": "失敗與診斷",
1099
+ "situation": "我寫錯或打錯時框架怎麼回應我",
1100
+ "claim": "失敗必須可見且可定位;靜默成功是最貴的失效,零產物與非零結束碼是硬保證。",
1101
+ "scenarios": [
1102
+ {
1103
+ "title": "斷鏈與重複 id 一次報齊",
1104
+ "situation": "拼錯了引用的 id,又不小心在兩個檔定義了同一個 id。",
1105
+ "code": [
1106
+ {
1107
+ "caption": "prompts/broken-ref.ts",
1108
+ "lang": "ts",
1109
+ "source": "import { defineSkill, useResource } from 'promptex-js'\n\n// 刻意拼錯 id(少一個字母):E101 斷鏈引用,附相近候選 commit-style。\nexport const brokenRef = defineSkill({\n id: 'broken-ref-holder',\n name: '斷鏈示範',\n config: { whenToUse: '示範用,不實際使用' },\n content: ['引用一個拼錯的識別:', useResource('commit-stlye')],\n})"
1110
+ },
1111
+ {
1112
+ "caption": "prompts/duplicate-a.ts",
1113
+ "lang": "ts",
1114
+ "source": "import { defineRule } from 'promptex-js'\n\n// E102 ID 重複之一:與 duplicate-b.ts 定義同一個 lint-rule。\nexport const lintRuleA = defineRule({\n id: 'lint-rule',\n name: 'Lint 規則(A 檔)',\n content: '第一份定義。',\n})"
1115
+ },
1116
+ {
1117
+ "caption": "prompts/duplicate-b.ts",
1118
+ "lang": "ts",
1119
+ "source": "import { defineRule } from 'promptex-js'\n\n// E102 ID 重複之二:與 duplicate-a.ts 定義同一個 lint-rule。\nexport const lintRuleB = defineRule({\n id: 'lint-rule',\n name: 'Lint 規則(B 檔)',\n content: '第二份定義,與 A 檔衝突。',\n})"
1120
+ },
1121
+ {
1122
+ "caption": "診斷輸出",
1123
+ "lang": "console",
1124
+ "source": "$ promptex build --install\n✗ E102 Duplicate ID:rule \"lint-rule\" is defined more than once\n at <project>/prompts/duplicate-a.ts:4\n at <project>/prompts/duplicate-b.ts:4\n✗ E101 Broken reference:useResource(\"commit-stlye\") has no definition\n at <project>/prompts/broken-ref.ts:8\n Close candidate:commit-style(defined at <project>/prompts/style.ts:4)\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1125
+ },
1126
+ {
1127
+ "caption": "加 --json 時的錯誤形狀",
1128
+ "lang": "console",
1129
+ "source": "$ promptex build --install --json\n{\"formatVersion\":\"1.2\",\"error\":{\"code\":\"RUN_DIAGNOSTICS_ABORT\",\"message\":\"Build failed: diagnostics include blocking items. 0 artifacts written (all or nothing).\",\"details\":{\"diagnostics\":[{\"code\":\"E102\",\"message\":\"Duplicate ID:rule \\\"lint-rule\\\" is defined more than once\",\"at\":[\"prompts/duplicate-a.ts:4\",\"prompts/duplicate-b.ts:4\"]},{\"code\":\"E101\",\"message\":\"Broken reference:useResource(\\\"commit-stlye\\\") has no definition\",\"at\":[\"prompts/broken-ref.ts:8\"],\"hint\":\"Close candidate:commit-style(defined at <project>/prompts/style.ts:4)\"}]}}}\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1130
+ }
1131
+ ],
1132
+ "outcome": [
1133
+ "`promptex build --install` 結束碼 1,先印 E102(lint-rule 重複,並列 `prompts/duplicate-a.ts:4` 與 `prompts/duplicate-b.ts:4`),再印 E101(commit-stlye 查無定義,附相近候選 commit-style 與它的定義位置)。",
1134
+ "最後一行是「Build failed: diagnostics include blocking items. 0 artifacts written (all or nothing).」,專案目錄沒有新增任何檔,不出現 .claude/ 也不出現所有權登記。",
1135
+ "加 `--json` 時錯誤碼是 RUN_DIAGNOSTICS_ABORT,兩筆診斷放在 details.diagnostics 裡,各自帶 code、message 與 at。"
1136
+ ],
1137
+ "points": [
1138
+ "求值期收集全部診斷才收場,不在第一個錯誤中止;每筆自帶位置,不依賴前後文。",
1139
+ "E102 並列兩處定義位置;E101 附編輯距離相近的候選與其定義位置。",
1140
+ "內容循環不在檢查之列:use* 建連結不展開,結構上無環可成。"
1141
+ ],
1142
+ "facts": [
1143
+ "promptex.diagnostics.e101",
1144
+ "promptex.diagnostics.e102",
1145
+ "promptex.cli.exit-codes",
1146
+ "promptex.formats.error"
1147
+ ]
1148
+ },
1149
+ {
1150
+ "title": "既無定義也無外部登記的引用",
1151
+ "situation": "引用了一個誰都沒宣告、也沒登記在 externals 的 id。",
1152
+ "code": [
1153
+ {
1154
+ "caption": "prompts/nodes.ts",
1155
+ "lang": "ts",
1156
+ "source": "import { defineRule, useSkill } from 'promptex-js'\n\n// 引用一個既不在本次求值的註冊表、也不在任何外部條目(registerExternal)中\n// 的技能:無本地定義、無近似候選、無外部登記,是斷鏈引用最單純的形態。本\n// 情境全庫沒有任何 registerExternal 呼叫,因此連「已裝在別處」這條退路都不\n// 成立,求值期直接中止。\nexport const preMergeChecklist = defineRule({\n id: 'pre-merge-checklist',\n name: '合併前檢查清單',\n content: [useSkill('nobody-declares-this')],\n})"
1157
+ },
1158
+ {
1159
+ "caption": "診斷輸出",
1160
+ "lang": "console",
1161
+ "source": "$ promptex build --install\n✗ E101 Broken reference:useSkill(\"nobody-declares-this\") has no definition\n at <project>/prompts/nodes.ts:10\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1162
+ }
1163
+ ],
1164
+ "outcome": [
1165
+ "結束碼 1,只印一筆 E101(useSkill(\"nobody-declares-this\") 查無定義)與 `prompts/nodes.ts:10` 一行位置,沒有相近候選那一行,不硬給建議。",
1166
+ "收尾同樣是 0 artifacts written,專案目錄沒有新增任何檔。"
1167
+ ],
1168
+ "points": [
1169
+ "斷鏈三形態:無定義無登記(E101 中止)、無定義但有相近候選(E101 附候選)、有定義但本次投影不產出(投影期報告、不中止)。",
1170
+ "第四種答案是在配置的 externals 登記落點:目標由別的來源安裝,引用照樣解析。"
1171
+ ],
1172
+ "facts": [
1173
+ "promptex.diagnostics.e101",
1174
+ "promptex.cli.exit-codes"
1175
+ ]
1176
+ },
1177
+ {
1178
+ "title": "兩類診斷共存時的零產物",
1179
+ "situation": "想確認建置失敗時目標端真的一個字都沒寫,而不是寫到一半。",
1180
+ "code": [
1181
+ {
1182
+ "caption": "prompts/duplicate-a.ts",
1183
+ "lang": "ts",
1184
+ "source": "import { defineResource } from 'promptex-js'\n\n// E102 ID 重複之一:與 duplicate-b.ts 定義同一個 commit-style。\nexport const commitStyleA = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例(A 檔)',\n content: '第一份定義。',\n})"
1185
+ },
1186
+ {
1187
+ "caption": "prompts/duplicate-b.ts",
1188
+ "lang": "ts",
1189
+ "source": "import { defineResource } from 'promptex-js'\n\n// E102 ID 重複之二:與 duplicate-a.ts 定義同一個 commit-style。\nexport const commitStyleB = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例(B 檔)',\n content: '第二份定義。',\n})"
1190
+ },
1191
+ {
1192
+ "caption": "prompts/broken-ref.ts",
1193
+ "lang": "ts",
1194
+ "source": "import { defineSkill, useResource } from 'promptex-js'\n\n// E101 斷鏈引用:拼錯 id(少一個字母)。它與 duplicate-a.ts/duplicate-b.ts\n// 的 ID 重複同時存在於本次求值中,三份宣告共用同一個識別 commit-style。\nexport const brokenRef = defineSkill({\n id: 'broken-ref-holder',\n name: '斷鏈示範',\n config: { whenToUse: '示範用,不實際使用' },\n content: ['引用一個拼錯的識別:', useResource('commit-stlye')],\n})"
1195
+ },
1196
+ {
1197
+ "caption": "同一個識別 commit-style 定義兩次、又被拼錯引用一次",
1198
+ "lang": "console",
1199
+ "source": "$ promptex build --install\n✗ E102 Duplicate ID:resource \"commit-style\" is defined more than once\n at <project>/prompts/duplicate-a.ts:4\n at <project>/prompts/duplicate-b.ts:4\n✗ E101 Broken reference:useResource(\"commit-stlye\") has no definition\n at <project>/prompts/broken-ref.ts:9\n Close candidate:commit-style(defined at <project>/prompts/duplicate-a.ts:4)\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1200
+ }
1201
+ ],
1202
+ "outcome": [
1203
+ "結束碼 1,印出 E102(resource commit-style 重複定義)與 E101(commit-stlye 查無定義,相近候選指向 `prompts/duplicate-a.ts:4`)。",
1204
+ "專案目錄沒有新增任何檔:查無 .claude/,也查無 .promptex/{單元}/claude.json,連「本次裝了哪些檔」的登記都沒產生。"
1205
+ ],
1206
+ "points": [
1207
+ "全有全無,任一阻斷級診斷存在,本次建置一個產物都不寫;退出碼只答「失敗」,零產物才答「沒被寫進東西」。"
1208
+ ],
1209
+ "facts": [
1210
+ "promptex.diagnostics.e101",
1211
+ "promptex.diagnostics.e102",
1212
+ "promptex.cli.exit-codes"
1213
+ ]
1214
+ },
1215
+ {
1216
+ "title": "rule 同時宣告 appliesTo 與 alwaysApply",
1217
+ "situation": "一份 rule 又填範圍載入又填常駐,想知道框架怎麼處理。",
1218
+ "code": [
1219
+ {
1220
+ "caption": "prompts/load-conflict.ts",
1221
+ "lang": "ts",
1222
+ "source": "import { defineRule } from 'promptex-js'\n\n// 刻意同時填範圍載入與常駐:E105 載入宣告衝突。兩者對「何時載入」給出矛盾\n// 的答案,框架判定互斥並在求值期中止,不靜默取其一。\nexport const scopedAndAlways = defineRule({\n id: 'scoped-and-always',\n name: '範圍與常駐並存',\n config: { appliesTo: ['docs/**/*.mdx'], alwaysApply: true },\n content: '本節點的載入宣告自相矛盾,求值期即被攔下。',\n})"
1223
+ },
1224
+ {
1225
+ "caption": "prompts/style.ts",
1226
+ "lang": "ts",
1227
+ "source": "import { defineRule } from 'promptex-js'\n\n// 三態各自合法的對照組:只填範圍載入,不觸發 E105。零產物是因為建置整批\n// 中止(全有全無),不是因為這份宣告本身有問題。\nexport const scopedOnly = defineRule({\n id: 'scoped-only',\n name: '僅範圍載入',\n config: { appliesTo: ['docs/**/*.mdx'] },\n content: '合法的範圍載入宣告。',\n})"
1228
+ },
1229
+ {
1230
+ "caption": "診斷輸出",
1231
+ "lang": "console",
1232
+ "source": "$ promptex build --install\n✗ E105 Load declaration conflict:rule \"scoped-and-always\" declares both appliesTo and alwaysApply\n at <project>/prompts/load-conflict.ts:5\n Remove one of them:keep only appliesTo for scoped loading, keep only alwaysApply for always-on loading, remove both to install without loading automatically\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1233
+ }
1234
+ ],
1235
+ "outcome": [
1236
+ "結束碼 1,印出 E105(rule \"scoped-and-always\" 同時宣告 appliesTo 與 alwaysApply),位置是 `prompts/load-conflict.ts:5`,並說明三種改法:只留 appliesTo、只留 alwaysApply、兩者皆刪。",
1237
+ "另一個檔 `prompts/style.ts` 裡合法的 rule scoped-only 也沒有產出,專案目錄沒有新增任何檔:零產物是整批中止的結果。"
1238
+ ],
1239
+ "points": [
1240
+ "載入三態:只填 appliesTo(範圍載入)、只填 alwaysApply(常駐)、兩者皆不填(安裝但不自動載入,供顯式引用)。",
1241
+ "求值期就攔,不替作者選:靜默取其一會產出看起來正常、行為與宣告不符的產物。",
1242
+ "`alwaysApply: false` 不算衝突;plugin 於改寫遍補上另一欄位時衝突也在解析遍被抓到。"
1243
+ ],
1244
+ "facts": [
1245
+ "promptex.diagnostics.e105",
1246
+ "promptex.cli.exit-codes"
1247
+ ]
1248
+ },
1249
+ {
1250
+ "title": "作者程式碼在求值當下炸開",
1251
+ "situation": "宣告檔裡對 undefined 呼叫方法、或任何執行期例外。",
1252
+ "code": [
1253
+ {
1254
+ "caption": "prompts/nodes.ts",
1255
+ "lang": "ts",
1256
+ "source": "// 作者的宣告碼在求值當下失敗:對照表沒有這一筆,取值取到 undefined 之後\n// 立刻炸開。求值即執行作者的程式碼(框架不設沙盒),這是它最常見的失敗\n// 形態 ── 不是框架語彙的診斷,而是執行環境自己的例外。\nimport { defineSkill } from 'promptex-js'\n\n/** 團隊對照表:刻意不含 ops 這一筆。 */\nconst OWNERS: Record<string, string> = { platform: '平台組', docs: '文件組' }\n\n// `OWNERS.ops` 是 undefined,`.trim()` 因此拋 TypeError。節點宣告連呼叫都沒\n// 呼叫到,本次求值一個節點都沒登記。\ndefineSkill({\n id: 'deploy-flow',\n name: '部署流程',\n config: { whenToUse: `由 ${OWNERS.ops!.trim()} 維護` },\n content: '確認 CI 全綠後執行部署。',\n})"
1257
+ },
1258
+ {
1259
+ "caption": "診斷輸出與 --json 形狀",
1260
+ "lang": "console",
1261
+ "source": "$ promptex build --install\n✗ Load failed: <project>/prompts/nodes.ts\n Cannot read properties of undefined (reading 'trim')\nBuild failed: source loading failed. 0 artifacts written (all or nothing).\n\n$ promptex build --install --json\n{\"formatVersion\":\"1.2\",\"error\":{\"code\":\"RUN_SOURCE_LOAD_FAILED\",\"message\":\"Failed to load the aggregate file: <project>/prompts/nodes.ts\",\"details\":{\"path\":\"<project>/prompts/nodes.ts\",\"detail\":\"Cannot read properties of undefined (reading 'trim')\"}}}\nFailed to load the aggregate file: <project>/prompts/nodes.ts"
1262
+ }
1263
+ ],
1264
+ "outcome": [
1265
+ "結束碼 1,印出「Load failed:」加檔案的完整路徑,下一行縮排接原始錯誤「Cannot read properties of undefined (reading 'trim')」,收尾是「Build failed: source loading failed. 0 artifacts written (all or nothing).」,沒有 E 碼。",
1266
+ "加 `--json` 時錯誤碼是 RUN_SOURCE_LOAD_FAILED,details 帶 path 與 detail。",
1267
+ "安裝根整個未產生,也查無 .promptex/{單元}/,專案目錄沒有新增任何檔。"
1268
+ ],
1269
+ "points": [
1270
+ "求值即執行作者程式碼,框架不設沙盒:換得宿主語言全部表達力,代價是執行期錯誤發生在求值當下。",
1271
+ "例外被整理成人讀診斷(「Load failed:」前綴、縮排續行、收尾摘要),不放裸堆疊;這類失敗不帶診斷碼。"
1272
+ ],
1273
+ "facts": [
1274
+ "promptex.cli.errors-run",
1275
+ "promptex.cli.exit-codes",
1276
+ "promptex.formats.error"
1277
+ ]
1278
+ },
1279
+ {
1280
+ "title": "命令列敲錯時得到指名問題的回應",
1281
+ "situation": "旗標拼錯、多打一個參數、帶值旗標缺值、源碼目錄不存在。",
1282
+ "code": [
1283
+ {
1284
+ "caption": "promptex.config.ts",
1285
+ "lang": "ts",
1286
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\n// 單元名宣告為 main:演練以 `--unit main` 篩選,那個旗標的值必須真的被讀到\n// 才選得中唯一那個單元。解析若把旗標值當成位置參數收下,這一步就選不到任何\n// 單元,產物也就建不出來。\nexport default defineConfig([\n {\n name: 'main',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude()],\n },\n])"
1287
+ },
1288
+ {
1289
+ "caption": "prompts/nodes.ts",
1290
+ "lang": "ts",
1291
+ "source": "import { defineRule } from 'promptex-js'\n\nexport const componentNaming = defineRule({\n id: 'component-naming',\n name: '元件命名',\n config: { alwaysApply: true },\n content: '元件檔名採 PascalCase,與匯出的元件同名。',\n})"
1292
+ },
1293
+ {
1294
+ "caption": "九則輸入與回應",
1295
+ "lang": "console",
1296
+ "source": "$ promptex build --install --unit main .\n✓ main: metadata written (graph + content)\n── main → claude (<project>) ──\n wrote .claude/rules/component-naming.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/component-naming.md\n\n$ promptex manifest --unit main --platform claude .\n── main → claude (<project>) ──\n{\n \"formatVersion\": \"1.2\",\n \"platform\": \"claude\",\n \"engineVersion\": \"<version>\",\n \"unit\": \"main\",\n \"installRoot\": \".\",\n \"supported\": true,\n \"nodes\": [\n {\n \"kind\": \"rule\",\n \"id\": \"component-naming\",\n \"name\": \"元件命名\",\n \"path\": \".claude/rules/component-naming.md\"\n }\n ],\n \"files\": [\n {\n \"path\": \".claude/rules/component-naming.md\",\n \"kind\": \"text\"\n }\n ]\n}\n\n$ promptex build --install . --platfrm codex\nUnknown flag: --platfrm\nUsage: promptex build [dir] [--install [--dry-run [--content]] | --export <export-dir>] [--watch] [--out <install-root>] [--unit <unit>] [--json] [--locale <locale>]\n promptex manifest [dir] [--platform <platform>] [--unit <unit>] [--json] [--locale <locale>]\n promptex init [dir] [--lang ts|python|rust] [--yes] (deprecated, removed in 2.0; use create-promptex-js instead)\n promptex config <read|write> [dir] [--ops <JSON>] [--apply]\n promptex config declare [dir] <package-name>\n promptex version [--json]\n\n [dir] defaults to the current directory. build alone writes framework metadata only; --install writes the artifacts and the ownership registry; --install --dry-run writes nothing and lists the changes; --export <export-dir> writes the same tree install would write under <export-dir>/<unit>/ without a registry; --watch reruns the same flags on change. --content rides with --install --dry-run --json and carries the bytes this install would write.\n\n$ promptex build --install . extra-arg\nSurplus positional argument: extra-arg\nUsage: promptex build [dir] [--install [--dry-run [--content]] | --export <export-dir>] [--watch] [--out <install-root>] [--unit <unit>] [--json] [--locale <locale>]\n promptex manifest [dir] [--platform <platform>] [--unit <unit>] [--json] [--locale <locale>]\n promptex init [dir] [--lang ts|python|rust] [--yes] (deprecated, removed in 2.0; use create-promptex-js instead)\n promptex config <read|write> [dir] [--ops <JSON>] [--apply]\n promptex config declare [dir] <package-name>\n promptex version [--json]\n\n [dir] defaults to the current directory. build alone writes framework metadata only; --install writes the artifacts and the ownership registry; --install --dry-run writes nothing and lists the changes; --export <export-dir> writes the same tree install would write under <export-dir>/<unit>/ without a registry; --watch reruns the same flags on change. --content rides with --install --dry-run --json and carries the bytes this install would write.\n\n$ promptex build --install . --platform\n--platform is missing its value\n\n$ promptex manifest . --platform clawde\nConfig file <project>/promptex.config.ts has no platform named \"clawde\" (built-in: claude, codex; also declared by this config: claude). The query command answers for a single platform, so a mistyped name yields an empty list meaning \"not installed there\", indistinguishable from genuinely having no target configured for it\n\n$ promptex build --install no-source-dir\n✗ E108 Source directory does not exist:<project>/no-source-dir/prompts has no such directory\n at <project>/no-source-dir/prompts\n Evaluation starts from the aggregate files under this directory:create the directory and put aggregate files in it, or point the config's srcDir (in single-directory mode, the project root given on the command line) at where the aggregate files actually are\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing).\n\n$ promptex build --install source-dir-is-file\n✗ E109 Source directory read failed:<project>/source-dir-is-file/prompts was found but its content could not be listed (Not a directory (os error 20))\n at <project>/source-dir-is-file/prompts\n The source directory exists but this evaluation could not list its aggregate files:for insufficient permissions, widen the read permission; if the path points at a file, point it at a directory instead\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing).\n\n$ promptex build --install empty-source-dir\n✓ Metadata written (graph + content)\nInstall report (claude)\n Projected artifacts, 0:"
1297
+ }
1298
+ ],
1299
+ "outcome": [
1300
+ "旗標寫在位置參數之前的 `--unit main .` 結束碼 0,寫出 `.claude/rules/component-naming.md`;`manifest --unit main --platform claude .` 結束碼 0,輸出裡的 unit 是 main,表示兩個帶值旗標的值都被讀到。",
1301
+ "`--platfrm` 印「Unknown flag: --platfrm」,`extra-arg` 印「Surplus positional argument: extra-arg」,這兩則後面接著 usage 說明,結束碼都是 1。",
1302
+ "`--platform` 缺值印「--platform is missing its value」,結束碼 1。`manifest . --platform clawde` 印出沒有名為 \"clawde\" 的平台並列出內建的 claude, codex,結束碼 1。",
1303
+ "`no-source-dir` 印 E108,指名找的路徑 prompts,並給出建目錄與改指 srcDir 兩條修法;`source-dir-is-file` 印 E109,兩者結束碼都是 1,`no-source-dir/` 下沒有新增任何檔。",
1304
+ "`empty-source-dir` 結束碼 0,報告是 Projected artifacts, 0:空目錄是合法的中間狀態。"
1305
+ ],
1306
+ "points": [
1307
+ "靜默忽略的共同後果是命令以預設值「成功」跑完,作者拿到看起來正常但不是他要的產物。",
1308
+ "`--platform` 只被單一目錄模式與 manifest 消費,配置模式下的 build 一律以各單元的 targets 為準。"
1309
+ ],
1310
+ "facts": [
1311
+ "promptex.cli.errors-usage",
1312
+ "promptex.cli.exit-codes",
1313
+ "promptex.cli.build",
1314
+ "promptex.cli.manifest",
1315
+ "promptex.diagnostics.e108",
1316
+ "promptex.diagnostics.e109"
1317
+ ]
1318
+ },
1319
+ {
1320
+ "title": "失敗的建置不動既有安裝",
1321
+ "situation": "目標端已裝著上一次成功的內容,這次建置壞了,會不會污染它。",
1322
+ "code": [
1323
+ {
1324
+ "caption": "prompts/nodes.ts",
1325
+ "lang": "ts",
1326
+ "source": "import { defineSkill } from 'promptex-js'\n\nexport const reviewHelper = defineSkill({\n id: 'review-helper',\n name: '審查助手',\n config: { whenToUse: '進行 code review 時使用' },\n content: '依序檢查正確性、安全性、可讀性三個面向。',\n})"
1327
+ },
1328
+ {
1329
+ "caption": "variants/prompts/broken.ts",
1330
+ "lang": "ts",
1331
+ "source": "import { defineSkill, useResource } from 'promptex-js'\n\nexport const brokenCaller = defineSkill({\n id: 'broken-caller',\n name: '斷鏈示範',\n config: { whenToUse: '示範用,不實際使用' },\n content: [useResource('nonexistent-glossary')],\n})"
1332
+ },
1333
+ {
1334
+ "caption": "先成功一輪,再加入斷鏈宣告重跑",
1335
+ "lang": "console",
1336
+ "source": "$ promptex build --install\n✓ failed-build-preserves-target: metadata written (graph + content)\n── failed-build-preserves-target → claude (<project>) ──\n wrote .claude/skills/review-helper/SKILL.md\nInstall report (claude)\n Projected artifacts, 1: .claude/skills/review-helper/SKILL.md\n Notices, 1:\n • skill \"review-helper\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n\n$ promptex build --install .\n✗ E101 Broken reference:useResource(\"nonexistent-glossary\") has no definition\n at <project>/prompts/broken.ts:7\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1337
+ }
1338
+ ],
1339
+ "outcome": [
1340
+ "第一輪結束碼 0,寫出 `.claude/skills/review-helper/SKILL.md` 與所有權登記 `.promptex/failed-build-preserves-target/claude.json`。",
1341
+ "加入 `prompts/broken.ts` 後第二輪結束碼 1,印出 E101(nonexistent-glossary 查無定義)與 0 artifacts written。",
1342
+ "兩輪結束後,SKILL.md 與 claude.json 仍是第一輪寫出的內容,登記裡沒有 broken-caller:失敗那一次沒有動目標端。"
1343
+ ],
1344
+ "points": [
1345
+ "零產物的保證同時涵蓋目標端原本為空與原本有內容兩種處境;比校驗和而非比存在與否。"
1346
+ ],
1347
+ "facts": [
1348
+ "promptex.diagnostics.e101",
1349
+ "promptex.formats.ownership-registry",
1350
+ "promptex.cli.exit-codes"
1351
+ ]
1352
+ }
1353
+ ]
1354
+ },
1355
+ {
1356
+ "id": "shipping",
1357
+ "name": "分發與安裝",
1358
+ "situation": "我要裝進真實專案並持續同步",
1359
+ "claim": "所有權登記是安裝生命週期的真相來源;框架只動自己寫過的東西,失敗與預覽都不寫檔。",
1360
+ "scenarios": [
1361
+ {
1362
+ "title": "多配置單元的內容、位置與投影目標",
1363
+ "situation": "一個倉庫有多份提示詞源碼,各自裝到不同落點、不同平台。",
1364
+ "code": [
1365
+ {
1366
+ "caption": "promptex.config.ts",
1367
+ "lang": "ts",
1368
+ "source": "import { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { claude, codex, defineConfig } from 'promptex-js'\n\nconst here = fileURLToPath(new URL('.', import.meta.url))\n\nexport default defineConfig([\n { name: 'global', srcDir: './prompts/global', scope: 'user', outDir: join(here, 'home'), targets: [claude(), codex()] },\n { name: 'web', srcDir: './apps/web/prompts', select: { tags: ['domain/frontend'], exclude: ['status/draft'] }, outDir: join(here, 'apps/web'), targets: [claude()] },\n { name: 'api', srcDir: './apps/api/prompts', outDir: join(here, 'apps/api'), targets: [claude()] },\n { name: 'api-external', srcDir: './apps/api/prompts', outDir: join(here, 'external-project'), targets: [claude()] }, // 同源另一落點\n])"
1369
+ },
1370
+ {
1371
+ "caption": "單次安裝的輸出",
1372
+ "lang": "console",
1373
+ "source": "$ promptex build --install\n✓ global: metadata written (graph + content)\n── global → claude (<project>/home) ──\n wrote .claude/rules/zh-tw-writing.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/zh-tw-writing.md\n── global → codex (<project>/home) ──\n wrote .codex/AGENTS.md\nInstall report (codex)\n Projected artifacts, 1: .codex/AGENTS.md\n✓ web: metadata written (graph + content)\n── web → claude (<project>/apps/web) ──\n wrote .claude/rules/component-naming.md\n wrote .claude/rules/style-review.md\nInstall report (claude)\n Projected artifacts, 2: .claude/rules/component-naming.md, .claude/rules/style-review.md\n✓ api: metadata written (graph + content)\n── api → claude (<project>/apps/api) ──\n wrote .claude/rules/endpoint-naming.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/endpoint-naming.md\n✓ api-external: metadata written (graph + content)\n── api-external → claude (<project>/external-project) ──\n wrote .claude/rules/endpoint-naming.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/endpoint-naming.md"
1374
+ }
1375
+ ],
1376
+ "outcome": [
1377
+ "單次 install 依陣列順序處理四個單元,輸出依序是 global、web、api、api-external,各自寫進自己的落點。",
1378
+ "每個單元的登記放在自己的落點下,依單元與平台分檔,例如 `home/.promptex/global/claude.json`、`home/.promptex/global/codex.json` 與 `external-project/.promptex/api-external/claude.json`。",
1379
+ "global 單元同一份源碼投影成 claude 與 codex 兩種格式,產出 `home/.claude/rules/zh-tw-writing.md` 與 `home/.codex/AGENTS.md`。",
1380
+ "web 的四個節點只裝兩個,產出 `apps/web/.claude/rules/component-naming.md` 與 `apps/web/.claude/rules/style-review.md`:status/draft 被 exclude 擋下、domain/backend 未命中 tags。",
1381
+ "api 與 api-external 共用同一份源碼,兩個落點各有一份:`apps/api/.claude/rules/endpoint-naming.md` 與 `external-project/.claude/rules/endpoint-naming.md`。"
1382
+ ],
1383
+ "points": [
1384
+ "一個單元回答三段語意:裝什麼(srcDir 加 select)、裝到哪(outDir 加 scope)、投影成什麼(targets)。同一份源碼裝到兩個落點就寫兩個單元。",
1385
+ "outDir 用 `join(here, ...)` 算出:配置是可執行的 TypeScript,中性資料格式表達不了。",
1386
+ "select.tags 前綴匹配、多值 OR。exclude 優先於 tags;select 只篩能力類型,素材不受影響。"
1387
+ ],
1388
+ "facts": [
1389
+ "promptex.sdk.config",
1390
+ "promptex.sdk.define-config",
1391
+ "promptex.sdk.platform-claude",
1392
+ "promptex.sdk.platform-codex",
1393
+ "promptex.formats.ownership-registry-path"
1394
+ ]
1395
+ },
1396
+ {
1397
+ "title": "不宣告 name 或 outDir 時的推導",
1398
+ "situation": "想知道省略單元名與落點會發生什麼。",
1399
+ "code": [
1400
+ {
1401
+ "caption": "promptex.config.ts",
1402
+ "lang": "ts",
1403
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\nexport default defineConfig([\n { srcDir: './prompts', select: { tags: ['domain/frontend'] }, targets: [claude()] },\n { srcDir: './prompts', select: { tags: ['domain/backend'] }, targets: [claude()] },\n])"
1404
+ },
1405
+ {
1406
+ "caption": "安裝輸出(警告走標準錯誤,結束碼仍為 0)",
1407
+ "lang": "console",
1408
+ "source": "$ promptex build --install\n✓ unit-0: metadata written (graph + content)\n── unit-0 → claude (<project>) ──\n wrote .claude/rules/component-naming.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/component-naming.md\n✓ unit-1: metadata written (graph + content)\n── unit-1 → claude (<project>) ──\n wrote .claude/rules/endpoint-naming.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/endpoint-naming.md\n※ 單元 \"unit-0\" 未宣告 name:名字由陣列位置推導,重排或插入單元即等同改名,所有權登記與框架中繼(.promptex/unit-0/)隨之錯位,留下孤兒登記或誤刪他人產物;補上 name 讓身分與順序脫鉤\n※ 單元 \"unit-1\" 未宣告 name:名字由陣列位置推導,重排或插入單元即等同改名,所有權登記與框架中繼(.promptex/unit-1/)隨之錯位,留下孤兒登記或誤刪他人產物;補上 name 讓身分與順序脫鉤"
1409
+ }
1410
+ ],
1411
+ "outcome": [
1412
+ "單元名推導為 `unit-{索引}`,登記與框架中繼分別留在 `.promptex/unit-0/` 與 `.promptex/unit-1/`。",
1413
+ "兩個單元的落點都是專案根,輸出標頭寫 `(<project>)`;兩個單元共用專案根,各自只裝自己 select 命中的規範。",
1414
+ "每個未具名的單元各印一行警告「單元 \"unit-0\" 未宣告 name」,安裝照常完成,結束碼 0。"
1415
+ ],
1416
+ "points": [
1417
+ "省略 name 時框架仍照常安裝,只在標準錯誤印出警告,結束碼仍是 0:不中止建置,隱患留到下次重排單元才爆。",
1418
+ "未宣告 scope 且 outDir 不是家目錄相對路徑時,scope 推導為 project。"
1419
+ ],
1420
+ "facts": [
1421
+ "promptex.sdk.config",
1422
+ "promptex.sdk.define-config"
1423
+ ]
1424
+ },
1425
+ {
1426
+ "title": "安裝期依登記移除舊產物,不碰使用者的東西",
1427
+ "situation": "刪了節點或改了落點,舊產物要被清掉,但我自己放進 .claude/ 的檔案不能動。",
1428
+ "code": [
1429
+ {
1430
+ "caption": "prompts/nodes.ts",
1431
+ "lang": "ts",
1432
+ "source": "// 當前版:刪掉 delete-me、old-rule 與 deny-rm 三個節點,且 style 資源的落點\n// 由隨附(bundled,預設)改為共用(shared)。\nimport { defineResource, defineSkill, useResource } from 'promptex-js'\n\ndefineResource({ id: 'style', name: '樣式指引', content: '樣式內容' })\n\ndefineSkill({\n id: 'keep-me',\n name: '保留的流程',\n meta: { resources: 'shared' },\n config: { whenToUse: '處理 A 類任務時' },\n content: ['流程內容。', useResource('style')],\n})"
1433
+ },
1434
+ {
1435
+ "caption": ".claude/settings.json",
1436
+ "lang": "json",
1437
+ "source": "{ \"model\": \"opus\" }"
1438
+ },
1439
+ {
1440
+ "caption": "安裝當前版的輸出",
1441
+ "lang": "console",
1442
+ "source": "$ promptex build --install\n✓ install-prune: metadata written (graph + content)\n── install-prune → claude (<project>) ──\n wrote .claude/resources/style.md\n wrote .claude/skills/keep-me/SKILL.md\n removed .claude/rules/old-rule.md\n removed .claude/skills/delete-me/SKILL.md\n removed .claude/skills/delete-me/style.md\n removed .claude/skills/keep-me/style.md\n cleared .claude/settings.json\nInstall report (claude)\n Projected artifacts, 2: .claude/resources/style.md, .claude/skills/keep-me/SKILL.md\n Notices, 1:\n • skill \"keep-me\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description"
1443
+ }
1444
+ ],
1445
+ "outcome": [
1446
+ "專案先裝過上一版(多了 delete-me、old-rule、deny-rm 三個節點),再裝當前版。當前版輸出的 `removed` 行逐一列出上一版才有的獨佔檔,包含已刪節點的產物與舊落點的 `removed .claude/skills/keep-me/style.md`。",
1447
+ "資源 style 的落點改為共用後,新落點 `.claude/resources/style.md` 以 `wrote` 寫出。",
1448
+ "共享檔 `.claude/settings.json` 的輸出是 `cleared`,檔案留著,使用者自己寫的 `model` 鍵保留。",
1449
+ "使用者手動建立的 `.claude/skills/my-manual/SKILL.md` 不在登記中,兩輪輸出都沒有提到它。",
1450
+ "登記 `.promptex/install-prune/claude.json` 更新成只剩 `.claude/resources/style.md` 與 `.claude/skills/keep-me/SKILL.md`。"
1451
+ ],
1452
+ "points": [
1453
+ "移除範圍是上次登記與本次產出的差集;登記入版控,中繼不入。登記遺失時框架無從得知舊產物,只能手動清。"
1454
+ ],
1455
+ "facts": [
1456
+ "promptex.formats.ownership-registry",
1457
+ "promptex.formats.ownership-registry-path"
1458
+ ]
1459
+ },
1460
+ {
1461
+ "title": "安裝預覽:五種變更標記,一個檔案都不寫",
1462
+ "situation": "安裝會刪檔、改共享檔,想在動手前看清楚會發生什麼。",
1463
+ "code": [
1464
+ {
1465
+ "caption": "預覽輸出",
1466
+ "lang": "console",
1467
+ "source": "$ promptex build --install --dry-run .\n── dry-run → claude (<project>) ──\n + .claude/skills/brand-new/SKILL.md\n M .claude/skills/keep-me/style.md\n = .claude/rules/stable-rule.md\n = .claude/skills/keep-me/SKILL.md\n − .claude/rules/old-rule.md\n ~ .claude/settings.json (cleared the framework-managed range)"
1468
+ },
1469
+ {
1470
+ "caption": "缺 --install 時",
1471
+ "lang": "console",
1472
+ "source": "$ promptex build --dry-run .\n✗ --dry-run needs --install: a dry run is the zero-write rehearsal of an install, and build alone already writes nothing but framework metadata. Add --install to rehearse the install, or drop --dry-run."
1473
+ },
1474
+ {
1475
+ "caption": "實際安裝",
1476
+ "lang": "console",
1477
+ "source": "$ promptex build --install .\n✓ dry-run: metadata written (graph + content)\n── dry-run → claude (<project>) ──\n wrote .claude/rules/stable-rule.md\n wrote .claude/skills/brand-new/SKILL.md\n wrote .claude/skills/keep-me/SKILL.md\n wrote .claude/skills/keep-me/style.md\n removed .claude/rules/old-rule.md\n cleared .claude/settings.json\nInstall report (claude)\n Projected artifacts, 4: .claude/rules/stable-rule.md, .claude/skills/brand-new/SKILL.md, .claude/skills/keep-me/SKILL.md, .claude/skills/keep-me/style.md\n Notices, 2:\n • skill \"brand-new\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n • skill \"keep-me\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description"
1478
+ }
1479
+ ],
1480
+ "outcome": [
1481
+ "專案先裝過上一版,預覽把本次與目標端現況的差異逐檔標出:`+` 新增、`M` 改變、`=` 未變、`−` 移除獨佔檔、`~` 清空共享檔的框架管理範圍。",
1482
+ "連跑兩次預覽,輸出逐字相同;第二次仍把 brand-new 標成 `+`,表示第一次預覽沒有寫檔。",
1483
+ "實際安裝的 `wrote` 4 個、`removed` 1 個、`cleared` 1 個,與預覽的 `+`、`M`、`=` 合計 4 個、`−` 1 個、`~` 1 個逐項一致。",
1484
+ "只寫 `--dry-run` 不帶 `--install` 時,結束碼為 1,訊息說明預覽必須搭配 `--install`。"
1485
+ ],
1486
+ "points": [
1487
+ "五種標記:`+` 新增、`M` 改變、`=` 未變、`−` 移除獨佔檔、`~` 清空共享檔的框架管理範圍。",
1488
+ "預覽與安裝走同一條計算路徑,共享檔的最終內容真的算一次合併結果。",
1489
+ "加 `--content --json` 帶出這次會寫入的位元組,供畫差異的消費端使用。"
1490
+ ],
1491
+ "facts": [
1492
+ "promptex.formats.install-dry",
1493
+ "promptex.cli.build",
1494
+ "promptex.cli.flag-requires"
1495
+ ]
1496
+ },
1497
+ {
1498
+ "title": "匯出與安裝逐位元組相同的產物樹",
1499
+ "situation": "不想動落點,只想看渲染結果,或把整棵樹交給別的工具。",
1500
+ "code": [
1501
+ {
1502
+ "caption": "promptex.config.ts",
1503
+ "lang": "ts",
1504
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\nexport default defineConfig([\n { name: 'app', srcDir: './prompts', outDir: '.', targets: [claude()] },\n { name: 'home', srcDir: './prompts', scope: 'user', targets: [claude()] },\n])"
1505
+ },
1506
+ {
1507
+ "caption": "匯出",
1508
+ "lang": "console",
1509
+ "source": "$ promptex build --export tmp/out .\n── app → claude (<project>/tmp/out/app) ──\n exported <project>/tmp/out/app/.claude/hooks/write-gate/gate.mjs\n exported <project>/tmp/out/app/.claude/rules/style.md\n exported <project>/tmp/out/app/.claude/settings.json\n exported <project>/tmp/out/app/.claude/skills/review/SKILL.md\n exported <project>/tmp/out/app/.promptex/app/content-index.json\n exported <project>/tmp/out/app/.promptex/app/content.json\n exported <project>/tmp/out/app/.promptex/app/graph.json\nInstall report (claude)\n Projected artifacts, 4: .claude/hooks/write-gate/gate.mjs, .claude/rules/style.md, .claude/settings.json, .claude/skills/review/SKILL.md\n Notices, 1:\n • skill \"review\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n── home → claude (<project>/tmp/out/home) ──\n exported <project>/tmp/out/home/.claude/hooks/write-gate/gate.mjs\n exported <project>/tmp/out/home/.claude/rules/style.md\n exported <project>/tmp/out/home/.claude/settings.json\n exported <project>/tmp/out/home/.claude/skills/review/SKILL.md\n exported <project>/tmp/out/home/.promptex/home/content-index.json\n exported <project>/tmp/out/home/.promptex/home/content.json\n exported <project>/tmp/out/home/.promptex/home/graph.json\nInstall report (claude)\n Projected artifacts, 4: .claude/hooks/write-gate/gate.mjs, .claude/rules/style.md, .claude/settings.json, .claude/skills/review/SKILL.md\n Notices, 1:\n • skill \"review\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description"
1510
+ }
1511
+ ],
1512
+ "outcome": [
1513
+ "先只安裝 app 單元,再匯出兩個單元:`tmp/out/app/` 的檔案與安裝根逐檔位元組相同,只少了登記 `.promptex/app/claude.json`。",
1514
+ "hook 指令的素材錨點依單元範圍:app 是 `$CLAUDE_PROJECT_DIR/...`(見 `tmp/out/app/.claude/settings.json`),home 是 `$HOME/...`(見 `tmp/out/home/.claude/settings.json`)。",
1515
+ "home 單元只匯出不安裝,只出現在 `tmp/out/home/`。",
1516
+ "先匯出上一版(多一條 old-rule)、再放入作者自己的 `tmp/out/notes.md`、最後匯出當前版:上一版才有的 old-rule 不在最後的子樹裡,`tmp/out/notes.md` 還在。"
1517
+ ],
1518
+ "points": [
1519
+ "重複匯出先清該單元子樹再寫入,上一輪才有的檔案不殘留;匯出目錄內作者自己的檔案不受影響。",
1520
+ "使用者層單元可以只匯出不安裝,家目錄一個位元組都不動。"
1521
+ ],
1522
+ "facts": [
1523
+ "promptex.cli.build",
1524
+ "promptex.cli.flag-conflicts",
1525
+ "promptex.cli.flag-requires"
1526
+ ]
1527
+ },
1528
+ {
1529
+ "title": "手寫 externals 引用不由本單元產出的目標",
1530
+ "situation": "要引用別人管的資源,例如非 promptex 專案的手寫檔、只有裝好產物的倉庫。",
1531
+ "code": [
1532
+ {
1533
+ "caption": "promptex.config.ts",
1534
+ "lang": "ts",
1535
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\nexport default defineConfig({\n name: 'app',\n srcDir: './prompts',\n outDir: './app',\n targets: [claude()],\n externals: [\n { kind: 'skill', id: 'vendor-lint', name: '供應商 lint 檢查', path: '../vendor-dist-repo/.claude/skills/vendor-lint/SKILL.md' },\n { kind: 'skill', id: 'legacy-guide', name: '既有指南(手動維護)', path: '../manual-resources/.claude/skills/legacy-guide/SKILL.md' },\n { kind: 'skill', id: 'retired-guide', name: '已退役指南', path: '../manual-resources/.claude/skills/retired-guide/SKILL.md' }, // 目標已不存在\n ],\n})"
1536
+ },
1537
+ {
1538
+ "caption": "prompts/nodes.ts",
1539
+ "lang": "ts",
1540
+ "source": "// 四個引用,源碼看不出目標歸誰管:review-flow 是本地定義,vendor-lint/\n// legacy-guide/retired-guide 都是外部登記(見 promptex.config.ts 的\n// externals),但這裡的寫法完全相同,都是 useSkill(id)。\nimport { defineRule, defineSkill, useSkill } from 'promptex-js'\n\ndefineSkill({\n id: 'review-flow',\n name: '合併前審查流程',\n config: { whenToUse: '準備合併變更前使用' },\n content: '依序核對測試、文件與變更說明是否齊備。',\n})\n\ndefineRule({\n id: 'pre-merge-checklist',\n name: '合併前檢查清單',\n content: [\n '合併前需完成以下檢查:',\n useSkill('review-flow'),\n useSkill('vendor-lint'),\n useSkill('legacy-guide'),\n useSkill('retired-guide'),\n ],\n})"
1541
+ },
1542
+ {
1543
+ "caption": "安裝輸出",
1544
+ "lang": "console",
1545
+ "source": "$ promptex build --install\n✓ app: metadata written (graph + content)\n── app → claude (<project>/app) ──\n wrote .claude/rules/pre-merge-checklist.md\n wrote .claude/skills/review-flow/SKILL.md\n External reference check:\n ✓ legacy-guide → ../manual-resources/.claude/skills/legacy-guide/SKILL.md (source: hand-written entry, no version to check)\n ✗ (path does not exist) retired-guide → ../manual-resources/.claude/skills/retired-guide/SKILL.md (source: hand-written entry, no version to check)\n ✓ vendor-lint → ../vendor-dist-repo/.claude/skills/vendor-lint/SKILL.md (source: hand-written entry, no version to check)\nInstall report (claude)\n Projected artifacts, 2: .claude/rules/pre-merge-checklist.md, .claude/skills/review-flow/SKILL.md\n Notices, 1:\n • skill \"review-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description"
1546
+ }
1547
+ ],
1548
+ "outcome": [
1549
+ "產物 `app/.claude/rules/pre-merge-checklist.md` 內的連結指向登記的路徑,例如 `[供應商 lint 檢查](../../../vendor-dist-repo/.claude/skills/vendor-lint/SKILL.md)`。",
1550
+ "外部目標不被複製進本單元產物,`app/.claude/skills/` 只有本地的 review-flow。",
1551
+ "安裝報告的「External reference check」逐筆標「hand-written entry, no version to check」。",
1552
+ "retired-guide 的登記路徑實際不存在,報告標 `(path does not exist)`,安裝照常完成、結束碼 0。"
1553
+ ],
1554
+ "points": [
1555
+ "path 相對本單元的安裝根,可用 `../` 跳出安裝根;正確性由作者負責。",
1556
+ "求值期核對「目標在註冊表或 externals」(否則 E101),安裝期核對「路徑實際存在」(否則列缺項不阻斷)。"
1557
+ ],
1558
+ "facts": [
1559
+ "promptex.sdk.config",
1560
+ "promptex.sdk.use-refs"
1561
+ ]
1562
+ },
1563
+ {
1564
+ "title": "免手寫的兩種落點來源:兄弟單元與另一個專案",
1565
+ "situation": "要引用同配置另一單元的節點、或另一個可在本地執行的 promptex 專案的節點。",
1566
+ "code": [
1567
+ {
1568
+ "caption": "promptex.config.ts",
1569
+ "lang": "ts",
1570
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\n\nexport default defineConfig([\n { name: 'docs-tool', srcDir: './docs-tool', outDir: './target/docs', targets: [claude(), codex()] },\n {\n name: 'app',\n srcDir: './prompts',\n outDir: './target',\n targets: [claude(), codex()],\n externals: [{ config: './vendor-cli-repo/promptex.config.ts' }], // 另一個專案:指向它的配置檔\n },\n])"
1571
+ },
1572
+ {
1573
+ "caption": "prompts/nodes.ts",
1574
+ "lang": "ts",
1575
+ "source": "// 三個引用,源碼看不出目標歸誰管:review-flow 是本地定義,docs-generate 由\n// 同一份配置的兄弟單元產出,security-scan 住另一個可在本地執行的專案。目標\n// 歸誰管是配置的事(見 promptex.config.ts),源碼只表達「我要用這個能力」。\nimport { defineRule, defineSkill, useSkill } from 'promptex-js'\n\ndefineSkill({\n id: 'review-flow',\n name: '合併前審查流程',\n config: { whenToUse: '準備合併變更前使用' },\n content: '依序核對測試、文件與變更說明是否齊備。',\n})\n\ndefineRule({\n id: 'pre-merge-checklist',\n name: '合併前檢查清單',\n content: [\n '合併前需完成以下檢查:',\n useSkill('review-flow'),\n useSkill('docs-generate'),\n useSkill('security-scan'),\n ],\n})"
1576
+ },
1577
+ {
1578
+ "caption": "只裝 app 單元",
1579
+ "lang": "console",
1580
+ "source": "$ promptex build --install --unit app\n✓ app: metadata written (graph + content)\n── app → claude (<project>/target) ──\n wrote .claude/rules/pre-merge-checklist.md\n wrote .claude/skills/review-flow/SKILL.md\n External reference check:\n ✗ (path does not exist) docs-generate → docs/.claude/skills/docs-generate/SKILL.md (source: unit:docs-tool, no version to check)\n ✗ (path does not exist) security-scan → ../vendor-cli-repo/dist/.claude/skills/security-scan/SKILL.md (source: config:./vendor-cli-repo/promptex.config.ts, version <version>)\nInstall report (claude)\n Projected artifacts, 2: .claude/rules/pre-merge-checklist.md, .claude/skills/review-flow/SKILL.md\n Notices, 1:\n • skill \"review-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n── app → codex (<project>/target) ──\n wrote .agents/rules/pre-merge-checklist.md\n wrote .agents/skills/review-flow/SKILL.md\n External reference check:\n ✗ (path does not exist) docs-generate → docs/.agents/skills/docs-generate/SKILL.md (source: unit:docs-tool, no version to check)\n ✗ (path does not exist) security-scan → ../vendor-cli-repo/dist/.agents/skills/security-scan/SKILL.md (source: config:./vendor-cli-repo/promptex.config.ts, version <version>)\nInstall report (codex)\n Projected artifacts, 2: .agents/rules/pre-merge-checklist.md, .agents/skills/review-flow/SKILL.md"
1581
+ },
1582
+ {
1583
+ "caption": "查詢未配置該平台的專案",
1584
+ "lang": "console",
1585
+ "source": "$ promptex manifest ./claude-only-repo --platform codex\n── legacy-toolkit → codex (<project>/claude-only-repo/dist) ──\n{\n \"formatVersion\": \"1.2\",\n \"platform\": \"codex\",\n \"engineVersion\": \"<version>\",\n \"unit\": \"legacy-toolkit\",\n \"installRoot\": \"dist\",\n \"supported\": false,\n \"nodes\": [],\n \"files\": []\n}"
1586
+ },
1587
+ {
1588
+ "caption": "引用查不到的節點",
1589
+ "lang": "console",
1590
+ "source": "$ promptex build --install ./broken-ref\n ※ <project>/claude-only-repo/promptex.config.ts has no codex platform configured; its nodes cannot be referenced in this projection\n✗ E101 Broken reference:useSkill(\"legacy-scan\") has no definition\n at <project>/broken-ref/prompts/nodes.ts:6\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1591
+ }
1592
+ ],
1593
+ "outcome": [
1594
+ "兄弟單元不需登記,即使 docs-tool 從未安裝過:`target/.claude/rules/pre-merge-checklist.md` 連到 `[文件產生](../../docs/.claude/skills/docs-generate/SKILL.md)`,`target/.agents/rules/pre-merge-checklist.md` 連到 `[文件產生](../../docs/.agents/skills/docs-generate/SKILL.md)`,落點依當次目標平台推導。",
1595
+ "另一個專案的節點連到它的安裝根,例如 `[安全掃描](../../../vendor-cli-repo/dist/.claude/skills/security-scan/SKILL.md)`。安裝報告的來源欄分別標 `unit:docs-tool` 與 `config:./vendor-cli-repo/promptex.config.ts`,後者帶版本。",
1596
+ "外部目標的安裝根尚未建立,所以報告都標 `(path does not exist)`,安裝照常完成、結束碼 0;對方的安裝根不會因為查詢被建立。",
1597
+ "對只配置 claude 的專案查詢 codex 平台,`promptex manifest` 回 `supported: false`、節點清單為空。",
1598
+ "引用那個專案節點的 broken-ref 在求值期以 E101 失敗,並提示對方沒有配置 codex 平台,結束碼 1,不編出不存在的路徑。"
1599
+ ],
1600
+ "points": [
1601
+ "判斷標準只有一個:對方能在本地跑起來就問它(externals.config),跑不起來就手寫一筆。",
1602
+ "寫下 `{ config: ... }` 即是執行對方程式碼的授權,框架不會查任何沒被寫下的專案。"
1603
+ ],
1604
+ "facts": [
1605
+ "promptex.cli.manifest",
1606
+ "promptex.formats.manifest",
1607
+ "promptex.sdk.config",
1608
+ "promptex.sdk.use-refs"
1609
+ ]
1610
+ },
1611
+ {
1612
+ "title": "條目合流時手寫登記勝出",
1613
+ "situation": "同一個目標既有兄弟單元推導的落點、又有手寫登記,想知道誰贏。",
1614
+ "code": [
1615
+ {
1616
+ "caption": "promptex.config.ts",
1617
+ "lang": "ts",
1618
+ "source": "import { claude, defineConfig } from 'promptex-js'\n\nexport default defineConfig([\n { name: 'docs-tool', srcDir: './docs-tool', outDir: './install/docs-tool', targets: [claude()] },\n { name: 'shared', srcDir: './shared', outDir: './install/shared', targets: [claude()] },\n {\n name: 'app',\n srcDir: './prompts',\n outDir: './install/app',\n targets: [claude()],\n externals: [{ kind: 'skill', id: 'docs-generate', name: '文件產生(改指供應商版本)', path: '../../vendor-dist/.claude/skills/docs-generate/SKILL.md' }],\n },\n])"
1619
+ },
1620
+ {
1621
+ "caption": "安裝輸出",
1622
+ "lang": "console",
1623
+ "source": "$ promptex build --install\n✓ docs-tool: metadata written (graph + content)\n── docs-tool → claude (<project>/install/docs-tool) ──\n wrote .claude/skills/docs-generate/SKILL.md\nInstall report (claude)\n Projected artifacts, 1: .claude/skills/docs-generate/SKILL.md\n Notices, 1:\n • skill \"docs-generate\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n✓ shared: metadata written (graph + content)\n── shared → claude (<project>/install/shared) ──\n wrote .claude/skills/style-review/SKILL.md\nInstall report (claude)\n Projected artifacts, 1: .claude/skills/style-review/SKILL.md\n Notices, 1:\n • skill \"style-review\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n✓ app: metadata written (graph + content)\n── app → claude (<project>/install/app) ──\n wrote .claude/rules/pre-merge-checklist.md\n External reference check:\n ✓ docs-generate → ../../vendor-dist/.claude/skills/docs-generate/SKILL.md (source: hand-written entry, no version to check)\n ✓ style-review → ../shared/.claude/skills/style-review/SKILL.md (source: unit:shared, no version to check)\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/pre-merge-checklist.md"
1624
+ }
1625
+ ],
1626
+ "outcome": [
1627
+ "`install/app/.claude/rules/pre-merge-checklist.md` 內 docs-generate 的連結與顯示文字都來自手寫登記,是 `[文件產生(改指供應商版本)](../../../../vendor-dist/.claude/skills/docs-generate/SKILL.md)`。",
1628
+ "style-review 沒有手寫登記,走兄弟單元推導,連到 `[風格審查](../../../shared/.claude/skills/style-review/SKILL.md)`。",
1629
+ "被蓋過的兄弟單元產物照樣產出(`install/docs-tool/.claude/skills/docs-generate/SKILL.md`),只是沒有連結指過去。報告分別標 `hand-written entry` 與 `unit:shared`。"
1630
+ ],
1631
+ "points": [
1632
+ "合流順序:兄弟單元、跨專案查詢、手寫登記,後者蓋前者,手寫最後套用而勝出。"
1633
+ ],
1634
+ "facts": [
1635
+ "promptex.sdk.config",
1636
+ "promptex.sdk.use-refs"
1637
+ ]
1638
+ },
1639
+ {
1640
+ "title": "監看沿用安裝流程",
1641
+ "situation": "改了源碼想讓目標端自動跟著變,並確認失敗時不會寫壞東西。",
1642
+ "code": [
1643
+ {
1644
+ "caption": "prompts/nodes.ts",
1645
+ "lang": "ts",
1646
+ "source": "import { defineRule } from 'promptex-js'\n\nexport const a = defineRule({ id: 'a', name: 'A', content: 'v1' })\n\nexport const b = defineRule({ id: 'b', name: 'B', content: 'keep-me' })"
1647
+ },
1648
+ {
1649
+ "caption": "啟動監看並依序改三次源碼",
1650
+ "lang": "console",
1651
+ "source": "$ promptex build --install --watch .\n── watch-mode → claude (<project>/out) ──\n wrote .claude/rules/a.md\n wrote .claude/rules/b.md\nInstall report (claude)\n Projected artifacts, 2: .claude/rules/a.md, .claude/rules/b.md\n✓ Watch: reinstalled from the latest changes.\n── watch-mode → claude (<project>/out) ──\n wrote .claude/rules/a.md\n removed .claude/rules/b.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/a.md\n✓ Watch: reinstalled from the latest changes.\n✗ E101 Broken reference:useSkill(\"missing-skill\") has no definition\n at <project>/prompts/nodes.ts:3\n✗ Watch: this evaluation or projection failed; the target keeps its last successful state (diagnostics above).\n── watch-mode → claude (<project>/out) ──\n wrote .claude/rules/a.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/a.md\n✓ Watch: reinstalled from the latest changes."
1652
+ }
1653
+ ],
1654
+ "outcome": [
1655
+ "專案依序做三次改動:刪掉節點 b、引入斷鏈引用、修正斷鏈。每一輪結束時行程仍在執行,就印出該輪的訊息。",
1656
+ "初次求值寫出節點 a 與 b 的規範並寫入登記;刪掉節點 b 後,同一個登記驅動輸出 `removed .claude/rules/b.md`,登記 `out/.promptex/watch-mode/claude.json` 只剩 a。",
1657
+ "引入斷鏈時,該輪印出 E101 與「the target keeps its last successful state」,沒有任何 `wrote` 行;修正後下一輪才再次寫出 `out/.claude/rules/a.md`。"
1658
+ ],
1659
+ "points": [
1660
+ "監看每輪都是全新的一次全量求值,不做增量推導;不帶旗標、--install、--export 三種皆可搭配,--dry-run --watch 阻斷。",
1661
+ "單一目錄模式(無配置檔)以 `--out` 與 `--platform` 指定落點與平台,單元名為 default。"
1662
+ ],
1663
+ "facts": [
1664
+ "promptex.cli.build",
1665
+ "promptex.cli.flag-conflicts",
1666
+ "promptex.cli.errors-usage",
1667
+ "promptex.cli.errors-run"
1668
+ ]
1669
+ },
1670
+ {
1671
+ "title": "監看範圍含 file() 掛載的來源檔",
1672
+ "situation": "只改掛載進來的 markdown、宣告碼一字未動,產物要跟著變。",
1673
+ "code": [
1674
+ {
1675
+ "caption": "prompts/nodes.ts",
1676
+ "lang": "ts",
1677
+ "source": "import { defineRule, file } from 'promptex-js'\n\nexport const guide = defineRule({ id: 'guide', name: '指南', content: file('docs/guide.md') })"
1678
+ },
1679
+ {
1680
+ "caption": "啟動監看並改兩次 docs/guide.md",
1681
+ "lang": "console",
1682
+ "source": "$ promptex build --install --watch .\n── watch-mounted-source → claude (<project>/out) ──\n wrote .claude/rules/guide.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/guide.md\n✓ Watch: reinstalled from the latest changes.\n── watch-mounted-source → claude (<project>/out) ──\n wrote .claude/rules/guide.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/guide.md\n✓ Watch: reinstalled from the latest changes.\n── watch-mounted-source → claude (<project>/out) ──\n wrote .claude/rules/guide.md\nInstall report (claude)\n Projected artifacts, 1: .claude/rules/guide.md\n✓ Watch: reinstalled from the latest changes."
1683
+ }
1684
+ ],
1685
+ "outcome": [
1686
+ "只改 `docs/guide.md`、`prompts/nodes.ts` 一字未動,每改一次監看就多印一輪「Watch: reinstalled」。",
1687
+ "連改兩次仍持續跟上:最後的 `out/.claude/rules/guide.md` 是第三版內容。"
1688
+ ],
1689
+ "points": [
1690
+ "file() 讀進的來源檔會交給監看記錄;缺檔那一支不記錄,避免監看在同步目標時中斷。"
1691
+ ],
1692
+ "facts": [
1693
+ "promptex.cli.build",
1694
+ "promptex.cli.errors-run"
1695
+ ]
1696
+ },
1697
+ {
1698
+ "title": "監看下第三方適配與擴充路徑都被走到",
1699
+ "situation": "目標是第三方適配(emit)或掛了擴充(extend)的內建平台,監看會不會漏掉它們。",
1700
+ "code": [
1701
+ {
1702
+ "caption": "promptex.config.ts",
1703
+ "lang": "ts",
1704
+ "source": "import { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { claude, defineConfig } from 'promptex-js'\nimport acme from './adapters/acme.ts'\nimport { ruleIndex } from './adapters/rule-index.ts'\n\nconst here = fileURLToPath(new URL('.', import.meta.url))\n\nexport default defineConfig([\n { name: 'adapter', srcDir: './prompts', outDir: join(here, 'adapter'), targets: [acme()] },\n { name: 'extension', srcDir: './prompts', outDir: join(here, 'extension'), targets: [claude({ extend: [ruleIndex] })] },\n])"
1705
+ },
1706
+ {
1707
+ "caption": "多單元配置直接監看、再各以單一單元的配置監看",
1708
+ "lang": "console",
1709
+ "source": "$ promptex build --install --watch .\n✗ Watch mode supports only a single unit with a single target. For several units or targets, run build --install for them one at a time: which units a round of changes should rebuild is not yet specified.\n\n$ promptex build --install --watch\n── adapter → acme (<project>/adapter) ──\n wrote .acme/prompts/alpha.md\n wrote .acme/prompts/beta.md\nInstall report (acme)\n Projected artifacts, 2: .acme/prompts/alpha.md, .acme/prompts/beta.md\n✓ Watch: reinstalled from the latest changes.\n── adapter → acme (<project>/adapter) ──\n wrote .acme/prompts/alpha.md\n wrote .acme/prompts/beta.md\n wrote .acme/prompts/gamma.md\nInstall report (acme)\n Projected artifacts, 3: .acme/prompts/alpha.md, .acme/prompts/beta.md, .acme/prompts/gamma.md\n✓ Watch: reinstalled from the latest changes.\n\n$ promptex build --install --watch ./\n── extension → claude (<project>/extension) ──\n added .claude/rules-index.md (extension)\n wrote .claude/rules-index.md\n wrote .claude/rules/alpha.md\n wrote .claude/rules/beta.md\n wrote .claude/rules/gamma.md\nInstall report (claude)\n Projected artifacts, 3: .claude/rules/alpha.md, .claude/rules/beta.md, .claude/rules/gamma.md\n✓ Watch: reinstalled from the latest changes.\n── extension → claude (<project>/extension) ──\n added .claude/rules-index.md (extension)\n wrote .claude/rules-index.md\n wrote .claude/rules/alpha.md\n wrote .claude/rules/beta.md\n wrote .claude/rules/gamma.md\nInstall report (claude)\n Projected artifacts, 3: .claude/rules/alpha.md, .claude/rules/beta.md, .claude/rules/gamma.md\n✓ Watch: reinstalled from the latest changes."
1710
+ }
1711
+ ],
1712
+ "outcome": [
1713
+ "直接用兩個單元的配置跑 `--watch`,結束碼 1,訊息說監看只支援單一單元單一目標。",
1714
+ "收窄成只剩 adapter 單元後監看:第三方適配走 emit,產出 `adapter/.acme/prompts/alpha.md`、`adapter/.acme/prompts/beta.md`,新增 gamma 節點後下一輪多出 `adapter/.acme/prompts/gamma.md`。登記 `adapter/.promptex/adapter/acme.json` 記錄這三個檔案。",
1715
+ "收窄成只剩 extension 單元後監看:擴充走 extend,每輪都輸出 `added .claude/rules-index.md (extension)`,索引 `extension/.claude/rules-index.md` 列出三個節點,登記 `extension/.promptex/extension/claude.json` 把它記為 extension 種類。"
1716
+ ],
1717
+ "points": [
1718
+ "監看只支援單一單元單一目標,多單元或多目標會被拒絕並提示改用安裝旗標逐一安裝。"
1719
+ ],
1720
+ "facts": [
1721
+ "promptex.sdk.define-target",
1722
+ "promptex.cli.build",
1723
+ "promptex.cli.errors-run",
1724
+ "promptex.formats.ownership-registry"
1725
+ ]
1726
+ }
1727
+ ]
1728
+ },
1729
+ {
1730
+ "id": "scaling-authoring",
1731
+ "name": "批次與複用",
1732
+ "situation": "我的資源多到不能逐份手寫",
1733
+ "claim": "批次與複用不需要框架 API,宿主語言的常數、函式、map、物件展開已足夠;框架只保證宣告函式呼叫一次即登記一個節點。",
1734
+ "scenarios": [
1735
+ {
1736
+ "title": "工廠函式批次建立節點",
1737
+ "situation": "一批同型節點(lint 規則、術語)逐份手寫,寫法一定會愈改愈不一致,想用資料表加工廠批次產生。",
1738
+ "code": [
1739
+ {
1740
+ "caption": "prompts/nodes.ts",
1741
+ "lang": "ts",
1742
+ "source": "import { defineResource, defineRule, defineSkill, section, table, useResource, useRule, type RuleDef } from 'promptex-js'\nimport { LINT_SPECS, type LintSpec } from './lint-specs.ts'\n\n// 工廠函式:把「規格資料 → 節點規格」的轉換封裝一次,\n// 共用的內容結構(statement/rationale 兩段)與配置只寫一處。\n// 工廠回傳 RuleDef 而不自己呼叫 defineRule:登記在呼叫端發生,框架記錄的\n// 來源位置因此是下面那行 map,不是工廠內部;工廠本身無副作用。\nfunction lintRuleDef(spec: LintSpec): RuleDef {\n return {\n id: spec.id,\n name: spec.name,\n description: `${spec.name}:適用 ${spec.scope.join('、')}`,\n config: { appliesTo: spec.scope },\n content: [section('約束', spec.statement), section('理由', spec.rationale)],\n }\n}\n\n// 批次建立:三個 rule 由同一工廠產生規格、在此逐一登記,形態必然一致,不會逐份漂移。\nexport const lintRules = LINT_SPECS.map(lintRuleDef).map(defineRule)\n\n// defineRule 回傳 Ref,可繼續參與引用;此處以陣列承載供其他節點取用。\nexport const lintRuleIds = lintRules.map((ref) => ref.id)\n\n// 由同一份規格資料產生總覽資源:規則增減時總覽自動同步。\nexport const lintOverview = defineResource({\n id: 'lint-overview',\n name: '靜態檢查規則總覽',\n content: table(\n ['規則', '適用範圍', '約束'],\n LINT_SPECS.map((s) => [s.name, s.scope.join('、'), s.statement]),\n ),\n})\n\n// 批次產生的節點可被正常引用:以登記回傳的 Ref 取 id,逐一 useRule。\nexport const lintSkill = defineSkill({\n id: 'lint-review',\n name: '靜態檢查審查',\n config: { whenToUse: '當使用者要求檢查程式碼是否違反專案靜態規則時使用' },\n content: [\n '逐條核對下列規則:',\n useResource('lint-overview'),\n '各規則全文:',\n ...lintRules.map((ref) => useRule(ref.id)),\n ],\n})"
1743
+ },
1744
+ {
1745
+ "caption": "prompts/lint-specs.ts",
1746
+ "lang": "ts",
1747
+ "source": "// 規格資料:一列一個 rule,新增規則只需加一列,不必複製整段 defineRule。\nexport interface LintSpec {\n id: string\n name: string\n scope: readonly string[]\n statement: string\n rationale: string\n}\n\nexport const LINT_SPECS: readonly LintSpec[] = [\n {\n id: 'no-any',\n name: '禁用 any',\n scope: ['src/**/*.{ts,tsx}'],\n statement: '禁止 `any`:`@types` 未涵蓋的第三方 API 除外,例外於 PR 註明。',\n rationale: '`any` 讓型別檢查在該處失效,錯誤延後到 runtime 才暴露。',\n },\n {\n id: 'no-default-export',\n name: '禁用預設匯出',\n scope: ['src/**/*.{ts,tsx}'],\n statement: '禁止 `export default`:一律具名匯出。',\n rationale: '預設匯出在匯入端可任意改名,跨檔搜尋同一符號時無法一次找齊。',\n },\n {\n id: 'no-console',\n name: '禁用 console',\n scope: ['src/**/*.ts'],\n statement: '禁止 `console.log`:改用專案 logger。',\n rationale: 'console 輸出無結構、無層級,日誌系統無法解析與過濾。',\n },\n]"
1748
+ }
1749
+ ],
1750
+ "outcome": [
1751
+ "`.claude/rules/no-any.md`、`.claude/rules/no-default-export.md`、`.claude/rules/no-console.md` 三份 rule 產物結構完全一致:同樣的 `paths` frontmatter、「約束」與「理由」兩個 `##` 段。",
1752
+ "`.promptex/factory-nodes/graph.json` 內三個 rule 的 `at` 都是 `prompts/nodes.ts:19`,即 `map(defineRule)` 那一行,節點在圖中依 id 排序。",
1753
+ "`.claude/skills/lint-review/SKILL.md` 末段三行連結的順序即資料表順序。"
1754
+ ],
1755
+ "points": [
1756
+ "資料表加工廠是這類批次節點的標準寫法:資料變動時去改資料表,不必逐檔手改。"
1757
+ ],
1758
+ "facts": [
1759
+ "promptex.sdk.define-rule",
1760
+ "promptex.sdk.define-resource",
1761
+ "promptex.sdk.define-skill",
1762
+ "promptex.sdk.use-refs",
1763
+ "promptex.formats.graph"
1764
+ ]
1765
+ }
1766
+ ]
1767
+ },
1768
+ {
1769
+ "id": "extending-plugins",
1770
+ "name": "擴充(plugin)",
1771
+ "situation": "框架沒有我要的解析期能力,我要在求值期補",
1772
+ "claim": "plugin 產出節點,不產出檔案,只吃 SDK。參數與診斷各有命名空間。改寫先於驗證,準備階段是唯一的非同步生命週期。",
1773
+ "scenarios": [
1774
+ {
1775
+ "title": "三個生命週期與順序契約",
1776
+ "situation": "想知道一個 plugin 長什麼樣、在哪個階段能做什麼。",
1777
+ "code": [
1778
+ {
1779
+ "caption": "plugins/lifecycle-demo.ts",
1780
+ "lang": "ts",
1781
+ "source": "import { defineResource, list, type Diagnostic, type Plugin } from 'promptex-js'\n\nexport const lifecycleDemoPlugin: Plugin = {\n name: 'lifecycle-demo',\n kinds: ['skill'], // 靜態宣告作用範圍\n\n // 改寫遍:改既有節點、以 define* 新增節點\n transform(ctx): void {\n const skills = ctx.entries.filter((e) => e.kind === 'skill')\n for (const entry of skills) ctx.appendContent(entry, '(由 plugin 於 transform 階段追加)')\n const catalogRef = defineResource({\n id: 'node-catalog',\n name: '節點清單',\n meta: { path: 'promptex' },\n content: list(ctx.entries.map((e) => `${e.kind} ${e.id}:${e.name}`)),\n })\n // 這份清單沒有自然的引用者,要被引用才會落地\n if (skills.length > 0) ctx.appendContent(skills[0], catalogRef)\n console.log(` [transform] 改寫 ${skills.length} 個 skill,新增 1 個資源節點`)\n },\n\n // 解析遍:回傳診斷,空陣列即通過\n validate({ entries }): Diagnostic[] {\n console.log(` [validate] 檢視 ${entries.length} 個節點(含 transform 新增的節點)`)\n return []\n },\n}"
1782
+ },
1783
+ {
1784
+ "caption": "promptex.config.ts",
1785
+ "lang": "ts",
1786
+ "source": "import { claude, defineConfig } from 'promptex-js'\nimport { lifecycleDemoPlugin } from './plugins/lifecycle-demo.ts'\n\nexport default defineConfig({\n name: 'plugin-lifecycle',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude()],\n plugins: [lifecycleDemoPlugin],\n})"
1787
+ }
1788
+ ],
1789
+ "outcome": [
1790
+ "`.claude/skills/code-review/SKILL.md` 內文多出追加的一行,以及指向 promptex/node-catalog.md 的連結。",
1791
+ "`.claude/skills/code-review/promptex/node-catalog.md` 是 plugin 新增的資源節點,列出 `skill code-review:程式碼審查`。",
1792
+ "終端輸出先印 transform 的訊息,再印 validate 的訊息。validate 檢視到 2 個節點,其中一個是 transform 新增的 `node-catalog`。"
1793
+ ],
1794
+ "points": [
1795
+ "順序是求值聚合檔 → transform → resolve(核心檢查在先、各 plugin 的 validate 隨後)→ 投影;plugin 新增的節點一併受 id 驗證與引用檢查。",
1796
+ "不自己寫檔:落點與格式由適配決定,同一份 plugin 自動支援全部平台。"
1797
+ ],
1798
+ "facts": [
1799
+ "promptex.sdk.config",
1800
+ "promptex.sdk.define-config",
1801
+ "promptex.sdk.define-resource"
1802
+ ]
1803
+ },
1804
+ {
1805
+ "title": "準備階段:唯一的非同步生命週期",
1806
+ "situation": "plugin 需要先抓外部資料才能決定要產生什麼節點。",
1807
+ "code": [
1808
+ {
1809
+ "caption": "plugins/remote-rules.ts",
1810
+ "lang": "ts",
1811
+ "source": "import { readFile } from 'node:fs/promises'\nimport { defineRule, type Plugin } from 'promptex-js'\n\ninterface RemoteRule {\n readonly id: string\n readonly name: string\n readonly content: string\n}\n\nexport function remoteRulesPlugin(endpoint: string, mirrorOverride?: URL): Plugin {\n let fetched: readonly RemoteRule[] = []\n return {\n name: 'remote-rules',\n // 收集遍之前,註冊表為空,拿不到 entries 與 params\n async prepare() {\n const mirror = mirrorOverride ?? new URL('../data/remote-rules.json', import.meta.url)\n void endpoint // 真實實作改為 await fetch(endpoint)\n fetched = JSON.parse(await readFile(mirror, 'utf8')) as RemoteRule[]\n },\n // 同步,只讀閉包\n transform: () => {\n for (const rule of fetched) {\n defineRule({ id: rule.id, name: rule.name, config: { alwaysApply: true }, content: rule.content })\n }\n },\n }\n}"
1812
+ },
1813
+ {
1814
+ "caption": "promptex.config.ts",
1815
+ "lang": "ts",
1816
+ "source": "import { claude, defineConfig } from 'promptex-js'\nimport { remoteRulesPlugin } from './plugins/remote-rules.ts'\n\nexport default defineConfig({\n name: 'prepare-stage',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude()],\n plugins: [remoteRulesPlugin('rules-endpoint')],\n})"
1817
+ }
1818
+ ],
1819
+ "outcome": [
1820
+ "`.claude/rules/` 下出現 `.claude/rules/coding-style.md`、`.claude/rules/commit-message.md`、`.claude/rules/review-checklist.md` 三條規則,內容就是 `data/remote-rules.json` 裡的文字,源碼 `prompts/nodes.ts` 只有一個 skill。",
1821
+ "把 plugin 的資料來源換成不存在的檔再建置一次,結束碼為 1,輸出 `plugin-prepare-failed`,訊息含擴充名 `remote-rules` 與原始的 `ENOENT` 錯誤,並印出 `0 artifacts written (all or nothing)`。"
1822
+ ],
1823
+ "points": [
1824
+ "資料經 plugin 閉包從 prepare 傳到 transform,不讀節點。",
1825
+ "準備階段失敗即中止:不進收集遍、不留部分產物,診斷含擴充名與原始錯誤。",
1826
+ "沒有 plugin 宣告 prepare 時整個編譯不涉及非同步等待,核心無網路的承諾不變。"
1827
+ ],
1828
+ "facts": [
1829
+ "promptex.sdk.config",
1830
+ "promptex.sdk.define-config",
1831
+ "promptex.sdk.define-rule"
1832
+ ]
1833
+ },
1834
+ {
1835
+ "title": "自訂參數住 meta.plugins 命名空間",
1836
+ "situation": "plugin 要讓作者逐節點調整行為。",
1837
+ "code": [
1838
+ {
1839
+ "caption": "prompts/nodes.ts",
1840
+ "lang": "ts",
1841
+ "source": "import { defineSkill } from 'promptex-js'\n\nexport const deploySkill = defineSkill({\n id: 'deploy-flow',\n name: '部署流程',\n meta: { plugins: { 'owner-tag': { team: 'platform', reviewCycleDays: 90 } } },\n config: { whenToUse: '部署時' },\n content: '確認 CI 全綠與回退方案後執行部署。',\n})\n\n// 未宣告參數的節點不受 owner-tag 影響\nexport const plainSkill = defineSkill({\n id: 'plain-flow',\n name: '一般流程',\n config: { whenToUse: '示範未宣告 plugin 參數的節點' },\n content: '本節點沒有 owner-tag 參數。',\n})"
1842
+ },
1843
+ {
1844
+ "caption": "plugins/owner-tag.ts",
1845
+ "lang": "ts",
1846
+ "source": "import type { Diagnostic, Plugin } from 'promptex-js'\n\ninterface OwnerParams {\n team: string\n reviewCycleDays?: number\n}\n\nexport const ownerPlugin: Plugin = {\n name: 'owner-tag',\n\n validate({ entries, params }): Diagnostic[] {\n return entries.flatMap((entry) => {\n const p = params(entry) as OwnerParams | undefined\n if (!p) return [] // 未宣告參數的節點不受本 plugin 約束\n return p.team\n ? []\n : [\n {\n code: 'OWN001',\n message: `節點 \"${entry.id}\" 的 owner-tag 參數缺 team`,\n at: [entry.at],\n hint: \"補上 meta.plugins: { 'owner-tag': { team: '團隊名' } }\",\n },\n ]\n })\n },\n\n transform(ctx): void {\n for (const entry of ctx.entries) {\n const p = ctx.params(entry) as OwnerParams | undefined\n if (p) ctx.appendContent(entry, `維護團隊:${p.team}${p.reviewCycleDays ? `,每 ${p.reviewCycleDays} 天複審` : ''}。`)\n }\n },\n}"
1847
+ },
1848
+ {
1849
+ "caption": "缺 team 時的診斷",
1850
+ "lang": "console",
1851
+ "source": "$ promptex build --install .\n✗ OWN001 節點 \"deploy-flow\" 的 owner-tag 參數缺 team\n at <project>/prompts/nodes.ts:3\n 補上 meta.plugins: { 'owner-tag': { team: '團隊名' } }\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1852
+ }
1853
+ ],
1854
+ "outcome": [
1855
+ "`.claude/skills/deploy-flow/SKILL.md` 內文末尾多出「維護團隊:platform,每 90 天複審。」,frontmatter 只有 name 與 when_to_use,查無 plugin 參數的鍵。",
1856
+ "`.claude/skills/plain-flow/SKILL.md` 沒宣告參數,內文不變。",
1857
+ "把 `deploy-flow` 的 `owner-tag` 參數改成缺 team 再建置一次,結束碼為 1,印出 `✗ OWN001` 與定義位置 `prompts/nodes.ts:3`,0 個產物寫入。"
1858
+ ],
1859
+ "points": [
1860
+ "ctx.params(entry) 只回本 plugin 命名空間下的值。",
1861
+ "三個環節:kinds 與 configSchema 宣告接受什麼、validate 檢查合法性、transform 消費它。"
1862
+ ],
1863
+ "facts": [
1864
+ "promptex.sdk.config",
1865
+ "promptex.sdk.define-skill",
1866
+ "promptex.sdk.define-config"
1867
+ ]
1868
+ },
1869
+ {
1870
+ "title": "擴充產出節點而非檔案,兩條寫進產物的路",
1871
+ "situation": "plugin 要產出索引或報告這類檔案。",
1872
+ "code": [
1873
+ {
1874
+ "caption": "plugins/catalog.ts",
1875
+ "lang": "ts",
1876
+ "source": "import { defineAsset, defineResource, table, type Plugin } from 'promptex-js'\n\nexport const indexPlugin: Plugin = {\n name: 'catalog',\n transform(ctx): void {\n const nodes = ctx.entries\n .filter((e) => !['resource', 'asset', 'dir'].includes(e.kind))\n .map((e) => ({ id: e.id, kind: e.kind, name: e.name }))\n const jsonRef = defineAsset({\n id: 'catalog-json',\n name: '節點索引(JSON)',\n meta: { path: 'promptex' },\n content: JSON.stringify({ nodes }, null, 2),\n fileName: 'catalog.json',\n })\n const mdRef = defineResource({\n id: 'catalog-md',\n name: '節點索引',\n meta: { path: 'promptex' },\n content: table(['kind', 'id'], nodes.map((n) => [n.kind, n.id])),\n })\n // 無條件產出\n defineResource({\n id: 'catalog-report',\n name: '索引產出報告',\n meta: { path: 'promptex', emit: 'always' },\n content: `掃過 ${nodes.length} 個能力節點。`,\n })\n const referrer = ctx.entries.find((e) => e.id === 'doc-authoring')\n if (referrer) {\n ctx.appendContent(referrer, jsonRef)\n ctx.appendContent(referrer, mdRef) // 讓它被引用\n }\n },\n}"
1877
+ },
1878
+ {
1879
+ "caption": "promptex.config.ts",
1880
+ "lang": "ts",
1881
+ "source": "import { claude, codex, defineConfig } from 'promptex-js'\nimport { indexPlugin } from './plugins/catalog.ts'\n\n// 單一單元、兩個平台:同一份 plugin 產出的節點分別投影成兩套平台格式,產物\n// 合流在同一棵樹下(.claude/… 與 .agents/…),落點差異全由適配決定。\nexport default defineConfig({\n name: 'generated-nodes',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), codex()],\n plugins: [indexPlugin],\n})"
1882
+ }
1883
+ ],
1884
+ "outcome": [
1885
+ "claude 平台:兩份索引 `.claude/skills/doc-authoring/promptex/catalog.json` 與 `.claude/skills/doc-authoring/promptex/catalog-md.md` 平鋪在引用方的目錄下,報告落在 `.claude/resources/promptex/catalog-report.md`。",
1886
+ "codex 平台:asset 進 `.agents/skills/doc-authoring/assets/promptex/catalog.json`,resource 進 `.agents/skills/doc-authoring/references/promptex/catalog-md.md`,報告落在 `.agents/resources/promptex/catalog-report.md`。",
1887
+ "兩個平台的 SKILL.md 末尾各有指向這兩份索引的連結,路徑依平台落點不同。plugin 內沒有任何平台路徑。"
1888
+ ],
1889
+ "points": [
1890
+ "無自然引用者的產出兩條路:ctx.appendContent(referrer, ref) 讓它被引用(落引用方隨附目錄),或 `meta.emit: 'always'` 無條件產出(落共用素材目錄加自己的 path)。",
1891
+ "產出節點免除自行寫檔的全部顧慮。"
1892
+ ],
1893
+ "facts": [
1894
+ "promptex.sdk.define-asset",
1895
+ "promptex.sdk.define-resource",
1896
+ "promptex.sdk.define-skill",
1897
+ "promptex.diagnostics.e211"
1898
+ ]
1899
+ },
1900
+ {
1901
+ "title": "核心給 meta.type 與 validate,語意由擴充定義",
1902
+ "situation": "想要「task 型資源必須是步驟清單」這類自訂型別約束。",
1903
+ "code": [
1904
+ {
1905
+ "caption": "plugins/task-type.ts",
1906
+ "lang": "ts",
1907
+ "source": "import type { Diagnostic, Plugin, PluginEntry } from 'promptex-js'\n\n// 擴充住專案層 plugins/,不住 prompts/:它是解析期擴充,不是提示詞源碼\n// (見 promptex.config.ts 的註冊處說明)。真實形態亦可為 npm 套件。\n//\n// core 提供 meta.type 欄位與 validate 擴充點(機制),\n// 「task 型內容必須是有序步驟清單」這條語意由本 plugin 定義(語意)。\nexport const taskTypePlugin: Plugin = {\n name: 'task-type',\n\n validate({ entries }): Diagnostic[] {\n const diags: Diagnostic[] = []\n for (const entry of entries as readonly PluginEntry[]) {\n const meta = entry.def.meta as { type?: string } | undefined\n if (entry.kind !== 'resource' || meta?.type !== 'task') continue\n\n const content = entry.def.content as { __piece?: { t: string } } | undefined\n const isStepList = content?.__piece?.t === 'list'\n if (!isStepList) {\n diags.push({\n code: 'P001',\n message: `type \"task\" 的資源 \"${entry.id}\" 內容必須是有序步驟清單(list())`,\n at: [entry.at],\n hint: '改用 list([...]) 逐步列出,或移除 meta.type: \"task\"',\n })\n }\n }\n return diags\n },\n}"
1908
+ },
1909
+ {
1910
+ "caption": "prompts/resources.ts",
1911
+ "lang": "ts",
1912
+ "source": "import { defineResource, list } from 'promptex-js'\n\n// type 是使用者自訂的語意分類(核心不內建 Task/Constraint 等語意,\n// ADR-005 分工:core 提供 meta.type 欄位與 validate 擴充點,語意由 plugin\n// 定義)。本專案自訂 'task' 型:內容須為有序步驟清單,由 plugin 驗證器把關。\nexport const releaseTask = defineResource({\n id: 'release-task',\n name: '發布流程',\n meta: { type: 'task' },\n content: list(['1. 決定版號', '2. 打 tag', '3. publish', '4. 發布後核對']),\n})\n\n// 同為 'task' 型,內容不是步驟清單,違反自訂型別的結構要求,由 plugin 於\n// validate 階段攔截(見 promptex.config.ts 註冊的 taskTypePlugin)。\nexport const brokenTask = defineResource({\n id: 'rollback-task',\n name: '回退流程',\n meta: { type: 'task' },\n content: '出問題就回退到上一版。',\n})\n\n// 未宣告 type 的資源不受該驗證器約束。\nexport const glossary = defineResource({\n id: 'release-glossary',\n name: '發布術語',\n content: 'tag:版本錨點;publish:對外發佈動作。',\n})"
1913
+ },
1914
+ {
1915
+ "caption": "建置失敗",
1916
+ "lang": "console",
1917
+ "source": "$ promptex build --install\n✗ P001 type \"task\" 的資源 \"rollback-task\" 內容必須是有序步驟清單(list())\n at <project>/prompts/resources.ts:15\n 改用 list([...]) 逐步列出,或移除 meta.type: \"task\"\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1918
+ }
1919
+ ],
1920
+ "outcome": [
1921
+ "建置結束碼為 1,違規的 `rollback-task` 報 `✗ P001`,附定義位置 `prompts/resources.ts:15` 與修正提示,並印出 `0 artifacts written (all or nothing)`。",
1922
+ "合規的 `release-task` 與未宣告 type 的 `release-glossary` 沒有被報。"
1923
+ ],
1924
+ "points": [
1925
+ "核心不內建任何語意類型,meta.type 是開放字串;擴充自訂診斷碼(如 P001)不占用框架保留的 P9xx。",
1926
+ "擴充的 validate 與核心診斷同批報出、同一套全有全無。"
1927
+ ],
1928
+ "facts": [
1929
+ "promptex.sdk.define-resource",
1930
+ "promptex.diagnostics.codes",
1931
+ "promptex.cli.exit-codes"
1932
+ ]
1933
+ },
1934
+ {
1935
+ "title": "擴充宣告相容的框架版本範圍",
1936
+ "situation": "擴充只在某個框架版本區間能用,想在編譯前就擋下不相容。",
1937
+ "code": [
1938
+ {
1939
+ "caption": "plugins/version-compat.ts",
1940
+ "lang": "ts",
1941
+ "source": "import type { Plugin } from 'promptex-js'\n\n// 範圍是單一 caret 前綴;省略 version 即不受檢查\nexport const compatibleVersion: Plugin = { name: 'compatible-version', version: '^1.0.0' }\nexport const incompatibleVersion: Plugin = { name: 'incompatible-version', version: '^2.0.0' }"
1942
+ },
1943
+ {
1944
+ "caption": "promptex.config.ts",
1945
+ "lang": "ts",
1946
+ "source": "import { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { claude, defineConfig } from 'promptex-js'\nimport { compatibleVersion, incompatibleVersion } from './plugins/version-compat.ts'\n\nconst here = fileURLToPath(new URL('.', import.meta.url))\n\n// 兩個單元共用同一份源碼,落點分開才看得出哪一個寫了\nexport default defineConfig([\n { name: 'compatible', srcDir: './prompts', outDir: join(here, 'compatible'), targets: [claude()], plugins: [compatibleVersion] },\n { name: 'incompatible', srcDir: './prompts', outDir: join(here, 'incompatible'), targets: [claude()], plugins: [incompatibleVersion] },\n])"
1947
+ },
1948
+ {
1949
+ "caption": "不相容時的診斷",
1950
+ "lang": "console",
1951
+ "source": "$ promptex build --install\n✓ compatible: metadata written (graph + content)\n── compatible → claude (<project>/compatible) ──\n wrote .claude/skills/greeting/SKILL.md\nInstall report (claude)\n Projected artifacts, 1: .claude/skills/greeting/SKILL.md\n Notices, 1:\n • skill \"greeting\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n✗ P901 Extension \"incompatible-version\" declares the compatible range \"^2.0.0\", which is incompatible with the installed framework version \"<version>\": the framework version is too old, upgrade the framework\n at plugin \"incompatible-version\"\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1952
+ }
1953
+ ],
1954
+ "outcome": [
1955
+ "結束碼為 1。先行的 `compatible` 單元已寫入 `compatible/.claude/skills/greeting/SKILL.md`。",
1956
+ "`incompatible` 單元報 `✗ P901`,訊息含擴充名 `incompatible-version`、宣告的範圍 `^2.0.0` 與已安裝的框架版本,並說明應升級框架。該單元零產物,連 prepare 都不執行。"
1957
+ ],
1958
+ "points": [
1959
+ "只接受單一 caret 前綴(同主版本相容,主版本 0 時同副版本);省略 version 即不受檢查。P9xx 是框架對擴充機制的保留碼。"
1960
+ ],
1961
+ "facts": [
1962
+ "promptex.diagnostics.p901",
1963
+ "promptex.cli.exit-codes"
1964
+ ]
1965
+ },
1966
+ {
1967
+ "title": "多擴充同時作用與衝突分級",
1968
+ "situation": "兩個 plugin 都動同一個節點,會不會互相蓋掉。",
1969
+ "code": [
1970
+ {
1971
+ "caption": "plugins/all.ts",
1972
+ "lang": "ts",
1973
+ "source": "import type { Plugin } from 'promptex-js'\n\n// 疊加型:兩者都追加內容,兩者皆生效,順序即宣告順序\nexport const prefixNote: Plugin = {\n name: 'prefix-note',\n transform: (ctx) => {\n for (const e of ctx.entries) ctx.appendContent(e, '由 prefix-note 追加。')\n },\n}\n\nexport const suffixNote: Plugin = {\n name: 'suffix-note',\n transform: (ctx) => {\n for (const e of ctx.entries) ctx.appendContent(e, '由 suffix-note 追加。')\n },\n}\n\n// 覆寫型:一次寫入多個配置鍵,鍵序即呼叫順序\nexport const budgetTier: Plugin = {\n name: 'budget-tier',\n transform: (ctx) => {\n for (const e of ctx.entries) {\n if (e.kind === 'skill') ctx.patchConfig(e, { effort: 'high', allowedTools: ['Read', 'Grep'], model: 'max' })\n }\n },\n}\n\n// modelFast 與 modelMax 都改 model:與 budgetTier 併用即覆寫同一個配置鍵\nexport const modelFast: Plugin = {\n name: 'model-fast',\n transform: (ctx) => {\n for (const e of ctx.entries) if (e.kind === 'skill') ctx.patchConfig(e, { model: 'fast' })\n },\n}\n\nexport const modelMax: Plugin = {\n name: 'model-max',\n transform: (ctx) => {\n for (const e of ctx.entries) if (e.kind === 'skill') ctx.patchConfig(e, { model: 'max' })\n },\n}"
1974
+ },
1975
+ {
1976
+ "caption": "promptex.config.ts",
1977
+ "lang": "ts",
1978
+ "source": "import { claude, defineConfig } from 'promptex-js'\nimport { budgetTier, prefixNote, suffixNote } from './plugins/all.ts'\n\nexport default defineConfig({\n name: 'multi-plugin-conflict',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude()],\n plugins: [prefixNote, suffixNote, budgetTier],\n})"
1979
+ },
1980
+ {
1981
+ "caption": "疊加與覆寫的診斷",
1982
+ "lang": "console",
1983
+ "source": "$ promptex build --install\n✓ multi-plugin-conflict: metadata written (graph + content)\n── multi-plugin-conflict → claude (<project>) ──\n wrote .claude/skills/review-flow/SKILL.md\nInstall report (claude)\n Projected artifacts, 1: .claude/skills/review-flow/SKILL.md\n Degraded, 1:\n • skill \"review-flow\": toolAllowlist:skill, enforcement processGate → wording\n The allowed-tools of a skill on this platform is a pre-approval list rather than a tool restriction (per the official docs it does not restrict which tools are available, and every tool can still be called), so mapping onto it would invert a narrowing declaration into an authorization expansion and it is not written into the artifact; express tool restrictions in the instruction text, or name this platform's native key directly at config.platforms.claude (write allowed-tools to pre-approve, disallowed-tools to remove tools)\n※ P900 The content of node \"review-flow\" is rewritten cumulatively by more than one plugin: prefix-note → suffix-note\n at <project>/prompts/nodes.ts:3\n\n$ promptex build --install .\n✗ P900 config.model of node \"review-flow\" is overwritten by more than one plugin: budget-tier → model-fast\n at <project>/prompts/nodes.ts:3\n✗ P900 config.model of node \"review-flow\" is overwritten by more than one plugin: budget-tier → model-max\n at <project>/prompts/nodes.ts:3\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
1984
+ }
1985
+ ],
1986
+ "outcome": [
1987
+ "疊加:建置成功,終端印出 `※ P900`(warning)說明 `review-flow` 的 content 被 `prefix-note → suffix-note` 疊加改寫。`.claude/skills/review-flow/SKILL.md` 內文末端依宣告順序先出現「由 prefix-note 追加。」再出現「由 suffix-note 追加。」。",
1988
+ "`budget-tier` 一次寫入三個鍵,`.promptex/multi-plugin-conflict/graph.json` 中 `review-flow` 的 config 依序為 `whenToUse`、`effort`、`allowedTools`、`model`。",
1989
+ "覆寫:把 plugins 換成 `budgetTier`、`modelFast`、`modelMax` 再建置一次,結束碼為 1,印出兩筆 `✗ P900`(`config.model` 被 `budget-tier → model-fast`、`budget-tier → model-max` 覆寫),0 個產物寫入。"
1990
+ ],
1991
+ "points": [
1992
+ "分級依操作型別分兩級,疊加(資料都在)warning、覆寫(前者靜默消失)error;偵測粒度是鍵。",
1993
+ "執行順序為 plugins 陣列順序,後者看得到前者新增的節點;改寫先於驗證。"
1994
+ ],
1995
+ "facts": [
1996
+ "promptex.diagnostics.p900"
1997
+ ]
1998
+ }
1999
+ ]
2000
+ },
2001
+ {
2002
+ "id": "extending-adapters-kinds",
2003
+ "name": "擴充(adapter 與自訂類型)",
2004
+ "situation": "我要支援自己的平台,或要一個框架沒有的類型",
2005
+ "claim": "第三方適配只用 AdapterContext 公開介面且能力與內建對等;自訂類型由 plugin(規格函式加驗證)與 adapter extend(產物形態)以 meta.type 對接,核心類型集合恆為十種。",
2006
+ "scenarios": [
2007
+ {
2008
+ "title": "自己寫一個平台適配",
2009
+ "situation": "框架沒內建我的平台,要一個帶 emit 的 target。",
2010
+ "code": [
2011
+ {
2012
+ "caption": "adapters/acme.ts",
2013
+ "lang": "ts",
2014
+ "source": "import { defineTarget, type AdapterContext, type AdapterTarget, type PluginEntry } from 'promptex-js'\n\nconst SUPPORTED = ['skill', 'rule', 'instruction'] as const\nconst UNSUPPORTED = ['agent', 'hook', 'mcp', 'permission'] as const\nconst nodePath = (e: PluginEntry) => `.acme/prompts/${e.id}.md`\nconst resourcePath = (e: PluginEntry) => `.acme/refs/${e.id}.md`\n\nconst emit = (ctx: AdapterContext): Map<string, string> => {\n const files = new Map<string, string>()\n const layout = new Map<string, string>() // ① 先算落點表,渲染要用它解析引用\n for (const kind of SUPPORTED) for (const e of ctx.entries(kind)) layout.set(`${kind}:${e.id}`, nodePath(e))\n for (const kind of ['resource', 'asset', 'dir'] as const) {\n for (const e of ctx.entries(kind)) layout.set(`${kind}:${e.id}`, resourcePath(e))\n }\n for (const kind of UNSUPPORTED) {\n // ② 不支援的類型逐一列報告\n for (const e of ctx.entries(kind)) {\n ctx.unsupported({ kind, nodeId: e.id, feature: kind, note: `acme 無 ${kind} 對應機制,該節點未產出` })\n }\n }\n for (const kind of SUPPORTED) {\n // ③ 產出\n for (const e of ctx.entries(kind)) {\n const path = nodePath(e)\n const cfg = e.def.config as { appliesTo?: readonly string[] } | undefined\n let note = ''\n if (kind === 'rule' && cfg?.appliesTo?.length) {\n note = `適用範圍:${cfg.appliesTo.join('、')}\\n\\n`\n ctx.degrade({\n kind,\n nodeId: e.id,\n feature: 'scopedLoading',\n note: 'acme 無範圍載入機制,改為常駐並於內文標註適用範圍',\n })\n }\n const overrides = ctx.overrides(e) // 覆寫一律經 ctx 讀\n const front = overrides\n ? `---\\n${Object.entries(overrides).map(([k, v]) => `${k}: ${String(v)}`).join('\\n')}\\n---\\n\\n`\n : ''\n files.set(path, `${front}# ${e.name}\\n\\n${note}${ctx.render(e, path, layout)}\\n`)\n }\n }\n for (const e of ctx.entries('resource')) {\n const path = resourcePath(e)\n files.set(path, `# ${e.name}\\n\\n${ctx.render(e, path, layout)}\\n`)\n }\n return files\n}\n\nexport default function acme(): AdapterTarget {\n return defineTarget('acme', emit)\n}"
2015
+ },
2016
+ {
2017
+ "caption": "promptex.config.ts",
2018
+ "lang": "ts",
2019
+ "source": "import { claude, defineConfig } from 'promptex-js'\nimport acme from './adapters/acme.ts'\n\nexport default defineConfig({\n name: 'third-party-adapter',\n srcDir: './prompts',\n outDir: '.',\n targets: [claude(), acme()],\n})"
2020
+ },
2021
+ {
2022
+ "caption": "建置輸出",
2023
+ "lang": "console",
2024
+ "source": "$ promptex build --install\n✓ third-party-adapter: metadata written (graph + content)\n── third-party-adapter → claude (<project>) ──\n wrote .claude/agents/spec-reviewer.md\n wrote .claude/rules/mdx-format.md\n wrote .claude/skills/commit-helper/SKILL.md\n wrote .claude/skills/commit-helper/commit-style.md\nInstall report (claude)\n Projected artifacts, 4: .claude/agents/spec-reviewer.md, .claude/rules/mdx-format.md, .claude/skills/commit-helper/SKILL.md, .claude/skills/commit-helper/commit-style.md\n── third-party-adapter → acme (<project>) ──\n wrote .acme/prompts/commit-helper.md\n wrote .acme/prompts/mdx-format.md\n wrote .acme/refs/commit-style.md\nInstall report (acme)\n Projected artifacts, 3: .acme/prompts/commit-helper.md, .acme/prompts/mdx-format.md, .acme/refs/commit-style.md\n Degraded, 1:\n • rule \"mdx-format\": scopedLoading\n acme 無範圍載入機制,改為常駐並於內文標註適用範圍\n Unsupported, 1:\n • agent \"spec-reviewer\": agent has no matching mechanism and was not emitted\n acme 無 agent 對應機制,該節點未產出"
2025
+ }
2026
+ ],
2027
+ "outcome": [
2028
+ "`.acme/prompts/commit-helper.md` 的連結是 ../refs/commit-style.md,由 ctx.render 依落點表算出。開頭欄位 `x-owner: acme-team` 取自 config.platforms.acme,claude 那一份取的是自己的 `claude-team`。",
2029
+ "`.acme/prompts/mdx-format.md` 內文開頭標註「適用範圍:**/*.mdx」。安裝報告把 rule `mdx-format` 的 scopedLoading 記在降級,把 agent `spec-reviewer` 記在不支援,acme 不產出 agent。",
2030
+ "`.promptex/third-party-adapter/acme.json` 與 `.promptex/third-party-adapter/claude.json` 的結構相同(formatVersion、platform、engineVersion、files),files 逐筆記路徑與 kind。"
2031
+ ],
2032
+ "points": [
2033
+ "plugin 作用在解析期(節點到節點),adapter 作用在投影期(節點到產物),兩者都在 SDK 層但不共用介面。",
2034
+ "渲染接受落點表而非回呼:只有資料進出,跨語言邊界不傳函式指標。先算 layout 再 render,順序不能顛倒。",
2035
+ "第三方適配綁語言:以 TypeScript 寫的只服務 TypeScript 專案。"
2036
+ ],
2037
+ "facts": [
2038
+ "promptex.sdk.define-target",
2039
+ "promptex.formats.ownership-registry",
2040
+ "promptex.sdk.define-skill",
2041
+ "promptex.sdk.define-rule"
2042
+ ]
2043
+ },
2044
+ {
2045
+ "title": "適配讀到的節點範圍",
2046
+ "situation": "想知道 ctx.entries 回傳全庫還是「本單元本平台該產出的那批」。",
2047
+ "code": [
2048
+ {
2049
+ "caption": "adapters/rule-index-support.ts",
2050
+ "lang": "ts",
2051
+ "source": "import type { AdapterContext, AdapterExtension } from 'promptex-js'\n\nexport const ruleIndexSupport: AdapterExtension = (ctx: AdapterContext, files: Map<string, string | Uint8Array>) => {\n const lines = ctx.entries('rule').map((e) => `- ${e.id}:${e.name}`)\n files.set('.claude/rules-index.md', `# 規範索引\\n\\n${lines.join('\\n')}\\n`)\n}"
2052
+ },
2053
+ {
2054
+ "caption": "promptex.config.ts",
2055
+ "lang": "ts",
2056
+ "source": "import { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { claude, defineConfig } from 'promptex-js'\nimport acme from './adapters/acme.ts'\nimport { ruleIndexSupport } from './adapters/rule-index-support.ts'\n\nconst here = fileURLToPath(new URL('.', import.meta.url))\nconst targets = () => [claude({ extend: [ruleIndexSupport] }), acme()]\n\n// 三個單元共用同一份源碼,差別只在 select;落點分開才能並排比對\nexport default defineConfig([\n { name: 'global', srcDir: './prompts', outDir: join(here, 'global'), targets: targets() },\n { name: 'web', srcDir: './prompts', select: { tags: ['domain/frontend'] }, outDir: join(here, 'web'), targets: targets() },\n { name: 'api', srcDir: './prompts', select: { tags: ['domain/backend'] }, outDir: join(here, 'api'), targets: targets() },\n])"
2057
+ },
2058
+ {
2059
+ "caption": "prompts/nodes.ts",
2060
+ "lang": "ts",
2061
+ "source": "// 三個配置單元共用這一份源碼(見 ../promptex.config.ts):每個單元都會重新\n// 求值本檔,因此都會登記下面全部節點。哪個單元的哪個平台真的產出哪幾筆,由\n// 節點自身宣告的平台限定(meta.targets)與單元的 select 共同決定。\nimport { defineResource, defineRule, useResource } from 'promptex-js'\n\n// 未標任何 tag 的素材:select 只篩能力類型節點,素材該不該進產物取決於是否\n// 被納入的節點引用。若 select 一併套用到它,帶 select 的單元會連引用得到的\n// 目標都失去。\ndefineResource({\n id: 'glossary',\n name: '術語表',\n content: '節點是宣告的單位,產物是投影的結果。',\n})\n\ndefineRule({\n id: 'component-naming',\n name: '元件命名',\n meta: { tags: ['domain/frontend'] },\n content: ['元件檔名採 PascalCase,與匯出的元件同名。用語見:', useResource('glossary')],\n})\n\ndefineRule({\n id: 'endpoint-naming',\n name: '端點命名',\n meta: { tags: ['domain/backend'] },\n content: ['端點路徑一律小寫,多字以連字號分隔。用語見:', useResource('glossary')],\n})\n\n// 平台限定只給內建平台:第三方適配不得產出它。tag 與 component-naming 相同,\n// 因此在 web 單元一起被 select 收下,兩道排除的效果可分開觀察。\ndefineRule({\n id: 'claude-only-note',\n name: 'Claude 專屬提醒',\n meta: { targets: ['claude'], tags: ['domain/frontend'] },\n content: '本條只分發到 claude。',\n})\n\n// 平台限定只給第三方適配:內建平台不得產出它。\ndefineRule({\n id: 'acme-only-note',\n name: 'acme 專屬提醒',\n meta: { targets: ['acme'], tags: ['domain/frontend'] },\n content: '本條只分發到 acme。',\n})"
2062
+ }
2063
+ ],
2064
+ "outcome": [
2065
+ "global 單元不篩選:claude 產出 `global/.claude/rules/` 下三條規範(`claude-only-note`、`component-naming`、`endpoint-naming`),acme 產出 `global/.acme/prompts/` 下三條(`acme-only-note`、`component-naming`、`endpoint-naming`)。",
2066
+ "web 單元只收 domain/frontend:claude 有 `claude-only-note` 與 `component-naming`,acme 有 `acme-only-note` 與 `component-naming`。api 單元只收 domain/backend:兩個平台都只有 `endpoint-naming`。",
2067
+ "三個單元的 claude 與 acme 都有 glossary 素材(例如 `api/.claude/resources/glossary.md`、`api/.acme/refs/glossary.md`),素材不受 select 影響。",
2068
+ "擴充寫出的 rules-index.md(例如 `web/.claude/rules-index.md`)行數與該單元 claude 可見的規範一致:global 3 行、web 2 行、api 1 行。終端輸出把它列為 added .claude/rules-index.md (extension),`global/.promptex/global/claude.json` 登記它的 kind 為 extension。",
2069
+ "建置同時印出 6 行 ⚠,說明第三方平台 acme 下兄弟單元的落點無法推導,所以彼此的節點不能在這次投影中被引用。"
2070
+ ],
2071
+ "points": [
2072
+ "兩道排除分屬不同軸,meta.targets 是節點自己說「我不去那個平台」(能力與素材皆適用);select 是單元說「這次不需要它」(只篩能力類型)。",
2073
+ "extend 與 emit 共用同一個 AdapterContext 建構處,節點過濾同時成立。擴充新增的檔案在安裝輸出另列「新增(擴充)」,登記種類為 extension。"
2074
+ ],
2075
+ "facts": [
2076
+ "promptex.sdk.define-target",
2077
+ "promptex.sdk.config",
2078
+ "promptex.sdk.platform-claude"
2079
+ ]
2080
+ },
2081
+ {
2082
+ "title": "適配讀覆寫的三個方法",
2083
+ "situation": "第三方適配要處理作者寫在 config.platforms 的覆寫。",
2084
+ "code": [
2085
+ {
2086
+ "caption": "adapters/ledger.ts",
2087
+ "lang": "ts",
2088
+ "source": "// 第三方適配(假想平台 ledger):本情境要看的是覆寫介面的三個方法,適配本身\n// 壓到只支援 hook 與 resource 兩種類型,其餘變因全部消掉。姊妹子樹\n// python/rust 的同名情境有等價的一份。\n//\n// 三個方法各守一件事:`overrides` 給出已濾掉保留鍵的平台原生鍵,`reserved`\n// 給出框架保留鍵(現行只有 promptex:event,僅 hook 合法),`unsupportedOverride`\n// 在本平台的產物沒有承載鍵的位置時記一筆不支援。三者都不自己走\n// `entry.def.config.platforms.ledger`:自己走那條路要重造「空物件視同未宣告」\n// 的判定、保留鍵的過濾,以及那句訊息的措辭與替代路徑(ADR-004)。\nimport { defineTarget, type AdapterContext, type AdapterTarget, type PluginEntry } from 'promptex-js'\n\n/** hook 的事件鍵:ledger 沒有中性 notification 的原生對應,靠保留鍵指名。 */\nconst NATIVE_EVENT_KEY = 'promptex:event'\n\n/** 參考資料在本平台的產物形態,交給 `unsupportedOverride` 拼進訊息。 */\nconst REFERENCE_FORM = '.ledger/refs/ 下的參考資料是純內文、無開頭欄位'\n\nconst hookPath = (e: PluginEntry) => `.ledger/hooks/${e.id}.md`\nconst referencePath = (e: PluginEntry) => `.ledger/refs/${e.id}.md`\n\n/**\n * 本節點在 ledger 的事件鍵:保留鍵指名時取它,未指名時退回中性事件。\n *\n * 事件鍵是唯一構不到的鍵:它決定 hook 落在哪個事件之下,而一般覆寫作用在那\n * 之後的鍵值面。`ctx.reserved` 是第三方適配讀它的唯一入口。\n */\nconst eventKey = (ctx: AdapterContext, e: PluginEntry): string => {\n const reserved = ctx.reserved(e)\n const neutral = (e.def.config as { event?: string } | undefined)?.event ?? ''\n return reserved?.[NATIVE_EVENT_KEY] ?? neutral\n}\n\n/** 開頭欄位:先事件鍵,再依宣告順序接上平台原生鍵。 */\nconst frontmatter = (pairs: readonly (readonly [string, string])[]): string =>\n `---\\n${pairs.map(([k, v]) => `${k}: ${v}`).join('\\n')}\\n---\\n\\n`\n\nconst emit = (ctx: AdapterContext): Map<string, string> => {\n const files = new Map<string, string>()\n\n // 落點表只需要 resource:hook 的產物只有開頭欄位與標題,本平台的 hook 不\n // 承載內文(節點的 actions 才是它的本體),因此沒有引用要解析。\n const layout = new Map<string, string>()\n for (const e of ctx.entries('resource')) layout.set(`resource:${e.id}`, referencePath(e))\n\n // hook 的產物有鍵值面:事件鍵由保留鍵決定,一般原生鍵原樣併在它後面。\n // `ctx.overrides` 交出的已經濾掉保留鍵,因此 promptex:event 不會混進來。\n for (const e of ctx.entries('hook')) {\n const path = hookPath(e)\n const native = ctx.overrides(e) ?? {}\n const pairs = [['event', eventKey(ctx, e)] as const, ...Object.entries(native).map(([k, v]) => [k, String(v)] as const)]\n files.set(path, `${frontmatter(pairs)}# ${e.name}\\n`)\n }\n\n // resource 的產物是純內文,沒有承載鍵的位置:宣告了覆寫的節點記一筆不支援,\n // 未宣告的什麼都不做(判斷在框架那一份,適配不自己判「有沒有宣告」)。\n for (const e of ctx.entries('resource')) {\n const path = referencePath(e)\n ctx.unsupportedOverride(e, REFERENCE_FORM)\n files.set(path, `# ${e.name}\\n\\n${ctx.render(e, path, layout)}\\n`)\n }\n\n return files\n}\n\nexport default function ledger(): AdapterTarget {\n return defineTarget('ledger', emit)\n}"
2089
+ },
2090
+ {
2091
+ "caption": "prompts/nodes.ts",
2092
+ "lang": "ts",
2093
+ "source": "// 三個節點各對應覆寫介面的一格:保留鍵有值、保留鍵未宣告、以及宣告了覆寫卻\n// 沒有鍵值面可落。\nimport { defineHook, defineResource } from 'promptex-js'\n\n// 產物無鍵值面的那一格:ledger 的參考資料是純內文(見 adapters/ledger.ts 的\n// REFERENCE_FORM),這兩個鍵在本平台沒有位置可寫。兩個鍵而非一個是刻意的:\n// 不支援訊息把鍵名以「、」串起來,單鍵在任何排序下都長得一樣,串接順序因此\n// 看不出來,而那正是各語言生態最容易分岔的地方。宣告順序另刻意寫成字典序的\n// 反面(x-stage 在 x-owner 之前):兩者相等時,一份把鍵排過序的實作與一份原樣\n// 保序的實作產出完全相同的輸出,這則觀察就什麼都看不到。\ndefineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n description: 'Conventional Commits 的 type 對照',\n config: {\n platforms: {\n ledger: { 'x-stage': 'review', 'x-owner': 'docs-team' },\n },\n },\n content: 'type(scope): subject;subject 用繁體中文。',\n})\n\n// 保留鍵有值的那一格:ledger 沒有中性 notification 對應的原生事件,作者以\n// promptex:event 直接指名。同一個覆寫物件內另有三個一般原生鍵,用來驗兩者\n// 確實被分離:保留鍵由框架消費、不進產物,一般鍵原樣依宣告順序寫進開頭欄位。\n//\n// 三個鍵且宣告順序(x-retry、x-owner、x-stage)與字典序(x-owner、x-retry、\n// x-stage)、與反序(x-stage、x-owner、x-retry)三者互不相同:少於三個鍵時\n// 這三種順序會有兩種重合,一份把鍵排過序的實作因此照樣看不出破綻。鍵序是跨\n// 語言契約,這一格是它在覆寫讀取介面上唯一的觀察點。\ndefineHook({\n id: 'deploy-audit',\n name: '部署稽核',\n config: {\n event: 'notification',\n platforms: {\n ledger: { 'promptex:event': 'AuditNotify', 'x-retry': '3', 'x-owner': 'platform-team', 'x-stage': 'canary' },\n },\n },\n actions: [{ type: 'command', command: 'node scripts/audit.mjs' }],\n})\n\n// 保留鍵未宣告的那一格:`ctx.reserved` 回傳空值,適配退回中性事件。沒有這個\n// 對照組時,一個「永遠回傳指名事件」的實作照樣全綠。\ndefineHook({\n id: 'release-notify',\n name: '發布通知',\n config: { event: 'session:start' },\n actions: [{ type: 'command', command: 'node scripts/notify.mjs' }],\n})"
2094
+ },
2095
+ {
2096
+ "caption": "建置輸出",
2097
+ "lang": "console",
2098
+ "source": "$ promptex build --install\n✓ adapter-override-surface: metadata written (graph + content)\n── adapter-override-surface → ledger (<project>) ──\n wrote .ledger/hooks/deploy-audit.md\n wrote .ledger/hooks/release-notify.md\n wrote .ledger/refs/commit-style.md\nInstall report (ledger)\n Projected artifacts, 3: .ledger/hooks/deploy-audit.md, .ledger/hooks/release-notify.md, .ledger/refs/commit-style.md\n Unsupported, 1:\n • resource \"commit-style\": platformOverride has no matching mechanism and was not emitted\n .ledger/refs/ 下的參考資料是純內文、無開頭欄位, so there is no place to carry keys and the declared override keys \"x-stage, x-owner\" were not written into any artifact; platform specialization for this kind of artifact takes the form of replacing the whole artifact rather than overriding keys, so use the adapter's extension mechanism instead"
2099
+ }
2100
+ ],
2101
+ "outcome": [
2102
+ "`.ledger/hooks/deploy-audit.md` 的開頭欄位是 `event: AuditNotify`,接著依宣告順序是 `x-retry`、`x-owner`、`x-stage`。保留鍵 `promptex:event` 不出現在產物。",
2103
+ "`.ledger/hooks/release-notify.md` 沒有宣告覆寫,開頭欄位退回中性事件 `event: session:start`。",
2104
+ "`.ledger/refs/commit-style.md` 是純內文。資源宣告的覆寫鍵 `x-stage, x-owner` 進安裝報告的 Unsupported,訊息帶 ctx.unsupportedOverride 傳入的說明,鍵名依宣告順序列出。"
2105
+ ],
2106
+ "points": [
2107
+ "三個方法各守一件事:overrides(原生鍵)、reserved(保留鍵)、unsupportedOverride(無鍵值面時的回報)。"
2108
+ ],
2109
+ "facts": [
2110
+ "promptex.sdk.define-target",
2111
+ "promptex.sdk.define-hook",
2112
+ "promptex.sdk.define-resource"
2113
+ ]
2114
+ },
2115
+ {
2116
+ "title": "emit 交回核心的產物必須是文字",
2117
+ "situation": "第三方適配把 Buffer 原樣塞進產物表,會怎樣。",
2118
+ "code": [
2119
+ {
2120
+ "caption": "adapters/vault.ts",
2121
+ "lang": "ts",
2122
+ "source": "// 第三方適配(假想平台 vault),刻意壓到最小:只把每則 rule 平鋪成單檔,不宣告\n// 降級也不列不支援。本情境要驗的是「emit 交出的位元組怎麼被把關」,適配本身的\n// 能力範圍不是判準。姊妹子樹 python/rust 的同名情境有等價的一份。\nimport { defineTarget, type AdapterContext, type AdapterTarget } from 'promptex-js'\n\n/** 開頭帶 BOM 的合法產物落點。 */\nconst BOM_PATH = '.vault/prompts/index.md'\n\n/** 非 UTF-8 產物的落點:錯誤訊息會逐字指名它。 */\nconst BROKEN_PATH = '.vault/prompts/broken.md'\n\n/**\n * 開頭帶 BOM 的合法 UTF-8 位元組。適配讀既有檔案再原樣交出時就是這個形態\n * (`readFileSync` 不指定編碼即回 Buffer),BOM 是那個檔案的內容之一。\n *\n * 各語言的解碼手段不同(TypeScript 的 `TextDecoder`、Python 的 `bytes.decode`、\n * Rust 的 `str::from_utf8`),只有第一個預設會把 BOM 吃掉。把關這一步只轉文字、\n * 不增刪內容,BOM 因此必須原樣落到產物裡。\n */\nconst bomBytes = (): Buffer => Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('# vault index\\n', 'utf8')])\n\n/** 兩個單元共用的產出:每則規範一份單檔,外加一份開頭帶 BOM 的索引。 */\nconst legalFiles = (ctx: AdapterContext): Map<string, string> => {\n const files = new Map<string, string>()\n\n const layout = new Map<string, string>()\n for (const e of ctx.entries('rule')) layout.set(`rule:${e.id}`, `.vault/prompts/${e.id}.md`)\n\n for (const e of ctx.entries('rule')) {\n const path = `.vault/prompts/${e.id}.md`\n files.set(path, `# ${e.name}\\n\\n${ctx.render(e, path, layout)}\\n`)\n }\n files.set(BOM_PATH, bomBytes() as unknown as string)\n return files\n}\n\n/** 合法產出:位元組全部是合法 UTF-8,照常建置。 */\nexport default function vault(): AdapterTarget {\n return defineTarget('vault', legalFiles)\n}\n\n/**\n * 多回傳一份非 UTF-8 位元組的同一個適配。\n *\n * 轉型是本情境的重點而非權宜:`AdapterEmit` 宣告的 `Map<string, string>` 是編譯\n * 期契約,而 `emit` 是求值期執行的作者程式碼——型別攔不住它回傳 Buffer\n * (`readFileSync` 不指定編碼即是)。命令列工具因此必須在把產物表交回核心之前\n * 自己擋一次:核心收的是「路徑對文字」,位元組要先變成文字才交得過去。\n */\nexport function vaultWithNonUtf8(): AdapterTarget {\n return defineTarget('vault', (ctx: AdapterContext) => {\n const files = legalFiles(ctx)\n // 0x80 是 UTF-8 的孤立續接位元組,任何合法序列都不會以它起頭。\n files.set(BROKEN_PATH, Buffer.from([0x80, 0x81, 0x82]) as unknown as string)\n return files\n })\n}"
2123
+ },
2124
+ {
2125
+ "caption": "promptex.config.ts",
2126
+ "lang": "ts",
2127
+ "source": "import { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { defineConfig } from 'promptex-js'\nimport vault, { vaultWithNonUtf8 } from './adapters/vault.ts'\n\nconst here = fileURLToPath(new URL('.', import.meta.url))\n\n// 兩個單元共用同一份源碼與同一個第三方平台,差別只在 emit 交出什麼位元組:\n// legal 全部是合法 UTF-8(含一份開頭帶 BOM 的),正常寫入產物。\n// broken 多一份非 UTF-8 位元組,命令列在交回核心之前擋下,該單元零產物。\n// 單元依宣告順序循序執行,legal 排在前,因此它的產物在中止前已完整寫入。落點\n// 各收在情境目錄下的同名子目錄:兩者分開才看得出「哪一個寫了、哪一個一個位元組\n// 都沒寫」。\n//\n// 配置只列第三方目標、不列任何內建平台:產物與框架中繼因此全部出自第三方那條\n// 投影路徑。\nexport default defineConfig([\n {\n name: 'legal',\n srcDir: './prompts',\n outDir: join(here, 'legal'),\n targets: [vault()],\n },\n {\n name: 'broken',\n srcDir: './prompts',\n outDir: join(here, 'broken'),\n targets: [vaultWithNonUtf8()],\n },\n])"
2128
+ },
2129
+ {
2130
+ "caption": "建置失敗",
2131
+ "lang": "console",
2132
+ "source": "$ promptex build --install\n✓ legal: metadata written (graph + content)\n── legal → vault (<project>/legal) ──\n wrote .vault/prompts/component-naming.md\n wrote .vault/prompts/index.md\nInstall report (vault)\n Projected artifacts, 2: .vault/prompts/component-naming.md, .vault/prompts/index.md\n✓ broken: metadata written (graph + content)\n── broken → vault (<project>/broken) ──\n⚠ The placement of sibling unit \"broken\" under the third-party platform vault cannot be derived (that platform's projection lives in the SDK layer); its nodes cannot be referenced in this projection\n⚠ The placement of sibling unit \"legal\" under the third-party platform vault cannot be derived (that platform's projection lives in the SDK layer); its nodes cannot be referenced in this projection\n✗ The third-party adaptation artifact .vault/prompts/broken.md is not UTF-8 text: the artifact table from emit is installed by the core, so its content must be text"
2133
+ }
2134
+ ],
2135
+ "outcome": [
2136
+ "結束碼為 1。先行的 legal 單元已寫入 `legal/.vault/prompts/component-naming.md` 與 `legal/.vault/prompts/index.md`,index.md 開頭的 BOM(EF BB BF)原樣保留。",
2137
+ "終端輸出最後一行是 `✗ The third-party adaptation artifact .vault/prompts/broken.md is not UTF-8 text`,沒有 E 開頭的診斷碼。",
2138
+ "最終磁碟上 broken 單元沒有任何檔案,包含 .promptex 中繼與登記。"
2139
+ ],
2140
+ "points": [
2141
+ "AdapterEmit 的 Map<string, string> 是編譯期契約,型別攔不住求值期回傳 Buffer;把關住命令列驅動交回核心那一步,全有全無的粒度是單元。"
2142
+ ],
2143
+ "facts": [
2144
+ "promptex.sdk.define-target",
2145
+ "promptex.cli.errors-run",
2146
+ "promptex.cli.exit-codes"
2147
+ ]
2148
+ },
2149
+ {
2150
+ "title": "宣告核對排在目標分派之前",
2151
+ "situation": "配置只有第三方適配、沒有內建平台,寫錯宣告還攔不攔得住。",
2152
+ "code": [
2153
+ {
2154
+ "caption": "prompts/nodes.ts",
2155
+ "lang": "ts",
2156
+ "source": "// 一份資源,以及一個引用它、且把素材落點模式寫錯的 skill。\nimport { defineResource, defineSkill, useResource } from 'promptex-js'\n\nexport const commitStyle = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n content: 'type(scope): subject;subject 用繁體中文。',\n})\n\n// meta.resources 的值域封閉於 bundled 與 shared,這裡刻意寫成大小寫手誤的\n// 'Shared'。型別聯集在建置路徑上沒有執行期效力(命令列以\n// node --experimental-strip-types 執行作者源碼),這個值因此原樣抵達中介表示,\n// 由核心的源碼層核對以 E207 擋下。\nexport const commitHelper = defineSkill({\n id: 'commit-helper',\n name: 'Commit 助手',\n // @ts-expect-error 刻意的手誤:型別層確實擋得下它,本情境要驗的是型別檢查\n // 缺席時(作者沒跑那道可選命令)核心那一層仍攔得住。標記本身即是斷言 ──\n // 哪天型別層不再擋這個值,`@ts-expect-error` 會因為「預期的錯誤沒發生」而\n // 讓型別檢查轉紅。\n meta: { resources: 'Shared' },\n config: { whenToUse: '整理提交時' },\n content: ['依慣例撰寫:', useResource('commit-style')],\n})"
2157
+ },
2158
+ {
2159
+ "caption": "promptex.config.ts",
2160
+ "lang": "ts",
2161
+ "source": "import { defineConfig } from 'promptex-js'\nimport acme from './adapters/acme.ts'\n\n// targets 只有帶 emit 的第三方適配,沒有任何內建平台:本次建置因此完全不經核心\n// 的投影入口。源碼層核對若只掛在那裡,這條路徑就一個字都不會說。\nexport default defineConfig({\n name: 'declaration-gate',\n srcDir: './prompts',\n outDir: '.',\n targets: [acme()],\n})"
2162
+ },
2163
+ {
2164
+ "caption": "建置失敗",
2165
+ "lang": "console",
2166
+ "source": "$ promptex build --install\n✗ E207 The asset placement declaration meta.resources received \"Shared\", which is not a usable placement mode: only bundled, shared are available\n at meta.resources of skill \"commit-helper\"\n Omitting this field means bundled (the asset ships inside the directory of the node that references it), except a claude rule, which defaults to shared; write shared only when several nodes must point at one and the same file\nBuild failed: diagnostics include blocking items. 0 artifacts written (all or nothing)."
2167
+ }
2168
+ ],
2169
+ "outcome": [
2170
+ "結束碼為 1,印出 `✗ E207`:meta.resources 收到 \"Shared\",不是可用的落點模式,只有 bundled、shared。定位在 skill `commit-helper` 的 meta.resources。",
2171
+ "輸出沒有 acme 的 `emit 被呼叫` 記號,0 個產物寫入。"
2172
+ ],
2173
+ "points": [
2174
+ "只讀中介表示的源碼層核對排在任何目標分派之前,內建與第三方兩條路受同一份把關;靜默消失(節點被判為不分發)是最壞形態。"
2175
+ ],
2176
+ "facts": [
2177
+ "promptex.diagnostics.e207",
2178
+ "promptex.sdk.define-target",
2179
+ "promptex.cli.exit-codes"
2180
+ ]
2181
+ }
2182
+ ]
2183
+ },
2184
+ {
2185
+ "id": "framework-contracts",
2186
+ "name": "框架契約",
2187
+ "situation": "我想確認框架憑什麼保證等價與決定性",
2188
+ "claim": "中介表示可序列化,不是字串化。關係圖放節點與引用邊,不含落點。配置只搬作者寫下的欄位。鍵序由作者宣告順序決定。三語言逐字比對是唯一驗得動的形態。",
2189
+ "scenarios": [
2190
+ {
2191
+ "title": "求值兩段的分界是可序列化而非字串化",
2192
+ "situation": "想看引用與表格在中介表示裡長什麼樣。",
2193
+ "code": [
2194
+ {
2195
+ "caption": "prompts/flow.ts",
2196
+ "lang": "ts",
2197
+ "source": "import { defineSkill, useResource } from 'promptex-js'\n\nexport const changeFlow = defineSkill({\n id: 'change-flow',\n name: '變更流程',\n config: { whenToUse: '當使用者要求推進變更、提交程式碼時使用' },\n content: [\n '推進變更時依階段執行,commit 慣例見:',\n useResource('commit-style'),\n ],\n})"
2198
+ },
2199
+ {
2200
+ "caption": "prompts/style.ts",
2201
+ "lang": "ts",
2202
+ "source": "import { defineResource, section, table } from 'promptex-js'\n\nconst commitTypes: Array<[string, string]> = [\n ['feat', '新功能'],\n ['fix', '缺陷修正'],\n ['docs', '文件調整'],\n]\n\nexport const commitStyle = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n content: [\n section('格式', 'type(scope): subject;subject 用繁體中文,50 字內。'),\n table(['type', '用途'], commitTypes),\n ],\n})"
2203
+ },
2204
+ {
2205
+ "caption": "建置",
2206
+ "lang": "console",
2207
+ "source": "$ promptex build --install\n✓ ir-boundary: metadata written (graph + content)\n── ir-boundary → claude (<project>) ──\n wrote .claude/skills/change-flow/SKILL.md\n wrote .claude/skills/change-flow/commit-style.md\nInstall report (claude)\n Projected artifacts, 2: .claude/skills/change-flow/SKILL.md, .claude/skills/change-flow/commit-style.md\n Notices, 1:\n • skill \"change-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description"
2208
+ }
2209
+ ],
2210
+ "outcome": [
2211
+ "`.promptex/ir-boundary/content.json` 的 change-flow 條目裡,引用是 `{ \"t\": \"ref\", \"target\": { \"kind\": \"resource\", \"id\": \"commit-style\" } }` 符號,不是路徑字串。",
2212
+ "commit-style 條目裡,表格是 `{ \"t\": \"table\", \"header\": [...], \"rows\": [...] }`,header 與 rows 的每一格都是 `{ \"t\": \"prose\", \"md\": ... }` 節點,三列 feat、fix、docs 都在 rows 陣列裡。",
2213
+ "最終產物才是字串:`.claude/skills/change-flow/SKILL.md` 內有 `[Commit 訊息慣例](commit-style.md)` 連結,`.claude/skills/change-flow/commit-style.md` 內是 markdown 表格。"
2214
+ ],
2215
+ "points": [
2216
+ "分界劃在「可序列化」,引用若在中介表示就渲染成路徑,換平台改不動它;表格若已是字串,擴充要改一列得去 parse。",
2217
+ "content.json 的頂層是 formatVersion、version 與 entries,entries 以節點 id 為鍵。"
2218
+ ],
2219
+ "facts": [
2220
+ "promptex.formats.content",
2221
+ "promptex.formats.graph"
2222
+ ]
2223
+ },
2224
+ {
2225
+ "title": "關係圖記錄節點與引用邊",
2226
+ "situation": "要做影響分析、未引用偵測、分發分析,該讀哪份檔、能讀到什麼。",
2227
+ "code": [
2228
+ {
2229
+ "caption": "prompts/nodes.ts",
2230
+ "lang": "ts",
2231
+ "source": "import { defineAsset, defineHook, defineResource, defineSkill, useAsset, useResource, useSkill } from 'promptex-js'\n\ndefineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n content: 'type(scope): subject;subject 用繁體中文。',\n})\n\ndefineSkill({\n id: 'commit-helper',\n name: 'Commit 助手',\n config: { whenToUse: '整理提交時使用' },\n meta: { tags: ['domain/vcs'] },\n content: ['依慣例撰寫:', useResource('commit-style')],\n})\n\ndefineSkill({\n id: 'pr-flow',\n name: '提交流程',\n config: { whenToUse: '要提 PR 時使用' },\n meta: { targets: ['claude'], tags: ['domain/vcs', 'layer/flow'] },\n content: ['先跑一次 commit 檢查:', useSkill('commit-helper'), ',訊息慣例見:', useResource('commit-style')],\n})\n\ndefineAsset({\n id: 'lint-script',\n name: 'Lint 腳本',\n file: './prompts/assets/lint.sh',\n})\n\ndefineHook({\n id: 'pre-commit-lint',\n name: '提交前檢查',\n config: { event: 'tool:after', matcher: 'Write|Edit' },\n actions: [{ type: 'command', command: [useAsset('lint-script')] }],\n})\n\ndefineResource({\n id: 'orphan-glossary',\n name: '未被引用的術語表',\n content: '術語定義。',\n})\n\ndefineResource({\n id: 'commit-draft',\n name: '提交訊息草稿',\n meta: { targets: 'none', tags: ['domain/vcs'] },\n content: '尚未定案的訊息措辭,留在源碼內但不分發。',\n})"
2232
+ },
2233
+ {
2234
+ "caption": "prompts/assets/lint.sh",
2235
+ "lang": "sh",
2236
+ "source": "#!/bin/sh\n# 示範用的最小格式檢查腳本,本身不是提示詞資源。\necho \"lint ok\""
2237
+ },
2238
+ {
2239
+ "caption": "建置",
2240
+ "lang": "console",
2241
+ "source": "$ promptex build --install\n✓ graph-artifact: metadata written (graph + content)\n── graph-artifact → claude (<project>) ──\n wrote .claude/hooks/pre-commit-lint/lint.sh\n wrote .claude/settings.json\n wrote .claude/skills/commit-helper/SKILL.md\n wrote .claude/skills/commit-helper/commit-style.md\n wrote .claude/skills/pr-flow/SKILL.md\n wrote .claude/skills/pr-flow/commit-style.md\nInstall report (claude)\n Projected artifacts, 6: .claude/hooks/pre-commit-lint/lint.sh, .claude/settings.json, .claude/skills/commit-helper/SKILL.md, .claude/skills/commit-helper/commit-style.md, .claude/skills/pr-flow/SKILL.md, .claude/skills/pr-flow/commit-style.md\n Notices, 2:\n • skill \"commit-helper\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description\n • skill \"pr-flow\"\n This skill has no description, so \"description\" is not written into the artifact and the platform falls back to the first non-empty line of the body; to have the skill listing show your own summary, fill in description"
2242
+ }
2243
+ ],
2244
+ "outcome": [
2245
+ "`.promptex/graph-artifact/graph.json` 的 formatVersion 是 1.2,nodes 有 7 個,一個不少,含沒有任何入邊的 orphan-glossary 與 targets 為 none 的 commit-draft。",
2246
+ "edges 有 4 條:三條 via 是 content(commit-helper 與 pr-flow 引用 commit-style,pr-flow 引用 commit-helper),一條 via 是 command(pre-commit-lint 的指令引用 lint-script)。",
2247
+ "沒宣告 tags 與 targets 的節點不寫該鍵;commit-draft 的 targets 維持作者寫的純量 \"none\",pr-flow 的 targets 維持陣列。",
2248
+ "這份專案沒有宣告平台 plugin,所以節點都沒有 plugins 鍵。",
2249
+ "產物只有 6 個:pre-commit-lint 的 `lint.sh` 與 settings.json,以及兩個 skill 各自帶一份 commit-style.md;orphan-glossary 與 commit-draft 沒有產出。"
2250
+ ],
2251
+ "points": [
2252
+ "放的是:節點識別、名稱、description(宣告了才寫出,與 config.whenToUse 各自獨立)、config、meta、tags、targets、plugins(目標名對平台 plugin 名)、宣告位置 at、引用邊。不放的是:內容(住 content.json)、產物落點、平台渲染形態。",
2253
+ "formatVersion 1.2 起,graph 的節點多選填的 plugins(目標名對平台 plugin 名)。",
2254
+ "build 與 manifest 輸出的 outcome.nodes[] 多選填的 plugin,是本次建置目標下節點所屬的 plugin 名。",
2255
+ "graph 節點的 plugins 與 outcome.nodes[] 的 plugin,只在節點真的打包進平台 plugin 時才寫出。config.targets[] 的 plugins 在目標宣告了平台 plugin 時寫出。這些都是選填欄位,舊版解碼器照常讀得懂。",
2256
+ "graph.json 是可重建的框架中繼,不入版控也不進所有權登記;build 與 install 都會寫。"
2257
+ ],
2258
+ "facts": [
2259
+ "promptex.formats.graph",
2260
+ "promptex.formats.format-version",
2261
+ "promptex.sdk.define-skill",
2262
+ "promptex.sdk.define-hook",
2263
+ "promptex.sdk.define-asset"
2264
+ ]
2265
+ },
2266
+ {
2267
+ "title": "配置以作者語言撰寫,求值結果穩定且只帶寫下的欄位",
2268
+ "situation": "想知道 promptex.config.ts 求值後長什麼樣、省略的欄位會不會被補預設值。",
2269
+ "code": [
2270
+ {
2271
+ "caption": "promptex.config.ts",
2272
+ "lang": "ts",
2273
+ "source": "import { claude, codex, defineConfig, type Plugin } from 'promptex-js'\n\nconst webNote: Plugin = { name: 'web-note' }\n\nexport default defineConfig([\n {\n name: 'global',\n srcDir: './prompts',\n scope: 'user',\n targets: [claude(), codex()],\n },\n {\n name: 'web',\n srcDir: './prompts',\n select: { tags: ['domain/frontend'], exclude: ['status/draft'] },\n outDir: './apps/web',\n targets: [claude()],\n plugins: [webNote],\n },\n])"
2274
+ },
2275
+ {
2276
+ "caption": "讀出配置",
2277
+ "lang": "console",
2278
+ "source": "$ promptex config read\n{\n \"formatVersion\": \"1.2\",\n \"units\": [\n {\n \"name\": \"global\",\n \"fields\": {\n \"name\": \"'global'\",\n \"srcDir\": \"'./prompts'\",\n \"scope\": \"'user'\"\n },\n \"targets\": [\n {\n \"symbol\": \"claude\",\n \"arguments\": \"\",\n \"bare\": false\n },\n {\n \"symbol\": \"codex\",\n \"arguments\": \"\",\n \"bare\": false\n }\n ],\n \"plugins\": [],\n \"selection\": {\n \"tags\": [],\n \"exclude\": []\n },\n \"externals\": []\n },\n {\n \"name\": \"web\",\n \"fields\": {\n \"name\": \"'web'\",\n \"srcDir\": \"'./prompts'\",\n \"outDir\": \"'./apps/web'\",\n \"select\": \"{ tags: ['domain/frontend'], exclude: ['status/draft'] }\"\n },\n \"targets\": [\n {\n \"symbol\": \"claude\",\n \"arguments\": \"\",\n \"bare\": false\n }\n ],\n \"plugins\": [\n {\n \"symbol\": \"webNote\",\n \"arguments\": \"\",\n \"bare\": true\n }\n ],\n \"selection\": {\n \"tags\": [\n \"domain/frontend\"\n ],\n \"exclude\": [\n \"status/draft\"\n ]\n },\n \"externals\": []\n }\n ],\n \"imports\": {\n \"claude\": \"promptex-js\",\n \"codex\": \"promptex-js\",\n \"defineConfig\": \"promptex-js\",\n \"type Plugin\": \"promptex-js\"\n }\n}"
2279
+ }
2280
+ ],
2281
+ "outcome": [
2282
+ "`promptex config read` 結束碼 0,units 依序是 global 與 web。",
2283
+ "global 的 fields 只有 name、srcDir、scope,沒有 outDir 鍵;web 的 fields 只有 name、srcDir、outDir、select,沒有 scope 鍵。",
2284
+ "plugins 與 externals 兩個鍵在兩個單元都出現,沒有宣告時是空陣列。web 的 plugins 只留符號名 webNote,不帶本體。"
2285
+ ],
2286
+ "points": [
2287
+ "省略欄位的填補住核心編排入口一份:scope 依 outDir 推導、outDir 依 scope 取預設、name 取 unit-{索引}。"
2288
+ ],
2289
+ "facts": [
2290
+ "promptex.sdk.config",
2291
+ "promptex.sdk.define-config",
2292
+ "promptex.formats.config-read",
2293
+ "promptex.cli.config"
2294
+ ]
2295
+ },
2296
+ {
2297
+ "title": "meta 的鍵序由作者的宣告順序決定",
2298
+ "situation": "想確認 meta 不會被重排成字典序、具名欄位不會被提到自訂鍵之前。",
2299
+ "code": [
2300
+ {
2301
+ "caption": "prompts/nodes.ts",
2302
+ "lang": "ts",
2303
+ "source": "import { defineResource, defineRule, useResource } from 'promptex-js'\n\nexport const commitStyle = defineResource({\n id: 'commit-style',\n name: 'Commit 訊息慣例',\n meta: { tags: ['domain/ssot'], path: 'guides' },\n content: 'subject 用繁體中文、50 字內。',\n})\n\nexport const prFlow = defineRule({\n id: 'pr-flow',\n name: '提交流程',\n meta: {\n plugins: { 'zeta-plugin': { order: 1 }, 'alpha-plugin': { order: 2 } },\n tags: ['domain/ssot'],\n targets: ['claude'],\n },\n content: ['提 PR 前依序完成分支整理與訊息核對。', useResource('commit-style')],\n})"
2304
+ },
2305
+ {
2306
+ "caption": "建置",
2307
+ "lang": "console",
2308
+ "source": "$ promptex build --install\n✓ meta-key-order: metadata written (graph + content)\n── meta-key-order → claude (<project>) ──\n wrote .claude/resources/guides/commit-style.md\n wrote .claude/rules/pr-flow.md\nInstall report (claude)\n Projected artifacts, 2: .claude/resources/guides/commit-style.md, .claude/rules/pr-flow.md"
2309
+ }
2310
+ ],
2311
+ "outcome": [
2312
+ "`.promptex/meta-key-order/graph.json` 裡 commit-style 的 meta 鍵序是 tags、path;pr-flow 的 meta 鍵序是 plugins、tags、targets,plugin 名依宣告順序 zeta-plugin、alpha-plugin,沒有被排成字典序。",
2313
+ "產物 `.claude/rules/pr-flow.md` 與 `.claude/resources/guides/commit-style.md` 裡查無 meta 的任何鍵,唯一影響產物的是 path(commit-style 落在 guides 目錄)。"
2314
+ ],
2315
+ "points": [
2316
+ "鍵序走樣在產物層沒有徵兆,要看中繼資料裡的 meta 才比對得出來。"
2317
+ ],
2318
+ "facts": [
2319
+ "promptex.formats.graph",
2320
+ "promptex.sdk.define-rule",
2321
+ "promptex.sdk.define-resource"
2322
+ ]
2323
+ }
2324
+ ]
2325
+ }
2326
+ ]
2327
+ }