promptex-js 0.0.0 → 1.0.0-alpha.1

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.
Files changed (61) hide show
  1. package/README.md +89 -55
  2. package/bin/promptex.mjs +17 -111
  3. package/dist/adapter.d.ts +109 -17
  4. package/dist/adapter.js +106 -17
  5. package/dist/cli/compile-engine.d.ts +123 -0
  6. package/dist/cli/compile-engine.js +410 -0
  7. package/dist/cli/core-types.d.ts +197 -0
  8. package/dist/cli/core-types.js +27 -0
  9. package/dist/cli/init.d.ts +12 -0
  10. package/dist/cli/init.js +24 -0
  11. package/dist/cli/main.d.ts +109 -0
  12. package/dist/cli/main.js +1883 -0
  13. package/dist/cli/scratch.d.ts +10 -0
  14. package/dist/cli/scratch.js +24 -0
  15. package/dist/cli/stdout.d.ts +2 -0
  16. package/dist/cli/stdout.js +30 -0
  17. package/dist/cli/teardown.d.ts +22 -0
  18. package/dist/cli/teardown.js +74 -0
  19. package/dist/cli/unit-scope-loader.d.ts +9 -0
  20. package/dist/cli/unit-scope-loader.js +63 -0
  21. package/dist/cli/watch-loader.d.ts +12 -0
  22. package/dist/cli/watch-loader.js +37 -0
  23. package/dist/cli/watch-pass.d.ts +2 -0
  24. package/dist/cli/watch-pass.js +162 -0
  25. package/dist/cli/watch-stamp.d.ts +2 -0
  26. package/dist/cli/watch-stamp.js +15 -0
  27. package/dist/cli/watch.d.ts +93 -0
  28. package/dist/cli/watch.js +301 -0
  29. package/dist/config-rewrite.d.ts +145 -0
  30. package/dist/config-rewrite.js +912 -0
  31. package/dist/config.d.ts +241 -0
  32. package/dist/config.js +94 -0
  33. package/dist/content.d.ts +156 -0
  34. package/dist/content.js +349 -4
  35. package/dist/diag.d.ts +42 -3
  36. package/dist/diag.js +118 -4
  37. package/dist/externals.d.ts +32 -0
  38. package/dist/externals.js +9 -0
  39. package/dist/i18n.d.ts +22 -0
  40. package/dist/i18n.js +55 -0
  41. package/dist/index.d.ts +386 -31
  42. package/dist/index.js +106 -49
  43. package/dist/ir.d.ts +25 -9
  44. package/dist/ir.js +6 -2
  45. package/dist/messages.generated.d.ts +2 -0
  46. package/dist/messages.generated.js +403 -0
  47. package/dist/native.d.ts +103 -0
  48. package/dist/native.js +73 -0
  49. package/dist/paths.d.ts +25 -0
  50. package/dist/paths.js +39 -0
  51. package/dist/plugin.d.ts +105 -13
  52. package/dist/plugin.js +19 -4
  53. package/dist/registry.d.ts +200 -5
  54. package/dist/registry.js +619 -4
  55. package/dist/sources.d.ts +12 -0
  56. package/dist/sources.js +45 -0
  57. package/dist/version.d.ts +44 -0
  58. package/dist/version.js +116 -0
  59. package/package.json +40 -13
  60. package/dist/stub.d.ts +0 -5
  61. package/dist/stub.js +0 -16
package/README.md CHANGED
@@ -1,87 +1,121 @@
1
- # promptex-js(alpha 原型)
1
+ # promptex-js
2
2
 
3
- promptex 的 TypeScript SDK **最小原型**。目前的能力只有一項:宣告 skill,
4
- 建置成 Claude Code 的 `SKILL.md` 產物。
3
+ Declare prompt resources in TypeScript, compile them into whatever your AI coding
4
+ tool expects on disk.
5
5
 
6
- ## 這不是什麼
6
+ Skills, rules, agents, hooks, MCP servers and permissions are ordinary files in a
7
+ directory tree, and every tool wants that tree laid out its own way. This package
8
+ lets you declare each resource once and project the result onto a target platform.
9
+ It ships the TypeScript SDK, a precompiled native binding to the shared core, and
10
+ the `promptex` command line tool.
7
11
 
8
- 本套件是早期 alpha,**不是**完整的 promptex。正式版的單一套件內含 SDK、核心的
9
- 預編譯二進位綁定與完整命令列工具三者;本原型只有 SDK 的最窄一條路徑,命令列
10
- 工具只有 `build` 一個動詞,沒有原生綁定、沒有多平台投影、沒有引用與關係圖、
11
- 沒有安裝與所有權登記。plugin 與 adapter 的契約面(`Plugin`、`AdapterContext`、
12
- `defineTarget` 等)是**介面樁**:型別與簽名逐字複製正式版,方法本體一律拋錯,
13
- 存在的目的是讓第三方擴充能對本套件編譯並發布;要實際執行,工作區得覆寫成本地的
14
- 正式版 SDK。
12
+ ## Install
15
13
 
