@inharness-ai/claude4spec 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/client/assets/{arc-BaZhf2Hh.js → arc-Ri_rE3A4.js} +1 -1
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-CSSQUdKD.js → architectureDiagram-3BPJPVTR-C7KU6Gxy.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-BlmKwjoI.js → blockDiagram-GPEHLZMM-XP57kEB6.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-C7helt0Z.js → c4Diagram-AAUBKEIU-vQE6EvG4.js} +1 -1
  6. package/dist/client/assets/channel-CXaSz-yc.js +1 -0
  7. package/dist/client/assets/{chunk-2J33WTMH-CC-e13sC.js → chunk-2J33WTMH-CDaw1-z0.js} +1 -1
  8. package/dist/client/assets/{chunk-4BX2VUAB-D-HpWY8q.js → chunk-4BX2VUAB-CdUEf5WH.js} +1 -1
  9. package/dist/client/assets/{chunk-55IACEB6-BSPwaiEp.js → chunk-55IACEB6-CJjUfYKD.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM-CQbEHPPO.js → chunk-727SXJPM-DkgytNym.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-BpSrKpmB.js → chunk-AQP2D5EJ-B23_HIH8.js} +1 -1
  12. package/dist/client/assets/{chunk-FMBD7UC4-zE1R3SR8.js → chunk-FMBD7UC4-BOxU6c6d.js} +1 -1
  13. package/dist/client/assets/{chunk-ND2GUHAM-CqN17qqH.js → chunk-ND2GUHAM-DM3Qjus5.js} +1 -1
  14. package/dist/client/assets/{chunk-QZHKN3VN-13BvG3Jp.js → chunk-QZHKN3VN-CapbZIUP.js} +1 -1
  15. package/dist/client/assets/classDiagram-4FO5ZUOK-CiPHXsyE.js +1 -0
  16. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CiPHXsyE.js +1 -0
  17. package/dist/client/assets/{cose-bilkent-S5V4N54A-CO574VR8.js → cose-bilkent-S5V4N54A-CTNIJ0dY.js} +1 -1
  18. package/dist/client/assets/{dagre-BM42HDAG-BEVJuqdY.js → dagre-BM42HDAG-BRrO0WSt.js} +1 -1
  19. package/dist/client/assets/{diagram-2AECGRRQ-CGCBS-dy.js → diagram-2AECGRRQ-CMCgz46p.js} +1 -1
  20. package/dist/client/assets/{diagram-5GNKFQAL-Dyq0_Rlp.js → diagram-5GNKFQAL-Be67M_wK.js} +1 -1
  21. package/dist/client/assets/{diagram-KO2AKTUF-BpiCh3Ed.js → diagram-KO2AKTUF-C1lam27A.js} +1 -1
  22. package/dist/client/assets/{diagram-LMA3HP47-XJeaihzL.js → diagram-LMA3HP47-3u3xFJ_q.js} +1 -1
  23. package/dist/client/assets/{diagram-OG6HWLK6-Bb_LX7wz.js → diagram-OG6HWLK6-DIVEfPgU.js} +1 -1
  24. package/dist/client/assets/{erDiagram-TEJ5UH35-D-slZ-dN.js → erDiagram-TEJ5UH35-CoXGGm8G.js} +1 -1
  25. package/dist/client/assets/{flowDiagram-I6XJVG4X-B_SOoctl.js → flowDiagram-I6XJVG4X-CZ5BV_Ix.js} +1 -1
  26. package/dist/client/assets/{ganttDiagram-6RSMTGT7-B3UsanUx.js → ganttDiagram-6RSMTGT7-BvPLC5Cv.js} +1 -1
  27. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-C9RzuImP.js → gitGraphDiagram-PVQCEYII-Dh1ZoVJb.js} +1 -1
  28. package/dist/client/assets/{index-BnYPnnod.js → index-BQG2YUwc.js} +1 -1
  29. package/dist/client/assets/{index-DamovIDe.js → index-C5IzEHNL.js} +180 -172
  30. package/dist/client/assets/{index-CQBB3Uf4.js → index-CiSTZTU5.js} +1 -1
  31. package/dist/client/assets/{index-yeyi8oQf.css → index-b9dqo5lW.css} +1 -1
  32. package/dist/client/assets/{infoDiagram-5YYISTIA-CcaL3dnH.js → infoDiagram-5YYISTIA-BuHIm-Jn.js} +1 -1
  33. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-Drl0TLDq.js → ishikawaDiagram-YF4QCWOH-I8nVafdM.js} +1 -1
  34. package/dist/client/assets/{journeyDiagram-JHISSGLW-Ia46_5df.js → journeyDiagram-JHISSGLW-shtpF1TZ.js} +1 -1
  35. package/dist/client/assets/{kanban-definition-UN3LZRKU-CkDPRy8x.js → kanban-definition-UN3LZRKU-BsO8TIvo.js} +1 -1
  36. package/dist/client/assets/{linear-BJ5HYD7N.js → linear-C3N1YJbK.js} +1 -1
  37. package/dist/client/assets/{mermaid.core-DohfSRYI.js → mermaid.core-DUTTNZnN.js} +4 -4
  38. package/dist/client/assets/{mindmap-definition-RKZ34NQL-CN1i3cXM.js → mindmap-definition-RKZ34NQL-DEMm3PNd.js} +1 -1
  39. package/dist/client/assets/{pieDiagram-4H26LBE5-Boz7UISz.js → pieDiagram-4H26LBE5-ZWkz7ewh.js} +1 -1
  40. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DOBPTFZq.js → quadrantDiagram-W4KKPZXB-DSdXKJ0w.js} +1 -1
  41. package/dist/client/assets/{requirementDiagram-4Y6WPE33-c0jMOQGX.js → requirementDiagram-4Y6WPE33-yqSjbYPV.js} +1 -1
  42. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-D-AwIne0.js → sankeyDiagram-5OEKKPKP-yXc5bZhC.js} +1 -1
  43. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BgMjP-Q1.js → sequenceDiagram-3UESZ5HK-Z1Nu6haY.js} +1 -1
  44. package/dist/client/assets/{stateDiagram-AJRCARHV-8qvFMuv5.js → stateDiagram-AJRCARHV-DLVVcNhF.js} +1 -1
  45. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DxGjPxxo.js +1 -0
  46. package/dist/client/assets/{timeline-definition-PNZ67QCA-Q1t7Xr5L.js → timeline-definition-PNZ67QCA-3TZN84mA.js} +1 -1
  47. package/dist/client/assets/{vennDiagram-CIIHVFJN-atIUY_uM.js → vennDiagram-CIIHVFJN-D8QmAxH6.js} +1 -1
  48. package/dist/client/assets/{wardley-L42UT6IY-Dg8nGqpF.js → wardley-L42UT6IY-r7LqMpHU.js} +1 -1
  49. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-B26t7Hde.js → wardleyDiagram-YWT4CUSO-KQreLEne.js} +1 -1
  50. package/dist/client/assets/{xychartDiagram-2RQKCTM6-uz4jM4Hi.js → xychartDiagram-2RQKCTM6-Bxy2QkZl.js} +1 -1
  51. package/dist/client/index.html +2 -2
  52. package/dist/server/config.d.ts +7 -0
  53. package/dist/server/config.js +8 -0
  54. package/dist/server/config.js.map +1 -1
  55. package/dist/server/db/migrations/031_release_push.sql +39 -0
  56. package/dist/server/index.js +21 -1
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/routes/errors.js +7 -0
  59. package/dist/server/routes/errors.js.map +1 -1
  60. package/dist/server/routes/release-pushes.d.ts +10 -0
  61. package/dist/server/routes/release-pushes.js +68 -0
  62. package/dist/server/routes/release-pushes.js.map +1 -0
  63. package/dist/server/routes/releases.js +9 -0
  64. package/dist/server/routes/releases.js.map +1 -1
  65. package/dist/server/services/brief.d.ts +2 -0
  66. package/dist/server/services/brief.js +10 -0
  67. package/dist/server/services/brief.js.map +1 -1
  68. package/dist/server/services/release-bundle.d.ts +92 -0
  69. package/dist/server/services/release-bundle.js +183 -0
  70. package/dist/server/services/release-bundle.js.map +1 -0
  71. package/dist/server/services/release-push.d.ts +38 -0
  72. package/dist/server/services/release-push.js +179 -0
  73. package/dist/server/services/release-push.js.map +1 -0
  74. package/dist/server/services/release.d.ts +39 -1
  75. package/dist/server/services/release.js +53 -1
  76. package/dist/server/services/release.js.map +1 -1
  77. package/dist/server/services/remote-auth.d.ts +25 -0
  78. package/dist/server/services/remote-auth.js +37 -0
  79. package/dist/server/services/remote-auth.js.map +1 -1
  80. package/dist/server/services/remote-http-client.d.ts +51 -0
  81. package/dist/server/services/remote-http-client.js +65 -0
  82. package/dist/server/services/remote-http-client.js.map +1 -1
  83. package/dist/shared/release-push.d.ts +49 -0
  84. package/dist/shared/release-push.js +9 -0
  85. package/dist/shared/release-push.js.map +1 -0
  86. package/dist/shared/types.d.ts +1 -0
  87. package/package.json +2 -1
  88. package/dist/client/assets/channel-CHGfOh3d.js +0 -1
  89. package/dist/client/assets/classDiagram-4FO5ZUOK-DxbfnZUt.js +0 -1
  90. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DxbfnZUt.js +0 -1
  91. 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
- constructor(db: Database.Database, host: PluginHost, versions: VersionService, pageVersions: PageVersionService, pageSerializer: PageSerializer, rawReader: RawEntityReader, tagsService: TagsService, pagesService: PagesService, watcher?: PagesWatcher | null);
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
- constructor(db, host, versions, pageVersions, pageSerializer, rawReader, tagsService, pagesService, watcher = null) {
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') {