i-repo 2.13.0 → 2.14.0
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/dist/commands/deliver/aggregate.js +2 -2
- package/dist/commands/deliver/index.js +1 -0
- package/dist/commands/deliver/orchestrator.js +28 -1
- package/dist/plugins/dispatch.d.ts +5 -0
- package/dist/plugins/dispatch.js +6 -0
- package/package.json +1 -1
- package/plugins/i-repo-archive +65 -7
- package/spec/gemba-adc/spec.md +15 -2
|
@@ -66,8 +66,8 @@ export function buildDeliverRun(meta, sinks) {
|
|
|
66
66
|
status,
|
|
67
67
|
delivered,
|
|
68
68
|
...(s.skippedReason ? { skippedReason: s.skippedReason } : {}),
|
|
69
|
-
...(s.dest !== undefined ? { dest: s.dest } : {}),
|
|
70
|
-
...(s.canonicalLandedUri !== undefined ? { canonicalLandedUri: s.canonicalLandedUri } : {}),
|
|
69
|
+
...(s.dest !== undefined ? { dest: maskUserinfo(s.dest) } : {}),
|
|
70
|
+
...(s.canonicalLandedUri !== undefined ? { canonicalLandedUri: maskUserinfo(s.canonicalLandedUri) } : {}),
|
|
71
71
|
...(s.failure ? { failure: s.failure } : {}),
|
|
72
72
|
receipts: maskUserinfo(s.receipts),
|
|
73
73
|
};
|
|
@@ -24,6 +24,7 @@ export const deliverCommand = new Command("deliver")
|
|
|
24
24
|
format: globals.format,
|
|
25
25
|
quiet: globals.quiet,
|
|
26
26
|
timeout: globals.timeout,
|
|
27
|
+
flowName: spec.flowName, // archive の {flow} 展開(#89)
|
|
27
28
|
};
|
|
28
29
|
const run = await runDeliver(spec, ctx, { dryRun, archiveClean: !!opts.archiveClean });
|
|
29
30
|
// run 集約は常に stdout へ(DC が結果に依らず永続化できる・§9.7)。
|
|
@@ -65,7 +65,31 @@ async function runSink(sink, archivePath, ctx, resolver) {
|
|
|
65
65
|
const cap = await capturePlugin(sink.plugin, [sink.subcommand, archivePath, ...extraArgs], ctx);
|
|
66
66
|
if (!cap)
|
|
67
67
|
return { ...base, failure: `plugin "i-repo-${sink.plugin}" not found` };
|
|
68
|
-
|
|
68
|
+
// bundle sink の receipt が焼いた canonical な store location(dest)/ storeKind を
|
|
69
|
+
// sink へ載せる(#89)。DC が catalog の dataset identity・store.location に使うため、
|
|
70
|
+
// **delivered(verify 成功)した sink のみ**に載せる — 失敗 sink の receipt も
|
|
71
|
+
// dest を持つが、未着地の宛先を identity として広告すると DC が phantom dataset を作る。
|
|
72
|
+
const verifiedOk = receiptVerifiedOk(cap.receipts);
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
code: cap.code,
|
|
76
|
+
receipts: cap.receipts,
|
|
77
|
+
...(verifiedOk ? sinkDestFromReceipts(cap.receipts) : {}),
|
|
78
|
+
...(sinkFailure(cap.code, cap.receipts)),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** push receipt が焼いた canonical `dest` / `storeKind` を拾う(#89・bundle sink)。 */
|
|
82
|
+
function sinkDestFromReceipts(receipts) {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const r of receipts) {
|
|
85
|
+
if (typeof r.dest === "string" && out.dest === undefined)
|
|
86
|
+
out.dest = r.dest;
|
|
87
|
+
if (typeof r.storeKind === "string" && out.storeKind === undefined)
|
|
88
|
+
out.storeKind = r.storeKind;
|
|
89
|
+
if (out.dest !== undefined && out.storeKind !== undefined)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
69
93
|
}
|
|
70
94
|
/** enrichLocator stream sink が依存する bundle sink id(明示 locatorFrom/単一 bundle なら自動)。 */
|
|
71
95
|
function locatorSourceId(sink, bundleIds) {
|
|
@@ -86,6 +110,9 @@ export async function runDeliver(spec, ctx, opts) {
|
|
|
86
110
|
const startedAt = localDash(new Date());
|
|
87
111
|
const dryRun = opts.dryRun;
|
|
88
112
|
const already = new Set(spec.alreadyDelivered ?? []);
|
|
113
|
+
// spec.flowName を IREPO_FLOW_NAME として全プラグインへ渡す(archive の {flow}
|
|
114
|
+
// 展開・#89)。caller(index.ts) に依らずオーケストレータが正本(直接呼び出しでも効く)。
|
|
115
|
+
ctx = { ...ctx, flowName: spec.flowName ?? ctx.flowName };
|
|
89
116
|
// 1. archive create を 1 回(ハブ)。
|
|
90
117
|
const archiveArgs = [
|
|
91
118
|
spec.archive.subcommand,
|
|
@@ -8,6 +8,11 @@ export interface PluginContext {
|
|
|
8
8
|
quiet?: boolean;
|
|
9
9
|
/** Request timeout in seconds (--timeout); forwarded as IREPO_TIMEOUT. */
|
|
10
10
|
timeout?: string;
|
|
11
|
+
/**
|
|
12
|
+
* フロー名。`deliver`(fan-out)が spec.flowName から渡す。IREPO_FLOW_NAME として
|
|
13
|
+
* プラグインへ転送し、archive の `--to`/`--prefix` の `{flow}` 展開に使う(#89)。
|
|
14
|
+
*/
|
|
15
|
+
flowName?: string;
|
|
11
16
|
/**
|
|
12
17
|
* --save のファイルパス。プラグインは stdout を inherit で直接所有する
|
|
13
18
|
* ため --save は適用できない — 呼び出し側 (run) で明示エラーにする
|
package/dist/plugins/dispatch.js
CHANGED
|
@@ -246,6 +246,12 @@ export function buildPluginEnv(ctx, opts = {}) {
|
|
|
246
246
|
env.IREPO_FORMAT = String(format);
|
|
247
247
|
if (timeout)
|
|
248
248
|
env.IREPO_TIMEOUT = String(timeout);
|
|
249
|
+
// フロー名(deliver fan-out 由来)。archive の {flow} 展開に使う(#89)。host が
|
|
250
|
+
// 権威的に設定する: 継承値を先に scrub(PATH バイナリが IREPO_FLOW_NAME を spoof して
|
|
251
|
+
// 着地パス/canonical dest を捻じ曲げるのを防ぐ=fingerprint 系と同じ規律)。
|
|
252
|
+
delete env.IREPO_FLOW_NAME;
|
|
253
|
+
if (ctx.flowName)
|
|
254
|
+
env.IREPO_FLOW_NAME = String(ctx.flowName);
|
|
249
255
|
env.IREPO_QUIET = quiet ? "1" : "";
|
|
250
256
|
env.IREPO_PLUGIN_API = "1";
|
|
251
257
|
// Provenance of the resolved i-Reporter endpoint, computed CLI-side — the CLI
|
package/package.json
CHANGED
package/plugins/i-repo-archive
CHANGED
|
@@ -44,7 +44,7 @@ async function loadBuiltins() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const PLUGIN = "i-repo-archive";
|
|
47
|
-
const VERSION = "0.6.
|
|
47
|
+
const VERSION = "0.6.2";
|
|
48
48
|
const PLUGIN_API = "1";
|
|
49
49
|
const defaultArchiveRoot = () => join(homedir(), ".i-repo", "archives");
|
|
50
50
|
|
|
@@ -817,12 +817,17 @@ async function pushArchiveBundle(options, backend) {
|
|
|
817
817
|
// 宛先サブパス。--prefix 指定時はテンプレ解決後の安全な相対パスが「実行ごとフォルダ」を置き換える。
|
|
818
818
|
// 未指定なら従来どおり batchDir(UUID)。destSub は宛先専用で、ローカル dir 名(batchDir)とは分離する。
|
|
819
819
|
const destSub = options.prefix ? resolvePrefixTemplate(options.prefix, { batchDir }) : batchDir;
|
|
820
|
+
// --to のテンプレ({flow} 等)を展開し、着地も dest も展開後の値で扱う(issue #89)。
|
|
821
|
+
const expandedTo = expandToTemplate(options.to);
|
|
822
|
+
// バッチ非依存の安定 store location(catalog 掲載用)。実行フォルダ destSub は含めない。
|
|
823
|
+
const dest = canonicalBundleDest(backend.store, expandedTo);
|
|
824
|
+
const storeKind = `${backend.store}-bundle`;
|
|
820
825
|
const env = buildPushEnv(options);
|
|
821
826
|
|
|
822
827
|
if (options.dryRun) {
|
|
823
828
|
// local は OS パス結合(joinDest)、cloud は scheme ベースの joinUri。
|
|
824
|
-
const previewDest = backend.joinDest ? backend.joinDest(
|
|
825
|
-
emitReceipt("write", jobId, count, 0, false, { archivePath, [backend.prefixKey]: previewDest, dryRun: true });
|
|
829
|
+
const previewDest = backend.joinDest ? backend.joinDest(expandedTo, destSub) : joinUri(expandedTo, destSub);
|
|
830
|
+
emitReceipt("write", jobId, count, 0, false, { archivePath, [backend.prefixKey]: previewDest, dest, storeKind, dryRun: true });
|
|
826
831
|
return;
|
|
827
832
|
}
|
|
828
833
|
|
|
@@ -832,10 +837,10 @@ async function pushArchiveBundle(options, backend) {
|
|
|
832
837
|
await fsp.rm(receiptPath, { force: true });
|
|
833
838
|
// Upload the bundle first; only after the recursive copy succeeds do we
|
|
834
839
|
// write the push receipt and upload it as a final, separate object.
|
|
835
|
-
const destination = await backend.upload(archivePath,
|
|
836
|
-
emitReceipt("write", jobId, count, 0, false, { archivePath, [backend.prefixKey]: destination });
|
|
840
|
+
const destination = await backend.upload(archivePath, expandedTo, destSub, env);
|
|
841
|
+
emitReceipt("write", jobId, count, 0, false, { archivePath, [backend.prefixKey]: destination, dest, storeKind });
|
|
837
842
|
|
|
838
|
-
await writeJsonFile(receiptPath, { jobId, count, archivePath, [backend.prefixKey]: destination, pushedAt: new Date().toISOString() });
|
|
843
|
+
await writeJsonFile(receiptPath, { jobId, count, archivePath, [backend.prefixKey]: destination, dest, storeKind, pushedAt: new Date().toISOString() });
|
|
839
844
|
await backend.uploadReceipt(receiptPath, destination, env);
|
|
840
845
|
|
|
841
846
|
// ── verify phase: read the receipt object back from the destination ──
|
|
@@ -845,7 +850,7 @@ async function pushArchiveBundle(options, backend) {
|
|
|
845
850
|
// (PLUGINS.md: 不可逆処理は exit 0 ではなく verified receipt を条件にする)。
|
|
846
851
|
// 削除失敗は配送の失敗ではないので exit 0 のまま、ただし receipt の
|
|
847
852
|
// cleanedUp:false と stderr で必ず報告する (沈黙させない)。
|
|
848
|
-
const extras = { archivePath, [backend.prefixKey]: destination };
|
|
853
|
+
const extras = { archivePath, [backend.prefixKey]: destination, dest, storeKind };
|
|
849
854
|
if (options.cleanup) {
|
|
850
855
|
if (verified) {
|
|
851
856
|
try {
|
|
@@ -1512,3 +1517,56 @@ function resolvePrefixTemplate(template, { batchDir }) {
|
|
|
1512
1517
|
function joinS3Uri(prefix, child) {
|
|
1513
1518
|
return `${prefix.replace(/\/+$/, "")}/${child.replace(/^\/+/, "")}`;
|
|
1514
1519
|
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* --to トークンを URI パスセグメント安全な**単一トークン**へ無害化する(issue #89)。
|
|
1523
|
+
* `sanitizeUnicodeFileName` が `/\` 等を `_` に潰す(パス深さ注入防止)のに加え、URI 予約/
|
|
1524
|
+
* 問題文字(`#`・`?`・`@`・`%`・空白)も `_` にする — これで dest が round-trippable かつ
|
|
1525
|
+
* 全ストアで安全(azure の `[?#]` 拒否や `#` フラグメント切れを防ぐ)。Windows 予約名も無害化。
|
|
1526
|
+
*/
|
|
1527
|
+
function sanitizeToToken(value) {
|
|
1528
|
+
let s = sanitizeUnicodeFileName(String(value).replace(/[\x00-\x1f\x7f]/g, ""));
|
|
1529
|
+
s = s.replace(/[#?@%\s]/g, "_");
|
|
1530
|
+
return WIN_RESERVED.test(s) ? `_${s}` : s;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* --to のテンプレートを展開する(issue #89・§9.4a)。dest は**バッチ非依存**でなければ
|
|
1535
|
+
* ならないので、--to で許すのは**安定キー `{flow}` のみ**。run-varying トークン
|
|
1536
|
+
* ({batchId}/{datetime}/{date})は --prefix(実行フォルダ)専用で、--to にあれば拒否する。
|
|
1537
|
+
* {flow} は host が env(IREPO_FLOW_NAME) で渡す。scheme/bucket/区切りは保持。
|
|
1538
|
+
*/
|
|
1539
|
+
function expandToTemplate(to) {
|
|
1540
|
+
const raw = String(to).replace(/[\x00-\x1f\x7f]/g, "");
|
|
1541
|
+
if (/\{(batchId|datetime|date)\}/.test(raw)) {
|
|
1542
|
+
throw new Error(
|
|
1543
|
+
"--to must not contain run-varying tokens ({batchId}/{datetime}/{date}) — dest must be batch-independent; " +
|
|
1544
|
+
"put those in --prefix. --to may use {flow} only.",
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
const flow = sanitizeToToken(process.env.IREPO_FLOW_NAME || "");
|
|
1548
|
+
const expanded = raw.replace(/\{flow\}/g, flow);
|
|
1549
|
+
// 空 {flow} 等で生じる連続スラッシュを畳む(scheme の :// は保持)。
|
|
1550
|
+
return expanded.replace(/(?<!:)\/{2,}/g, "/");
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* 展開後 --to から、バッチに依らず安定した canonical な store location(dest)を作る(issue #89)。
|
|
1555
|
+
* DC(gemba-os) catalog.rs の canonical_dest と同形(単一コロン): s3:bucket/prefix・gcs:bucket/prefix・
|
|
1556
|
+
* azure:account/container[/prefix]・dir:absPath。実行フォルダ(--prefix/destSub)は dest に含めない。
|
|
1557
|
+
* secret(userinfo) は含めない(azure --to は query/SAS を parseAzureTo で既に拒否)。
|
|
1558
|
+
*/
|
|
1559
|
+
function canonicalBundleDest(store, expandedTo) {
|
|
1560
|
+
if (store === "s3" || store === "gcs") {
|
|
1561
|
+
const scheme = store === "s3" ? "s3://" : "gs://";
|
|
1562
|
+
const rest = String(expandedTo).slice(scheme.length).replace(/\/+$/, ""); // bucket[/prefix...]
|
|
1563
|
+
return `${store === "s3" ? "s3" : "gcs"}:${rest}`;
|
|
1564
|
+
}
|
|
1565
|
+
if (store === "azure") {
|
|
1566
|
+
const { account, container, prefix } = parseAzureTo(expandedTo);
|
|
1567
|
+
return prefix ? `azure:${account}/${container}/${prefix}` : `azure:${account}/${container}`;
|
|
1568
|
+
}
|
|
1569
|
+
// local: 絶対パスへ正規化して dir: スキーム(dir-bundle と同表現)。
|
|
1570
|
+
const abs = resolve(normalizeLocalDest(expandedTo));
|
|
1571
|
+
return `dir:${abs}`;
|
|
1572
|
+
}
|
package/spec/gemba-adc/spec.md
CHANGED
|
@@ -667,6 +667,18 @@ GUI 右側に claude / codex CLI を子プロセスで起動するパネル構
|
|
|
667
667
|
- URI は round-trippable(§4.4.1)= objectKey のパスセグメントは予約文字(`#`・空白・`@` 等)を **percent-encode** してから連結する。
|
|
668
668
|
- secret(userinfo 等)は URI に載せない(§6.2 マスク対象)。**enrich 経路(レコードへの注入)も必ず §6.2 マスクを通す**(receipt だけでなく注入値も対象)。
|
|
669
669
|
|
|
670
|
+
### 9.4a per-sink の安定 `dest` / `storeKind`(catalog 掲載用・#89)
|
|
671
|
+
|
|
672
|
+
`canonicalLandedUri` が**レコード単位**の着地 URI なのに対し、`deliver-run.sinks[].dest` は**その配信先が読み返し対象として指す、バッチに依らず安定した store location**(sink 単位)。DC はこれを dataset の identity・`store.location` に使う。
|
|
673
|
+
|
|
674
|
+
- **算出はプラグイン(bundle backend)が責任を持つ**(store 固有の `--to`/`--prefix` 展開・URL 解析を host で二重化しない=脱料理)。push receipt に **canonical `dest` + `storeKind`** を焼き、`deliver` はそれを sink へコピーするだけ。
|
|
675
|
+
- **`dest` = canonical(展開後 `--to`)**。**`--to` で展開できる安定キーは `{flow}` のみ**(host が `IREPO_FLOW_NAME` で渡す)。**run-varying トークン(`{batchId}`/`{datetime}`/`{date}`)を `--to` に置くのは拒否**(dest がバッチ非依存でなくなる)=それらは `--prefix`(run-folder)専用。**実行フォルダ(`--prefix`/destSub)は dest に含めない**。
|
|
676
|
+
- トークン値は **URI パスセグメント安全な単一トークン**へ無害化(`/\` と `#`・`?`・`@`・`%`・空白 → `_`)=パス深さ注入・`#` フラグメント切れ・azure の `[?#]` 拒否を防ぎ、raw のまま round-trip できる(DC は受信 dest を verbatim 採用するので一致は構成的に保たれる)。空 `{flow}` の連続スラッシュは畳む。
|
|
677
|
+
- **canonical 形(DC `canonical_dest` と同形・単一コロン)**: `s3:<bucket>/<prefix>` / `gcs:<bucket>/<prefix>` / `azure:<account>/<container>[/<prefix>]` / `dir:<absPath>`(prefix 空なら `s3:<bucket>` 等)。secret(userinfo) 非掲載。
|
|
678
|
+
- **`storeKind`**: `s3-bundle` / `gcs-bundle` / `azure-bundle` / `local-bundle`。
|
|
679
|
+
- **delivered(verify 成功)した sink のみ `dest`/`storeKind` を出す**(失敗 sink が未着地の宛先を identity として広告すると DC が phantom dataset を作る)。
|
|
680
|
+
- stream sink(es/sqlite/mongo)は接続値が具体なので `dest`/`storeKind` は任意(現状 bundle のみ populated)。
|
|
681
|
+
|
|
670
682
|
### 9.5 per-sink `exported` 次元と再実行(skip-delivered)
|
|
671
683
|
|
|
672
684
|
- sink の識別は **`id`**(プラグイン名ではない)。同一プラグインで 2 宛先(例: ES の 2 インデックス)を別 sink として許容する。
|
|
@@ -703,8 +715,8 @@ L3 カタログの単一書き手は GEMBA OS(§4.2)。`deliver` は **catal
|
|
|
703
715
|
"sinks": [
|
|
704
716
|
{ "id": "es-main", "plugin": "elastic", "kind": "stream", "storeKind": "elasticsearch", "status": "success", "delivered": true,
|
|
705
717
|
"dest": "elasticsearch:irepo-reports@https://es.local", "canonicalLandedUri": null, "receipts": [ /* write+verify・masked */ ] },
|
|
706
|
-
{ "id": "s3-archive", "plugin": "archive", "kind": "bundle", "storeKind": "s3-bundle", "status": "
|
|
707
|
-
"
|
|
718
|
+
{ "id": "s3-archive", "plugin": "archive", "kind": "bundle", "storeKind": "s3-bundle", "status": "success", "delivered": true,
|
|
719
|
+
"dest": "s3:my-bucket/reports/設備点検", "receipts": [ /* write+verify・masked */ ] },
|
|
708
720
|
{ "id": "mongo-x", "plugin": "mongo", "kind": "stream", "storeKind": "mongodb", "status": "failed", "delivered": false,
|
|
709
721
|
"failure": "verify failed: count mismatch", "receipts": [ /* write のみ */ ] } ],
|
|
710
722
|
"deliveredSinkIds": ["es-main","s3-archive"], "retrySinkIds": ["mongo-x"],
|
|
@@ -714,6 +726,7 @@ L3 カタログの単一書き手は GEMBA OS(§4.2)。`deliver` は **catal
|
|
|
714
726
|
- **`batchId` / `finishedAtIso` / `watermarkAdvanced` / `deliverApi` は必須**(DC が `runs/<batchId>.json` 命名・前進判定に使う=省略を schema が拒否する。`catalog.schema.json` の `deliverRun` で強制)。
|
|
715
727
|
- **per-sink `delivered`** = §9.2 の sink「成功」定義(今回 `verified:true && failedCount==0`、または `skipped`=前回配信済みを継承)。**`delivered:true` かつ非 `skipped` の sink は `receipts[]` に該当 phase の receipt(`verified` 等)を必ず持つ**(裏付けなしに `delivered:true` を主張させない=§0.1 原則1)。`skipped`(前回 verify 済みを継承)と、**未 delivered(失敗・あるいは locator source 未着地で実行を held した sink=receipt 皆無)** は `receipts:[]` を許す。`catalog.schema.json` の `deliverRun` も同条件で強制。
|
|
716
728
|
- **run `delivered`** = **全 sink が delivered(fresh または skip)** かつ `!dryRun`。`status`: 全 sink delivered=`success`(全 skip も含む=前進)/一部 delivered+一部失敗=`partial`(停止)/archive create 失敗・配信ゼロ=`failed`(停止)。`watermarkAdvanced = (status=="success" && !dryRun)`。
|
|
729
|
+
- **bundle sink の `dest`/`storeKind`**(§9.4a・#89): プラグインが push receipt に焼いた canonical な store location(展開後 `--to`・実行フォルダ除外)と storeKind を、`deliver` が sink へコピーする。DC はこれを dataset identity・`store.location` に使い、テンプレート宛先 bundle も `datasets/` に掲載できる。
|
|
717
730
|
- `pluginReceipts` は全 receipt 行を **§6.2 のマスクを通してから**載せる(elastic dest の userinfo 混入が実在・§8.1.1)。
|
|
718
731
|
- 予約 recordType に `deliver-run` を追加(`catalog.schema.json`)。fail-open(§4.7): 未知 store の sink は `storeKind:"unknown"` として run にのみ現れ、datasets は作らない(`storeKind` は sink オブジェクトの宣言フィールド)。
|
|
719
732
|
- exit code: `success`=0、`partial`/`failed`=非 0。ただし **JSON は常に stdout へ出す**(DC が結果に依らず run を永続化できる)。
|