@timurproko/a1 0.1.1-dev.4 → 0.1.1-dev.6
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/src/foundation/release/bootstrap.d.ts +2 -2
- package/dist/src/foundation/release/bootstrap.js +5 -3
- package/dist/src/foundation/release/release-store.d.ts +15 -1
- package/dist/src/foundation/release/release-store.js +53 -28
- package/dist/src/foundation/release/release.d.ts +15 -0
- package/dist/src/foundation/release/release.js +62 -35
- package/dist/src/foundation/release/update.d.ts +17 -1
- package/dist/src/foundation/release/update.js +43 -8
- package/docs/ci-release-runbook.md +63 -0
- package/package.json +10 -6
|
@@ -2,7 +2,7 @@ import { type OwnershipProbe } from "./cohort-selection.js";
|
|
|
2
2
|
import { type SupervisorEndpointMetadata } from "./cohort-state.js";
|
|
3
3
|
import { type LaunchProfileId } from "../lifecycle/index.js";
|
|
4
4
|
import { cleanupProvenIdleOwner } from "./process-cleanup.js";
|
|
5
|
-
import { type MaterializedRelease } from "./release-store.js";
|
|
5
|
+
import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
|
|
6
6
|
export interface BootstrapOptions {
|
|
7
7
|
readonly packageRoot: string;
|
|
8
8
|
readonly launchIntent?: {
|
|
@@ -15,7 +15,7 @@ export interface BootstrapOptions {
|
|
|
15
15
|
readonly output?: Pick<NodeJS.WriteStream, "write">;
|
|
16
16
|
}
|
|
17
17
|
export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
|
|
18
|
-
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string): Promise<string>;
|
|
18
|
+
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
|
|
19
19
|
export declare function startSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<void>;
|
|
20
20
|
export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease): NodeJS.ProcessEnv;
|
|
21
21
|
export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number): Promise<void>;
|
|
@@ -8,7 +8,7 @@ import { CohortStateStore } from "./cohort-state.js";
|
|
|
8
8
|
import { assertLaunchProfileId, resolveProductPaths } from "../lifecycle/index.js";
|
|
9
9
|
import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
|
|
10
10
|
import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
|
|
11
|
-
import { materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
11
|
+
import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
12
12
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
13
13
|
export async function runBootstrap(options) {
|
|
14
14
|
const environment = { ...(options.environment ?? process.env) };
|
|
@@ -123,8 +123,10 @@ async function readInstalledVersion(packageRoot) {
|
|
|
123
123
|
}
|
|
124
124
|
return manifest.version;
|
|
125
125
|
}
|
|
126
|
-
export async function certifyMaterializedRelease(release, dataDir) {
|
|
127
|
-
|
|
126
|
+
export async function certifyMaterializedRelease(release, dataDir, verification = {}) {
|
|
127
|
+
if (!consumeMaterializationProof(release)) {
|
|
128
|
+
await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
|
|
129
|
+
}
|
|
128
130
|
const path = resolve(dataDir, `certification-${release.releaseId}.json`);
|
|
129
131
|
await writeFile(path, JSON.stringify({
|
|
130
132
|
schema: PRODUCT_IDENTITY.evidence.releaseCertificationSchema,
|
|
@@ -3,11 +3,23 @@ export declare const RELEASE_MANIFEST_FILENAME: string;
|
|
|
3
3
|
export interface MaterializedRelease extends ReleaseIdentity {
|
|
4
4
|
readonly releaseRoot: string;
|
|
5
5
|
}
|
|
6
|
+
export type ReleaseContentOperation = "source-read" | "candidate-write" | "verification-read";
|
|
7
|
+
export interface ReleaseContentOperationEvent {
|
|
8
|
+
readonly operation: ReleaseContentOperation;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly bytes: number;
|
|
11
|
+
}
|
|
6
12
|
export interface MaterializeReleaseOptions {
|
|
7
13
|
readonly onProgress?: (progress: {
|
|
8
14
|
readonly phase: "copying";
|
|
9
15
|
readonly fileCount: number;
|
|
10
16
|
}) => void;
|
|
17
|
+
readonly onOperation?: (event: ReleaseContentOperationEvent) => void;
|
|
18
|
+
/** Test seam for deterministic write-failure coverage. */
|
|
19
|
+
readonly writeCandidateFile?: (path: string, bytes: Uint8Array, mode: number) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface VerifyMaterializedReleaseOptions {
|
|
22
|
+
readonly onOperation?: (event: ReleaseContentOperationEvent) => void;
|
|
11
23
|
}
|
|
12
24
|
export interface CertifiedReleaseRecord {
|
|
13
25
|
readonly releaseId: string;
|
|
@@ -16,6 +28,8 @@ export interface CertifiedReleaseRecord {
|
|
|
16
28
|
readonly contentDigest: string;
|
|
17
29
|
}
|
|
18
30
|
export declare function materializeRelease(packageRoot: string, dataDir: string, options?: MaterializeReleaseOptions): Promise<MaterializedRelease>;
|
|
31
|
+
/** Consume proof that this exact object was freshly materialized or fully verified in this process. */
|
|
32
|
+
export declare function consumeMaterializationProof(release: MaterializedRelease): boolean;
|
|
19
33
|
export declare function readMaterializedRelease(releaseRoot: string): Promise<MaterializedRelease>;
|
|
20
34
|
/**
|
|
21
35
|
* Load metadata for a release whose bytes were already certified by the
|
|
@@ -24,6 +38,6 @@ export declare function readMaterializedRelease(releaseRoot: string): Promise<Ma
|
|
|
24
38
|
* verification.
|
|
25
39
|
*/
|
|
26
40
|
export declare function readCertifiedReleaseManifest(record: CertifiedReleaseRecord, selectedStoreRoot: string): Promise<MaterializedRelease>;
|
|
27
|
-
export declare function verifyMaterializedRelease(releaseRoot: string, expected?: ReleaseIdentity, selectedStoreRoot?: string): Promise<MaterializedRelease>;
|
|
41
|
+
export declare function verifyMaterializedRelease(releaseRoot: string, expected?: ReleaseIdentity, selectedStoreRoot?: string, options?: VerifyMaterializedReleaseOptions): Promise<MaterializedRelease>;
|
|
28
42
|
export declare function assertImmutableExecutionRoot(release: MaterializedRelease, dataDir: string): Promise<void>;
|
|
29
43
|
export declare function resolveReleaseEntryPoint(release: MaterializedRelease, entryPoint: string): Promise<string>;
|
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod,
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { PRODUCT_PACKAGE_NAME,
|
|
4
|
+
import { PRODUCT_PACKAGE_NAME, createReleaseIdentity, digestManifestFiles, discoverReleasePayload, releaseFileIdentity, resolveWithin, } from "./release.js";
|
|
5
5
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
6
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
7
7
|
const RELEASE_FILE_IO_CONCURRENCY = 32;
|
|
8
|
+
const certificationReady = new WeakSet();
|
|
8
9
|
export const RELEASE_MANIFEST_FILENAME = PRODUCT_IDENTITY.manifest.releaseFilename;
|
|
9
10
|
export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
10
|
-
const
|
|
11
|
+
const payload = await discoverReleasePayload(packageRoot, {
|
|
12
|
+
onSourceRead: (path, bytes) => options.onOperation?.({ operation: "source-read", path, bytes }),
|
|
13
|
+
});
|
|
11
14
|
const storeRoot = resolve(dataDir, "releases");
|
|
12
15
|
await mkdir(storeRoot, { recursive: true, mode: 0o700 });
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
if (existing)
|
|
16
|
-
return await verifyMaterializedRelease(releaseRoot, identity);
|
|
17
|
-
options.onProgress?.({ phase: "copying", fileCount: identity.files.length });
|
|
18
|
-
const candidate = resolveWithin(storeRoot, `.candidate-${identity.releaseId}-${randomUUID()}`);
|
|
16
|
+
options.onProgress?.({ phase: "copying", fileCount: payload.paths.length });
|
|
17
|
+
const candidate = resolveWithin(storeRoot, `.candidate-${randomUUID()}`);
|
|
19
18
|
await mkdir(candidate, { recursive: false, mode: 0o700 });
|
|
20
19
|
try {
|
|
21
|
-
const directories = [...new Set(
|
|
20
|
+
const directories = [...new Set(payload.paths.map(path => dirname(resolveWithin(candidate, path))))];
|
|
22
21
|
await mapWithConcurrency(directories, RELEASE_FILE_IO_CONCURRENCY, async (directory) => {
|
|
23
22
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
24
23
|
});
|
|
25
|
-
await mapWithConcurrency(
|
|
26
|
-
const source = resolveWithin(
|
|
27
|
-
const destination = resolveWithin(candidate,
|
|
28
|
-
await
|
|
29
|
-
|
|
24
|
+
const files = await mapWithConcurrency(payload.paths, RELEASE_FILE_IO_CONCURRENCY, async (path) => {
|
|
25
|
+
const source = resolveWithin(payload.packageRoot, path);
|
|
26
|
+
const destination = resolveWithin(candidate, path);
|
|
27
|
+
const metadata = await lstat(source);
|
|
28
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
29
|
+
throw new Error(`release payload is not a regular file: ${path}`);
|
|
30
|
+
const cached = payload.cachedFiles.get(path);
|
|
31
|
+
const bytes = cached ?? await readFile(source);
|
|
32
|
+
if (!cached)
|
|
33
|
+
options.onOperation?.({ operation: "source-read", path, bytes: bytes.length });
|
|
34
|
+
const mode = (metadata.mode & 0o111) !== 0 ? 0o500 : 0o400;
|
|
35
|
+
if (options.writeCandidateFile)
|
|
36
|
+
await options.writeCandidateFile(destination, bytes, mode);
|
|
37
|
+
else {
|
|
38
|
+
await writeFile(destination, bytes, { flag: "wx", mode });
|
|
39
|
+
await chmod(destination, mode);
|
|
40
|
+
}
|
|
41
|
+
options.onOperation?.({ operation: "candidate-write", path, bytes: bytes.length });
|
|
42
|
+
return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
|
|
30
43
|
});
|
|
44
|
+
const identity = createReleaseIdentity(payload.packageRoot, payload.packageVersion, files);
|
|
45
|
+
const releaseRoot = resolveWithin(storeRoot, identity.releaseId);
|
|
46
|
+
if (await lstat(releaseRoot).catch(() => null)) {
|
|
47
|
+
await rm(candidate, { recursive: true, force: true });
|
|
48
|
+
return certificationReadyRelease(await verifyMaterializedRelease(releaseRoot, identity));
|
|
49
|
+
}
|
|
31
50
|
await writeFile(resolve(candidate, RELEASE_MANIFEST_FILENAME), JSON.stringify(identity, null, 2), { mode: 0o400, flag: "wx" });
|
|
32
51
|
try {
|
|
33
52
|
await rename(candidate, releaseRoot);
|
|
@@ -36,15 +55,22 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
|
36
55
|
if (!await lstat(releaseRoot).catch(() => null))
|
|
37
56
|
throw error;
|
|
38
57
|
await rm(candidate, { recursive: true, force: true });
|
|
39
|
-
return await verifyMaterializedRelease(releaseRoot, identity);
|
|
58
|
+
return certificationReadyRelease(await verifyMaterializedRelease(releaseRoot, identity));
|
|
40
59
|
}
|
|
41
|
-
return { ...identity, releaseRoot: await realpath(releaseRoot) };
|
|
60
|
+
return certificationReadyRelease({ ...identity, releaseRoot: await realpath(releaseRoot) });
|
|
42
61
|
}
|
|
43
62
|
catch (error) {
|
|
44
63
|
await rm(candidate, { recursive: true, force: true });
|
|
45
64
|
throw error;
|
|
46
65
|
}
|
|
47
66
|
}
|
|
67
|
+
/** Consume proof that this exact object was freshly materialized or fully verified in this process. */
|
|
68
|
+
export function consumeMaterializationProof(release) {
|
|
69
|
+
if (!certificationReady.has(release))
|
|
70
|
+
return false;
|
|
71
|
+
certificationReady.delete(release);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
48
74
|
export async function readMaterializedRelease(releaseRoot) {
|
|
49
75
|
const canonical = await realpath(releaseRoot);
|
|
50
76
|
const manifest = JSON.parse(await readFile(resolve(canonical, RELEASE_MANIFEST_FILENAME), "utf8"));
|
|
@@ -69,7 +95,7 @@ export async function readCertifiedReleaseManifest(record, selectedStoreRoot) {
|
|
|
69
95
|
throw new Error(`release directory does not match identity ${manifest.releaseId}`);
|
|
70
96
|
return { ...manifest, releaseRoot: canonical };
|
|
71
97
|
}
|
|
72
|
-
export async function verifyMaterializedRelease(releaseRoot, expected, selectedStoreRoot) {
|
|
98
|
+
export async function verifyMaterializedRelease(releaseRoot, expected, selectedStoreRoot, options = {}) {
|
|
73
99
|
const canonical = await realpath(releaseRoot);
|
|
74
100
|
if (selectedStoreRoot)
|
|
75
101
|
assertContained(await realpath(selectedStoreRoot), canonical, "release root is outside the selected release store");
|
|
@@ -83,7 +109,7 @@ export async function verifyMaterializedRelease(releaseRoot, expected, selectedS
|
|
|
83
109
|
throw new Error(`release directory does not match identity ${manifest.releaseId}`);
|
|
84
110
|
}
|
|
85
111
|
await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
|
|
86
|
-
await verifyFile(canonical, file);
|
|
112
|
+
await verifyFile(canonical, file, options);
|
|
87
113
|
});
|
|
88
114
|
const recomputed = digestManifestFiles(manifest.files);
|
|
89
115
|
if (recomputed !== manifest.contentDigest)
|
|
@@ -109,17 +135,23 @@ export async function resolveReleaseEntryPoint(release, entryPoint) {
|
|
|
109
135
|
assertContained(release.releaseRoot, canonical, "entry point resolves outside the selected release root");
|
|
110
136
|
return canonical;
|
|
111
137
|
}
|
|
112
|
-
async function verifyFile(root, file) {
|
|
138
|
+
async function verifyFile(root, file, options) {
|
|
113
139
|
const path = resolveWithin(root, file.path);
|
|
114
140
|
const metadata = await lstat(path).catch(() => null);
|
|
115
141
|
if (!metadata?.isFile() || metadata.isSymbolicLink())
|
|
116
142
|
throw new Error(`release candidate is incomplete: ${file.path}`);
|
|
117
143
|
if (metadata.size !== file.bytes)
|
|
118
144
|
throw new Error(`release file size mismatch: ${file.path}`);
|
|
119
|
-
const
|
|
145
|
+
const bytes = await readFile(path);
|
|
146
|
+
options.onOperation?.({ operation: "verification-read", path: file.path, bytes: bytes.length });
|
|
147
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
120
148
|
if (digest !== file.sha256)
|
|
121
149
|
throw new Error(`release file digest mismatch: ${file.path}`);
|
|
122
150
|
}
|
|
151
|
+
function certificationReadyRelease(release) {
|
|
152
|
+
certificationReady.add(release);
|
|
153
|
+
return release;
|
|
154
|
+
}
|
|
123
155
|
function validateManifest(value) {
|
|
124
156
|
if (value.packageName !== PRODUCT_PACKAGE_NAME || typeof value.packageVersion !== "string")
|
|
125
157
|
throw new Error(PRODUCT_TEXT.diagnostic("release manifest metadata is invalid"));
|
|
@@ -135,13 +167,6 @@ function validateManifest(value) {
|
|
|
135
167
|
throw new Error(`invalid release manifest file identity: ${file.path}`);
|
|
136
168
|
}
|
|
137
169
|
}
|
|
138
|
-
function digestManifestFiles(files) {
|
|
139
|
-
const digest = createHash("sha256");
|
|
140
|
-
for (const file of [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) {
|
|
141
|
-
digest.update(`${file.path}\0${file.bytes}\0${file.sha256}\0${file.executable ? 1 : 0}\n`);
|
|
142
|
-
}
|
|
143
|
-
return digest.digest("hex");
|
|
144
|
-
}
|
|
145
170
|
function assertContained(parent, child, message) {
|
|
146
171
|
const fromParent = relative(parent, child);
|
|
147
172
|
if (fromParent === "" || (!fromParent.startsWith(`..${sep}`) && fromParent !== ".." && !isAbsolute(fromParent)))
|
|
@@ -13,11 +13,26 @@ export interface ReleaseIdentity {
|
|
|
13
13
|
readonly packageRoot: string;
|
|
14
14
|
readonly files: readonly ReleaseFileIdentity[];
|
|
15
15
|
}
|
|
16
|
+
export interface DiscoveredReleasePayload {
|
|
17
|
+
readonly packageRoot: string;
|
|
18
|
+
readonly packageVersion: string;
|
|
19
|
+
readonly paths: readonly string[];
|
|
20
|
+
/** Package manifests already read to discover the dependency closure. */
|
|
21
|
+
readonly cachedFiles: ReadonlyMap<string, Buffer>;
|
|
22
|
+
}
|
|
23
|
+
export interface DiscoverReleasePayloadOptions {
|
|
24
|
+
readonly onSourceRead?: (path: string, bytes: number) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Discover the complete payload without reading ordinary file contents. */
|
|
27
|
+
export declare function discoverReleasePayload(packageRoot: string, options?: DiscoverReleasePayloadOptions): Promise<DiscoveredReleasePayload>;
|
|
16
28
|
/**
|
|
17
29
|
* Derive release execution identity only from installed distribution metadata and
|
|
18
30
|
* bytes. The version remains display metadata; the digest selects executable
|
|
19
31
|
* content.
|
|
20
32
|
*/
|
|
21
33
|
export declare function deriveReleaseIdentity(packageRoot: string): Promise<ReleaseIdentity>;
|
|
34
|
+
export declare function createReleaseIdentity(packageRoot: string, packageVersion: string, files: readonly ReleaseFileIdentity[]): ReleaseIdentity;
|
|
35
|
+
export declare function releaseFileIdentity(path: string, bytes: Uint8Array, executable: boolean): ReleaseFileIdentity;
|
|
36
|
+
export declare function digestManifestFiles(files: readonly ReleaseFileIdentity[]): string;
|
|
22
37
|
export declare function resolveWithin(root: string, candidate: string): string;
|
|
23
38
|
export declare function packageRootFromModule(moduleUrl: string): string;
|
|
@@ -5,53 +5,78 @@ import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
|
5
5
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
6
6
|
const RELEASE_FILE_IO_CONCURRENCY = 32;
|
|
7
7
|
export const PRODUCT_PACKAGE_NAME = PRODUCT_IDENTITY.packageName;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
* bytes. The version remains display metadata; the digest selects executable
|
|
11
|
-
* content.
|
|
12
|
-
*/
|
|
13
|
-
export async function deriveReleaseIdentity(packageRoot) {
|
|
8
|
+
/** Discover the complete payload without reading ordinary file contents. */
|
|
9
|
+
export async function discoverReleasePayload(packageRoot, options = {}) {
|
|
14
10
|
const canonicalRoot = await realpath(packageRoot);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
11
|
+
const cachedFiles = new Map();
|
|
12
|
+
const readManifest = async (manifestRoot) => {
|
|
13
|
+
const manifestPath = resolve(manifestRoot, "package.json");
|
|
14
|
+
const bytes = await readFile(manifestPath);
|
|
15
|
+
const path = normalizeRelative(relative(canonicalRoot, manifestPath));
|
|
16
|
+
cachedFiles.set(path, bytes);
|
|
17
|
+
options.onSourceRead?.(path, bytes.length);
|
|
18
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
19
|
+
};
|
|
20
|
+
const manifest = await readManifest(canonicalRoot);
|
|
17
21
|
if (manifest.name !== PRODUCT_PACKAGE_NAME)
|
|
18
22
|
throw new Error(`unexpected ${PRODUCT_TEXT.displayName} package name: ${String(manifest.name)}`);
|
|
19
23
|
if (typeof manifest.version !== "string" || manifest.version.length === 0)
|
|
20
24
|
throw new Error(PRODUCT_TEXT.diagnostic("package metadata has no version"));
|
|
21
25
|
const roots = distributionRoots(manifest);
|
|
22
26
|
const paths = new Set(["package.json"]);
|
|
23
|
-
for (const root of roots)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
for (const root of roots)
|
|
28
|
+
await collectFiles(canonicalRoot, resolveWithin(canonicalRoot, root), paths);
|
|
29
|
+
await collectDependencyClosure(canonicalRoot, canonicalRoot, manifest, paths, new Set(), readManifest);
|
|
30
|
+
return {
|
|
31
|
+
packageRoot: canonicalRoot,
|
|
32
|
+
packageVersion: manifest.version,
|
|
33
|
+
paths: [...paths].sort(),
|
|
34
|
+
cachedFiles,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Derive release execution identity only from installed distribution metadata and
|
|
39
|
+
* bytes. The version remains display metadata; the digest selects executable
|
|
40
|
+
* content.
|
|
41
|
+
*/
|
|
42
|
+
export async function deriveReleaseIdentity(packageRoot) {
|
|
43
|
+
const payload = await discoverReleasePayload(packageRoot);
|
|
44
|
+
const files = await mapWithConcurrency(payload.paths, RELEASE_FILE_IO_CONCURRENCY, async (path) => {
|
|
45
|
+
const absolute = resolveWithin(payload.packageRoot, path);
|
|
31
46
|
const metadata = await lstat(absolute);
|
|
32
|
-
if (!metadata.isFile())
|
|
47
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
33
48
|
throw new Error(`release payload is not a regular file: ${path}`);
|
|
34
|
-
const bytes = await readFile(absolute);
|
|
35
|
-
return
|
|
36
|
-
path: normalizeRelative(path),
|
|
37
|
-
bytes: bytes.length,
|
|
38
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
39
|
-
executable: (metadata.mode & 0o111) !== 0,
|
|
40
|
-
};
|
|
49
|
+
const bytes = payload.cachedFiles.get(path) ?? await readFile(absolute);
|
|
50
|
+
return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
|
|
41
51
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const contentDigest =
|
|
52
|
+
return createReleaseIdentity(payload.packageRoot, payload.packageVersion, files);
|
|
53
|
+
}
|
|
54
|
+
export function createReleaseIdentity(packageRoot, packageVersion, files) {
|
|
55
|
+
const contentDigest = digestManifestFiles(files);
|
|
46
56
|
return {
|
|
47
57
|
packageName: PRODUCT_PACKAGE_NAME,
|
|
48
|
-
packageVersion
|
|
58
|
+
packageVersion,
|
|
49
59
|
contentDigest,
|
|
50
|
-
releaseId: `${
|
|
51
|
-
packageRoot
|
|
52
|
-
files,
|
|
60
|
+
releaseId: `${packageVersion}-${contentDigest.slice(0, 20)}`,
|
|
61
|
+
packageRoot,
|
|
62
|
+
files: [...files].sort(compareReleaseFiles),
|
|
53
63
|
};
|
|
54
64
|
}
|
|
65
|
+
export function releaseFileIdentity(path, bytes, executable) {
|
|
66
|
+
return {
|
|
67
|
+
path: normalizeRelative(path),
|
|
68
|
+
bytes: bytes.length,
|
|
69
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
70
|
+
executable,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function digestManifestFiles(files) {
|
|
74
|
+
const digest = createHash("sha256");
|
|
75
|
+
for (const file of [...files].sort(compareReleaseFiles)) {
|
|
76
|
+
digest.update(`${file.path}\0${file.bytes}\0${file.sha256}\0${file.executable ? 1 : 0}\n`);
|
|
77
|
+
}
|
|
78
|
+
return digest.digest("hex");
|
|
79
|
+
}
|
|
55
80
|
export function resolveWithin(root, candidate) {
|
|
56
81
|
if (candidate.includes("\0"))
|
|
57
82
|
throw new Error("release path contains a null byte");
|
|
@@ -93,7 +118,7 @@ async function collectFiles(root, path, output, skipNodeModules = false) {
|
|
|
93
118
|
await collectFiles(root, resolve(path, entry), output, skipNodeModules);
|
|
94
119
|
}
|
|
95
120
|
}
|
|
96
|
-
async function collectDependencyClosure(root, requesterRoot, manifest, output, visited) {
|
|
121
|
+
async function collectDependencyClosure(root, requesterRoot, manifest, output, visited, readManifest) {
|
|
97
122
|
const required = dependencyNames(manifest.dependencies);
|
|
98
123
|
const optional = new Set(dependencyNames(manifest.optionalDependencies));
|
|
99
124
|
for (const name of [...new Set([...required, ...optional])].sort()) {
|
|
@@ -107,8 +132,7 @@ async function collectDependencyClosure(root, requesterRoot, manifest, output, v
|
|
|
107
132
|
continue;
|
|
108
133
|
visited.add(packagePath);
|
|
109
134
|
await collectFiles(root, packagePath, output, true);
|
|
110
|
-
|
|
111
|
-
await collectDependencyClosure(root, packagePath, dependencyManifest, output, visited);
|
|
135
|
+
await collectDependencyClosure(root, packagePath, await readManifest(packagePath), output, visited, readManifest);
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
async function findInstalledDependency(root, requesterRoot, name) {
|
|
@@ -136,6 +160,9 @@ function dependencyNames(value) {
|
|
|
136
160
|
throw new Error(PRODUCT_TEXT.diagnostic("dependency metadata is invalid"));
|
|
137
161
|
return Object.keys(value);
|
|
138
162
|
}
|
|
163
|
+
function compareReleaseFiles(left, right) {
|
|
164
|
+
return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
165
|
+
}
|
|
139
166
|
function normalizeRelative(path) {
|
|
140
167
|
return path.split(sep).join("/").replace(/^\.\//, "");
|
|
141
168
|
}
|
|
@@ -18,6 +18,18 @@ export interface UpdateOutput {
|
|
|
18
18
|
stdout(message: string): void;
|
|
19
19
|
stderr(message: string): void;
|
|
20
20
|
}
|
|
21
|
+
export type UpdateMeasuredPhase = "package-version" | "target-resolution" | "global-root" | "ownership-release" | "npm-install" | "materialized" | "certified" | "active-reference-committed" | "supervisor-verified" | "transaction-complete";
|
|
22
|
+
export interface UpdatePhaseTimingEvent {
|
|
23
|
+
readonly phase: UpdateMeasuredPhase;
|
|
24
|
+
readonly durationMs: number;
|
|
25
|
+
}
|
|
26
|
+
export interface UpdatePerformanceEvidence {
|
|
27
|
+
readonly fileCount: number;
|
|
28
|
+
readonly sourceReads: number;
|
|
29
|
+
readonly candidateWrites: number;
|
|
30
|
+
readonly verificationReads: number;
|
|
31
|
+
readonly postNpmDurationMs: number;
|
|
32
|
+
}
|
|
21
33
|
export interface SelfUpdateOptions {
|
|
22
34
|
packageRoot: string;
|
|
23
35
|
channel?: UpdateChannel;
|
|
@@ -27,14 +39,17 @@ export interface SelfUpdateOptions {
|
|
|
27
39
|
runner?: UpdateProcessRunner;
|
|
28
40
|
lifecycle?: UpdateLifecycleCoordinator;
|
|
29
41
|
transactionStore?: UpdateTransactionJournal;
|
|
42
|
+
onPhaseTiming?: (event: UpdatePhaseTimingEvent) => void;
|
|
43
|
+
now?: () => number;
|
|
30
44
|
}
|
|
45
|
+
export type UpdateActivationPhase = Extract<UpdateTransactionPhase, "materialized" | "certified" | "active-reference-committed">;
|
|
31
46
|
export interface UpdateLifecycleCoordinator {
|
|
32
47
|
targetIsActive(targetVersion: string): Promise<boolean>;
|
|
33
48
|
shutdownVerifiedOwners(targetVersion: string): Promise<{
|
|
34
49
|
priorActiveVersion: string | null;
|
|
35
50
|
}>;
|
|
36
51
|
verifyPackageUnlocked(packageRoot: string): Promise<void>;
|
|
37
|
-
activateInstalled(packageRoot: string, targetVersion: string, phase: (phase:
|
|
52
|
+
activateInstalled(packageRoot: string, targetVersion: string, phase: (phase: UpdateActivationPhase) => Promise<void>): Promise<void>;
|
|
38
53
|
}
|
|
39
54
|
export interface UpdateTransactionJournal {
|
|
40
55
|
readonly path: string;
|
|
@@ -52,3 +67,4 @@ export interface UpdateTransactionJournal {
|
|
|
52
67
|
export declare function createNpmProcessRunner(platform?: NodeJS.Platform): UpdateProcessRunner;
|
|
53
68
|
export declare function createUpdateLifecycleCoordinator(environment?: NodeJS.ProcessEnv, fileSystem?: UpdateFileSystem, output?: UpdateOutput): UpdateLifecycleCoordinator;
|
|
54
69
|
export declare function runSelfUpdate(options: SelfUpdateOptions): Promise<number>;
|
|
70
|
+
export declare function assertUpdatePerformanceBudget(evidence: UpdatePerformanceEvidence, maximumPostNpmDurationMs?: number): void;
|
|
@@ -127,9 +127,19 @@ export async function runSelfUpdate(options) {
|
|
|
127
127
|
const runner = options.runner ?? createNpmProcessRunner();
|
|
128
128
|
const channel = options.channel ?? "stable";
|
|
129
129
|
const distTag = UPDATE_DIST_TAGS[channel];
|
|
130
|
+
const now = options.now ?? (() => performance.now());
|
|
131
|
+
const measure = async (phase, operation) => {
|
|
132
|
+
const startedAt = now();
|
|
133
|
+
try {
|
|
134
|
+
return await operation();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - startedAt) });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
130
140
|
let runningVersion;
|
|
131
141
|
try {
|
|
132
|
-
const packageJson = JSON.parse(await fileSystem.readFile(resolve(options.packageRoot, "package.json")));
|
|
142
|
+
const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
|
|
133
143
|
const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
|
|
134
144
|
if (parsedVersion === null)
|
|
135
145
|
throw new Error("package.json does not contain a valid semantic version");
|
|
@@ -139,7 +149,7 @@ export async function runSelfUpdate(options) {
|
|
|
139
149
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
|
|
140
150
|
return 1;
|
|
141
151
|
}
|
|
142
|
-
const targetLookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`);
|
|
152
|
+
const targetLookup = await measure("target-resolution", async () => await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`));
|
|
143
153
|
if (targetLookup.result === null)
|
|
144
154
|
return targetLookup.exitCode;
|
|
145
155
|
const targetVersion = validSemver(targetLookup.result.stdout.trim());
|
|
@@ -148,7 +158,7 @@ export async function runSelfUpdate(options) {
|
|
|
148
158
|
return 1;
|
|
149
159
|
}
|
|
150
160
|
output.stdout(`${PRODUCT_TEXT.diagnostic(`update (${channel}): ${runningVersion} → ${targetVersion}.`)}\n`);
|
|
151
|
-
const rootLookup = await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root");
|
|
161
|
+
const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
|
|
152
162
|
if (rootLookup.result === null)
|
|
153
163
|
return rootLookup.exitCode;
|
|
154
164
|
if (rootLookup.result.stdout.trim().length === 0) {
|
|
@@ -191,12 +201,14 @@ export async function runSelfUpdate(options) {
|
|
|
191
201
|
priorActiveReleaseId: cohortState.references.active,
|
|
192
202
|
});
|
|
193
203
|
if (phaseBefore(transaction.phase, "ownership-released")) {
|
|
194
|
-
await
|
|
195
|
-
|
|
204
|
+
await measure("ownership-release", async () => {
|
|
205
|
+
await lifecycle.shutdownVerifiedOwners(targetVersion);
|
|
206
|
+
await lifecycle.verifyPackageUnlocked(packageRoot);
|
|
207
|
+
});
|
|
196
208
|
transaction = await transactionStore.advance("ownership-released");
|
|
197
209
|
}
|
|
198
210
|
if (phaseBefore(transaction.phase, "package-installed")) {
|
|
199
|
-
const installation = await runNpm(runner, ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_PACKAGE}@${targetVersion}`], true, output, "start the global npm installation", false);
|
|
211
|
+
const installation = await measure("npm-install", async () => await runNpm(runner, ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_PACKAGE}@${targetVersion}`], true, output, "start the global npm installation", false));
|
|
200
212
|
if (installation.result === null)
|
|
201
213
|
throw new UpdateFailure(installation.exitCode, "npm process failed");
|
|
202
214
|
if (installation.result.code !== 0) {
|
|
@@ -209,11 +221,19 @@ export async function runSelfUpdate(options) {
|
|
|
209
221
|
// Ownership can be reacquired after an interrupted installation (for
|
|
210
222
|
// example, if bare A1 is launched before the update is resumed). Recheck
|
|
211
223
|
// immediately before activation so recovery cannot start a second cohort.
|
|
212
|
-
await lifecycle.shutdownVerifiedOwners(targetVersion);
|
|
213
|
-
|
|
224
|
+
await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
|
|
225
|
+
let activationPhaseStartedAt = now();
|
|
226
|
+
await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
|
|
227
|
+
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
228
|
+
transaction = await transactionStore.advance(phase);
|
|
229
|
+
activationPhaseStartedAt = now();
|
|
230
|
+
});
|
|
231
|
+
options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
232
|
+
const transactionStartedAt = now();
|
|
214
233
|
await transactionStore.advance("supervisor-verified");
|
|
215
234
|
await transactionStore.finish("completed");
|
|
216
235
|
await transactionStore.clearCompleted();
|
|
236
|
+
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
217
237
|
output.stdout(`${PRODUCT_TEXT.diagnostic(`updated successfully: ${targetVersion} (${channel}).`)}\n`);
|
|
218
238
|
return 0;
|
|
219
239
|
}
|
|
@@ -228,6 +248,21 @@ export async function runSelfUpdate(options) {
|
|
|
228
248
|
return error instanceof UpdateFailure ? error.exitCode : 1;
|
|
229
249
|
}
|
|
230
250
|
}
|
|
251
|
+
export function assertUpdatePerformanceBudget(evidence, maximumPostNpmDurationMs = 30_000) {
|
|
252
|
+
const failures = [];
|
|
253
|
+
if (evidence.fileCount < 1)
|
|
254
|
+
failures.push("fixture contains no payload files");
|
|
255
|
+
if (evidence.sourceReads !== evidence.fileCount)
|
|
256
|
+
failures.push(`source payload read count is ${evidence.sourceReads} for ${evidence.fileCount} files`);
|
|
257
|
+
if (evidence.candidateWrites !== evidence.fileCount)
|
|
258
|
+
failures.push(`candidate payload write count is ${evidence.candidateWrites} for ${evidence.fileCount} files`);
|
|
259
|
+
if (evidence.verificationReads > 0)
|
|
260
|
+
failures.push(`fresh certification reread ${evidence.verificationReads} candidate files`);
|
|
261
|
+
if (evidence.postNpmDurationMs > maximumPostNpmDurationMs)
|
|
262
|
+
failures.push(`post-npm activation took ${Math.round(evidence.postNpmDurationMs)}ms; budget is ${maximumPostNpmDurationMs}ms`);
|
|
263
|
+
if (failures.length > 0)
|
|
264
|
+
throw new Error(`update performance budget failed: ${failures.join("; ")}`);
|
|
265
|
+
}
|
|
231
266
|
async function requestUpdateShutdown(metadata, targetVersion, timeoutMs) {
|
|
232
267
|
if (!processIsAlive(metadata.pid))
|
|
233
268
|
return { accepted: false, reason: "recorded owner is dead" };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# CI and release operations
|
|
2
|
+
|
|
3
|
+
This runbook is the operational source for scoped development validation, immutable npm candidates, stable certification, and branch enforcement. GitHub Actions is the only automation platform.
|
|
4
|
+
|
|
5
|
+
## Stable status names
|
|
6
|
+
|
|
7
|
+
Repository rules use job display names, not internal job keys. Keep these names stable:
|
|
8
|
+
|
|
9
|
+
| Protected flow | Required check | Producer |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Pull request into `develop` | `Development validation required` | `.github/workflows/ci.yml` |
|
|
12
|
+
| Accepted `develop` commit | `Development validation required` | `.github/workflows/ci.yml` push run |
|
|
13
|
+
| Promotion into `master` | `Stable candidate required` | `.github/workflows/certify-stable.yml` |
|
|
14
|
+
|
|
15
|
+
Matrix and tier job names are implementation details. Change a required name only by updating the reviewable ruleset definition, its governance tests, this runbook, and the live ruleset together.
|
|
16
|
+
|
|
17
|
+
## Advisory rollout and widening validation
|
|
18
|
+
|
|
19
|
+
New or materially changed selection rules run in advisory mode before their check becomes required. Compare at least one feature-only change and one cross-cutting change with a complete run. A selection miss is a policy defect: widen `config/validation-impact.json`, add a regression test, and rerun complete validation before enabling enforcement.
|
|
20
|
+
|
|
21
|
+
The selector can only widen validation. To override an affected plan, dispatch **Development validation** at the exact `develop` ref with `full: true`. This selects `full-release`; it cannot suppress mandatory tiers. Dispatch **Full regression** for the complete non-physical package and clean-install path. Scheduled Full regression is also the backstop for impact-map mistakes.
|
|
22
|
+
|
|
23
|
+
When selection is uncertain, deleted, renamed, unmapped, or based on an unavailable Git range, it must fail closed to full validation. Do not bypass this fallback with labels or edited workflow inputs.
|
|
24
|
+
|
|
25
|
+
## Preview candidate and `next` publication
|
|
26
|
+
|
|
27
|
+
1. Confirm `Development validation required` is green for the exact `develop` tip.
|
|
28
|
+
2. Dispatch **Build npm next candidate** on `develop` with the exact source commit, a trusted ancestor base commit, and `confirm_candidate=build-uncertified-next-candidate`. Use `full: true` whenever ordinary affected coverage is not sufficient for the release decision.
|
|
29
|
+
3. Review `candidate-evidence.json`, selected scopes, gate outcomes, package integrity, and source tree. The candidate remains explicitly stable-ineligible.
|
|
30
|
+
4. Approve the protected `npm-next` environment and dispatch **Publish npm next** with the successful candidate run id and `confirm_next=publish-certified-next`.
|
|
31
|
+
5. The publisher downloads and verifies the certified tarball, then publishes those bytes without checkout, installation, build, or tests. Verify its registry digest and `next` tag result.
|
|
32
|
+
|
|
33
|
+
Preview candidates expire after 14 days. An expired, missing, failed, or mismatched artifact is never reconstructed in the publisher. Build and certify a new candidate.
|
|
34
|
+
|
|
35
|
+
## Stable candidate, physical evidence, and `latest` publication
|
|
36
|
+
|
|
37
|
+
1. Commit the final non-prerelease version to `develop`. Confirm it is clean, registry-unpublished, and `v<package-version>` is the intended tag.
|
|
38
|
+
2. Dispatch **Build stable candidate** on that exact `develop` commit with `confirm_candidate=build-stable-candidate`. It packs once on Windows and fans the same verified digest to Windows, Linux, and macOS complete automated validation and clean installation.
|
|
39
|
+
3. Review the `Stable automated candidate` artifact. It is not stable-eligible; physical evidence is still required.
|
|
40
|
+
4. On dedicated isolated workers only, dispatch **Certify stable physical platforms** for the same source and automated-candidate run with `confirm_isolated=run-isolated-physical-certification`. Workers must carry `self-hosted`, `a1-physical`, and platform-specific labels, set `PHYSICAL_WORKER_ISOLATED=true`, and be protected by the `stable-physical` environment. Never run physical host probes on a developer workstation or ordinary hosted runner.
|
|
41
|
+
5. Dispatch **Certify stable candidate** with the successful automated and physical run ids and `confirm_certification=certify-stable-candidate`. `Stable candidate required` passes only when all three automated and all three isolated physical verdicts bind the same commit, tree, version, integrity, and shasum.
|
|
42
|
+
6. Promote that exact commit to `master` without source or package changes and create `v<version>` at the same commit. The protected `master` rule requires the existing `Stable candidate required` check on that commit.
|
|
43
|
+
7. Dispatch **Publish npm stable** on the tag with the certification run id and `confirm_stable=publish-certified-stable-latest`, then approve `npm-stable`. It requires the current `master` and tag to equal the certified source, confirms the version is unpublished, publishes the exact tarball to `latest` with provenance, and verifies registry bytes.
|
|
44
|
+
|
|
45
|
+
Stable automated and physical candidate artifacts expire after 30 days. Publication evidence is retained for 90 days. Artifact expiry requires a new pack and complete recertification; it does not permit repacking during publication.
|
|
46
|
+
|
|
47
|
+
## Failure recovery
|
|
48
|
+
|
|
49
|
+
- **Development failure:** inspect impact and outcome artifacts. Fix the source or mapping and rerun. Do not mark a failed tier optional.
|
|
50
|
+
- **Candidate validation failure:** discard the candidate. Any source change, package change, or uncertain evidence requires a new candidate run.
|
|
51
|
+
- **Physical failure:** quarantine the worker result, fix or replace the isolated worker, and rerun all evidence needed for one exact package. A hosted matrix cannot substitute for physical evidence.
|
|
52
|
+
- **Approval or artifact expiry:** create and certify a new candidate. Never upload locally rebuilt bytes.
|
|
53
|
+
- **Publisher failure before npm accepts bytes:** retain the candidate and diagnose identity, permissions, registry, or provenance. Retry only with the same candidate run if the registry still proves the version unpublished and the artifact has not expired.
|
|
54
|
+
- **Publisher uncertainty after npm accepts bytes:** do not republish or rebuild. Query the registry for version, dist-tag, integrity, and shasum; repair a dist-tag only through a separately reviewed registry operation.
|
|
55
|
+
- **Partial stable certification:** stable eligibility remains false. Missing, duplicated, failed, non-isolated, or mismatched platform evidence fails closed.
|
|
56
|
+
|
|
57
|
+
## Enforcement rollout and rollback
|
|
58
|
+
|
|
59
|
+
Ruleset mutation is a separate administrative operation. First run `node scripts/check-github-rulesets.mjs` in report mode and review the proposed diff. Apply only after workflows exist on the default branch, representative advisory runs pass, and a maintainer explicitly confirms the exact ruleset change. Capture the post-apply repository API response as evidence.
|
|
60
|
+
|
|
61
|
+
If a required check is operationally broken, prefer correcting the workflow. Emergency rollback may disable only the affected required context after restoring the previous blocking validation path and recording maintainer approval. Never weaken force-push/deletion protection to release. Never route around certification by rebuilding inside a publisher.
|
|
62
|
+
|
|
63
|
+
After rollback, publication still requires exact certified bytes. A failed or unavailable candidate workflow means release waits for a new candidate; it does not authorize an ad hoc npm upload.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.6",
|
|
4
4
|
"description": "Standalone terminal workspace for supervised native and managed agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.13.0",
|
|
@@ -21,16 +21,20 @@
|
|
|
21
21
|
"clean": "node scripts/clean.mjs",
|
|
22
22
|
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
23
23
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
-
"check:architecture": "node scripts/check-architecture.mjs && node scripts/product-identifier-policy.mjs --check && node scripts/check-product-identity-boundaries.mjs && node scripts/check-package-identity.mjs && node scripts/check-pinned-pi-source-ledger.mjs && node scripts/check-terminal-host-provenance.mjs",
|
|
24
|
+
"check:architecture": "node scripts/check-architecture.mjs && node scripts/product-identifier-policy.mjs --check && node scripts/check-product-identity-boundaries.mjs && node scripts/check-package-identity.mjs && node scripts/check-pinned-pi-source-ledger.mjs && node scripts/check-terminal-host-provenance.mjs && node scripts/check-validation-governance.mjs",
|
|
25
25
|
"check:customization-ready": "node scripts/check-owned-ui-customization-prerequisites.mjs",
|
|
26
26
|
"check:deprecated": "node scripts/check-deprecated-dependencies.mjs",
|
|
27
27
|
"branches:prune": "node scripts/prune-merged-branches.mjs",
|
|
28
|
-
"
|
|
28
|
+
"report:validation-inventory": "node scripts/report-validation-inventory.mjs",
|
|
29
|
+
"check": "npm run test:release",
|
|
29
30
|
"validate:agent": "npm run check",
|
|
30
31
|
"publish:next": "tsx scripts/publish-next.ts",
|
|
31
|
-
"test": "
|
|
32
|
-
"test:
|
|
33
|
-
"test:
|
|
32
|
+
"test": "npm run test:fast",
|
|
33
|
+
"test:fast": "node scripts/run-validation-tier.mjs invariants fast",
|
|
34
|
+
"test:unit": "node scripts/run-validation-tier.mjs fast",
|
|
35
|
+
"test:integration": "node scripts/run-validation-tier.mjs launch-integration pi-engine-conformance release-update structured-runtime-integration",
|
|
36
|
+
"test:scope": "node scripts/run-validation-tier.mjs",
|
|
37
|
+
"test:full": "node scripts/run-validation-tier.mjs full-release",
|
|
34
38
|
"test:release": "node scripts/run-release-gates.mjs",
|
|
35
39
|
"test:terminal-host": "node scripts/run-terminal-host-probe.mjs",
|
|
36
40
|
"test:pi-terminal-parity": "npm run build --silent && node scripts/run-pi-terminal-parity.mjs",
|