@inharness-ai/claude4spec 1.0.7 → 1.0.9
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/CHANGELOG.md +21 -0
- package/dist/client/assets/{arc-BaZhf2Hh.js → arc-BbQk3YNU.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-CSSQUdKD.js → architectureDiagram-3BPJPVTR-DTNs5kv9.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-BlmKwjoI.js → blockDiagram-GPEHLZMM-BR8Pfqtq.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-C7helt0Z.js → c4Diagram-AAUBKEIU-CQ7-XtOM.js} +1 -1
- package/dist/client/assets/channel-DohotR4X.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CC-e13sC.js → chunk-2J33WTMH-3Mu_Omdr.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-D-HpWY8q.js → chunk-4BX2VUAB-Be8xsUyQ.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-BSPwaiEp.js → chunk-55IACEB6-C3wH8L1v.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-CQbEHPPO.js → chunk-727SXJPM-B8Cspzxz.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-BpSrKpmB.js → chunk-AQP2D5EJ-C4mIaD2I.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-zE1R3SR8.js → chunk-FMBD7UC4-mi8e4Jpb.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-CqN17qqH.js → chunk-ND2GUHAM-BSmIDlkx.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-13BvG3Jp.js → chunk-QZHKN3VN-Dn_rpqMI.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BEbP0iRU.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BEbP0iRU.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-CO574VR8.js → cose-bilkent-S5V4N54A-CaorGydk.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-BEVJuqdY.js → dagre-BM42HDAG-ByHsZOJc.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-CGCBS-dy.js → diagram-2AECGRRQ-DDBQfr03.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-Dyq0_Rlp.js → diagram-5GNKFQAL-C49ea1cH.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BpiCh3Ed.js → diagram-KO2AKTUF-BfTxVvlv.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-XJeaihzL.js → diagram-LMA3HP47-Df3uriI8.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-Bb_LX7wz.js → diagram-OG6HWLK6-Ctgih-GF.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D-slZ-dN.js → erDiagram-TEJ5UH35-BirGQyAh.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-B_SOoctl.js → flowDiagram-I6XJVG4X-pRNvZZ3A.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-B3UsanUx.js → ganttDiagram-6RSMTGT7-Dp74nfB8.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-C9RzuImP.js → gitGraphDiagram-PVQCEYII-DpQvPYeS.js} +1 -1
- package/dist/client/assets/{index-BnYPnnod.js → index-BhoTrwkr.js} +1 -1
- package/dist/client/assets/index-CmCv_a_L.js +760 -0
- package/dist/client/assets/{index-CQBB3Uf4.js → index-DfRr1BnL.js} +1 -1
- package/dist/client/assets/{index-yeyi8oQf.css → index-jLEIT-VL.css} +1 -1
- package/dist/client/assets/{infoDiagram-5YYISTIA-CcaL3dnH.js → infoDiagram-5YYISTIA-DYDzOj3P.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-Drl0TLDq.js → ishikawaDiagram-YF4QCWOH-Cy5vPrcI.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Ia46_5df.js → journeyDiagram-JHISSGLW-CwZS2YNP.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-CkDPRy8x.js → kanban-definition-UN3LZRKU-CYcLDOOm.js} +1 -1
- package/dist/client/assets/{linear-BJ5HYD7N.js → linear-MgxFADSW.js} +1 -1
- package/dist/client/assets/{mermaid.core-DohfSRYI.js → mermaid.core-BLh1mA7k.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-CN1i3cXM.js → mindmap-definition-RKZ34NQL-Du5qeSLp.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-Boz7UISz.js → pieDiagram-4H26LBE5-BZfSSy7P.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DOBPTFZq.js → quadrantDiagram-W4KKPZXB-BpnU4ra9.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-c0jMOQGX.js → requirementDiagram-4Y6WPE33-BXPdIVmR.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-D-AwIne0.js → sankeyDiagram-5OEKKPKP-xv3-AxfK.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BgMjP-Q1.js → sequenceDiagram-3UESZ5HK-BlC7_u7W.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-8qvFMuv5.js → stateDiagram-AJRCARHV-DBK7X-jZ.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4ERKDfK.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-Q1t7Xr5L.js → timeline-definition-PNZ67QCA-BncS_dqm.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-atIUY_uM.js → vennDiagram-CIIHVFJN-DhSN1hIy.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-Dg8nGqpF.js → wardley-L42UT6IY-Chwqk3uY.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-B26t7Hde.js → wardleyDiagram-YWT4CUSO-BRAeZzQx.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-uz4jM4Hi.js → xychartDiagram-2RQKCTM6-DRHJB-Md.js} +1 -1
- package/dist/client/index.html +16 -2
- package/dist/server/config.d.ts +7 -0
- package/dist/server/config.js +8 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/db/migrations/031_release_push.sql +39 -0
- package/dist/server/index.js +107 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/reference-tools.js +3 -3
- package/dist/server/mcp/reference-tools.js.map +1 -1
- package/dist/server/routes/errors.js +7 -0
- package/dist/server/routes/errors.js.map +1 -1
- package/dist/server/routes/release-pushes.d.ts +10 -0
- package/dist/server/routes/release-pushes.js +68 -0
- package/dist/server/routes/release-pushes.js.map +1 -0
- package/dist/server/routes/releases.js +9 -0
- package/dist/server/routes/releases.js.map +1 -1
- package/dist/server/routes/remote-project.d.ts +16 -0
- package/dist/server/routes/remote-project.js +85 -0
- package/dist/server/routes/remote-project.js.map +1 -0
- package/dist/server/serialization/resolve-page.js +2 -2
- package/dist/server/serialization/resolve-page.js.map +1 -1
- package/dist/server/services/brief.d.ts +2 -0
- package/dist/server/services/brief.js +10 -0
- package/dist/server/services/brief.js.map +1 -1
- package/dist/server/services/page-serializer.js +2 -2
- package/dist/server/services/page-serializer.js.map +1 -1
- package/dist/server/services/references.js +4 -4
- package/dist/server/services/references.js.map +1 -1
- package/dist/server/services/release-bundle.d.ts +92 -0
- package/dist/server/services/release-bundle.js +183 -0
- package/dist/server/services/release-bundle.js.map +1 -0
- package/dist/server/services/release-push.d.ts +38 -0
- package/dist/server/services/release-push.js +179 -0
- package/dist/server/services/release-push.js.map +1 -0
- package/dist/server/services/release.d.ts +39 -1
- package/dist/server/services/release.js +53 -1
- package/dist/server/services/release.js.map +1 -1
- package/dist/server/services/remote-auth.d.ts +46 -1
- package/dist/server/services/remote-auth.js +71 -1
- package/dist/server/services/remote-auth.js.map +1 -1
- package/dist/server/services/remote-http-client.d.ts +69 -0
- package/dist/server/services/remote-http-client.js +83 -0
- package/dist/server/services/remote-http-client.js.map +1 -1
- package/dist/server/services/section-indexer.js +2 -2
- package/dist/server/services/section-indexer.js.map +1 -1
- package/dist/server/services/sections.js +2 -2
- package/dist/server/services/sections.js.map +1 -1
- package/dist/server/services/todos-indexer.js +2 -2
- package/dist/server/services/todos-indexer.js.map +1 -1
- package/dist/shared/code-ranges.d.ts +46 -0
- package/dist/shared/code-ranges.js +123 -0
- package/dist/shared/code-ranges.js.map +1 -0
- package/dist/shared/release-push.d.ts +49 -0
- package/dist/shared/release-push.js +9 -0
- package/dist/shared/release-push.js.map +1 -0
- package/dist/shared/remote-project.d.ts +27 -0
- package/dist/shared/remote-project.js +2 -0
- package/dist/shared/remote-project.js.map +1 -0
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/xml-tags.d.ts +13 -0
- package/dist/shared/xml-tags.js +20 -0
- package/dist/shared/xml-tags.js.map +1 -1
- package/package.json +3 -2
- package/dist/client/assets/channel-CHGfOh3d.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-DxbfnZUt.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DxbfnZUt.js +0 -1
- package/dist/client/assets/index-DamovIDe.js +0 -752
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Doe04F42.js +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable bundle — the third representation of a spec (alongside live HEAD and
|
|
3
|
+
* versioned history). A `tar.gz` holding the full, self-contained state of a
|
|
4
|
+
* release N, derived ONLY from the versioning tables (via `getReleaseSnapshot`),
|
|
5
|
+
* never from `pagesDir` on disk or entity HEADs.
|
|
6
|
+
*
|
|
7
|
+
* Spec reference: brief `0-1-27-to-0-1-28.md`. Direct consumers (future):
|
|
8
|
+
* M25 (push to remote) writes; M26 (`c4s import`) reads. The restore direction
|
|
9
|
+
* lives in `ReleaseService.restoreBundleArchive` (contract-only in v1).
|
|
10
|
+
*
|
|
11
|
+
* This module owns the pure write logic + the constants/types M26 will import.
|
|
12
|
+
* The two public methods stay on `ReleaseService` per the M17 contract; they
|
|
13
|
+
* are thin delegations to `buildBundleArchive(...)` here.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { create as tarCreate } from 'tar';
|
|
21
|
+
import { nanoid } from 'nanoid';
|
|
22
|
+
/**
|
|
23
|
+
* Bundle layout version. Bump = breaking change in the bundle shape (layout,
|
|
24
|
+
* manifest, sanitization semantics). M26 import compares
|
|
25
|
+
* `manifest.bundleSchemaVersion` against the highest version it supports →
|
|
26
|
+
* mismatch ⇒ `BUNDLE_SCHEMA_UNSUPPORTED`. NOT bumped when an entity's
|
|
27
|
+
* `serializer_version` changes — each bundle is self-contained w.r.t. entity
|
|
28
|
+
* schema (carried per-type in the manifest's `serializerVersions`).
|
|
29
|
+
*/
|
|
30
|
+
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
31
|
+
/**
|
|
32
|
+
* Strict singular entity type → plural bundle file name. Published for M26
|
|
33
|
+
* (read direction maps plural file → entity type via the reverse).
|
|
34
|
+
*/
|
|
35
|
+
export const ENTITY_TYPE_TO_PLURAL_FILE = {
|
|
36
|
+
endpoint: 'endpoints.json',
|
|
37
|
+
dto: 'dtos.json',
|
|
38
|
+
'database-table': 'database-tables.json',
|
|
39
|
+
'ui-view': 'ui-views.json',
|
|
40
|
+
ac: 'acs.json',
|
|
41
|
+
};
|
|
42
|
+
/** claude4spec version read once at module load (pattern from `src/bin/c4s-mcp.ts`). */
|
|
43
|
+
function readC4sVersion() {
|
|
44
|
+
try {
|
|
45
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
const candidates = [
|
|
47
|
+
path.resolve(here, '..', 'package.json'),
|
|
48
|
+
path.resolve(here, '..', '..', 'package.json'),
|
|
49
|
+
path.resolve(here, '..', '..', '..', 'package.json'),
|
|
50
|
+
path.resolve(here, '..', '..', '..', '..', 'package.json'),
|
|
51
|
+
];
|
|
52
|
+
for (const pkgPath of candidates) {
|
|
53
|
+
if (fs.existsSync(pkgPath)) {
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
55
|
+
if (pkg.version)
|
|
56
|
+
return pkg.version;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* ignore — fall through to fallback */
|
|
62
|
+
}
|
|
63
|
+
return '0.0.0';
|
|
64
|
+
}
|
|
65
|
+
export const C4S_VERSION = readC4sVersion();
|
|
66
|
+
/**
|
|
67
|
+
* Explicit allow-list (fail-closed). This is the ONLY edit point when M01 adds
|
|
68
|
+
* a new `Config` field: a new field is dropped from the bundle until someone
|
|
69
|
+
* consciously decides to keep it here. No allow-list entry → no leak.
|
|
70
|
+
*/
|
|
71
|
+
export function sanitizeConfigForBundle(config) {
|
|
72
|
+
return {
|
|
73
|
+
$schemaVersion: config.$schemaVersion,
|
|
74
|
+
name: config.name,
|
|
75
|
+
pagesDir: config.pagesDir,
|
|
76
|
+
mode: config.mode,
|
|
77
|
+
writingStyle: config.writingStyle,
|
|
78
|
+
onboardingCompleted: config.onboardingCompleted,
|
|
79
|
+
entities: config.entities,
|
|
80
|
+
agent: { claudeUsePreset: config.agent?.claudeUsePreset },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Recursively collect file entries under `dir`, as sorted posix-style relative paths. */
|
|
84
|
+
function collectSortedEntries(dir) {
|
|
85
|
+
const out = [];
|
|
86
|
+
const walk = (abs, rel) => {
|
|
87
|
+
for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
88
|
+
const childAbs = path.join(abs, entry.name);
|
|
89
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
90
|
+
if (entry.isDirectory())
|
|
91
|
+
walk(childAbs, childRel);
|
|
92
|
+
else
|
|
93
|
+
out.push(childRel);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(dir, '');
|
|
97
|
+
return out.sort();
|
|
98
|
+
}
|
|
99
|
+
/** Streaming SHA-256 over a file → lowercase hex64. */
|
|
100
|
+
function sha256File(file) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const hash = createHash('sha256');
|
|
103
|
+
const stream = fs.createReadStream(file);
|
|
104
|
+
stream.on('error', reject);
|
|
105
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
106
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build a portable `tar.gz` for release N from an already-resolved snapshot.
|
|
111
|
+
*
|
|
112
|
+
* Pure `(snapshot, release, config) → bytes` — no DB / disk reads beyond the
|
|
113
|
+
* temp working dir. The caller (`ReleaseService.buildBundleArchive`) resolves
|
|
114
|
+
* `snapshot`/`release` from the versioning tables and `config` via `readConfig`.
|
|
115
|
+
*
|
|
116
|
+
* Determinism: entries are sorted and tar headers use `portable` + `noMtime` to
|
|
117
|
+
* strip system-specific noise. The returned `sha256` is an integrity hash over
|
|
118
|
+
* the ACTUAL produced bytes (round-trip self-consistent) — gzip OS-byte/level
|
|
119
|
+
* differences mean it is NOT a cross-machine reproducible build hash.
|
|
120
|
+
*
|
|
121
|
+
* `tarGzPath` is NOT cleaned up here — the consumer (M25 push / M26 import /
|
|
122
|
+
* test) owns it. The internal temp dir IS cleaned up in `finally`.
|
|
123
|
+
*/
|
|
124
|
+
export async function buildBundleArchive(snapshot, release, config) {
|
|
125
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'c4s-bundle-'));
|
|
126
|
+
try {
|
|
127
|
+
// 1. manifest.json
|
|
128
|
+
const manifest = {
|
|
129
|
+
bundleSchemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
130
|
+
release: {
|
|
131
|
+
id: release.id,
|
|
132
|
+
name: release.name,
|
|
133
|
+
description: release.description,
|
|
134
|
+
createdAt: release.createdAt,
|
|
135
|
+
},
|
|
136
|
+
c4sVersion: C4S_VERSION,
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
serializerVersions: snapshot.serializer_versions,
|
|
139
|
+
};
|
|
140
|
+
fs.writeFileSync(path.join(tempDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
141
|
+
// 2. config.json (sanitized allow-list)
|
|
142
|
+
fs.writeFileSync(path.join(tempDir, 'config.json'), JSON.stringify(sanitizeConfigForBundle(config), null, 2), 'utf8');
|
|
143
|
+
// 3. pages/<path>.md — byte-equal content, skip delete tombstones.
|
|
144
|
+
for (const page of snapshot.pages) {
|
|
145
|
+
if (page.op === 'delete')
|
|
146
|
+
continue;
|
|
147
|
+
const data = page.data;
|
|
148
|
+
const dest = path.join(tempDir, 'pages', data.path);
|
|
149
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
150
|
+
fs.writeFileSync(dest, data.content, 'utf8');
|
|
151
|
+
}
|
|
152
|
+
// 4. entities/<typePlural>.json — one file per active type with rows.
|
|
153
|
+
const byType = new Map();
|
|
154
|
+
for (const entity of snapshot.entities) {
|
|
155
|
+
if (entity.op === 'delete')
|
|
156
|
+
continue;
|
|
157
|
+
const list = byType.get(entity.type) ?? [];
|
|
158
|
+
list.push(entity);
|
|
159
|
+
byType.set(entity.type, list);
|
|
160
|
+
}
|
|
161
|
+
if (byType.size > 0) {
|
|
162
|
+
const entitiesDir = path.join(tempDir, 'entities');
|
|
163
|
+
fs.mkdirSync(entitiesDir, { recursive: true });
|
|
164
|
+
for (const [type, rows] of byType) {
|
|
165
|
+
const fileName = ENTITY_TYPE_TO_PLURAL_FILE[type];
|
|
166
|
+
if (!fileName)
|
|
167
|
+
continue; // defensively skip unknown types
|
|
168
|
+
fs.writeFileSync(path.join(entitiesDir, fileName), JSON.stringify(rows, null, 2), 'utf8');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 5. tar -czf (sorted entries, portable headers for stable-ish output).
|
|
172
|
+
const tarGzPath = path.join(os.tmpdir(), `c4s-bundle-${nanoid()}.tar.gz`);
|
|
173
|
+
await tarCreate({ gzip: true, file: tarGzPath, cwd: tempDir, portable: true, noMtime: true }, collectSortedEntries(tempDir));
|
|
174
|
+
// 6. SHA-256 + size of the final archive.
|
|
175
|
+
const sha256 = await sha256File(tarGzPath);
|
|
176
|
+
const sizeBytes = fs.statSync(tarGzPath).size;
|
|
177
|
+
return { tarGzPath, sizeBytes, sha256, bundleSchemaVersion: BUNDLE_SCHEMA_VERSION };
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=release-bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-bundle.js","sourceRoot":"","sources":["../../../src/server/services/release-bundle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M25 Release Push — service-owner of the `release_push` table and the push flow.
|
|
3
|
+
*
|
|
4
|
+
* `push(releaseId)` is a COORDINATOR: it gates on the M24 session, delegates the
|
|
5
|
+
* bundle build to M17 (`releaseService.buildBundleArchive`), transports it via the
|
|
6
|
+
* M24 client (`remoteAuth.pushBundle`), records the attempt, and cleans up. It
|
|
7
|
+
* never builds the bundle itself — there are intentionally NO `tar`/`gzip`/
|
|
8
|
+
* `archiver`/`crypto.createHash` imports here (grep-checkable, AC m25).
|
|
9
|
+
*
|
|
10
|
+
* Every completed attempt (success OR error) is an append-only INSERT; in-progress
|
|
11
|
+
* attempts are not persisted. Retry = a new INSERT (no UPDATE, no DELETE).
|
|
12
|
+
*
|
|
13
|
+
* Spec: brief 0-1-28-to-0-1-29.md (M25).
|
|
14
|
+
*/
|
|
15
|
+
import type Database from 'better-sqlite3';
|
|
16
|
+
import type { ReleaseService } from './release.js';
|
|
17
|
+
import type { RemoteAuthService } from './remote-auth.js';
|
|
18
|
+
import type { ReleasePushResponse } from '../../shared/release-push.js';
|
|
19
|
+
export declare class ReleasePushService {
|
|
20
|
+
private db;
|
|
21
|
+
private releaseService;
|
|
22
|
+
private remoteAuth;
|
|
23
|
+
private cwd;
|
|
24
|
+
constructor(db: Database.Database, releaseService: ReleaseService, remoteAuth: RemoteAuthService, cwd: string);
|
|
25
|
+
/**
|
|
26
|
+
* Synchronous push of release N to the remote. Algorithm (brief §1.3):
|
|
27
|
+
* gate → validate release → build bundle (M17) → transport (M24) → persist →
|
|
28
|
+
* cleanup. The bundle's `tarGzPath` is unlinked in `finally` ALWAYS (M17 does
|
|
29
|
+
* not clean it up — the consumer owns it).
|
|
30
|
+
*/
|
|
31
|
+
push(releaseId: number): Promise<ReleasePushResponse>;
|
|
32
|
+
/** Audit log for one release, newest first (uses idx_release_push_release_id). */
|
|
33
|
+
listForRelease(releaseId: number): ReleasePushResponse[];
|
|
34
|
+
getById(id: number): ReleasePushResponse | null;
|
|
35
|
+
/** Forward-looking — the whole audit log, newest first. */
|
|
36
|
+
listAll(): ReleasePushResponse[];
|
|
37
|
+
private insertRow;
|
|
38
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M25 Release Push — service-owner of the `release_push` table and the push flow.
|
|
3
|
+
*
|
|
4
|
+
* `push(releaseId)` is a COORDINATOR: it gates on the M24 session, delegates the
|
|
5
|
+
* bundle build to M17 (`releaseService.buildBundleArchive`), transports it via the
|
|
6
|
+
* M24 client (`remoteAuth.pushBundle`), records the attempt, and cleans up. It
|
|
7
|
+
* never builds the bundle itself — there are intentionally NO `tar`/`gzip`/
|
|
8
|
+
* `archiver`/`crypto.createHash` imports here (grep-checkable, AC m25).
|
|
9
|
+
*
|
|
10
|
+
* Every completed attempt (success OR error) is an append-only INSERT; in-progress
|
|
11
|
+
* attempts are not persisted. Retry = a new INSERT (no UPDATE, no DELETE).
|
|
12
|
+
*
|
|
13
|
+
* Spec: brief 0-1-28-to-0-1-29.md (M25).
|
|
14
|
+
*/
|
|
15
|
+
import { unlink } from 'node:fs/promises';
|
|
16
|
+
import { DomainError } from './tags.js';
|
|
17
|
+
import { readConfig, writeConfig } from '../config.js';
|
|
18
|
+
import { RemoteUnauthorizedError, RemoteRequestError } from './remote-http-client.js';
|
|
19
|
+
/** Shared projection — the row plus the local release name (saves the UI a fetch). */
|
|
20
|
+
const SELECT_WITH_RELEASE = `SELECT rp.*, sr.name AS release_name
|
|
21
|
+
FROM release_push rp
|
|
22
|
+
JOIN spec_release sr ON sr.id = rp.release_id`;
|
|
23
|
+
export class ReleasePushService {
|
|
24
|
+
db;
|
|
25
|
+
releaseService;
|
|
26
|
+
remoteAuth;
|
|
27
|
+
cwd;
|
|
28
|
+
constructor(db, releaseService, remoteAuth, cwd) {
|
|
29
|
+
this.db = db;
|
|
30
|
+
this.releaseService = releaseService;
|
|
31
|
+
this.remoteAuth = remoteAuth;
|
|
32
|
+
this.cwd = cwd;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Synchronous push of release N to the remote. Algorithm (brief §1.3):
|
|
36
|
+
* gate → validate release → build bundle (M17) → transport (M24) → persist →
|
|
37
|
+
* cleanup. The bundle's `tarGzPath` is unlinked in `finally` ALWAYS (M17 does
|
|
38
|
+
* not clean it up — the consumer owns it).
|
|
39
|
+
*/
|
|
40
|
+
async push(releaseId) {
|
|
41
|
+
// 1. Gate (M24). Snapshot identity here so it survives a 401 mid-push (which
|
|
42
|
+
// wipes remote_session) — the audit row still records who attempted it.
|
|
43
|
+
const account = this.remoteAuth.getCurrentAccount();
|
|
44
|
+
if (!account.connected) {
|
|
45
|
+
throw new DomainError('NOT_CONNECTED', 'Connect to the remote server before pushing');
|
|
46
|
+
}
|
|
47
|
+
if (account.accountStatus !== 'active') {
|
|
48
|
+
throw new DomainError('ACCOUNT_NOT_ACTIVE', 'Account deactivated — push is blocked');
|
|
49
|
+
}
|
|
50
|
+
const accountId = account.remoteAccountId ?? '';
|
|
51
|
+
const accountEmail = account.accountEmail ?? null;
|
|
52
|
+
// 2. Validate the local release exists (frozen releases are allowed).
|
|
53
|
+
try {
|
|
54
|
+
this.releaseService.getRelease(releaseId);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err instanceof DomainError && err.code === 'NOT_FOUND') {
|
|
58
|
+
throw new DomainError('RELEASE_NOT_FOUND', `release '${releaseId}' not found`);
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
// 3. Build the bundle (M17). All bytes-derived values come from here.
|
|
63
|
+
const bundle = await this.releaseService.buildBundleArchive(releaseId);
|
|
64
|
+
const config = readConfig(this.cwd);
|
|
65
|
+
const firstPush = config.remoteProjectId == null;
|
|
66
|
+
try {
|
|
67
|
+
let outcome;
|
|
68
|
+
try {
|
|
69
|
+
// 4. Transport (M24 injects the Bearer; handles 401 → session wipe).
|
|
70
|
+
outcome = await this.remoteAuth.pushBundle({
|
|
71
|
+
tarGzPath: bundle.tarGzPath,
|
|
72
|
+
sizeBytes: bundle.sizeBytes,
|
|
73
|
+
sha256: bundle.sha256,
|
|
74
|
+
projectName: config.name,
|
|
75
|
+
remoteProjectId: config.remoteProjectId ?? null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// 5a. Error attempt → append an error row, then re-throw mapped to 502.
|
|
80
|
+
const sessionExpired = err instanceof RemoteUnauthorizedError;
|
|
81
|
+
const errorMessage = sessionExpired
|
|
82
|
+
? 'Session expired'
|
|
83
|
+
: err instanceof RemoteRequestError
|
|
84
|
+
? err.message
|
|
85
|
+
: 'Network error';
|
|
86
|
+
this.insertRow({
|
|
87
|
+
releaseId,
|
|
88
|
+
remoteProjectId: config.remoteProjectId ?? '',
|
|
89
|
+
remoteReleaseId: null,
|
|
90
|
+
remoteReleaseSequence: null,
|
|
91
|
+
contentSha256: bundle.sha256,
|
|
92
|
+
contentSizeBytes: bundle.sizeBytes,
|
|
93
|
+
deduplicated: false,
|
|
94
|
+
accountId,
|
|
95
|
+
accountEmail,
|
|
96
|
+
bundleSchemaVersion: bundle.bundleSchemaVersion,
|
|
97
|
+
status: 'error',
|
|
98
|
+
errorMessage,
|
|
99
|
+
});
|
|
100
|
+
throw new DomainError(sessionExpired ? 'SESSION_EXPIRED' : 'PUSH_FAILED', sessionExpired ? 'Session expired, log in and try again' : errorMessage);
|
|
101
|
+
}
|
|
102
|
+
// 5b. Success. On first push, persist the new remoteProjectId atomically
|
|
103
|
+
// so subsequent pushes go to POST /v1/projects/:id/releases.
|
|
104
|
+
if (firstPush) {
|
|
105
|
+
writeConfig(this.cwd, { remoteProjectId: outcome.remoteProjectId });
|
|
106
|
+
}
|
|
107
|
+
const id = this.insertRow({
|
|
108
|
+
releaseId,
|
|
109
|
+
remoteProjectId: outcome.remoteProjectId,
|
|
110
|
+
remoteReleaseId: outcome.remoteReleaseId,
|
|
111
|
+
remoteReleaseSequence: outcome.remoteReleaseSequence,
|
|
112
|
+
contentSha256: bundle.sha256,
|
|
113
|
+
contentSizeBytes: bundle.sizeBytes,
|
|
114
|
+
deduplicated: outcome.deduplicated,
|
|
115
|
+
accountId,
|
|
116
|
+
accountEmail,
|
|
117
|
+
bundleSchemaVersion: bundle.bundleSchemaVersion,
|
|
118
|
+
status: 'success',
|
|
119
|
+
errorMessage: null,
|
|
120
|
+
});
|
|
121
|
+
return this.getById(id);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
// 6. ALWAYS clean up the bundle the consumer owns.
|
|
125
|
+
await unlink(bundle.tarGzPath).catch(() => { });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Audit log for one release, newest first (uses idx_release_push_release_id). */
|
|
129
|
+
listForRelease(releaseId) {
|
|
130
|
+
const rows = this.db
|
|
131
|
+
.prepare(`${SELECT_WITH_RELEASE} WHERE rp.release_id = ? ORDER BY rp.pushed_at DESC, rp.id DESC`)
|
|
132
|
+
.all(releaseId);
|
|
133
|
+
return rows.map(toDto);
|
|
134
|
+
}
|
|
135
|
+
getById(id) {
|
|
136
|
+
const row = this.db
|
|
137
|
+
.prepare(`${SELECT_WITH_RELEASE} WHERE rp.id = ?`)
|
|
138
|
+
.get(id);
|
|
139
|
+
return row ? toDto(row) : null;
|
|
140
|
+
}
|
|
141
|
+
/** Forward-looking — the whole audit log, newest first. */
|
|
142
|
+
listAll() {
|
|
143
|
+
const rows = this.db
|
|
144
|
+
.prepare(`${SELECT_WITH_RELEASE} ORDER BY rp.pushed_at DESC, rp.id DESC`)
|
|
145
|
+
.all();
|
|
146
|
+
return rows.map(toDto);
|
|
147
|
+
}
|
|
148
|
+
insertRow(a) {
|
|
149
|
+
const info = this.db
|
|
150
|
+
.prepare(`INSERT INTO release_push
|
|
151
|
+
(release_id, remote_project_id, remote_release_id, remote_release_sequence,
|
|
152
|
+
content_sha256, content_size_bytes, deduplicated,
|
|
153
|
+
pushed_by_account_id, pushed_by_account_email, bundle_schema_version,
|
|
154
|
+
status, error_message)
|
|
155
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
156
|
+
.run(a.releaseId, a.remoteProjectId, a.remoteReleaseId, a.remoteReleaseSequence, a.contentSha256, a.contentSizeBytes, a.deduplicated ? 1 : 0, a.accountId, a.accountEmail, a.bundleSchemaVersion, a.status, a.errorMessage);
|
|
157
|
+
return Number(info.lastInsertRowid);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function toDto(row) {
|
|
161
|
+
return {
|
|
162
|
+
id: row.id,
|
|
163
|
+
releaseId: row.release_id,
|
|
164
|
+
release: { id: row.release_id, name: row.release_name },
|
|
165
|
+
remoteProjectId: row.remote_project_id,
|
|
166
|
+
remoteReleaseId: row.remote_release_id ?? undefined,
|
|
167
|
+
remoteReleaseSequence: row.remote_release_sequence ?? undefined,
|
|
168
|
+
contentSha256: row.content_sha256,
|
|
169
|
+
contentSizeBytes: row.content_size_bytes,
|
|
170
|
+
deduplicated: row.deduplicated === 1,
|
|
171
|
+
pushedByAccountId: row.pushed_by_account_id,
|
|
172
|
+
pushedByAccountEmail: row.pushed_by_account_email ?? undefined,
|
|
173
|
+
bundleSchemaVersion: row.bundle_schema_version,
|
|
174
|
+
status: row.status === 'error' ? 'error' : 'success',
|
|
175
|
+
errorMessage: row.error_message ?? undefined,
|
|
176
|
+
pushedAt: row.pushed_at,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=release-push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-push.js","sourceRoot":"","sources":["../../../src/server/services/release-push.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,6 +18,7 @@ import type { TagsService } from './tags.js';
|
|
|
18
18
|
import type { PagesService } from './pages.js';
|
|
19
19
|
import type { PagesWatcher } from '../fs/watcher.js';
|
|
20
20
|
import type { RestoreResult } from '../serialization/types.js';
|
|
21
|
+
import { type BuildBundleResult } from './release-bundle.js';
|
|
21
22
|
export interface RestoreEntityInput {
|
|
22
23
|
type: RawEntityType;
|
|
23
24
|
slug: string;
|
|
@@ -56,9 +57,16 @@ export declare class ReleaseService {
|
|
|
56
57
|
private tagsService;
|
|
57
58
|
private pagesService;
|
|
58
59
|
private watcher;
|
|
59
|
-
|
|
60
|
+
private cwd;
|
|
61
|
+
constructor(db: Database.Database, host: PluginHost, versions: VersionService, pageVersions: PageVersionService, pageSerializer: PageSerializer, rawReader: RawEntityReader, tagsService: TagsService, pagesService: PagesService, watcher?: PagesWatcher | null, cwd?: string);
|
|
60
62
|
listReleases(): Release[];
|
|
61
63
|
getRelease(idOrName: number | string): ReleaseDetail;
|
|
64
|
+
/**
|
|
65
|
+
* Count of captures still queued at HEAD — entity_version + page_version rows
|
|
66
|
+
* with `release_id IS NULL`. Drives the M25 "You have N unreleased changes"
|
|
67
|
+
* banner shown only on the latest (mutable) release card.
|
|
68
|
+
*/
|
|
69
|
+
countUnreleased(): number;
|
|
62
70
|
/**
|
|
63
71
|
* Manual release creation (decyzja 9: zero auto-trigger). Validates
|
|
64
72
|
* non-empty + UNIQUE name and non-empty description; in a single
|
|
@@ -119,6 +127,36 @@ export declare class ReleaseService {
|
|
|
119
127
|
* pages. Each step generates normal mutations → all visible in timeline.
|
|
120
128
|
*/
|
|
121
129
|
restoreSpec(input: RestoreSpecInput, actor?: ChangedBy): Promise<RestoreSpecResult>;
|
|
130
|
+
/**
|
|
131
|
+
* Build a portable `tar.gz` of release N, deterministically reconstructed
|
|
132
|
+
* from the versioning tables (`getReleaseSnapshot`) + sanitized `config.json`.
|
|
133
|
+
* Does NOT read `pagesDir` on disk nor entity HEADs. The returned `tarGzPath`
|
|
134
|
+
* points at a temp file the CALLER owns (M25 deletes after streaming to
|
|
135
|
+
* remote); the internal working dir is cleaned up before returning.
|
|
136
|
+
*/
|
|
137
|
+
buildBundleArchive(releaseId: number): Promise<BuildBundleResult>;
|
|
138
|
+
/**
|
|
139
|
+
* CONTRACT-ONLY in v1 — implementation deferred to M26 (Release Import,
|
|
140
|
+
* `c4s import <bundle>`). The signature is public so the bidirectional bundle
|
|
141
|
+
* contract is fixed now (write here, read in M26).
|
|
142
|
+
*
|
|
143
|
+
* M26 read-direction algorithm (do not implement here):
|
|
144
|
+
* 1. Parse `manifest.json` FIRST (first tar entry). Missing → throw
|
|
145
|
+
* `BUNDLE_MANIFEST_MISSING`. `bundleSchemaVersion` > max supported →
|
|
146
|
+
* throw `BUNDLE_SCHEMA_UNSUPPORTED { foundVersion, maxSupportedVersion }`.
|
|
147
|
+
* 2. Optionally verify SHA-256 (caller supplies expected hash, e.g. from an
|
|
148
|
+
* M25 push header). Mismatch → throw `BUNDLE_HASH_MISMATCH { expected, actual }`.
|
|
149
|
+
* 3. Read `config.json` via a `bundleSchemaVersion`-aware parser; unknown
|
|
150
|
+
* fields → warn + ignore (forward-compat).
|
|
151
|
+
* 4. Stream `pages/<path>.md`; reject `..` / absolute / null-byte paths →
|
|
152
|
+
* throw `BUNDLE_MALFORMED_ENTRY { path, reason }`.
|
|
153
|
+
* 5. Stream `entities/<typePlural>.json`; plural absent from local
|
|
154
|
+
* `config.entities` → throw `BUNDLE_UNKNOWN_ENTITY_TYPE { type }`.
|
|
155
|
+
* 6. Compose a `SpecSnapshot` (same shape as `getReleaseSnapshot`). M26 owns
|
|
156
|
+
* what to do with it (UPSERT restore / dry-run diff / read-only mount).
|
|
157
|
+
* All errors are structured (code + payload), never bare strings.
|
|
158
|
+
*/
|
|
159
|
+
restoreBundleArchive(_stream: NodeJS.ReadableStream): Promise<SpecSnapshot>;
|
|
122
160
|
private findReleaseRow;
|
|
123
161
|
private toRelease;
|
|
124
162
|
private computeCountBreakdown;
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { DomainError } from './tags.js';
|
|
11
11
|
import { HostEntityWriter } from './entity-writer.js';
|
|
12
|
+
import { readConfig } from '../config.js';
|
|
13
|
+
import { buildBundleArchive as buildBundleArchiveImpl, } from './release-bundle.js';
|
|
12
14
|
const ENTITY_TYPES = ['endpoint', 'dto', 'database-table', 'ui-view', 'ac'];
|
|
13
15
|
const ENTITY_TABLES = {
|
|
14
16
|
endpoint: 'endpoint',
|
|
@@ -27,7 +29,8 @@ export class ReleaseService {
|
|
|
27
29
|
tagsService;
|
|
28
30
|
pagesService;
|
|
29
31
|
watcher;
|
|
30
|
-
|
|
32
|
+
cwd;
|
|
33
|
+
constructor(db, host, versions, pageVersions, pageSerializer, rawReader, tagsService, pagesService, watcher = null, cwd = process.cwd()) {
|
|
31
34
|
this.db = db;
|
|
32
35
|
this.host = host;
|
|
33
36
|
this.versions = versions;
|
|
@@ -37,6 +40,7 @@ export class ReleaseService {
|
|
|
37
40
|
this.tagsService = tagsService;
|
|
38
41
|
this.pagesService = pagesService;
|
|
39
42
|
this.watcher = watcher;
|
|
43
|
+
this.cwd = cwd;
|
|
40
44
|
}
|
|
41
45
|
// ─── Listing & retrieval ─────────────────────────────────────────────────
|
|
42
46
|
listReleases() {
|
|
@@ -52,6 +56,17 @@ export class ReleaseService {
|
|
|
52
56
|
const release = this.toRelease(row);
|
|
53
57
|
return { ...release, countBreakdown: this.computeCountBreakdown(row.id) };
|
|
54
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Count of captures still queued at HEAD — entity_version + page_version rows
|
|
61
|
+
* with `release_id IS NULL`. Drives the M25 "You have N unreleased changes"
|
|
62
|
+
* banner shown only on the latest (mutable) release card.
|
|
63
|
+
*/
|
|
64
|
+
countUnreleased() {
|
|
65
|
+
const row = this.db
|
|
66
|
+
.prepare(`SELECT COUNT(*) AS n FROM entity_version WHERE release_id IS NULL`)
|
|
67
|
+
.get();
|
|
68
|
+
return row.n + this.pageVersions.countUnreleased();
|
|
69
|
+
}
|
|
55
70
|
// ─── Mutations ───────────────────────────────────────────────────────────
|
|
56
71
|
/**
|
|
57
72
|
* Manual release creation (decyzja 9: zero auto-trigger). Validates
|
|
@@ -457,6 +472,43 @@ export class ReleaseService {
|
|
|
457
472
|
}
|
|
458
473
|
return { releaseId, entityResults, pageResults };
|
|
459
474
|
}
|
|
475
|
+
// ─── Portable bundle (transport format — M25 push / M26 import) ────────────
|
|
476
|
+
/**
|
|
477
|
+
* Build a portable `tar.gz` of release N, deterministically reconstructed
|
|
478
|
+
* from the versioning tables (`getReleaseSnapshot`) + sanitized `config.json`.
|
|
479
|
+
* Does NOT read `pagesDir` on disk nor entity HEADs. The returned `tarGzPath`
|
|
480
|
+
* points at a temp file the CALLER owns (M25 deletes after streaming to
|
|
481
|
+
* remote); the internal working dir is cleaned up before returning.
|
|
482
|
+
*/
|
|
483
|
+
async buildBundleArchive(releaseId) {
|
|
484
|
+
const snapshot = this.getReleaseSnapshot(releaseId); // throws NOT_FOUND if missing
|
|
485
|
+
const release = this.getRelease(releaseId);
|
|
486
|
+
return buildBundleArchiveImpl(snapshot, release, readConfig(this.cwd));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* CONTRACT-ONLY in v1 — implementation deferred to M26 (Release Import,
|
|
490
|
+
* `c4s import <bundle>`). The signature is public so the bidirectional bundle
|
|
491
|
+
* contract is fixed now (write here, read in M26).
|
|
492
|
+
*
|
|
493
|
+
* M26 read-direction algorithm (do not implement here):
|
|
494
|
+
* 1. Parse `manifest.json` FIRST (first tar entry). Missing → throw
|
|
495
|
+
* `BUNDLE_MANIFEST_MISSING`. `bundleSchemaVersion` > max supported →
|
|
496
|
+
* throw `BUNDLE_SCHEMA_UNSUPPORTED { foundVersion, maxSupportedVersion }`.
|
|
497
|
+
* 2. Optionally verify SHA-256 (caller supplies expected hash, e.g. from an
|
|
498
|
+
* M25 push header). Mismatch → throw `BUNDLE_HASH_MISMATCH { expected, actual }`.
|
|
499
|
+
* 3. Read `config.json` via a `bundleSchemaVersion`-aware parser; unknown
|
|
500
|
+
* fields → warn + ignore (forward-compat).
|
|
501
|
+
* 4. Stream `pages/<path>.md`; reject `..` / absolute / null-byte paths →
|
|
502
|
+
* throw `BUNDLE_MALFORMED_ENTRY { path, reason }`.
|
|
503
|
+
* 5. Stream `entities/<typePlural>.json`; plural absent from local
|
|
504
|
+
* `config.entities` → throw `BUNDLE_UNKNOWN_ENTITY_TYPE { type }`.
|
|
505
|
+
* 6. Compose a `SpecSnapshot` (same shape as `getReleaseSnapshot`). M26 owns
|
|
506
|
+
* what to do with it (UPSERT restore / dry-run diff / read-only mount).
|
|
507
|
+
* All errors are structured (code + payload), never bare strings.
|
|
508
|
+
*/
|
|
509
|
+
async restoreBundleArchive(_stream) {
|
|
510
|
+
throw new Error('NOT_IMPLEMENTED — restoreBundleArchive deferred to M26 (Release Import)');
|
|
511
|
+
}
|
|
460
512
|
// ─── Internals ───────────────────────────────────────────────────────────
|
|
461
513
|
findReleaseRow(idOrName) {
|
|
462
514
|
if (typeof idOrName === 'number') {
|