akari-video 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- package/bin/akari.mjs +19 -3
- package/package.json +1 -1
- package/src/accept-command.mjs +185 -0
- package/src/capability-command.mjs +47 -0
- package/src/capability-sources.mjs +122 -0
- package/src/capability.mjs +180 -0
- package/src/cli.mjs +33 -7
- package/src/first-run.mjs +217 -0
- package/src/init-command.mjs +135 -0
- package/src/messages.mjs +85 -0
- package/src/repo-assets.mjs +14 -4
- package/src/sounds-setup.mjs +107 -0
- package/src/status-command.mjs +36 -0
- package/src/status-core/events.mjs +242 -0
- package/src/status-core/integrity.mjs +373 -0
- package/src/status-core/review.mjs +196 -0
- package/src/status-core/status.mjs +441 -0
- package/src/store-command.mjs +243 -0
- package/src/store-device-connect.mjs +184 -0
- package/vendor/.akari-capability-sources.json +166 -0
- package/vendor/assets/font/noto-sans-jp/NotoSansJP-Variable.ttf +0 -0
- package/vendor/docs/contract-2026-07-13-asset-library.md +422 -0
- package/vendor/docs/contract-2026-07-13-m1-m4.md +214 -0
- package/vendor/docs/contract-2026-07-13-m5-analysis-report.md +160 -0
- package/vendor/docs/contract-2026-07-14-3d-bake-recipe.md +149 -0
- package/vendor/docs/contract-2026-07-14-edit-json-v1-audio.md +241 -0
- package/vendor/docs/contract-2026-07-14-edit-json-v1-crop.md +251 -0
- package/vendor/docs/contract-2026-07-17-data-contract-versioning.md +66 -0
- package/vendor/docs/contract-2026-07-18-edit-json-v1-sources.md +113 -0
- package/vendor/docs/contract-2026-07-20-edit-json-v1-narration.md +179 -0
- package/vendor/docs/contract-2026-07-20-plan-json-v0.md +233 -0
- package/vendor/docs/contract-2026-07-20-review-json-v1-annotation-model.md +229 -0
- package/vendor/docs/contract-2026-07-22-edit-json-v1-beats.md +185 -0
- package/vendor/docs/contract-2026-07-22-render-basics.md +36 -0
- package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +210 -0
- package/vendor/docs/contract-2026-07-23-edit-json-v1-direction.md +180 -0
- package/vendor/docs/contract-2026-07-23-edit-json-v1-emphasis-words.md +221 -0
- package/vendor/docs/contract-2026-07-25-memory-connection-v0.md +198 -0
- package/vendor/docs/contract-2026-07-25-plan-comments-v0.md +168 -0
- package/vendor/docs/contract-2026-07-25-project-structure-v0.md +134 -0
- package/vendor/docs/contract-2026-07-25-r6-audio-tracks-and-trim.md +65 -0
- package/vendor/docs/contract-2026-07-25-recipe-v0.md +219 -0
- package/vendor/docs/contract-2026-07-26-avatar-registry-v0.md +263 -0
- package/vendor/docs/contract-2026-08-01-export-nle-beta.md +95 -0
- package/vendor/docs/contract-2026-08-02-creator-root-v1.md +153 -0
- package/vendor/docs/contract-2026-08-02-preview-parity.md +102 -0
- package/vendor/docs/contract-2026-08-02-setup-remote-v0.md +81 -0
- package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +64 -0
- package/vendor/docs/contract-2026-08-03-cut-candidate-bridge-v1.md +106 -0
- package/vendor/docs/contract-2026-08-03-status-integrity-v1.md +98 -0
- package/vendor/packages/akari-launcher/README.md +107 -0
- package/vendor/packages/akari-launcher/bin/akari.mjs +30 -0
- package/vendor/packages/akari-launcher/package.json +42 -0
- package/vendor/packages/analysis-report/README.md +102 -0
- package/vendor/packages/analysis-report/package.json +13 -0
- package/vendor/packages/analysis-report/render-analysis-report.mjs +529 -0
- package/vendor/packages/asset-resolver/README.md +111 -0
- package/vendor/packages/asset-resolver/bin/akari-assets.mjs +105 -0
- package/vendor/packages/asset-resolver/package.json +16 -0
- package/vendor/packages/audio-library-setup/README.md +86 -0
- package/vendor/packages/audio-library-setup/bin/beat-grid.mjs +173 -0
- package/vendor/packages/audio-library-setup/bin/declare-helper.mjs +35 -0
- package/vendor/packages/audio-library-setup/bin/fetch-akari-sounds.mjs +267 -0
- package/vendor/packages/audio-library-setup/bin/gallery-helper.mjs +34 -0
- package/vendor/packages/audio-library-setup/bin/generate-candidates-html.mjs +276 -0
- package/vendor/packages/audio-library-setup/bin/register-drop-folder.mjs +352 -0
- package/vendor/packages/audio-library-setup/bin/review-sfx-mapping.mjs +184 -0
- package/vendor/packages/audio-library-setup/bin/suggest-bgm.mjs +164 -0
- package/vendor/packages/audio-library-setup/bin/suggest-sfx.mjs +122 -0
- package/vendor/packages/audio-library-setup/declare-server.mjs +362 -0
- package/vendor/packages/audio-library-setup/declare-template.html +750 -0
- package/vendor/packages/audio-library-setup/gallery-server.mjs +218 -0
- package/vendor/packages/audio-library-setup/gallery-template.html +208 -0
- package/vendor/packages/audio-library-setup/package.json +15 -0
- package/vendor/packages/audio-library-setup/shared/akari-sounds.mjs +164 -0
- package/vendor/packages/audio-library-setup/shared/beat-grid.mjs +175 -0
- package/vendor/packages/audio-library-setup/shared/bgm-suggest.mjs +249 -0
- package/vendor/packages/audio-library-setup/shared/candidates.mjs +293 -0
- package/vendor/packages/audio-library-setup/shared/decision-log.mjs +94 -0
- package/vendor/packages/audio-library-setup/shared/sfx-suggest.mjs +125 -0
- package/vendor/packages/audio-library-setup/shared/waveform-preview.mjs +39 -0
- package/vendor/packages/audio-library-setup/test/akari-sounds.test.mjs +87 -0
- package/vendor/packages/audio-library-setup/test/beat-grid.test.mjs +202 -0
- package/vendor/packages/audio-library-setup/test/bgm-suggest.test.mjs +256 -0
- package/vendor/packages/audio-library-setup/test/decision-log.test.mjs +174 -0
- package/vendor/packages/audio-library-setup/test/declare-server.test.mjs +209 -0
- package/vendor/packages/audio-library-setup/test/gallery-server.test.mjs +105 -0
- package/vendor/packages/audio-library-setup/test/generate-candidates-html.test.mjs +117 -0
- package/vendor/packages/audio-library-setup/test/register-drop-folder.test.mjs +331 -0
- package/vendor/packages/audio-library-setup/test/sfx-suggest.test.mjs +146 -0
- package/vendor/packages/bake-layer/README.md +73 -0
- package/vendor/packages/bake-layer/bin/bake-layer.mjs +159 -0
- package/vendor/packages/bake-layer/package.json +19 -0
- package/vendor/packages/creator-root/package.json +11 -0
- package/vendor/packages/creator-root/src/index.mjs +408 -0
- package/vendor/packages/creator-root/test/adopt-project.test.mjs +118 -0
- package/vendor/packages/creator-root/test/create-creator-root.test.mjs +79 -0
- package/vendor/packages/creator-root/test/default-root-path.test.mjs +29 -0
- package/vendor/packages/creator-root/test/helpers.mjs +13 -0
- package/vendor/packages/creator-root/test/machine-pointer.test.mjs +52 -0
- package/vendor/packages/creator-root/test/move-directory.test.mjs +60 -0
- package/vendor/packages/creator-root/test/resolve-creator-root.test.mjs +98 -0
- package/vendor/packages/creator-root/test/root-manifest.test.mjs +70 -0
- package/vendor/packages/decision-cards/README.md +72 -0
- package/vendor/packages/decision-cards/package.json +14 -0
- package/vendor/packages/decision-cards/report-helper.mjs +519 -0
- package/vendor/packages/edit-lint/bin/edit-lint.mjs +6 -0
- package/vendor/packages/edit-lint/package.json +13 -0
- package/vendor/packages/edit-store/package.json +20 -0
- package/vendor/packages/export-nle/bin/export-nle.mjs +162 -0
- package/vendor/packages/export-nle/package.json +19 -0
- package/vendor/packages/intake-form/README.md +44 -0
- package/vendor/packages/intake-form/intake-form-helper.mjs +37 -0
- package/vendor/packages/intake-form/package.json +14 -0
- package/vendor/packages/media-bin/package.json +18 -0
- package/vendor/packages/media-bin/scripts/fetch-binaries.mjs +171 -0
- package/vendor/packages/media-bin/src/binary-manifest.mjs +139 -0
- package/vendor/packages/media-bin/src/index.mjs +96 -0
- package/vendor/packages/media-bin/test/media-bin.test.mjs +101 -0
- package/vendor/packages/overlay-runtime/README.md +209 -0
- package/vendor/packages/overlay-runtime/package.json +15 -0
- package/vendor/packages/pen-visuals/package.json +16 -0
- package/vendor/packages/preview-engine/README.md +233 -0
- package/vendor/packages/preview-engine/package.json +25 -0
- package/vendor/packages/preview-server/package.json +22 -0
- package/vendor/packages/preview-server/src/server.mjs +565 -0
- package/vendor/packages/project-scaffold/src/index.mjs +6 -0
- package/vendor/packages/project-scaffold/test/create-project.test.mjs +31 -1
- package/vendor/packages/render-cut/bin/akari-apply-textstyle.mjs +202 -0
- package/vendor/packages/render-cut/bin/render-cut.mjs +5 -0
- package/vendor/packages/render-cut/package.json +18 -0
- package/vendor/packages/schemas/bin/validate-captions.mjs +97 -5
- package/vendor/packages/schemas/bin/validate-edit.mjs +21 -0
- package/vendor/packages/schemas/bin/validate-review.mjs +2 -2
- package/vendor/packages/schemas/captions.schema.json +53 -1
- package/vendor/packages/schemas/cut-candidates.schema.json +685 -0
- package/vendor/packages/schemas/edit.schema.json +11 -2
- package/vendor/packages/schemas/fixtures/review/invalid-input-unknown/review.json +6 -0
- package/vendor/packages/schemas/fixtures/review/invalid-status-unknown/review.json +6 -0
- package/vendor/packages/schemas/fixtures/review/valid-status-lifecycle/review.json +8 -0
- package/vendor/packages/schemas/recipe.schema.json +1 -1
- package/vendor/packages/schemas/semantic-keep-plan.schema.json +84 -0
- package/vendor/packages/schemas/test/validate-captions.test.mjs +120 -0
- package/vendor/packages/schemas/test/validate-cut-candidates.test.mjs +116 -0
- package/vendor/packages/schemas/test/validate-edit.test.mjs +30 -0
- package/vendor/packages/schemas/test/validate-review.test.mjs +19 -1
- package/vendor/packages/schemas/test/validate-semantic-keep-plan.test.mjs +62 -0
- package/vendor/packages/template-render/README.ja.md +107 -0
- package/vendor/packages/template-render/README.md +107 -0
- package/vendor/packages/template-render/bin/akari-template-render.mjs +257 -0
- package/vendor/packages/template-render/package.json +22 -0
- package/vendor/presets/luts/INDEX.md +75 -0
- package/vendor/presets/luts/bake-luts.mjs +207 -0
- package/vendor/presets/luts/cinematic/cinematic.cube +35939 -0
- package/vendor/presets/luts/cool-clear/cool-clear.cube +35939 -0
- package/vendor/presets/luts/film-warm/film-warm.cube +35939 -0
- package/vendor/presets/luts/forest-soft/forest-soft.cube +35939 -0
- package/vendor/presets/luts/index.jsonl +10 -0
- package/vendor/presets/luts/mono/mono.cube +35939 -0
- package/vendor/presets/luts/natural/natural.cube +35939 -0
- package/vendor/presets/luts/night-neon/night-neon.cube +35939 -0
- package/vendor/presets/luts/silver-retain/silver-retain.cube +35939 -0
- package/vendor/presets/luts/sunset-gold/sunset-gold.cube +35939 -0
- package/vendor/presets/luts/vintage-fade/vintage-fade.cube +35939 -0
- package/vendor/skills/bake-3d/SKILL.md +18 -1
- package/vendor/skills/beat-sync-edit/SKILL.md +74 -0
- package/vendor/skills/beat-sync-edit/bin/beatmap.mjs +131 -0
- package/vendor/skills/beat-sync-edit/bin/probe-frame.mjs +96 -0
- package/vendor/skills/beat-sync-edit/bin/render-when-idle.sh +46 -0
- package/vendor/skills/beat-sync-edit/generator.md +98 -0
- package/vendor/skills/beat-sync-edit/pitfalls.md +55 -0
- package/vendor/skills/beat-sync-edit/templates/gen-timeline.mjs +173 -0
- package/vendor/skills/beat-sync-edit/verify-loop.md +77 -0
- package/vendor/skills/declare-audio/SKILL.md +84 -0
- package/vendor/skills/declare-audio/after-save.md +67 -0
- package/vendor/skills/declare-audio/launch.md +69 -0
- package/vendor/skills/declare-audio/what-to-declare.md +42 -0
- package/vendor/skills/edit-lint/SKILL.md +18 -0
- package/vendor/skills/edit-plan/SKILL.md +15 -6
- package/vendor/skills/edit-plan/beat-sync.md +90 -6
- package/vendor/skills/edit-plan/bin/candidate-core.mjs +613 -0
- package/vendor/skills/edit-plan/bin/canonical-json.mjs +107 -0
- package/vendor/skills/edit-plan/bin/contract-semantics.mjs +619 -0
- package/vendor/skills/edit-plan/bin/errors.mjs +73 -0
- package/vendor/skills/edit-plan/bin/generated/contract-validators.cjs +11921 -0
- package/vendor/skills/edit-plan/bin/generated/runtime/equal.cjs +6 -0
- package/vendor/skills/edit-plan/bin/generated/runtime/fast-deep-equal-index-9a360c74.cjs +46 -0
- package/vendor/skills/edit-plan/bin/generated/runtime/licenses/ajv.txt +21 -0
- package/vendor/skills/edit-plan/bin/generated/runtime/licenses/fast-deep-equal.txt +21 -0
- package/vendor/skills/edit-plan/bin/generated/runtime/ucs2length.cjs +23 -0
- package/vendor/skills/edit-plan/bin/propose-cut-candidates.mjs +569 -0
- package/vendor/skills/edit-plan/bin/runtime-support.mjs +558 -0
- package/vendor/skills/edit-plan/bin/test/candidate-core.test.mjs +380 -0
- package/vendor/skills/edit-plan/bin/test/errors.test.mjs +46 -0
- package/vendor/skills/edit-plan/bin/test/l1-real-media.test.mjs +98 -0
- package/vendor/skills/edit-plan/bin/test/propose-cut-candidates.test.mjs +372 -0
- package/vendor/skills/edit-plan/bin/test/runtime-support.test.mjs +292 -0
- package/vendor/skills/edit-plan/bin/test/scale.test.mjs +215 -0
- package/vendor/skills/edit-plan/execution.md +29 -0
- package/vendor/skills/edit-plan/recipe.md +8 -0
- package/vendor/skills/edit-plan/references/cut-candidate-policy.a4-conversation-v1.json +14 -0
- package/vendor/skills/edit-plan/report-guide.md +62 -0
- package/vendor/skills/edit-plan/workflow.md +29 -0
- package/vendor/skills/export-nle/SKILL.md +72 -0
- package/vendor/skills/overlay-authoring/3d.md +166 -19
- package/vendor/skills/overlay-authoring/telop.md +26 -0
- package/vendor/skills/render-cut/SKILL.md +14 -6
- package/vendor/skills/setup-audio-library/SKILL.md +39 -31
- package/vendor/skills/setup-audio-library/assisted-fetch.md +38 -0
- package/vendor/skills/setup-audio-library/candidate-list.md +17 -22
- package/vendor/skills/setup-audio-library/first-party.md +67 -0
- package/vendor/skills/setup-audio-library/gallery.md +8 -1
- package/vendor/skills/setup-audio-library/store-unlock.md +27 -0
- package/vendor/skills/setup-remote/SKILL.md +37 -0
- package/vendor/skills/setup-remote/bin/doctor.mjs +160 -0
- package/vendor/skills/setup-remote/doctor-and-install.md +43 -0
- package/vendor/skills/setup-remote/serve-preview.md +30 -0
- package/vendor/skills/setup-remote/taildrop-inbox.md +34 -0
package/README.md
CHANGED
|
@@ -21,13 +21,40 @@ akari
|
|
|
21
21
|
│ .akari/connections.json の doctor ブロックを更新・表示する
|
|
22
22
|
│ (キーの値は一切表示しない)
|
|
23
23
|
│
|
|
24
|
-
|
|
24
|
+
├─ 4. 公式音源ライブラリ(AKARI Sounds)の初回セットアップ(src/sounds-setup.mjs):
|
|
25
|
+
│ 未導入かつ TTY のとき生涯 1 回だけ [Y/n](既定 Yes)を聞き、Yes なら
|
|
26
|
+
│ packages/audio-library-setup/bin/fetch-akari-sounds.mjs で一括ダウンロード。
|
|
27
|
+
│ n は marker(~/.akari/assets/audio/.akari-sounds-declined.json)を書いて以後
|
|
28
|
+
│ 聞かない。再入口は `akari sounds`。失敗しても起動は止めない
|
|
29
|
+
│
|
|
30
|
+
└─ 5. 最後に opencode を exec する
|
|
25
31
|
(PATH に opencode が無ければ、インストール案内を出して終了する)
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
サブコマンド: `akari update`(更新確認)/ `akari init`(作業場の作成・確認のみ)/
|
|
35
|
+
`akari sounds [--variant wav] [--force]`(公式音源の一括ダウンロード。プロンプトなし・headless 可)/
|
|
36
|
+
`akari store <connect|status|download|disconnect>`(AKARI Store 連携。マイページで発行した
|
|
37
|
+
接続トークンを `~/.akari/store-credentials.json`(0600)に保存し、購入済み一覧の確認と
|
|
38
|
+
配布物の取得ができる。`src/store-command.mjs`)。
|
|
39
|
+
|
|
28
40
|
`akari` に渡した引数はそのまま `opencode` に転送する(例: `akari --continue` は
|
|
29
41
|
`opencode --continue` を起動する)。
|
|
30
42
|
|
|
43
|
+
## 状態・受理・能力検索
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
akari status [project-path] --json
|
|
47
|
+
akari status [project-path] --full --json
|
|
48
|
+
akari accept [project-path] # 実 TTY でのみ対話記録
|
|
49
|
+
akari capability <query> --json
|
|
50
|
+
akari capability <query> --record-miss --json # 0 hit のときだけ absence receipt
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
fast status は現在工程を決定的に返すが、最終受理を true にしない。full status だけが immutable
|
|
54
|
+
render receipt の全 input / output と人間受理 event を再検証し、`release.accepted:true` を返せる。
|
|
55
|
+
`accept` が作るのは協調的ローカル運用の人間操作記録であり、暗号学的な本人署名ではない。
|
|
56
|
+
capability の 0 hit receipt は `approved_to_build:false` 固定で、新設許可を意味しない。
|
|
57
|
+
|
|
31
58
|
## 3 入口の対応表
|
|
32
59
|
|
|
33
60
|
AKARI Video は「同じファイル契約(`.akari/` 配下の JSON)に収束する 3 つの入口」を
|
|
@@ -62,18 +89,11 @@ npm i -g akari-video && akari
|
|
|
62
89
|
npx akari-video
|
|
63
90
|
```
|
|
64
91
|
|
|
65
|
-
`package.json` は `"private": true` のままにしてある(誤って publish されることを防ぐ
|
|
66
|
-
セーフティネット)。実際に npm へ配信する契約が別途 approve されたときに、まず
|
|
67
|
-
この 1 行を外すところから始める。
|
|
68
|
-
|
|
69
92
|
## 既知の制約
|
|
70
93
|
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
これはこのリポジトリの checkout 内で実行される前提であり、npm レジストリ経由で
|
|
75
|
-
単体インストールされた場合にこれらのアセットを同梱する build/vendor 手順はまだ
|
|
76
|
-
実装していない(マーケットプレイス配布・審査は上位契約 §7 でスコープ外)。
|
|
94
|
+
- npm tarball は追跡済み skills・雛形・schemas と capability source set を `vendor/` に同梱する。
|
|
95
|
+
Claude plugin の status は単体コピーでも動くが、skill symlink と CLI capability は checkout / CLI
|
|
96
|
+
の有無に依存し、利用不能なら明示的に unsupported とする。
|
|
77
97
|
- Windows での動作は未検証(`claude.exe` / `claude.cmd` の探索ロジックはあるが
|
|
78
98
|
実機確認していない)。
|
|
79
99
|
|
package/bin/akari.mjs
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run, runUpdateCommand } from '../src/cli.mjs';
|
|
3
|
+
import { runInitCommand } from '../src/init-command.mjs';
|
|
4
|
+
import { runSoundsCommand } from '../src/sounds-setup.mjs';
|
|
5
|
+
import { runStatusCommand } from '../src/status-command.mjs';
|
|
6
|
+
import { runAcceptCommand } from '../src/accept-command.mjs';
|
|
7
|
+
import { runCapabilityCommand } from '../src/capability-command.mjs';
|
|
8
|
+
import { runStoreCommand } from '../src/store-command.mjs';
|
|
3
9
|
|
|
4
10
|
const argv = process.argv.slice(2);
|
|
5
|
-
// `akari update`
|
|
6
|
-
//
|
|
7
|
-
|
|
11
|
+
// `akari update` / `akari init` / `akari sounds` / `akari status` / `akari accept` /
|
|
12
|
+
// `akari capability` / `akari store` は claude へ転送せず、
|
|
13
|
+
// 専用のサブコマンドとして扱う(契約 §4-1 / タスク契約 launcher-init(内部リポ)/
|
|
14
|
+
// 音源カタログ既定化のオーナー裁定 2026-08-03 / AKARI Store 連携)。
|
|
15
|
+
// それ以外の引数はすべて従来どおり claude へ転送する。
|
|
16
|
+
const invoke = argv[0] === 'update' ? runUpdateCommand(argv.slice(1))
|
|
17
|
+
: argv[0] === 'init' ? runInitCommand(argv.slice(1))
|
|
18
|
+
: argv[0] === 'sounds' ? runSoundsCommand(argv.slice(1))
|
|
19
|
+
: argv[0] === 'status' ? runStatusCommand(argv.slice(1))
|
|
20
|
+
: argv[0] === 'accept' ? runAcceptCommand(argv.slice(1))
|
|
21
|
+
: argv[0] === 'capability' ? runCapabilityCommand(argv.slice(1))
|
|
22
|
+
: argv[0] === 'store' ? runStoreCommand(argv.slice(1))
|
|
23
|
+
: run(argv);
|
|
8
24
|
|
|
9
25
|
const result = await invoke.catch((error) => {
|
|
10
26
|
console.error(error instanceof Error ? error.message : String(error));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
|
|
7
|
+
import { inspectFullIntegrity } from "./status-core/integrity.mjs";
|
|
8
|
+
import { resolveFullProjectStatus } from "./status-core/status.mjs";
|
|
9
|
+
|
|
10
|
+
export async function runAcceptCommand(argv, options = {}) {
|
|
11
|
+
const log = options.log ?? ((line) => process.stdout.write(line));
|
|
12
|
+
const error = options.error ?? ((line) => process.stderr.write(`${line}\n`));
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = parseAcceptArguments(argv, options.cwd ?? process.cwd());
|
|
16
|
+
} catch (cause) {
|
|
17
|
+
error(messageOf(cause));
|
|
18
|
+
return { exitCode: 2 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isTTY = options.isTTY ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
|
|
22
|
+
if (!isTTY) {
|
|
23
|
+
error("akari accept requires an interactive input and output TTY; pipes, flags, and environment confirmation are not accepted");
|
|
24
|
+
return { exitCode: 2 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const status = await resolveFullProjectStatus(parsed.projectRoot);
|
|
28
|
+
if (status.release.accepted) {
|
|
29
|
+
error("the current verified artifact already has an active human acceptance record");
|
|
30
|
+
return { exitCode: 1, status };
|
|
31
|
+
}
|
|
32
|
+
if (status.workflow_stage !== "acceptance_pending" || status.state_health !== "valid") {
|
|
33
|
+
error(`the project is not ready for human acceptance: ${status.workflow_stage} (${status.state_health})`);
|
|
34
|
+
return { exitCode: 1, status };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const before = await inspectFullIntegrity(parsed.projectRoot);
|
|
38
|
+
if (!before.ok || !before.candidate || before.activeAcceptance) {
|
|
39
|
+
error(`the verified artifact cannot be accepted: ${before.problems.join("; ") || "no current acceptance candidate"}`);
|
|
40
|
+
return { exitCode: 1, status };
|
|
41
|
+
}
|
|
42
|
+
const candidate = before.candidate;
|
|
43
|
+
const phrase = `ACCEPT ${candidate.artifact_sha256}`;
|
|
44
|
+
log(`Artifact: ${candidate.artifact}\nArtifact SHA-256: ${candidate.artifact_sha256}\nReceipt: ${candidate.receipt}\nReceipt SHA-256: ${candidate.receipt_sha256}\n`);
|
|
45
|
+
if (candidate.audio_qc?.verdict === "INCONCLUSIVE") {
|
|
46
|
+
log(`WARNING: audio_qc is INCONCLUSIVE; this is not an audio conformance PASS.\nConfigured: ${JSON.stringify(candidate.audio_qc.configured)}\nFilter report: ${JSON.stringify(candidate.audio_qc.filter_report)}\nDecoded artifact measurement: ${JSON.stringify(candidate.audio_qc.decoded_measurement)}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const prompt = options.prompt ?? promptFromTTY;
|
|
50
|
+
let actorId;
|
|
51
|
+
let acceptanceStatement;
|
|
52
|
+
let checksumConfirmation;
|
|
53
|
+
try {
|
|
54
|
+
actorId = (await prompt("Human identity for this cooperative local record: ")).trim();
|
|
55
|
+
if (actorId === "") throw new Error("human identity must not be empty");
|
|
56
|
+
acceptanceStatement = await prompt("Your final acceptance statement for this artifact: ");
|
|
57
|
+
if (acceptanceStatement.trim() === "") throw new Error("final acceptance statement must not be empty");
|
|
58
|
+
checksumConfirmation = await prompt(`Type exactly “${phrase}” to confirm this artifact checksum: `);
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
error(`acceptance was not recorded: ${messageOf(cause)}`);
|
|
61
|
+
return { exitCode: 1, status };
|
|
62
|
+
}
|
|
63
|
+
if (checksumConfirmation !== phrase) {
|
|
64
|
+
error("artifact checksum confirmation did not match exactly; nothing was recorded");
|
|
65
|
+
return { exitCode: 1, status };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const after = await inspectFullIntegrity(parsed.projectRoot);
|
|
69
|
+
if (!after.ok || !after.candidate || after.activeAcceptance || !sameCandidate(candidate, after.candidate)) {
|
|
70
|
+
error("project integrity or acceptance state changed during confirmation; nothing was recorded");
|
|
71
|
+
return { exitCode: 1, status };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const written = await writeAcceptanceEvent({
|
|
76
|
+
projectRoot: parsed.projectRoot,
|
|
77
|
+
actorId,
|
|
78
|
+
verbatim: acceptanceStatement,
|
|
79
|
+
candidate,
|
|
80
|
+
now: options.now,
|
|
81
|
+
id: options.id,
|
|
82
|
+
});
|
|
83
|
+
log(`Recorded cooperative local acceptance event ${written.event.id} at ${written.path}. This record is not a cryptographic human-identity proof.\n`);
|
|
84
|
+
return { exitCode: 0, event: written.event, path: written.path };
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
error(`acceptance was not recorded: ${messageOf(cause)}`);
|
|
87
|
+
return { exitCode: 1, status };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function parseAcceptArguments(argv, cwd = process.cwd()) {
|
|
92
|
+
let projectRoot = cwd;
|
|
93
|
+
let pathSeen = false;
|
|
94
|
+
for (const argument of argv) {
|
|
95
|
+
if (argument.startsWith("-")) throw new Error(`Unknown accept option: ${argument}`);
|
|
96
|
+
if (pathSeen) throw new Error("accept accepts only one project path");
|
|
97
|
+
projectRoot = argument;
|
|
98
|
+
pathSeen = true;
|
|
99
|
+
}
|
|
100
|
+
return { projectRoot };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function writeAcceptanceEvent({ projectRoot, actorId, verbatim, candidate, now, id }) {
|
|
104
|
+
const root = realpathSync(resolve(projectRoot));
|
|
105
|
+
if (typeof actorId !== "string" || actorId.trim() === "") throw new Error("human actor identity is required");
|
|
106
|
+
if (typeof verbatim !== "string" || verbatim.trim() === "") throw new Error("verbatim confirmation is required");
|
|
107
|
+
assertCandidate(candidate);
|
|
108
|
+
const eventId = id ?? randomUUID();
|
|
109
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/u.test(eventId)) throw new Error("event id contains unsafe characters");
|
|
110
|
+
const occurredAt = now instanceof Date ? now.toISOString() : typeof now === "string" ? now : new Date().toISOString();
|
|
111
|
+
if (!/(?:Z|[+-]\d{2}:?\d{2})$/u.test(occurredAt) || !Number.isFinite(Date.parse(occurredAt))) {
|
|
112
|
+
throw new Error("acceptance timestamp must include an explicit UTC offset");
|
|
113
|
+
}
|
|
114
|
+
const event = {
|
|
115
|
+
version: 1,
|
|
116
|
+
id: eventId,
|
|
117
|
+
type: "final-acceptance",
|
|
118
|
+
occurredAt,
|
|
119
|
+
actor: { kind: "human", id: actorId.trim() },
|
|
120
|
+
issuer: { kind: "akari-cli-tty", version: 1 },
|
|
121
|
+
artifact: candidate.artifact,
|
|
122
|
+
artifact_sha256: candidate.artifact_sha256,
|
|
123
|
+
render_receipt: candidate.receipt,
|
|
124
|
+
render_receipt_sha256: candidate.receipt_sha256,
|
|
125
|
+
review_sha256: candidate.review_sha256,
|
|
126
|
+
verbatim,
|
|
127
|
+
};
|
|
128
|
+
const eventsDirectory = await prepareEventsDirectory(root);
|
|
129
|
+
const compactTime = occurredAt.replace(/[-:.]/gu, "");
|
|
130
|
+
const eventPath = join(eventsDirectory, `${compactTime}-${eventId}-final-acceptance.json`);
|
|
131
|
+
await writeFile(eventPath, `${JSON.stringify(event, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
132
|
+
return { event, path: relative(root, eventPath) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function prepareEventsDirectory(root) {
|
|
136
|
+
const akariDirectory = join(root, ".akari");
|
|
137
|
+
const actualAkari = realpathSync(akariDirectory);
|
|
138
|
+
if (!isWithin(root, actualAkari) || !lstatSync(akariDirectory).isDirectory()) {
|
|
139
|
+
throw new Error(".akari is not a regular project directory");
|
|
140
|
+
}
|
|
141
|
+
const eventsDirectory = join(akariDirectory, "events");
|
|
142
|
+
await mkdir(eventsDirectory, { recursive: true });
|
|
143
|
+
const actualEvents = realpathSync(eventsDirectory);
|
|
144
|
+
if (!isWithin(root, actualEvents) || !lstatSync(eventsDirectory).isDirectory()) {
|
|
145
|
+
throw new Error(".akari/events is not a regular project directory");
|
|
146
|
+
}
|
|
147
|
+
return actualEvents;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function promptFromTTY(question) {
|
|
151
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
152
|
+
try {
|
|
153
|
+
return await readline.question(question);
|
|
154
|
+
} finally {
|
|
155
|
+
readline.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sameCandidate(left, right) {
|
|
160
|
+
return left.receipt === right.receipt
|
|
161
|
+
&& left.receipt_sha256 === right.receipt_sha256
|
|
162
|
+
&& left.artifact === right.artifact
|
|
163
|
+
&& left.artifact_sha256 === right.artifact_sha256
|
|
164
|
+
&& left.review_sha256 === right.review_sha256;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function assertCandidate(value) {
|
|
168
|
+
if (!isSafeRelativePath(value?.artifact) || !isSafeRelativePath(value?.receipt)) throw new Error("acceptance candidate paths are invalid");
|
|
169
|
+
for (const key of ["artifact_sha256", "receipt_sha256", "review_sha256"]) {
|
|
170
|
+
if (!/^[a-f0-9]{64}$/u.test(value?.[key] ?? "")) throw new Error(`acceptance candidate ${key} is invalid`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isSafeRelativePath(value) {
|
|
175
|
+
return typeof value === "string" && value !== "" && !isAbsolute(value) && !value.split(/[\\/]/u).includes("..");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isWithin(root, target) {
|
|
179
|
+
const value = relative(root, target);
|
|
180
|
+
return value === "" || (!value.startsWith("..") && !isAbsolute(value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function messageOf(error) {
|
|
184
|
+
return error instanceof Error ? error.message : String(error);
|
|
185
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildCapabilityCatalog, queryCapability, recordCapabilityMiss } from "./capability.mjs";
|
|
2
|
+
|
|
3
|
+
export async function runCapabilityCommand(argv, options = {}) {
|
|
4
|
+
const log = options.log ?? ((line) => process.stdout.write(line));
|
|
5
|
+
const error = options.error ?? ((line) => process.stderr.write(`${line}\n`));
|
|
6
|
+
let parsed;
|
|
7
|
+
try {
|
|
8
|
+
parsed = parseCapabilityArguments(argv, options.cwd ?? process.cwd());
|
|
9
|
+
const catalog = buildCapabilityCatalog(options.catalogOptions);
|
|
10
|
+
const result = queryCapability(catalog, parsed.query);
|
|
11
|
+
if (parsed.recordMiss && result.matches.length === 0) {
|
|
12
|
+
const recorded = await recordCapabilityMiss(parsed.projectRoot, catalog, result);
|
|
13
|
+
log(parsed.json ? `${JSON.stringify(recorded.receipt, null, 2)}\n` : `No text match; review required. Receipt: ${recorded.path}\n`);
|
|
14
|
+
return { exitCode: 0, result, receipt: recorded };
|
|
15
|
+
}
|
|
16
|
+
if (parsed.json) log(`${JSON.stringify(result, null, 2)}\n`);
|
|
17
|
+
else if (result.matches.length === 0) log("No text match. Review the source set before proposing a new capability.\n");
|
|
18
|
+
else {
|
|
19
|
+
for (const match of result.matches) log(`${match.path} — ${match.heading} (score ${match.score})\n${match.snippet}\n`);
|
|
20
|
+
}
|
|
21
|
+
return { exitCode: 0, result };
|
|
22
|
+
} catch (cause) {
|
|
23
|
+
error(cause instanceof Error ? cause.message : String(cause));
|
|
24
|
+
return { exitCode: 1 };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseCapabilityArguments(argv, cwd = process.cwd()) {
|
|
29
|
+
const queryParts = [];
|
|
30
|
+
let json = false;
|
|
31
|
+
let recordMiss = false;
|
|
32
|
+
let projectRoot = cwd;
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const argument = argv[index];
|
|
35
|
+
if (argument === "--json") json = true;
|
|
36
|
+
else if (argument === "--record-miss") recordMiss = true;
|
|
37
|
+
else if (argument === "--project") {
|
|
38
|
+
const value = argv[++index];
|
|
39
|
+
if (!value || value.startsWith("-")) throw new Error("--project requires a path");
|
|
40
|
+
projectRoot = value;
|
|
41
|
+
} else if (argument.startsWith("-")) throw new Error(`Unknown capability option: ${argument}`);
|
|
42
|
+
else queryParts.push(argument);
|
|
43
|
+
}
|
|
44
|
+
const query = queryParts.join(" ").trim();
|
|
45
|
+
if (query === "") throw new Error("capability requires a query");
|
|
46
|
+
return { query, json, recordMiss, projectRoot };
|
|
47
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export function discoverCheckoutCapabilitySources(repoRoot, { trackedFiles = null } = {}) {
|
|
6
|
+
const root = realpathSync(resolve(repoRoot));
|
|
7
|
+
const tracked = trackedFiles ?? execFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
8
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
9
|
+
}).toString("utf8").split("\0").filter(Boolean);
|
|
10
|
+
const trackedSet = new Set(tracked);
|
|
11
|
+
const selected = new Set(tracked.filter(isBaseCapabilitySource));
|
|
12
|
+
const manifests = [...selected].filter((path) => /^packages\/[^/]+\/package\.json$/u.test(path)).sort(compare);
|
|
13
|
+
for (const manifestPath of manifests) {
|
|
14
|
+
const manifest = parseManifest(join(root, manifestPath), manifestPath);
|
|
15
|
+
const packageRoot = dirname(manifestPath);
|
|
16
|
+
for (const target of manifestBinTargets(manifest)) {
|
|
17
|
+
if (typeof target !== "string" || target.trim() === "") {
|
|
18
|
+
throw new Error(`${manifestPath} has an invalid package.json#bin target`);
|
|
19
|
+
}
|
|
20
|
+
const canonical = normalizeRelative(join(packageRoot, target));
|
|
21
|
+
if (!isWithinRelative(packageRoot, canonical)) {
|
|
22
|
+
throw new Error(`${manifestPath} package.json#bin escapes its package root: ${target}`);
|
|
23
|
+
}
|
|
24
|
+
if (!trackedSet.has(canonical)) {
|
|
25
|
+
throw new Error(`${manifestPath} package.json#bin target is not tracked: ${canonical}`);
|
|
26
|
+
}
|
|
27
|
+
assertRegularContainedFile(root, canonical, `${manifestPath} package.json#bin`);
|
|
28
|
+
selected.add(canonical);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const paths = [...selected].sort(compare);
|
|
32
|
+
for (const path of paths) assertRegularContainedFile(root, path, "capability source");
|
|
33
|
+
return paths;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadVendoredCapabilitySources(packageRoot) {
|
|
37
|
+
const root = realpathSync(resolve(packageRoot));
|
|
38
|
+
const manifestPath = join(root, "vendor", ".akari-capability-sources.json");
|
|
39
|
+
let manifest;
|
|
40
|
+
try {
|
|
41
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`vendored capability manifest is unavailable: ${messageOf(error)}`);
|
|
44
|
+
}
|
|
45
|
+
if (manifest?.version !== 1 || !Array.isArray(manifest.sources)) {
|
|
46
|
+
throw new Error("vendored capability manifest has an unsupported shape");
|
|
47
|
+
}
|
|
48
|
+
const paths = [...manifest.sources];
|
|
49
|
+
if (paths.some((path) => typeof path !== "string") || JSON.stringify(paths) !== JSON.stringify([...paths].sort(compare))) {
|
|
50
|
+
throw new Error("vendored capability manifest source list is not sorted strings");
|
|
51
|
+
}
|
|
52
|
+
if (new Set(paths).size !== paths.length) throw new Error("vendored capability manifest contains duplicate sources");
|
|
53
|
+
for (const path of paths) assertRegularContainedFile(join(root, "vendor"), path, "vendored capability source");
|
|
54
|
+
return paths;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isCheckoutRepoRoot(root) {
|
|
58
|
+
try {
|
|
59
|
+
return execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim() === realpathSync(root);
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isBaseCapabilitySource(path) {
|
|
66
|
+
return /^skills\/.+\.md$/u.test(path)
|
|
67
|
+
|| /^docs\/contract-[^/]+\.md$/u.test(path)
|
|
68
|
+
|| /^packages\/[^/]+\/README[^/]*\.md$/u.test(path)
|
|
69
|
+
|| /^packages\/[^/]+\/package\.json$/u.test(path);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function manifestBinTargets(manifest) {
|
|
73
|
+
if (typeof manifest?.bin === "string") return [manifest.bin];
|
|
74
|
+
if (manifest?.bin && typeof manifest.bin === "object" && !Array.isArray(manifest.bin)) {
|
|
75
|
+
return Object.keys(manifest.bin).sort(compare).map((key) => manifest.bin[key]);
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseManifest(path, label) {
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("root must be an object");
|
|
84
|
+
return value;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`${label} is not a valid package manifest: ${messageOf(error)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertRegularContainedFile(root, canonical, label) {
|
|
91
|
+
if (typeof canonical !== "string" || canonical === "" || isAbsolute(canonical) || canonical.split(/[\\/]/u).includes("..")) {
|
|
92
|
+
throw new Error(`${label} path is unsafe: ${String(canonical)}`);
|
|
93
|
+
}
|
|
94
|
+
const lexical = resolve(root, canonical);
|
|
95
|
+
if (!existsSync(lexical)) throw new Error(`${label} does not exist: ${canonical}`);
|
|
96
|
+
const actual = realpathSync(lexical);
|
|
97
|
+
if (!isWithin(root, actual) || !lstatSync(lexical).isFile()) {
|
|
98
|
+
throw new Error(`${label} is not a regular contained file: ${canonical}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeRelative(path) {
|
|
103
|
+
return path.replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isWithinRelative(root, target) {
|
|
107
|
+
const value = relative(root, target);
|
|
108
|
+
return value === "" || (!value.startsWith("..") && !isAbsolute(value));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isWithin(root, target) {
|
|
112
|
+
const value = relative(root, target);
|
|
113
|
+
return value === "" || (!value.startsWith("..") && !isAbsolute(value));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function compare(left, right) {
|
|
117
|
+
return left.localeCompare(right, "en");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function messageOf(error) {
|
|
121
|
+
return error instanceof Error ? error.message : String(error);
|
|
122
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { discoverCheckoutCapabilitySources, isCheckoutRepoRoot, loadVendoredCapabilitySources } from "./capability-sources.mjs";
|
|
8
|
+
|
|
9
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const CHECKOUT_ROOT = resolve(PACKAGE_ROOT, "..", "..");
|
|
11
|
+
const MISS_VERDICT = "NO_TEXT_MATCH_REQUIRES_REVIEW";
|
|
12
|
+
|
|
13
|
+
export function buildCapabilityCatalog(options = {}) {
|
|
14
|
+
const topology = resolveCapabilityTopology(options);
|
|
15
|
+
const sources = topology.paths.map((path) => {
|
|
16
|
+
const absolutePath = join(topology.contentRoot, path);
|
|
17
|
+
const bytes = readFileSync(absolutePath);
|
|
18
|
+
return {
|
|
19
|
+
path,
|
|
20
|
+
bytes: bytes.byteLength,
|
|
21
|
+
sha256: sha256(bytes),
|
|
22
|
+
text: bytes.toString("utf8"),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
const sourceManifest = sources.map(({ path, bytes, sha256: digest }) => ({ path, bytes, sha256: digest }));
|
|
26
|
+
return {
|
|
27
|
+
version: 1,
|
|
28
|
+
topology: topology.kind,
|
|
29
|
+
sources,
|
|
30
|
+
source_set_sha256: sha256(canonicalJson(sourceManifest)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function queryCapability(catalog, query, { maximum = 20 } = {}) {
|
|
35
|
+
if (typeof query !== "string" || query.trim() === "") throw new Error("capability query must not be empty");
|
|
36
|
+
const tokens = tokenize(query);
|
|
37
|
+
const candidates = [];
|
|
38
|
+
for (const source of catalog.sources) {
|
|
39
|
+
for (const section of sectionsForSource(source)) {
|
|
40
|
+
const pathText = normalize(source.path);
|
|
41
|
+
const headingText = normalize(section.heading);
|
|
42
|
+
const bodyText = normalize(section.body);
|
|
43
|
+
if (!tokens.every((token) => pathText.includes(token) || headingText.includes(token) || bodyText.includes(token))) continue;
|
|
44
|
+
const score = tokens.reduce((total, token) => total
|
|
45
|
+
+ (pathText.includes(token) ? 12 : 0)
|
|
46
|
+
+ (headingText.includes(token) ? 8 : 0)
|
|
47
|
+
+ (bodyText.includes(token) ? 3 : 0), 0);
|
|
48
|
+
candidates.push({
|
|
49
|
+
path: source.path,
|
|
50
|
+
heading: section.heading,
|
|
51
|
+
score,
|
|
52
|
+
snippet: snippet(section.body || section.heading),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
candidates.sort((left, right) => right.score - left.score
|
|
57
|
+
|| left.path.localeCompare(right.path, "en")
|
|
58
|
+
|| left.heading.localeCompare(right.heading, "en")
|
|
59
|
+
|| left.snippet.localeCompare(right.snippet, "en"));
|
|
60
|
+
return {
|
|
61
|
+
version: 1,
|
|
62
|
+
query,
|
|
63
|
+
source_set_sha256: catalog.source_set_sha256,
|
|
64
|
+
source_count: catalog.sources.length,
|
|
65
|
+
matches: candidates.slice(0, maximum),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function recordCapabilityMiss(projectInput, catalog, result) {
|
|
70
|
+
if (result.matches.length !== 0) throw new Error("absence receipt may be recorded only for a zero-hit query");
|
|
71
|
+
const projectRoot = realpathSync(resolve(projectInput));
|
|
72
|
+
const connections = join(projectRoot, ".akari", "connections.json");
|
|
73
|
+
if (!lstatSync(connections).isFile()) throw new Error("absence receipt requires an AKARI project");
|
|
74
|
+
const akariDirectory = realpathSync(join(projectRoot, ".akari"));
|
|
75
|
+
if (!isWithin(projectRoot, akariDirectory) || !lstatSync(join(projectRoot, ".akari")).isDirectory()) {
|
|
76
|
+
throw new Error(".akari is not a contained project directory");
|
|
77
|
+
}
|
|
78
|
+
const reportsDirectory = await ensureContainedDirectory(projectRoot, join(akariDirectory, "reports"));
|
|
79
|
+
const absenceDirectory = await ensureContainedDirectory(projectRoot, join(reportsDirectory, "absence"));
|
|
80
|
+
const receipt = {
|
|
81
|
+
version: 1,
|
|
82
|
+
query: result.query,
|
|
83
|
+
source_set_sha256: catalog.source_set_sha256,
|
|
84
|
+
sources: catalog.sources.map(({ path, sha256: digest }) => ({ path, sha256: digest })),
|
|
85
|
+
matches: [],
|
|
86
|
+
verdict: MISS_VERDICT,
|
|
87
|
+
approved_to_build: false,
|
|
88
|
+
};
|
|
89
|
+
const bytes = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
90
|
+
const digest = sha256(bytes);
|
|
91
|
+
const path = join(absenceDirectory, `${digest}.json`);
|
|
92
|
+
try {
|
|
93
|
+
await writeFile(path, bytes, { encoding: "utf8", flag: "wx" });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code !== "EEXIST") throw error;
|
|
96
|
+
if (await readFile(path, "utf8") !== bytes) throw new Error("absence receipt digest collision");
|
|
97
|
+
}
|
|
98
|
+
return { receipt, path: relative(projectRoot, path), sha256: digest };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function canonicalJson(value) {
|
|
102
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
103
|
+
if (value !== null && typeof value === "object") {
|
|
104
|
+
return `{${Object.keys(value).sort((left, right) => left.localeCompare(right, "en"))
|
|
105
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
106
|
+
}
|
|
107
|
+
return JSON.stringify(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveCapabilityTopology(options) {
|
|
111
|
+
if (options.sourceRoot) {
|
|
112
|
+
const root = realpathSync(resolve(options.sourceRoot));
|
|
113
|
+
return {
|
|
114
|
+
kind: "checkout",
|
|
115
|
+
contentRoot: root,
|
|
116
|
+
paths: discoverCheckoutCapabilitySources(root, { trackedFiles: options.trackedFiles }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (isCheckoutRepoRoot(CHECKOUT_ROOT)) {
|
|
120
|
+
return { kind: "checkout", contentRoot: CHECKOUT_ROOT, paths: discoverCheckoutCapabilitySources(CHECKOUT_ROOT) };
|
|
121
|
+
}
|
|
122
|
+
return { kind: "vendor", contentRoot: join(PACKAGE_ROOT, "vendor"), paths: loadVendoredCapabilitySources(PACKAGE_ROOT) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sectionsForSource(source) {
|
|
126
|
+
if (source.path.endsWith("package.json")) {
|
|
127
|
+
try {
|
|
128
|
+
const manifest = JSON.parse(source.text);
|
|
129
|
+
const body = JSON.stringify({ name: manifest.name, description: manifest.description, bin: manifest.bin });
|
|
130
|
+
return [{ heading: manifest.name ?? source.path, body }];
|
|
131
|
+
} catch {
|
|
132
|
+
return [{ heading: source.path, body: source.text }];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!source.path.endsWith(".md")) return [{ heading: source.path, body: source.text }];
|
|
136
|
+
const sections = [];
|
|
137
|
+
let heading = source.path;
|
|
138
|
+
let lines = [];
|
|
139
|
+
for (const line of source.text.split(/\r?\n/u)) {
|
|
140
|
+
const match = line.match(/^#{1,6}\s+(.+)$/u);
|
|
141
|
+
if (match) {
|
|
142
|
+
if (lines.some((value) => value.trim() !== "")) sections.push({ heading, body: lines.join("\n") });
|
|
143
|
+
heading = match[1].trim();
|
|
144
|
+
lines = [];
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(line);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (lines.some((value) => value.trim() !== "") || sections.length === 0) sections.push({ heading, body: lines.join("\n") });
|
|
150
|
+
return sections;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function tokenize(value) {
|
|
154
|
+
const tokens = normalize(value).split(/\s+/u).filter(Boolean);
|
|
155
|
+
return [...new Set(tokens)];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalize(value) {
|
|
159
|
+
return String(value ?? "").normalize("NFKC").toLocaleLowerCase("en").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function snippet(value) {
|
|
163
|
+
return String(value ?? "").replace(/\s+/gu, " ").trim().slice(0, 240);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function ensureContainedDirectory(root, path) {
|
|
167
|
+
await mkdir(path, { recursive: true });
|
|
168
|
+
const actual = realpathSync(path);
|
|
169
|
+
if (!isWithin(root, actual) || !lstatSync(path).isDirectory()) throw new Error("absence receipt directory escapes the project");
|
|
170
|
+
return actual;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isWithin(root, target) {
|
|
174
|
+
const value = relative(root, target);
|
|
175
|
+
return value === "" || (!value.startsWith("..") && !isAbsolute(value));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sha256(value) {
|
|
179
|
+
return createHash("sha256").update(value).digest("hex");
|
|
180
|
+
}
|