i-repo 2.11.0 → 2.13.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/bin/irepo.js +2 -0
- package/dist/commands/deliver/aggregate.d.ts +90 -0
- package/dist/commands/deliver/aggregate.js +130 -0
- package/dist/commands/deliver/enrich.d.ts +17 -0
- package/dist/commands/deliver/enrich.js +64 -0
- package/dist/commands/deliver/index.d.ts +9 -0
- package/dist/commands/deliver/index.js +36 -0
- package/dist/commands/deliver/locator.d.ts +27 -0
- package/dist/commands/deliver/locator.js +87 -0
- package/dist/commands/deliver/orchestrator.d.ts +9 -0
- package/dist/commands/deliver/orchestrator.js +244 -0
- package/dist/commands/deliver/spec.d.ts +42 -0
- package/dist/commands/deliver/spec.js +123 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +1 -0
- package/dist/plugins/capture.d.ts +29 -0
- package/dist/plugins/capture.js +125 -0
- package/dist/plugins/dispatch.d.ts +32 -0
- package/dist/plugins/dispatch.js +63 -25
- package/package.json +5 -5
- package/plugins/i-repo-archive +34 -2
- package/spec/gemba-adc/catalog.schema.json +103 -3
- package/spec/gemba-adc/deliver.schema.json +124 -0
- package/spec/gemba-adc/spec.md +92 -0
package/dist/bin/irepo.js
CHANGED
|
@@ -23,6 +23,7 @@ import { reportsCommand } from "../commands/reports/index.js";
|
|
|
23
23
|
import { definitionBatchesCommand } from "../commands/definition-batches/index.js";
|
|
24
24
|
import { reportBatchesCommand } from "../commands/report-batches/index.js";
|
|
25
25
|
import { systemsCommand } from "../commands/systems/index.js";
|
|
26
|
+
import { deliverCommand } from "../commands/deliver/index.js";
|
|
26
27
|
import { rawCommand } from "../commands/raw.js";
|
|
27
28
|
import { schemaCommand } from "../commands/schema.js";
|
|
28
29
|
import { pluginCommand } from "../commands/plugin.js";
|
|
@@ -154,6 +155,7 @@ program.addCommand(reportsCommand);
|
|
|
154
155
|
program.addCommand(definitionBatchesCommand);
|
|
155
156
|
program.addCommand(reportBatchesCommand);
|
|
156
157
|
program.addCommand(systemsCommand);
|
|
158
|
+
program.addCommand(deliverCommand);
|
|
157
159
|
program.addCommand(rawCommand);
|
|
158
160
|
program.addCommand(schemaCommand);
|
|
159
161
|
program.addCommand(pluginCommand);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run 集約 `deliver-run` の組み立て(spec.md §9.7 / catalog.schema.json deliverRun)。
|
|
3
|
+
*
|
|
4
|
+
* per-sink「成功(delivered)」と run status の唯一の判定をここに集約する(§9.2)。
|
|
5
|
+
* CLI は catalog/watermark を書かず、この 1 オブジェクトを stdout に出す。
|
|
6
|
+
*/
|
|
7
|
+
import type { SinkKind } from "./spec.js";
|
|
8
|
+
export type SinkStatus = "success" | "partial" | "failed" | "skipped";
|
|
9
|
+
export type RunStatus = "success" | "partial" | "failed";
|
|
10
|
+
export interface SinkResult {
|
|
11
|
+
id: string;
|
|
12
|
+
plugin: string;
|
|
13
|
+
kind: SinkKind;
|
|
14
|
+
storeKind?: string;
|
|
15
|
+
dest?: string | null;
|
|
16
|
+
canonicalLandedUri?: string | null;
|
|
17
|
+
/** alreadyDelivered によりスキップしたか。 */
|
|
18
|
+
skipped: boolean;
|
|
19
|
+
skippedReason?: string;
|
|
20
|
+
/** capture の exit code(skipped のときは undefined)。 */
|
|
21
|
+
code?: number;
|
|
22
|
+
/** その sink の receipt 行(skipped は [])。 */
|
|
23
|
+
receipts: Record<string, unknown>[];
|
|
24
|
+
/** 失敗時の人間可読メモ。 */
|
|
25
|
+
failure?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DeliverRunMeta {
|
|
28
|
+
batchId: string;
|
|
29
|
+
flowId?: string;
|
|
30
|
+
flowName?: string;
|
|
31
|
+
trigger?: string;
|
|
32
|
+
startedAt: string;
|
|
33
|
+
finishedAtIso: string;
|
|
34
|
+
dryRun: boolean;
|
|
35
|
+
archiveOk: boolean;
|
|
36
|
+
archivePath: string | null;
|
|
37
|
+
/** archive 抽出のレコード件数(counts.target)。 */
|
|
38
|
+
archiveCount: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DeliverRunSink {
|
|
41
|
+
id: string;
|
|
42
|
+
plugin: string;
|
|
43
|
+
kind: SinkKind;
|
|
44
|
+
storeKind?: string;
|
|
45
|
+
status: SinkStatus;
|
|
46
|
+
delivered: boolean;
|
|
47
|
+
skippedReason?: string;
|
|
48
|
+
dest?: string | null;
|
|
49
|
+
canonicalLandedUri?: string | null;
|
|
50
|
+
failure?: string;
|
|
51
|
+
receipts: Record<string, unknown>[];
|
|
52
|
+
}
|
|
53
|
+
export interface DeliverRun {
|
|
54
|
+
recordType: "deliver-run";
|
|
55
|
+
schemaVersion: string;
|
|
56
|
+
contract: "gemba-deliver/1.0";
|
|
57
|
+
deliverApi: string[];
|
|
58
|
+
batchId: string;
|
|
59
|
+
flowId?: string;
|
|
60
|
+
flowName?: string;
|
|
61
|
+
trigger?: string;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
finishedAtIso: string;
|
|
64
|
+
status: RunStatus;
|
|
65
|
+
delivered: boolean;
|
|
66
|
+
dryRun: boolean;
|
|
67
|
+
watermarkAdvanced: boolean;
|
|
68
|
+
archivePath: string | null;
|
|
69
|
+
counts: {
|
|
70
|
+
target: number;
|
|
71
|
+
success: number;
|
|
72
|
+
failed: number;
|
|
73
|
+
};
|
|
74
|
+
sinks: DeliverRunSink[];
|
|
75
|
+
deliveredSinkIds: string[];
|
|
76
|
+
retrySinkIds: string[];
|
|
77
|
+
pluginReceipts: Record<string, unknown>[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* verify-phase receipt が verified:true && failedCount==0 を出したか(§0.1 原則1)。
|
|
81
|
+
* failedCount は**実数 0** を要求する(欠落/null を 0 と推定して成功扱いしない=
|
|
82
|
+
* 壊れた receipt から delivered を導かない)。
|
|
83
|
+
*/
|
|
84
|
+
export declare function receiptVerifiedOk(receipts: Record<string, unknown>[]): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* userinfo(`scheme://user:pass@host`)を含む secret を receipt から落とす(§6.2)。
|
|
87
|
+
* elastic dest 等への混入が実在するため、stdout へ出す前に必ず通す。
|
|
88
|
+
*/
|
|
89
|
+
export declare function maskUserinfo<T>(value: T): T;
|
|
90
|
+
export declare function buildDeliverRun(meta: DeliverRunMeta, sinks: SinkResult[]): DeliverRun;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-phase receipt が verified:true && failedCount==0 を出したか(§0.1 原則1)。
|
|
3
|
+
* failedCount は**実数 0** を要求する(欠落/null を 0 と推定して成功扱いしない=
|
|
4
|
+
* 壊れた receipt から delivered を導かない)。
|
|
5
|
+
*/
|
|
6
|
+
export function receiptVerifiedOk(receipts) {
|
|
7
|
+
return receipts.some((r) => r.phase === "verify" && r.verified === true && typeof r.failedCount === "number" && r.failedCount === 0);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* verify-phase receipt の failedCount(数値のみ)。無ければ undefined。
|
|
11
|
+
* write 相の failedCount を拾わない — 失敗 sink の write receipt が failedCount:0
|
|
12
|
+
* を持っていても、それで「失敗 0 件」と誤計上しないため(verify が真実の相)。
|
|
13
|
+
*/
|
|
14
|
+
function verifyFailedCount(receipts) {
|
|
15
|
+
for (let i = receipts.length - 1; i >= 0; i--) {
|
|
16
|
+
const r = receipts[i];
|
|
17
|
+
if (r.phase === "verify" && typeof r.failedCount === "number")
|
|
18
|
+
return r.failedCount;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* userinfo(`scheme://user:pass@host`)を含む secret を receipt から落とす(§6.2)。
|
|
24
|
+
* elastic dest 等への混入が実在するため、stdout へ出す前に必ず通す。
|
|
25
|
+
*/
|
|
26
|
+
export function maskUserinfo(value) {
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
return value.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/@\s]+@/g, "$1***@");
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value))
|
|
31
|
+
return value.map((v) => maskUserinfo(v));
|
|
32
|
+
if (value !== null && typeof value === "object") {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [k, v] of Object.entries(value))
|
|
35
|
+
out[k] = maskUserinfo(v);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
export function buildDeliverRun(meta, sinks) {
|
|
41
|
+
const dryRun = meta.dryRun;
|
|
42
|
+
const outSinks = sinks.map((s) => {
|
|
43
|
+
let status;
|
|
44
|
+
let delivered;
|
|
45
|
+
if (s.skipped) {
|
|
46
|
+
status = "skipped";
|
|
47
|
+
// already-delivered は前回 verify 済みを継承(§9.2)。dry-run は何も配信
|
|
48
|
+
// しないので delivered:false(run も !dryRun を要求するので整合)。
|
|
49
|
+
delivered = !dryRun;
|
|
50
|
+
}
|
|
51
|
+
else if (dryRun) {
|
|
52
|
+
// dry-run は配信/検証しない: exit 0 を実行成功とみなすが delivered は false。
|
|
53
|
+
status = s.code === 0 ? "success" : "failed";
|
|
54
|
+
delivered = false;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const ok = receiptVerifiedOk(s.receipts);
|
|
58
|
+
status = ok ? "success" : "failed";
|
|
59
|
+
delivered = ok;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
id: s.id,
|
|
63
|
+
plugin: s.plugin,
|
|
64
|
+
kind: s.kind,
|
|
65
|
+
...(s.storeKind ? { storeKind: s.storeKind } : {}),
|
|
66
|
+
status,
|
|
67
|
+
delivered,
|
|
68
|
+
...(s.skippedReason ? { skippedReason: s.skippedReason } : {}),
|
|
69
|
+
...(s.dest !== undefined ? { dest: s.dest } : {}),
|
|
70
|
+
...(s.canonicalLandedUri !== undefined ? { canonicalLandedUri: s.canonicalLandedUri } : {}),
|
|
71
|
+
...(s.failure ? { failure: s.failure } : {}),
|
|
72
|
+
receipts: maskUserinfo(s.receipts),
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
// run status(§9.7): archive 失敗は failed。以降は sink 実行結果で判定。
|
|
76
|
+
let status;
|
|
77
|
+
if (!meta.archiveOk) {
|
|
78
|
+
status = "failed";
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const execOk = (x) => x.status === "success" || x.status === "skipped";
|
|
82
|
+
const allOk = outSinks.every(execOk);
|
|
83
|
+
const anyOk = outSinks.some(execOk);
|
|
84
|
+
const anyFail = outSinks.some((x) => x.status === "failed");
|
|
85
|
+
if (allOk)
|
|
86
|
+
status = "success";
|
|
87
|
+
else if (anyOk && anyFail)
|
|
88
|
+
status = "partial";
|
|
89
|
+
else
|
|
90
|
+
status = "failed";
|
|
91
|
+
}
|
|
92
|
+
const delivered = meta.archiveOk && outSinks.every((s) => s.delivered) && !dryRun;
|
|
93
|
+
const watermarkAdvanced = status === "success" && !dryRun;
|
|
94
|
+
// counts はレコード件数(archive 由来。sink 数ではない)。最悪 sink の
|
|
95
|
+
// failedCount を run 全体の失敗とみなす(fan-out は全 sink 到達で1件成功)。
|
|
96
|
+
const target = meta.archiveCount;
|
|
97
|
+
let failed = 0;
|
|
98
|
+
for (const s of sinks) {
|
|
99
|
+
if (s.skipped)
|
|
100
|
+
continue;
|
|
101
|
+
const ok = !dryRun && receiptVerifiedOk(s.receipts);
|
|
102
|
+
const f = ok ? 0 : (verifyFailedCount(s.receipts) ?? (dryRun ? 0 : target));
|
|
103
|
+
if (f > failed)
|
|
104
|
+
failed = f;
|
|
105
|
+
}
|
|
106
|
+
const success = Math.max(0, target - failed);
|
|
107
|
+
const pluginReceipts = maskUserinfo(sinks.flatMap((s) => s.receipts));
|
|
108
|
+
return {
|
|
109
|
+
recordType: "deliver-run",
|
|
110
|
+
schemaVersion: "1.0",
|
|
111
|
+
contract: "gemba-deliver/1.0",
|
|
112
|
+
deliverApi: ["1"],
|
|
113
|
+
batchId: meta.batchId,
|
|
114
|
+
...(meta.flowId ? { flowId: meta.flowId } : {}),
|
|
115
|
+
...(meta.flowName ? { flowName: meta.flowName } : {}),
|
|
116
|
+
...(meta.trigger ? { trigger: meta.trigger } : {}),
|
|
117
|
+
startedAt: meta.startedAt,
|
|
118
|
+
finishedAtIso: meta.finishedAtIso,
|
|
119
|
+
status,
|
|
120
|
+
delivered,
|
|
121
|
+
dryRun,
|
|
122
|
+
watermarkAdvanced,
|
|
123
|
+
archivePath: meta.archivePath,
|
|
124
|
+
counts: { target, success, failed },
|
|
125
|
+
sinks: outSinks,
|
|
126
|
+
deliveredSinkIds: outSinks.filter((s) => s.delivered).map((s) => s.id),
|
|
127
|
+
retrySinkIds: outSinks.filter((s) => s.status === "failed").map((s) => s.id),
|
|
128
|
+
pluginReceipts,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locator enrich Transform(spec.md §9.4)。
|
|
3
|
+
*
|
|
4
|
+
* archive bundle の reports.ndjson と stream sink の stdin の間に挟まり、各封筒
|
|
5
|
+
* レコードへ `canonicalLandedUri` を注入する。`values` は触らない。stream-end
|
|
6
|
+
* トレーラ(§2.6)は完全性検証に必須なので**無加工で透過**。壊れた行も透過
|
|
7
|
+
* (fail-open・§4.7)=enrich の不調で sink ストリームからレコードを落とさない。
|
|
8
|
+
*
|
|
9
|
+
* 実パス(landedPrefix + objectKey)の解決は resolveLocator が担う(locator.ts。
|
|
10
|
+
* landedPrefix は bundle sink の push receipt 由来・objectKey は archive receipt
|
|
11
|
+
* 由来)。解決できないレコード(text-only・bundle 未着地)は null=「既知の不在」を
|
|
12
|
+
* 明示注入する。
|
|
13
|
+
*/
|
|
14
|
+
import { Transform } from "node:stream";
|
|
15
|
+
/** レコード(封筒)から locator を解決する。実パスが確定できなければ null。 */
|
|
16
|
+
export type LocatorResolver = (record: Record<string, unknown>) => string | null;
|
|
17
|
+
export declare function createLocatorEnrich(resolveLocator: LocatorResolver): Transform;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locator enrich Transform(spec.md §9.4)。
|
|
3
|
+
*
|
|
4
|
+
* archive bundle の reports.ndjson と stream sink の stdin の間に挟まり、各封筒
|
|
5
|
+
* レコードへ `canonicalLandedUri` を注入する。`values` は触らない。stream-end
|
|
6
|
+
* トレーラ(§2.6)は完全性検証に必須なので**無加工で透過**。壊れた行も透過
|
|
7
|
+
* (fail-open・§4.7)=enrich の不調で sink ストリームからレコードを落とさない。
|
|
8
|
+
*
|
|
9
|
+
* 実パス(landedPrefix + objectKey)の解決は resolveLocator が担う(locator.ts。
|
|
10
|
+
* landedPrefix は bundle sink の push receipt 由来・objectKey は archive receipt
|
|
11
|
+
* 由来)。解決できないレコード(text-only・bundle 未着地)は null=「既知の不在」を
|
|
12
|
+
* 明示注入する。
|
|
13
|
+
*/
|
|
14
|
+
import { Transform } from "node:stream";
|
|
15
|
+
export function createLocatorEnrich(resolveLocator) {
|
|
16
|
+
let buffer = "";
|
|
17
|
+
const transformLine = (line) => {
|
|
18
|
+
if (line.length === 0)
|
|
19
|
+
return "";
|
|
20
|
+
let rec;
|
|
21
|
+
try {
|
|
22
|
+
rec = JSON.parse(line);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// 壊れた行は透過(fail-open)。sink 側の完全性検証に委ねる。
|
|
26
|
+
process.stderr.write("deliver: passing through a non-JSON manifest line unchanged\n");
|
|
27
|
+
return line;
|
|
28
|
+
}
|
|
29
|
+
if (rec === null || typeof rec !== "object" || Array.isArray(rec))
|
|
30
|
+
return line;
|
|
31
|
+
const obj = rec;
|
|
32
|
+
// トレーラは無加工で透過(count/形を変えない)。
|
|
33
|
+
if (obj.recordType === "stream-end")
|
|
34
|
+
return line;
|
|
35
|
+
obj.canonicalLandedUri = resolveLocator(obj);
|
|
36
|
+
return JSON.stringify(obj);
|
|
37
|
+
};
|
|
38
|
+
return new Transform({
|
|
39
|
+
readableObjectMode: false,
|
|
40
|
+
writableObjectMode: false,
|
|
41
|
+
transform(chunk, _enc, cb) {
|
|
42
|
+
buffer += chunk.toString("utf-8");
|
|
43
|
+
let nl = buffer.indexOf("\n");
|
|
44
|
+
const out = [];
|
|
45
|
+
while (nl !== -1) {
|
|
46
|
+
const line = buffer.slice(0, nl);
|
|
47
|
+
out.push(transformLine(line));
|
|
48
|
+
buffer = buffer.slice(nl + 1);
|
|
49
|
+
nl = buffer.indexOf("\n");
|
|
50
|
+
}
|
|
51
|
+
cb(null, out.length ? out.join("\n") + "\n" : "");
|
|
52
|
+
},
|
|
53
|
+
flush(cb) {
|
|
54
|
+
// 末尾改行が無いまま終端した最終行も取りこぼさない。
|
|
55
|
+
if (buffer.length > 0) {
|
|
56
|
+
const last = transformLine(buffer);
|
|
57
|
+
buffer = "";
|
|
58
|
+
cb(null, last.length ? last + "\n" : "");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
cb();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* irepo deliver — fan-out オーケストレータ(spec.md §9 / SCHEMA.md §9)。
|
|
3
|
+
*
|
|
4
|
+
* 1 回の抽出(archive create)を複数 sink へ同時配信する host 動詞。既存単機能
|
|
5
|
+
* sink(elastic/sqlite/mongo, archive push-*)は不変のまま並べて呼ぶ。run 集約
|
|
6
|
+
* `deliver-run` を stdout に 1 オブジェクトとして出す(CLI は catalog を書かない)。
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
export declare const deliverCommand: Command;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* irepo deliver — fan-out オーケストレータ(spec.md §9 / SCHEMA.md §9)。
|
|
3
|
+
*
|
|
4
|
+
* 1 回の抽出(archive create)を複数 sink へ同時配信する host 動詞。既存単機能
|
|
5
|
+
* sink(elastic/sqlite/mongo, archive push-*)は不変のまま並べて呼ぶ。run 集約
|
|
6
|
+
* `deliver-run` を stdout に 1 オブジェクトとして出す(CLI は catalog を書かない)。
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { ExitCode } from "../../core/errors.js";
|
|
10
|
+
import { loadDeliverSpec } from "./spec.js";
|
|
11
|
+
import { runDeliver } from "./orchestrator.js";
|
|
12
|
+
export const deliverCommand = new Command("deliver")
|
|
13
|
+
.description("Fan out one extraction to multiple sinks (SCHEMA.md §9)")
|
|
14
|
+
.option("--spec <file>", "deliver job spec JSON (gemba-deliver/1.0); reads stdin when omitted")
|
|
15
|
+
.option("--dry-run", "validate the spec and check extraction feasibility without delivering")
|
|
16
|
+
.option("--archive-clean", "remove the archive bundle dir on a full success")
|
|
17
|
+
.action(async (opts, cmd) => {
|
|
18
|
+
const globals = cmd.optsWithGlobals();
|
|
19
|
+
const spec = loadDeliverSpec(opts.spec);
|
|
20
|
+
const dryRun = !!opts.dryRun || spec.dryRun === true;
|
|
21
|
+
const ctx = {
|
|
22
|
+
endpoint: globals.endpoint,
|
|
23
|
+
user: globals.user,
|
|
24
|
+
format: globals.format,
|
|
25
|
+
quiet: globals.quiet,
|
|
26
|
+
timeout: globals.timeout,
|
|
27
|
+
};
|
|
28
|
+
const run = await runDeliver(spec, ctx, { dryRun, archiveClean: !!opts.archiveClean });
|
|
29
|
+
// run 集約は常に stdout へ(DC が結果に依らず永続化できる・§9.7)。
|
|
30
|
+
process.stdout.write(JSON.stringify(run) + "\n");
|
|
31
|
+
// exit code: success=0 / partial=7(失敗 sink だけ再送)/ failed=1(全体リトライ)。
|
|
32
|
+
process.exitCode =
|
|
33
|
+
run.status === "success" ? ExitCode.Success
|
|
34
|
+
: run.status === "partial" ? ExitCode.PartialFailure
|
|
35
|
+
: 1;
|
|
36
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact locator の実パス解決(spec.md §9.4・Phase 3)。
|
|
3
|
+
*
|
|
4
|
+
* canonicalLandedUri = bundle sink の**実 landed prefix**(push receipt 由来=
|
|
5
|
+
* `<--to>/<実行フォルダ>`)+ archive receipt の per-record `objectKey`。
|
|
6
|
+
* どちらも「実際に着地した」値で、予測計算ではない(幽霊リンク無し)。
|
|
7
|
+
*/
|
|
8
|
+
import type { LocatorResolver } from "./enrich.js";
|
|
9
|
+
/** 封筒レコードの同定キー(itemId[+revNo])。archive receipt 側と同形。 */
|
|
10
|
+
export declare function recordKey(itemId: string, revNo: string | null): string;
|
|
11
|
+
/**
|
|
12
|
+
* archive create の receipt 群(`artifacts:[{itemId,revNo,type,objectKey}]`)から
|
|
13
|
+
* record → objectKey マップを作る。1 レコードに複数 type があるときは pdf を優先。
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildObjectKeyMap(archiveReceipts: Record<string, unknown>[]): Map<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* bundle sink の receipt から**実 landed prefix**(`<--to>/<実行フォルダ>`)を取り出す。
|
|
18
|
+
* 既知キーのみを見る(`/Prefix$/` の広すぎる一致で無関係フィールドを拾わない)。
|
|
19
|
+
* `localPrefix` は push-local が生のファイルパスを焼くため、§4.4.1 の
|
|
20
|
+
* `dir:<absPath>/…` 形へ正規化する(scheme でディスパッチする消費側が認識できるように)。
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractLandedPrefix(sinkReceipts: Record<string, unknown>[]): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* resolver を作る。landedPrefix が無い(bundle 未着地)/objectKey が無い
|
|
25
|
+
* (当該レコードに着地成果物が無い=text 行・要求タイプ不在)レコードは null(§9.4)。
|
|
26
|
+
*/
|
|
27
|
+
export declare function makeLocatorResolver(landedPrefix: string | null, objectKeyMap: Map<string, string>): LocatorResolver;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** 封筒レコードの同定キー(itemId[+revNo])。archive receipt 側と同形。 */
|
|
2
|
+
export function recordKey(itemId, revNo) {
|
|
3
|
+
return `${itemId}::${revNo ?? ""}`;
|
|
4
|
+
}
|
|
5
|
+
function recordKeyOf(record) {
|
|
6
|
+
const id = record.itemId ?? record.topId;
|
|
7
|
+
const itemId = id == null ? "" : String(id);
|
|
8
|
+
const revNo = record.revNo == null ? "" : String(record.revNo);
|
|
9
|
+
return recordKey(itemId, revNo);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* archive create の receipt 群(`artifacts:[{itemId,revNo,type,objectKey}]`)から
|
|
13
|
+
* record → objectKey マップを作る。1 レコードに複数 type があるときは pdf を優先。
|
|
14
|
+
*/
|
|
15
|
+
export function buildObjectKeyMap(archiveReceipts) {
|
|
16
|
+
const map = new Map();
|
|
17
|
+
const isPdf = new Set();
|
|
18
|
+
for (const r of archiveReceipts) {
|
|
19
|
+
const arts = r.artifacts;
|
|
20
|
+
if (!Array.isArray(arts))
|
|
21
|
+
continue;
|
|
22
|
+
for (const a of arts) {
|
|
23
|
+
if (a === null || typeof a !== "object")
|
|
24
|
+
continue;
|
|
25
|
+
const e = a;
|
|
26
|
+
if (typeof e.itemId !== "string" || typeof e.objectKey !== "string")
|
|
27
|
+
continue;
|
|
28
|
+
const k = recordKey(e.itemId, e.revNo == null ? null : String(e.revNo));
|
|
29
|
+
// pdf を最優先。既に pdf を採用済みのキーは上書きしない。
|
|
30
|
+
if (isPdf.has(k))
|
|
31
|
+
continue;
|
|
32
|
+
map.set(k, e.objectKey);
|
|
33
|
+
if (e.type === "pdf")
|
|
34
|
+
isPdf.add(k);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
/** push プラグインが実 landed base URI を焼く receipt フィールド(store 別・閉じた集合)。 */
|
|
40
|
+
const LANDED_PREFIX_KEYS = ["s3Prefix", "gcsPrefix", "azurePrefix", "localPrefix"];
|
|
41
|
+
/**
|
|
42
|
+
* bundle sink の receipt から**実 landed prefix**(`<--to>/<実行フォルダ>`)を取り出す。
|
|
43
|
+
* 既知キーのみを見る(`/Prefix$/` の広すぎる一致で無関係フィールドを拾わない)。
|
|
44
|
+
* `localPrefix` は push-local が生のファイルパスを焼くため、§4.4.1 の
|
|
45
|
+
* `dir:<absPath>/…` 形へ正規化する(scheme でディスパッチする消費側が認識できるように)。
|
|
46
|
+
*/
|
|
47
|
+
export function extractLandedPrefix(sinkReceipts) {
|
|
48
|
+
for (const r of sinkReceipts) {
|
|
49
|
+
for (const k of LANDED_PREFIX_KEYS) {
|
|
50
|
+
const v = r[k];
|
|
51
|
+
if (typeof v !== "string" || v.length === 0)
|
|
52
|
+
continue;
|
|
53
|
+
// push の prefix(--prefix/{flow} 等)は予約文字(空白・`#`・`@`)を残しうる
|
|
54
|
+
// (プラグインの無害化は `/\<>:"|?*` のみ)。round-trippable な URI にするため
|
|
55
|
+
// パスセグメントを percent-encode する(§4.4.1/§6.2)。
|
|
56
|
+
// - local: 生パス全体を `dir:` + encode。
|
|
57
|
+
// - cloud: scheme://authority は保持し、その後のパスだけ encode。
|
|
58
|
+
if (k === "localPrefix")
|
|
59
|
+
return v.startsWith("dir:") ? v : `dir:${encodePathSegments(v)}`;
|
|
60
|
+
return encodeUriPrefixPath(v);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** `/` 区切りの各セグメントを percent-encode(区切りは保持)。`#`・空白・`@` で URI が壊れない(§4.4.1/§6.2)。 */
|
|
66
|
+
function encodePathSegments(path) {
|
|
67
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
68
|
+
}
|
|
69
|
+
/** `scheme://authority` を保持し、その後のパスセグメントだけ percent-encode する。 */
|
|
70
|
+
function encodeUriPrefixPath(uri) {
|
|
71
|
+
const m = /^([a-z][a-z0-9+.-]*:\/\/[^/]+)(\/.*)?$/i.exec(uri);
|
|
72
|
+
if (!m)
|
|
73
|
+
return uri; // scheme://authority 形でなければ触らない
|
|
74
|
+
return m[1] + (m[2] ? encodePathSegments(m[2]) : "");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* resolver を作る。landedPrefix が無い(bundle 未着地)/objectKey が無い
|
|
78
|
+
* (当該レコードに着地成果物が無い=text 行・要求タイプ不在)レコードは null(§9.4)。
|
|
79
|
+
*/
|
|
80
|
+
export function makeLocatorResolver(landedPrefix, objectKeyMap) {
|
|
81
|
+
return (record) => {
|
|
82
|
+
if (!landedPrefix)
|
|
83
|
+
return null;
|
|
84
|
+
const objectKey = objectKeyMap.get(recordKeyOf(record));
|
|
85
|
+
return objectKey ? `${landedPrefix}/${encodePathSegments(objectKey)}` : null;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginContext } from "../../plugins/dispatch.js";
|
|
2
|
+
import { type DeliverRun } from "./aggregate.js";
|
|
3
|
+
import type { DeliverSpec } from "./spec.js";
|
|
4
|
+
export interface DeliverRunOptions {
|
|
5
|
+
dryRun: boolean;
|
|
6
|
+
/** full success のときだけ bundle dir を削除(任意・将来 --archive-clean)。 */
|
|
7
|
+
archiveClean?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function runDeliver(spec: DeliverSpec, ctx: PluginContext, opts: DeliverRunOptions): Promise<DeliverRun>;
|