16
- 介面與產物格式在正式版之前都會變,不保證任何相容性。正式版發布時本原型會被
17
- 整份取代。
18
-
19
- ## 安裝
20
-
21
- ```bash
22
- npm install promptex-js@alpha
14
+ ```console
15
+ npm install promptex-js
23
16
  ```
24
17
 
25
- 需要 Node.js 22.14 以上。
26
-
27
- ## 用法
18
+ ## Minimal example
28
19
 
29
- 把宣告寫在 `prompts/` 下:
20
+ Declarations live in a `prompts/` directory. Anything you `define*` at module
21
+ scope is registered when the file is evaluated.
30
22
 
31
23
  ```ts
32
- // prompts/skill.ts
24
+ // my-project/prompts/review.ts
33
25
  import { defineSkill } from 'promptex-js'
34
26
 
35
- export const commitHelper = defineSkill({
36
- id: 'commit-helper',
37
- name: 'Commit 訊息協助',
38
- config: { whenToUse: '當使用者要求撰寫 commit 訊息時使用' },
39
- content: [
40
- '依 Conventional Commits 撰寫:type(scope): subject。',
41
- 'subject 用繁體中文、50 字內,不以句號結尾;body 說明變更動機而非變更內容。',
42
- ],
27
+ defineSkill({
28
+ id: 'code-review',
29
+ name: 'Code Review',
30
+ description: 'Reviews a pull request for correctness and readability',
31
+ config: { whenToUse: 'Reviewing a pull request' },
32
+ content: 'Check correctness first, then readability.',
43
33
  })
44
34
  ```
45
35
 
46
- 建置:
36
+ Point the command line tool at the directory that *contains* `prompts/`:
47
37
 
48
- ```bash
49
- npx promptex build
38
+ ```console
39
+ $ npx promptex build --install my-project --out my-project --platform claude
40
+ ✓ Metadata written (graph + content)
41
+ wrote .claude/skills/code-review/SKILL.md
42
+ Install report (claude)
43
+ Projected artifacts, 1: .claude/skills/code-review/SKILL.md
50
44
  ```
51
45
 
52
- 產出 `./.claude/skills/commit-helper/SKILL.md`:
46
+ Which writes:
53
47
 
54
48
  ```text
49
+ my-project/.claude/skills/code-review/SKILL.md
50
+ ```
51
+
52
+ ```markdown
55
53
  ---
56
- name: commit-helper
57
- description: 當使用者要求撰寫 commit 訊息時使用
54
+ name: code-review
55
+ description: Reviews a pull request for correctness and readability
56
+ when_to_use: Reviewing a pull request
58
57
  ---
59
58
 
60
- 依 Conventional Commits 撰寫:type(scope): subject。
61
-
62
- subject 用繁體中文、50 字內,不以句號結尾;body 說明變更動機而非變更內容。
59
+ Check correctness first, then readability.
63
60
  ```
64
61
 
65
- `.ts` 宣告檔不需要先經過建置步驟,命令列工具靠 Node 的型別剝離直接載入。
62
+ Switch `--platform codex` and the same declaration lands in Codex's layout instead.
66
63
 
67
- ### 命令列選項
64
+ ## Commands
68
65
 
69
- ```text
70
- promptex build [--src <目錄>] [--out <目錄>]
66
+ | Command | What it does |
67
+ | :--- | :--- |
68
+ | `promptex build <dir>` | Compile and refresh the framework metadata under `.promptex/`. No platform artifacts, no ownership registry, no pruning. |
69
+ | `promptex build --install <dir>` | Compile, write the platform artifacts, record ownership, and prune what you deleted. This is the one to use for real installs. |
70
+ | `promptex build --install --dry-run <dir>` | Show what `--install` would change, without writing anything. Add `--content --json` to get the bytes it would write. |
71
+ | `promptex build --export <export-dir> <dir>` | Write the exact tree `--install` would write under `<export-dir>/<unit>/`, with no registry. The unit subtree is cleared first. |
72
+ | `promptex build --watch <dir>` | Rerun the same flags (`build` alone, `--install` or `--export`) as sources change. |
73
+ | `promptex manifest <dir>` | Answer registry queries from the current projection. |
74
+ | `promptex init <dir>` | Scaffold a new project (`--lang ts`, `python` or `rust`). |
71
75
 
72
- --src <目錄> 宣告檔所在目錄,預設 prompts
73
- --out <目錄> 產物輸出目錄,預設當前目錄
74
- ```
76
+ Common flags: `--out <install root>` (single-directory mode), `--platform <name>` (`manifest` only), `--unit <unit name>`, `--json`, `--locale <tag>`.
75
77
 
76
- ### 程式化呼叫
78
+ ## What you can declare
77
79
 
78
- ```ts
79
- import { build, defineSkill, renderSkill } from 'promptex-js'
80
+ `defineSkill`, `defineRule`, `defineAgent`, `defineInstruction`, `defineResource`,
81
+ `defineHook`, `defineMcp`, `definePermission`, `defineAsset`, `defineDir`.
80
82
 
81
- defineSkill({ /* ... */ })
82
- const written = await build('.')
83
- ```
83
+ Per-platform native settings are not declared in your sources: they belong to the
84
+ target, so you write them as `claude({ settings: { … } })` in `promptex.config.ts`.
85
+ Keys pass through verbatim into that platform's own settings file and the framework
86
+ keeps no allowlist.
87
+
88
+ Content is composed with builders rather than hand-written markdown:
89
+ `section`, `list`, `ordered`, `table`, `code`, `check`, `link`, `file`, `image`,
90
+ `quote`, `details`, `divider`, plus the inline containers `inline`, `bold`,
91
+ `italic`, `inlineCode` and `inlineLink`.
92
+
93
+ Anything that takes children takes the same set: plain text, another builder's
94
+ result, or a reference. That includes list items and table cells, so a step can
95
+ link straight to the resource it depends on. `inline` is the unstyled container:
96
+ it joins its children with no separator and emits no markup, which is what a
97
+ whole sentence made of text and a reference needs.
98
+
99
+ Larger projects add a `promptex.config.ts` with `defineConfig` to split the tree
100
+ into units, and register third-party platforms with `defineTarget`.
101
+
102
+ ## Notes
103
+
104
+ The command line tool runs your declarations directly with Node's built-in type
105
+ stripping, so there is no separate transpile step and promptex does not impose a
106
+ build toolchain on your project.
107
+
108
+ Everything past evaluation — projection, install, registry, graph, diagnostics —
109
+ happens in a shared Rust core called through an in-process native binding, not a
110
+ subprocess. The Python and Rust packages call the same core, so identical
111
+ declarations produce byte-identical output across all three.
112
+
113
+ ## Links
114
+
115
+ - Source and issues: <https://github.com/promptex-ai/promptex>
116
+ - Architecture notes: <https://github.com/promptex-ai/promptex/blob/main/docs/architecture.md>
84
117
 
85
- ## 授權
118
+ ## License
86
119
 
87
- MIT OR Apache-2.0,見 `LICENSE-MIT` 與 `LICENSE-APACHE`。
120
+ Dual-licensed under either of MIT or Apache-2.0, at your option. See `LICENSE-MIT`
121
+ and `LICENSE-APACHE`.
package/bin/promptex.mjs CHANGED
@@ -1,113 +1,19 @@
1
1
  #!/usr/bin/env node
2
- // promptex 命令列工具(alpha 原型)。
2
+ // promptex 的命令列轉發殼。
3
3
  //
4
- // 只有一個動詞:`build`。載入來源目錄下的宣告檔,把登記處內的 skill 投影
5
- // 成 SKILL.md。正式版的其餘動詞(evaluate、declare、config、diagnostics)
6
- // 不在本原型範圍內。
7
-
8
- import { spawnSync } from 'node:child_process'
9
- import { readdir, stat } from 'node:fs/promises'
10
- import { join, resolve } from 'node:path'
11
- import { pathToFileURL } from 'node:url'
12
-
13
- const USAGE = `promptex(alpha 原型)
14
-
15
- 用法:
16
- promptex build [--src <目錄>] [--out <目錄>]
17
-
18
- 選項:
19
- --src <目錄> 宣告檔所在目錄,預設 prompts
20
- --out <目錄> 產物輸出目錄,預設當前目錄
21
- --help 顯示本說明
22
- `
23
-
24
- /** 宣告檔的副檔名。`.ts` 依賴 Node 的型別剝離,不經過額外的建置步驟。 */
25
- const SOURCE_EXTENSIONS = ['.ts', '.mts', '.js', '.mjs']
26
-
27
- function parseArgs(argv) {
28
- const args = { verb: argv[0], src: 'prompts', out: '.' }
29
- for (let i = 1; i < argv.length; i += 1) {
30
- const flag = argv[i]
31
- if (flag === '--src' || flag === '--out') {
32
- const value = argv[i + 1]
33
- if (value === undefined) throw new Error(`${flag} 後面缺少值`)
34
- args[flag.slice(2)] = value
35
- i += 1
36
- } else {
37
- throw new Error(`未知的選項:${flag}`)
38
- }
39
- }
40
- return args
41
- }
42
-
43
- /** 遞迴列出來源目錄下的宣告檔,依路徑排序讓載入順序不隨檔案系統而變。 */
44
- async function collectSources(dir) {
45
- const entries = await readdir(dir, { withFileTypes: true })
46
- const found = []
47
- for (const entry of entries) {
48
- const path = join(dir, entry.name)
49
- if (entry.isDirectory()) found.push(...(await collectSources(path)))
50
- else if (SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) found.push(path)
51
- }
52
- return found.sort()
53
- }
54
-
55
- async function main() {
56
- const argv = process.argv.slice(2)
57
- if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
58
- process.stdout.write(USAGE)
59
- return
60
- }
61
-
62
- const args = parseArgs(argv)
63
- if (args.verb !== 'build') {
64
- process.stderr.write(`未知的動詞:${args.verb}\n\n${USAGE}`)
65
- process.exitCode = 1
66
- return
67
- }
68
-
69
- const srcDir = resolve(args.src)
70
- try {
71
- if (!(await stat(srcDir)).isDirectory()) throw new Error('不是目錄')
72
- } catch {
73
- process.stderr.write(`找不到來源目錄:${srcDir}\n`)
74
- process.exitCode = 1
75
- return
76
- }
77
-
78
- const sources = await collectSources(srcDir)
79
- if (sources.length === 0) {
80
- process.stderr.write(`來源目錄沒有宣告檔(${SOURCE_EXTENSIONS.join('、')}):${srcDir}\n`)
81
- process.exitCode = 1
82
- return
83
- }
84
-
85
- // 載入宣告檔與呼叫 build 必須共用同一個模組實例,登記處才看得到宣告;
86
- // 兩邊都經 `promptex-js` 的 dist 進入點解析,因此指向同一個模組。
87
- const { build, declaredCount } = await import('../dist/index.js')
88
- for (const source of sources) await import(pathToFileURL(source).href)
89
-
90
- if (declaredCount() === 0) {
91
- process.stderr.write(`載入了 ${sources.length} 份宣告檔,但沒有任何 skill 被宣告。\n`)
92
- process.exitCode = 1
93
- return
94
- }
95
-
96
- for (const path of await build(resolve(args.out))) process.stdout.write(`${path}\n`)
97
- }
98
-
99
- // Node 未內建型別剝離時(22.18 之前)以旗標重跑一次,讓 `.ts` 宣告檔不必
100
- // 先經過建置步驟。已內建的版本不重跑,避免多開一個行程。
101
- if (!process.features.typescript) {
102
- const result = spawnSync(
103
- process.execPath,
104
- ['--experimental-strip-types', new URL(import.meta.url).pathname, ...process.argv.slice(2)],
105
- { stdio: 'inherit' },
106
- )
107
- process.exit(result.status ?? 1)
108
- } else {
109
- await main().catch((error) => {
110
- process.stderr.write(`${error.message}\n`)
111
- process.exitCode = 1
112
- })
113
- }
4
+ // 邏輯住 `src/cli/main.ts`(發布物為 `dist/cli/main.js`),本檔只把 argv 交過去:
5
+ // 命令列的接線與指令分派是 TypeScript 源碼的一部分,與 SDK 同受型別檢查
6
+ // 與同一次 `tsc` 建置,不再是套件裡唯一沒有型別的那一批 `.mjs`。
7
+ //
8
+ // 靠 Node 內建的型別剝除執行作者源碼:作者的提示詞源碼是 TypeScript,求值即
9
+ // 執行,用內建能力才不必把某套建置工具鏈的偏好強加給使用者專案。
10
+ //
11
+ // shebang 不帶 --experimental-strip-types:型別剝除自 Node 22.18 起已是預設,
12
+ // 旗標多餘;而它留在 shebang 裡會讓本套件在 Linux 上完全無法執行 ── GNU 的
13
+ // env 不拆解 shebang 後面的參數(需 -S),症狀是
14
+ // `env: 'node --experimental-strip-types': No such file or directory`。
15
+ // macOS 的 env 會拆,因此這個缺陷只在 Linux 顯現。下界由 package.json 的
16
+ // engines 宣告。
17
+ import { main } from '../dist/cli/main.js'
18
+
19
+ await main(process.argv)
package/dist/adapter.d.ts CHANGED
@@ -1,59 +1,151 @@
1
1
  import type { Kind } from './ir.js';
2
2
  import type { PluginEntry } from './plugin.js';
3
- import type { SelectSpec } from './registry.js';
4
- /** 降級或不支援的一筆記錄,與核心 `adapter::DegradeEntry` 同型。 */
3
+ import { type SelectSpec } from './registry.js';
4
+ /**
5
+ * 降級或不支援的一筆記錄,與核心 `adapter::DegradeEntry` 同型(AC-098):
6
+ * 欄位名與序列化形態逐一對應,`formatReport` 因此可以把整份報告原樣交給
7
+ * 核心格式化。
8
+ */
5
9
  export interface DegradeItem {
6
10
  readonly kind: Kind;
7
11
  readonly nodeId: string;
8
12
  readonly feature: string;
9
- /** 降級前後的強制力層級;純機制缺失(不支援)而無強制力落差時省略。 */
13
+ /**
14
+ * 降級前後的強制力層級(AC-093 的三段封閉列舉);純機制缺失(不支援)
15
+ * 而無強制力落差時省略。
16
+ *
17
+ * 第三方適配同樣填得出這兩個欄位,不是內建平台專屬:報告的形態一旦讓
18
+ * 內建適配表達得比第三方多,「官方與第三方使用同一組公開能力」
19
+ * (AC-097)就只剩宣稱。
20
+ */
10
21
  readonly from?: Enforcement;
11
22
  readonly to?: Enforcement;
12
23
  readonly note: string;
13
24
  }
14
- /** 強制力階梯:機械閘 > 流程閘 > 措辭層,列舉封閉。 */
25
+ /** 強制力階梯(AC-093):機械閘 > 流程閘 > 措辭層,列舉封閉。 */
15
26
  export type Enforcement = 'mechanical' | 'processGate' | 'wording';
16
- /** 一次第三方投影的安裝報告,與內建平台的 `Report` 同一形態。 */
27
+ /**
28
+ * 一次第三方投影的安裝報告,與內建平台的 `Report`/`EmitReport` 同一形態
29
+ * (AC-098):降級與不支援兩類皆進報告,不得靜默丟棄,因此第三方與內建
30
+ * 平台的產物能一併呈現在同一份人讀輸出中,不需要另一套報告格式。
31
+ */
17
32
  export interface EmitReport {
18
33
  readonly platform: string;
19
34
  readonly emitted: readonly string[];
20
35
  readonly degraded: readonly DegradeItem[];
21
36
  readonly unsupported: readonly DegradeItem[];
22
37
  }
23
- /** 第三方 adapter 的公開介面:官方與第三方適配使用同一組方法。 */
38
+ /**
39
+ * 第三方 adapter 的公開介面(AC-097):官方與第三方適配使用同一組方法。
40
+ * `entries` 讀本平台要產出的節點;`render` 依落點表渲染節點內容;
41
+ * `overrides`/`reserved` 讀本節點的平台覆寫與框架保留鍵;
42
+ * `unsupportedOverride` 記下沒有鍵值面時那筆不支援;`degrade`/`unsupported`
43
+ * 記錄能力落差;`report`/`hasNotes` 取出累積的落差記錄。
44
+ *
45
+ * 兩份累積清單為內部狀態、經 `report` 取出,不另以同名屬性暴露:兩個記錄
46
+ * 方法的名稱(`degrade`/`unsupported`)本身即介面的一部分,把清單也用同名
47
+ * 屬性暴露會與方法名相撞。三個 SDK 的方法集合完全相同,目前沒有自動比對;
48
+ * 少一個方法就是「這個生態的第三方適配做得到的事比另兩個少」。
49
+ */
24
50
  export interface AdapterContext {
25
- /** 本平台要產出的節點(省略 kind 即全部)。 */
51
+ /**
52
+ * 本平台要產出的節點(省略 kind 即全部):已套用節點自身宣告的平台限定
53
+ * (`meta.targets`)與配置單元的分發選取(`select`),見
54
+ * `createAdapterContext`。
55
+ */
26
56
  entries(kind?: Kind): readonly PluginEntry[];
27
- /** 依落點表渲染節點內容:第三個參數是落點對照表,不是回呼函式。 */
57
+ /** 依落點表渲染節點內容(AC-100):第三個參數是落點對照表,不是回呼函式。 */
28
58
  render(entry: PluginEntry, selfPath: string, layout: ReadonlyMap<string, string>): string;
29
- /** 本節點為本平台宣告的平台原生鍵覆寫;未宣告或空物件時回傳 `undefined`。 */
59
+ /**
60
+ * 本節點為本平台宣告的平台原生鍵覆寫(`config.platforms.{平台}`),未宣告
61
+ * 或宣告了空物件時回傳 `undefined`(ADR-004「空物件視同未宣告」)。
62
+ *
63
+ * 框架保留鍵(`promptex:` 前綴)已濾掉,回傳的每個鍵都是可原樣併進本平台
64
+ * 產物的原生鍵;保留鍵另由 `reserved` 取出。
65
+ *
66
+ * 這個方法存在的理由是「官方適配也只使用這組公開介面」(ADR-004)此前在
67
+ * 覆寫這一格只是宣稱:內建適配讀的是核心的 `adapter::platform_overrides`,
68
+ * 第三方適配只能自己走 `entry.def.config.platforms[平台]`,於是「空物件視同
69
+ * 未宣告」的判定與保留鍵的過濾各自重造。兩種失效都沒有訊號:判定漂移只在
70
+ * 特定宣告下現形,保留鍵漏濾則直接把框架的指令洩漏成平台不認得的欄位。
71
+ */
30
72
  overrides(entry: PluginEntry): Readonly<Record<string, unknown>> | undefined;
31
- /** 本節點宣告的框架保留鍵(`promptex:` 命名空間);未宣告時回傳 `undefined`。 */
73
+ /**
74
+ * 本節點宣告的框架保留鍵(`promptex:` 命名空間),鍵是保留鍵名、值是它的
75
+ * 字串值;未宣告時回傳 `undefined`。
76
+ *
77
+ * 保留鍵是「覆寫的鍵即平台原生鍵」這條形態唯一的例外:它是寫給框架的指令,
78
+ * 由框架消費、不寫進產物(ADR-004)。宣告位置與值形態的錯誤已在投影入口以
79
+ * `E203` 阻斷,本方法回傳的都是合法且可直接使用的值。現行保留鍵只有
80
+ * `promptex:event`(僅 hook 合法)。
81
+ */
32
82
  reserved(entry: PluginEntry): Readonly<Record<string, string>> | undefined;
33
- /** 本節點在本平台的產物沒有承載鍵的位置時,把該筆記進不支援清單。 */
83
+ /**
84
+ * 本節點在本平台的產物沒有承載鍵的位置時,把該筆記進不支援清單;節點未宣告
85
+ * 覆寫時什麼都不做。
86
+ *
87
+ * `form` 由呼叫端寫出本平台該類型的產物形態:哪些類型在本平台沒有鍵值面是
88
+ * 平台知識、住各適配(ADR-009),但「宣告的覆寫沒有落點」這件事對作者的意義
89
+ * 每個平台相同,訊息模板因此共用核心那一份。
90
+ */
34
91
  unsupportedOverride(entry: PluginEntry, form: string): void;
35
92
  /** 記錄能力降級,進安裝報告。 */
36
93
  degrade(item: DegradeItem): void;
37
94
  /** 記錄平台不支援而未產出的節點,進安裝報告。 */
38
95
  unsupported(item: DegradeItem): void;
39
- /** 把本次累積的降級與不支援併成安裝報告。 */
96
+ /**
97
+ * 把本次累積的降級與不支援併成安裝報告;`emitted` 由呼叫端依實際寫出的
98
+ * 產物決定,不在本介面判斷。
99
+ */
40
100
  report(platform: string, emitted: readonly string[]): EmitReport;
41
- /** 是否累積了任何降級或不支援記錄。 */
101
+ /**
102
+ * 是否累積了任何降級或不支援記錄:擴充路徑據此決定要不要印報告(無落差時
103
+ * 多印一份空報告只是噪音)。
104
+ */
42
105
  hasNotes(): boolean;
43
106
  }
44
107
  export type AdapterEmit = (ctx: AdapterContext) => Map<string, string>;
45
- /** 第三方適配的 target:與內建 `claude()`/`codex()` 同形,多帶一個 `emit`。 */
108
+ /**
109
+ * 第三方適配的 target:與內建 `claude()`/`codex()` 回傳同一種 `{ platform
110
+ * }` 形狀,多帶一個 `emit` 實作(AC-099)。`defineConfig` 的 `targets`
111
+ * 陣列因此可與內建平台原樣並列宣告,schema 不因擴充而改變。
112
+ */
46
113
  export interface AdapterTarget {
47
114
  readonly platform: string;
48
115
  readonly emit: AdapterEmit;
49
- /** 本適配接受哪些建構參數,標準 JSON Schema。 */
116
+ /**
117
+ * 本適配接受哪些建構參數,標準 JSON Schema(ADR-013)。形態與寫法見
118
+ * `Plugin.configSchema`;`emit` 帶函式本體、不進中介表示,不得寫進宣告。
119
+ */
50
120
  readonly configSchema?: unknown;
51
121
  }
52
122
  /** 第三方套件用本函式建構 target。 */
53
123
  export declare function defineTarget(platform: string, emit: AdapterEmit, options?: {
54
124
  readonly configSchema?: unknown;
55
125
  }): AdapterTarget;
56
- /** 格式化為人讀文字:正式版委派核心 `adapter::Report::format`。 */
126
+ /**
127
+ * 格式化為人讀文字:委派核心 `adapter::Report::format`(經
128
+ * `promptex-js-binding` 的 `formatReport` 匯出,ADR-008),不在 SDK 側重寫。
129
+ *
130
+ * 這裡原本是一份 TypeScript 對等實作。它與核心那份已經分岔:核心會印強制力
131
+ * 階梯(`,強制力 {from} → {to}`),SDK 這份沒有,因為當時的 `DegradeItem`
132
+ * 連 `from`/`to` 兩個欄位都沒有。改為委派之後,第三方適配與內建平台的報告
133
+ * 是同一份機器碼產出的,不再有「寫的當下對得上、核心改了才分岔」的缺口。
134
+ */
57
135
  export declare function formatReport(report: EmitReport): string;
58
- /** 建立一份 `AdapterContext` 供命令列呼叫第三方 `emit` 或 `extend` 函式。 */
136
+ /**
137
+ * 建立一份 `AdapterContext` 供命令列呼叫第三方 `emit` 或 `extend` 函式。
138
+ *
139
+ * 收平台名與配置單元的 `select`,兩者一起決定 `entries` 回傳哪些節點
140
+ * (見 `registry.ts` 的 `entriesFor`):節點過濾在內建平台由核心的投影承擔
141
+ * (`meta.targets` 走 `adapter::is_distributed_to`、`select` 走 `toIR`),
142
+ * 第三方適配不經核心投影,同樣兩道排除因此必須在建構脈絡時就帶進來。
143
+ * 少了它們,宣告 `targets: ['claude']` 的節點會被第三方適配照樣產出,多個
144
+ * 配置單元共用同一個 `srcDir` 時每個落點還會收到全庫節點。
145
+ *
146
+ * 累積的落差經 `ctx.report(platform, emitted)` 取出,不再回傳兩個裸陣列:
147
+ * 裸陣列讓呼叫端能繞過介面直接組裝報告,作者的 `emit` 實作卻拿不到同一條
148
+ * 路徑,Python/Rust 兩側則是兩者都走 `report()`。同一件事在三個生態有兩種
149
+ * 形態,就是「這個生態的第三方適配做得到的事比另兩個少」。
150
+ */
59
151
  export declare function createAdapterContext(platform: string, select?: SelectSpec): AdapterContext;
package/dist/adapter.js CHANGED
@@ -1,11 +1,28 @@
1
- // 第三方 adapter 的公開介面(介面樁):型別逐字複製正式版
2
- // packages/promptex-js/src/adapter.ts。
1
+ // 第三方 adapter 的公開介面(AC-097 至 AC-101,platform-adaptation BC 的
2
+ // 適配可擴充性):官方(claude/codex)與第三方適配使用同一組
3
+ // `AdapterContext` 方法,不存在「內建能用而擴充不能用」的私有 API。
3
4
  //
4
- // 照抄與樁的分界:`defineTarget` 只是組一個物件,照抄;`createAdapterContext`
5
- // 回傳物件的 `degrade`/`unsupported`/`report`/`hasNotes` 只碰自身閉包,照抄;
6
- // `entries`/`render`/`overrides`/`reserved`/`unsupportedOverride` 要查註冊表
7
- // 或委派核心,本體為樁;`formatReport` 委派核心的報告渲染,本體為樁。
8
- import { stub } from './stub.js';
5
+ // 兩條 adapter 路徑分工不同軸:內建平台的實際投影住 Rust core
6
+ // (crates/promptex-core/src/adapter),以核心語言隨二進位分發;第三方
7
+ // 適配住這裡(SDK 層),以作者選用的語言撰寫(本 SDK 為 TypeScript),
8
+ // 不需要修改核心程式碼即可分發(AC-099):command-line 工具
9
+ // (src/cli/main.ts)偵測到 target 帶有 `emit` 時改呼叫這裡的函式,不
10
+ // 透過核心二進位。第三方適配因此綁語言:以 TypeScript 實作者只服務
11
+ // TypeScript 專案,這是擴充自由度的顯性代價(AC-101)。
12
+ //
13
+ // 渲染(`render`)接受落點表而非回呼函式(AC-100):算 `[名稱](相對路徑)`
14
+ // 需要同時知道引用方與目標各在哪,若介面設計成接受回呼,跨語言邊界就得
15
+ // 傳函式指標;改為接受落點表後只有資料進出。渲染邏輯本身(`Piece[]` →
16
+ // markdown 字串,含 `ref` 解析與相對路徑計算)委派 Rust core 的
17
+ // `crates/promptex-core/src/project/mod.rs`(`render_pieces`/`render_ref`/
18
+ // `relative_path`,經 `promptex-js-binding` 的 `renderPieces` 匯出,ADR-008):
19
+ // 本檔只做「求值期概念(`Content`,含函式呼叫、引用建構子)轉成純資料
20
+ // `Piece[]`」這一步,那一步依賴 SDK 側的註冊表(`engine`),核心無從得知;
21
+ // 轉換完成後的渲染是同一份機器碼,不再有第二份 TypeScript 對等實作要與
22
+ // 核心人工核對是否同步。
23
+ import { contentToPieces, engine, entriesFor } from './registry.js';
24
+ import { currentLocale } from './i18n.js';
25
+ import { loadNativeCore } from './native.js';
9
26
  /** 第三方套件用本函式建構 target。 */
10
27
  export function defineTarget(platform, emit, options = {}) {
11
28
  return {
@@ -14,25 +31,97 @@ export function defineTarget(platform, emit, options = {}) {
14
31
  ...(options.configSchema !== undefined ? { configSchema: options.configSchema } : {}),
15
32
  };
16
33
  }
17
- /** 格式化為人讀文字:正式版委派核心 `adapter::Report::format`。 */
34
+ /**
35
+ * 格式化為人讀文字:委派核心 `adapter::Report::format`(經
36
+ * `promptex-js-binding` 的 `formatReport` 匯出,ADR-008),不在 SDK 側重寫。
37
+ *
38
+ * 這裡原本是一份 TypeScript 對等實作。它與核心那份已經分岔:核心會印強制力
39
+ * 階梯(`,強制力 {from} → {to}`),SDK 這份沒有,因為當時的 `DegradeItem`
40
+ * 連 `from`/`to` 兩個欄位都沒有。改為委派之後,第三方適配與內建平台的報告
41
+ * 是同一份機器碼產出的,不再有「寫的當下對得上、核心改了才分岔」的缺口。
42
+ */
18
43
  export function formatReport(report) {
19
- void report;
20
- return stub('formatReport');
44
+ return loadNativeCore().formatReport(JSON.stringify(report), currentLocale());
21
45
  }
22
- /** 建立一份 `AdapterContext` 供命令列呼叫第三方 `emit` 或 `extend` 函式。 */
46
+ /**
47
+ * 建立一份 `AdapterContext` 供命令列呼叫第三方 `emit` 或 `extend` 函式。
48
+ *
49
+ * 收平台名與配置單元的 `select`,兩者一起決定 `entries` 回傳哪些節點
50
+ * (見 `registry.ts` 的 `entriesFor`):節點過濾在內建平台由核心的投影承擔
51
+ * (`meta.targets` 走 `adapter::is_distributed_to`、`select` 走 `toIR`),
52
+ * 第三方適配不經核心投影,同樣兩道排除因此必須在建構脈絡時就帶進來。
53
+ * 少了它們,宣告 `targets: ['claude']` 的節點會被第三方適配照樣產出,多個
54
+ * 配置單元共用同一個 `srcDir` 時每個落點還會收到全庫節點。
55
+ *
56
+ * 累積的落差經 `ctx.report(platform, emitted)` 取出,不再回傳兩個裸陣列:
57
+ * 裸陣列讓呼叫端能繞過介面直接組裝報告,作者的 `emit` 實作卻拿不到同一條
58
+ * 路徑,Python/Rust 兩側則是兩者都走 `report()`。同一件事在三個生態有兩種
59
+ * 形態,就是「這個生態的第三方適配做得到的事比另兩個少」。
60
+ */
23
61
  export function createAdapterContext(platform, select) {
24
- void select;
25
62
  const degraded = [];
26
63
  const unsupported = [];
27
64
  return {
28
- entries: () => stub('AdapterContext.entries'),
29
- render: () => stub('AdapterContext.render'),
30
- overrides: () => stub('AdapterContext.overrides'),
31
- reserved: () => stub('AdapterContext.reserved'),
32
- unsupportedOverride: () => stub('AdapterContext.unsupportedOverride'),
65
+ entries: (kind) => entriesFor(platform, select, kind),
66
+ render: (entry, selfPath, layout) => renderEntry(entry, selfPath, layout),
67
+ overrides: (entry) => nonEmpty(resolveOverrides(entry, platform).native),
68
+ reserved: (entry) => nonEmpty(resolveOverrides(entry, platform).reserved),
69
+ unsupportedOverride: (entry, form) => {
70
+ const nodeJson = JSON.stringify(overridePayload(entry));
71
+ const item = JSON.parse(loadNativeCore().unsupportedOverride(nodeJson, platform, form, currentLocale()));
72
+ if (item)
73
+ unsupported.push(item);
74
+ },
33
75
  degrade: (item) => degraded.push(item),
34
76
  unsupported: (item) => unsupported.push(item),
35
77
  report: (platform, emitted) => ({ platform, emitted: [...emitted], degraded: [...degraded], unsupported: [...unsupported] }),
36
78
  hasNotes: () => degraded.length > 0 || unsupported.length > 0,
37
79
  };
38
80
  }
81
+ // ── 覆寫讀取:委派核心 adapter::resolve_overrides(ADR-004),SDK 不自行解析 ──
82
+ /**
83
+ * 交給核心的節點形態:只要 `kind`/`id`/`config` 三欄。
84
+ *
85
+ * 不序列化整個節點:`name` 與 `content` 與覆寫無關,而 `content` 在求值期
86
+ * 含函式呼叫與引用建構子,要求它先轉成 `Piece[]` 才讀得到覆寫,等於為了讀
87
+ * 幾個鍵付一次完整的內容正規化。
88
+ */
89
+ function overridePayload(entry) {
90
+ return { kind: entry.kind, id: entry.id, config: entry.def.config ?? null };
91
+ }
92
+ /** 委派核心把覆寫拆成平台原生鍵與框架保留鍵兩份。 */
93
+ function resolveOverrides(entry, platform) {
94
+ const nodeJson = JSON.stringify(overridePayload(entry));
95
+ return JSON.parse(loadNativeCore().platformOverrides(nodeJson, platform, currentLocale()));
96
+ }
97
+ /** 空物件與未宣告在介面上是同一件事(ADR-004),一律回 `undefined`。 */
98
+ function nonEmpty(map) {
99
+ return Object.keys(map).length > 0 ? map : undefined;
100
+ }
101
+ // ── 渲染:委派核心 project::render_pieces(AC-100),SDK 不再重複實作 ──
102
+ /**
103
+ * `Content` → `Piece[]` 這一步留在 SDK(依賴 `entry.def.content` 與求值期
104
+ * 概念,核心無從得知),轉換完成後把純資料連同落點表、名稱表一併交給
105
+ * 原生綁定的 `renderPieces`,回傳的字串就是最終 markdown。
106
+ *
107
+ * 名稱表覆蓋 `engine.entries()` 當下的全部節點(依 `kind:id` 複合鍵,與核心
108
+ * `key()` 同構):渲染發生在單次呼叫裡,無法像原本的 TypeScript 實作那樣
109
+ * 在遇到 `ref` 當下才惰性查一次,因此在呼叫前把可能用得到的名稱全部帶過去。
110
+ */
111
+ function renderEntry(entry, selfPath, layout) {
112
+ const pieces = contentToPieces(entry.def.content);
113
+ const layoutObj = {};
114
+ for (const [k, v] of layout)
115
+ layoutObj[k] = v;
116
+ const namesObj = {};
117
+ for (const e of engine.entries())
118
+ namesObj[`${e.kind}:${e.id}`] = e.name;
119
+ // 逐個引數先接成綁定那側的參數名再傳,讓位置引數的對應關係在源碼上看得出
120
+ // 來;三方的參數順序目前沒有自動對拍。
121
+ // 內文從第幾級標題起算不在這裡給:三個 SDK 的 `ctx.render` 必須渲染出逐字
122
+ // 相同的 markdown,該值住核心(`ops::THIRD_PARTY_BODY_DEPTH`)。
123
+ const piecesJson = JSON.stringify(pieces);
124
+ const layoutJson = JSON.stringify(layoutObj);
125
+ const namesJson = JSON.stringify(namesObj);
126
+ return loadNativeCore().renderPieces(piecesJson, selfPath, layoutJson, namesJson, currentLocale());
127
+ }