@warpgogol/forge 2.8.0 → 2.8.2
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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>forge pinned.validate — checks the working tree against .forge/pinned.yaml.
|
|
4
|
+
Scans git diff (staged or last-commit) for delete/move/modify operations on
|
|
5
|
+
pinned files. Supports --allow-pinned-override for audited escape hatch,
|
|
6
|
+
--json for structured output, and --mode ci for CI integration.</purpose>
|
|
7
|
+
<non-goals>
|
|
8
|
+
<item>Does not create or modify the manifest — use pinned.init.</item>
|
|
9
|
+
<item>Does not install hooks or CI workflows — use pinned.init.</item>
|
|
10
|
+
<item>Does not block archive commands — that is the pre-check utility in pinned-check.ts.</item>
|
|
11
|
+
</non-goals>
|
|
12
|
+
</MODULE_CONTRACT>
|
|
13
|
+
<CHANGE_SUMMARY>
|
|
14
|
+
<item>RFC-0733: initial pinned.validate handler with staged/ci modes, override, audit log, manifest integrity check.</item>
|
|
15
|
+
<item>Gap fix: read FORGE_PINNED_OVERRIDE env var for pre-commit hook override support.</item>
|
|
16
|
+
</CHANGE_SUMMARY>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs/promises";
|
|
20
|
+
import { exec } from "node:child_process";
|
|
21
|
+
import { promisify } from "node:util";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import type {
|
|
24
|
+
ForgeCommandInput,
|
|
25
|
+
ForgeCommandResult,
|
|
26
|
+
ForgeRuntimeContext,
|
|
27
|
+
} from "../../../src/types.ts";
|
|
28
|
+
import {
|
|
29
|
+
loadPinnedManifest,
|
|
30
|
+
isPinned,
|
|
31
|
+
PINNED_MANIFEST_PATH,
|
|
32
|
+
PinnedManifestMalformedError,
|
|
33
|
+
} from "./pinned-check.ts";
|
|
34
|
+
import type {
|
|
35
|
+
PinnedManifest,
|
|
36
|
+
PinnedValidateMode,
|
|
37
|
+
PinnedValidateResult,
|
|
38
|
+
PinnedViolation,
|
|
39
|
+
} from "./pinned-types.ts";
|
|
40
|
+
|
|
41
|
+
const execAsync = promisify(exec);
|
|
42
|
+
const AUDIT_LOG_PATH = path.join(".forge", "pinned-audit.log");
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse git diff --name-status output into file entries with operation types.
|
|
46
|
+
* Git status codes:
|
|
47
|
+
* D = deleted, R = renamed (move), M = modified, A = added, C = copied
|
|
48
|
+
* Format: "R100\tdocs/old.md\tdocs/new.md" or "D\tdocs/file.md"
|
|
49
|
+
*/
|
|
50
|
+
function parseGitNameStatus(
|
|
51
|
+
output: string,
|
|
52
|
+
): Array<{ relPath: string; operation: PinnedViolation["operation"] }> {
|
|
53
|
+
const entries: Array<{ relPath: string; operation: PinnedViolation["operation"] }> = [];
|
|
54
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const parts = line.split("\t");
|
|
58
|
+
if (parts.length < 2) continue;
|
|
59
|
+
|
|
60
|
+
const statusCode = parts[0] ?? "";
|
|
61
|
+
const oldPath = parts[1] ?? "";
|
|
62
|
+
const newPath = parts[2] ?? oldPath;
|
|
63
|
+
|
|
64
|
+
if (statusCode.startsWith("D")) {
|
|
65
|
+
entries.push({ relPath: oldPath, operation: "delete" });
|
|
66
|
+
} else if (statusCode.startsWith("R") || statusCode.startsWith("C")) {
|
|
67
|
+
entries.push({ relPath: oldPath, operation: "move" });
|
|
68
|
+
if (newPath !== oldPath) {
|
|
69
|
+
entries.push({ relPath: newPath, operation: "move" });
|
|
70
|
+
}
|
|
71
|
+
} else if (statusCode.startsWith("M")) {
|
|
72
|
+
entries.push({ relPath: oldPath, operation: "modify" });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check manifest integrity by comparing current manifest against the last-committed version.
|
|
81
|
+
* If entries have been removed, return a tampered violation.
|
|
82
|
+
*/
|
|
83
|
+
async function checkManifestIntegrity(
|
|
84
|
+
repoRoot: string,
|
|
85
|
+
manifest: PinnedManifest,
|
|
86
|
+
): Promise<PinnedViolation | null> {
|
|
87
|
+
try {
|
|
88
|
+
const { stdout } = await execAsync(`git show HEAD:${PINNED_MANIFEST_PATH}`, {
|
|
89
|
+
cwd: repoRoot,
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
});
|
|
92
|
+
// Parse the committed manifest to compare entry paths
|
|
93
|
+
const { parse: parseYaml } = await import("yaml");
|
|
94
|
+
const committedParsed = parseYaml(stdout) as Record<string, unknown> | null;
|
|
95
|
+
if (!committedParsed || !Array.isArray(committedParsed["pinned"])) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const committedEntries = (committedParsed["pinned"] as Array<Record<string, unknown>>).map(
|
|
99
|
+
(e) => String(e["path"] ?? ""),
|
|
100
|
+
);
|
|
101
|
+
const currentPaths = new Set(manifest.pinned.map((e) => e.path));
|
|
102
|
+
|
|
103
|
+
for (const committedPath of committedEntries) {
|
|
104
|
+
if (!currentPaths.has(committedPath)) {
|
|
105
|
+
return {
|
|
106
|
+
path: PINNED_MANIFEST_PATH,
|
|
107
|
+
mode: "freeze",
|
|
108
|
+
operation: "modify",
|
|
109
|
+
reason: `PINNED_MANIFEST_TAMPERED: entry "${committedPath}" was removed from the manifest`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// No committed manifest — nothing to compare against. This is normal for new repos.
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Append an override event to the audit log. Uses fs.appendFile for atomic per-line writes.
|
|
122
|
+
*/
|
|
123
|
+
async function appendAuditLog(
|
|
124
|
+
repoRoot: string,
|
|
125
|
+
entry: {
|
|
126
|
+
timestamp: string;
|
|
127
|
+
path: string;
|
|
128
|
+
mode: string;
|
|
129
|
+
reason: string;
|
|
130
|
+
},
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const logPath = path.join(repoRoot, AUDIT_LOG_PATH);
|
|
133
|
+
const line = JSON.stringify(entry) + "\n";
|
|
134
|
+
await fs.appendFile(logPath, line, "utf8");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function runPinnedValidate(
|
|
138
|
+
input: ForgeCommandInput,
|
|
139
|
+
context: ForgeRuntimeContext,
|
|
140
|
+
): Promise<ForgeCommandResult<PinnedValidateResult>> {
|
|
141
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
142
|
+
|
|
143
|
+
const mode: PinnedValidateMode =
|
|
144
|
+
(input.flags["mode"] as PinnedValidateMode | undefined) ?? "staged";
|
|
145
|
+
const json = input.flags["json"] === true || outputFormat === "json";
|
|
146
|
+
const flagRaw = input.flags["allow-pinned-override"];
|
|
147
|
+
const flagOverrides: string[] = flagRaw
|
|
148
|
+
? Array.isArray(flagRaw)
|
|
149
|
+
? (flagRaw as string[])
|
|
150
|
+
: [flagRaw as string]
|
|
151
|
+
: [];
|
|
152
|
+
|
|
153
|
+
// Gap fix: also read FORGE_PINNED_OVERRIDE env var (comma-separated paths).
|
|
154
|
+
// This allows the pre-commit hook to support overrides without changing the hook script.
|
|
155
|
+
// Operator sets: FORGE_PINNED_OVERRIDE=docs/rfcs/rfc-0076.md git commit
|
|
156
|
+
const envOverride = process.env["FORGE_PINNED_OVERRIDE"];
|
|
157
|
+
const envOverrides: string[] = envOverride
|
|
158
|
+
? envOverride
|
|
159
|
+
.split(",")
|
|
160
|
+
.map((s) => s.trim())
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
: [];
|
|
163
|
+
|
|
164
|
+
const overrides: string[] = [...flagOverrides, ...envOverrides];
|
|
165
|
+
|
|
166
|
+
// Load manifest
|
|
167
|
+
let manifest: PinnedManifest | null;
|
|
168
|
+
try {
|
|
169
|
+
manifest = await loadPinnedManifest(workspaceRoot);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err instanceof PinnedManifestMalformedError) {
|
|
172
|
+
return {
|
|
173
|
+
data: {
|
|
174
|
+
command: "pinned.validate",
|
|
175
|
+
status: "fail",
|
|
176
|
+
violations: [],
|
|
177
|
+
overrides: [],
|
|
178
|
+
},
|
|
179
|
+
exitCode: 2,
|
|
180
|
+
summary: `Error: ${err.message}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!manifest) {
|
|
187
|
+
const msg = "No .forge/pinned.yaml found — pinned-files protection inactive.";
|
|
188
|
+
if (!json) {
|
|
189
|
+
logger.info(msg);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
data: {
|
|
193
|
+
command: "pinned.validate",
|
|
194
|
+
status: "pass",
|
|
195
|
+
violations: [],
|
|
196
|
+
overrides: [],
|
|
197
|
+
},
|
|
198
|
+
summary: msg,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Get git diff
|
|
203
|
+
let diffOutput: string;
|
|
204
|
+
try {
|
|
205
|
+
if (mode === "ci") {
|
|
206
|
+
const { stdout } = await execAsync("git diff --name-status HEAD~1 HEAD", {
|
|
207
|
+
cwd: workspaceRoot,
|
|
208
|
+
encoding: "utf8",
|
|
209
|
+
});
|
|
210
|
+
diffOutput = stdout;
|
|
211
|
+
} else {
|
|
212
|
+
const { stdout } = await execAsync("git diff --cached --name-status", {
|
|
213
|
+
cwd: workspaceRoot,
|
|
214
|
+
encoding: "utf8",
|
|
215
|
+
});
|
|
216
|
+
diffOutput = stdout;
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
diffOutput = "";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const changedFiles = parseGitNameStatus(diffOutput);
|
|
223
|
+
|
|
224
|
+
// Check each changed file against the manifest
|
|
225
|
+
const allViolations: PinnedViolation[] = [];
|
|
226
|
+
for (const { relPath, operation } of changedFiles) {
|
|
227
|
+
const entry = isPinned(manifest, relPath);
|
|
228
|
+
if (entry) {
|
|
229
|
+
// For protect mode, only delete and move are violations — modify is allowed
|
|
230
|
+
if (entry.mode === "protect" && operation === "modify") {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
allViolations.push({
|
|
234
|
+
path: relPath,
|
|
235
|
+
mode: entry.mode,
|
|
236
|
+
operation,
|
|
237
|
+
reason: entry.reason,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check manifest integrity (tamper detection)
|
|
243
|
+
const tamperViolation = await checkManifestIntegrity(workspaceRoot, manifest);
|
|
244
|
+
if (tamperViolation) {
|
|
245
|
+
allViolations.push(tamperViolation);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Filter out overridden violations
|
|
249
|
+
const overrideSet = new Set(overrides);
|
|
250
|
+
const remainingViolations: PinnedViolation[] = [];
|
|
251
|
+
const appliedOverrides: string[] = [];
|
|
252
|
+
|
|
253
|
+
for (const v of allViolations) {
|
|
254
|
+
if (overrideSet.has(v.path)) {
|
|
255
|
+
appliedOverrides.push(v.path);
|
|
256
|
+
// Log to audit file
|
|
257
|
+
await appendAuditLog(workspaceRoot, {
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
path: v.path,
|
|
260
|
+
mode: v.mode,
|
|
261
|
+
reason: v.reason,
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
remainingViolations.push(v);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const status: "pass" | "fail" = remainingViolations.length > 0 ? "fail" : "pass";
|
|
269
|
+
const result: PinnedValidateResult = {
|
|
270
|
+
command: "pinned.validate",
|
|
271
|
+
status,
|
|
272
|
+
violations: remainingViolations,
|
|
273
|
+
overrides: appliedOverrides,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (!json) {
|
|
277
|
+
if (status === "pass") {
|
|
278
|
+
if (appliedOverrides.length > 0) {
|
|
279
|
+
logger.success(
|
|
280
|
+
`pinned.validate: passed with ${appliedOverrides.length} override(s) (logged to audit)`,
|
|
281
|
+
);
|
|
282
|
+
} else {
|
|
283
|
+
logger.success("pinned.validate: no violations");
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
logger.error(`pinned.validate: ${remainingViolations.length} violation(s) found`);
|
|
287
|
+
for (const v of remainingViolations) {
|
|
288
|
+
logger.error(` ${v.operation}: ${v.path} (mode: ${v.mode}) — ${v.reason}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
data: result,
|
|
295
|
+
exitCode: status === "pass" ? 0 : 1,
|
|
296
|
+
summary:
|
|
297
|
+
status === "pass"
|
|
298
|
+
? `No violations${appliedOverrides.length > 0 ? ` (${appliedOverrides.length} override(s) applied)` : ""}`
|
|
299
|
+
: `${remainingViolations.length} violation(s) found`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Shared active profile resolution for lifecycle commands (forge.dev, forge.build, forge.validate). Reads forge.yaml, resolves the active profile id, loads the profile YAML, and returns the StackProfile or null.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement command-specific logic — this is shared resolution only.</item>
|
|
6
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0674: initial shared profile resolution for lifecycle commands.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { parse as parseYaml } from "yaml";
|
|
17
|
+
import { resolveForgeRoot } from "../../../src/config/forge-config.ts";
|
|
18
|
+
import { listStackProfiles, type StackProfile } from "../../../src/profiles/stack-profile.ts";
|
|
19
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../src/types.ts";
|
|
20
|
+
|
|
21
|
+
export interface ResolvedProfile {
|
|
22
|
+
profile: StackProfile;
|
|
23
|
+
profilePath: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readProfileIdFromForgeYaml(workspaceRoot: string): string | undefined {
|
|
27
|
+
const configPath = path.join(workspaceRoot, "forge.yaml");
|
|
28
|
+
if (!fs.existsSync(configPath)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
33
|
+
const parsed = parseYaml(raw) as Record<string, unknown>;
|
|
34
|
+
if (typeof parsed["profile"] === "string") {
|
|
35
|
+
return parsed["profile"];
|
|
36
|
+
}
|
|
37
|
+
const project = parsed["project"] as Record<string, unknown> | undefined;
|
|
38
|
+
if (project && Array.isArray(project["stack"]) && project["stack"].length > 0) {
|
|
39
|
+
return project["stack"][0] as string;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// forge.yaml not parseable — no profile
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LifecycleFlags {
|
|
48
|
+
dryRun: boolean;
|
|
49
|
+
profileIdOverride?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveLifecycleFlags(
|
|
53
|
+
input: ForgeCommandInput,
|
|
54
|
+
context: ForgeRuntimeContext,
|
|
55
|
+
): LifecycleFlags {
|
|
56
|
+
return {
|
|
57
|
+
dryRun: context.dryRun || input.flags["dry-run"] === true,
|
|
58
|
+
profileIdOverride:
|
|
59
|
+
typeof input.flags["profile"] === "string" ? (input.flags["profile"] as string) : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveActiveProfile(
|
|
64
|
+
workspaceRoot: string,
|
|
65
|
+
forgeRoot?: string,
|
|
66
|
+
profileIdOverride?: string,
|
|
67
|
+
): ResolvedProfile | null {
|
|
68
|
+
const root = forgeRoot ?? resolveForgeRoot(workspaceRoot);
|
|
69
|
+
const profiles = listStackProfiles(root);
|
|
70
|
+
|
|
71
|
+
if (profiles.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let profileId: string | undefined = profileIdOverride;
|
|
76
|
+
|
|
77
|
+
if (!profileId) {
|
|
78
|
+
profileId = readProfileIdFromForgeYaml(workspaceRoot);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!profileId) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const profile = profiles.find((p) => p.id === profileId);
|
|
86
|
+
if (!profile) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const profilePath = path.join(root, "profiles", `${profileId}.yaml`);
|
|
91
|
+
return { profile, profilePath };
|
|
92
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>forge.release.prepare — bundle built artifacts into a release package with a manifest. Supports --dry-run, --json, --profile.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement publish logic — that lives in release-publish.ts.</item>
|
|
6
|
+
<item>Do not import from @warpgogol/* in autonomous modules — os/core/ may import from @warpgogol/*.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0680: initial forge.release.prepare handler with manifest generation, artifact hashing, --dry-run.</item>
|
|
11
|
+
<item>RFC-0680 review fix: remove unused artifactId param from findBuiltArtifacts (A-2).</item>
|
|
12
|
+
<item>RFC-0680 review fix: use writeFileIfChanged for manifest write (B-1, DNA-34).</item>
|
|
13
|
+
<item>RFC-0680 review fix: document validationPassed semantics (G-3).</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFile, copyFile, mkdir, stat } from "node:fs/promises";
|
|
18
|
+
import { join, basename } from "node:path";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import type {
|
|
21
|
+
ForgeCommandInput,
|
|
22
|
+
ForgeCommandResult,
|
|
23
|
+
ForgeRuntimeContext,
|
|
24
|
+
} from "../../../src/types.ts";
|
|
25
|
+
import { resolveActiveProfile, resolveLifecycleFlags } from "./profile-resolve.ts";
|
|
26
|
+
import { loadWorkspaceDeps } from "./workspace-deps.ts";
|
|
27
|
+
import { writeFileIfChanged } from "../../../src/utils/fs-idempotent.ts";
|
|
28
|
+
import type { ProfileRelease } from "../../../src/profiles/profile-schema.ts";
|
|
29
|
+
|
|
30
|
+
export interface ReleaseManifest {
|
|
31
|
+
schemaVersion: string;
|
|
32
|
+
releaseId: string;
|
|
33
|
+
profileId: string;
|
|
34
|
+
version: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
artifacts: Array<{
|
|
37
|
+
artifactId: string;
|
|
38
|
+
path: string;
|
|
39
|
+
hash: string;
|
|
40
|
+
size: number;
|
|
41
|
+
deterministic: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
determinismChecked: boolean;
|
|
44
|
+
validationPassed: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ForgeReleasePrepareResult {
|
|
48
|
+
command: "forge.release.prepare";
|
|
49
|
+
profileId: string;
|
|
50
|
+
releaseDir: string;
|
|
51
|
+
manifest: ReleaseManifest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function generateReleaseId(profileId: string, shortHash: string): string {
|
|
55
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
|
|
56
|
+
return `${profileId}-${ts}-${shortHash}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readVersionFromPackageJson(workspaceRoot: string): Promise<string> {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(join(workspaceRoot, "package.json"), "utf8");
|
|
62
|
+
const pkg = JSON.parse(raw) as { version?: string };
|
|
63
|
+
return pkg.version ?? "0.0.0";
|
|
64
|
+
} catch {
|
|
65
|
+
return "0.0.0";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function findBuiltArtifacts(
|
|
70
|
+
workspaceRoot: string,
|
|
71
|
+
extensions: string[],
|
|
72
|
+
produceOutput?: string,
|
|
73
|
+
): Promise<string[]> {
|
|
74
|
+
if (produceOutput) {
|
|
75
|
+
const { collectFiles } = await loadWorkspaceDeps();
|
|
76
|
+
const outputPattern = produceOutput.replace("{composition}", "*");
|
|
77
|
+
const basePattern = basename(outputPattern);
|
|
78
|
+
const isGlob = basePattern.includes("*");
|
|
79
|
+
if (!isGlob) {
|
|
80
|
+
const fullPath = join(workspaceRoot, produceOutput);
|
|
81
|
+
try {
|
|
82
|
+
const _s = await stat(fullPath);
|
|
83
|
+
return [fullPath];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Extract extension from the output pattern (e.g. "*.mp4" → ".mp4")
|
|
89
|
+
const extMatch = basePattern.match(/(\.[^.]+)$/);
|
|
90
|
+
const outputExts = extMatch ? [extMatch[1]] : extensions;
|
|
91
|
+
const distDir = join(workspaceRoot, "dist");
|
|
92
|
+
const files = await collectFiles(distDir, {
|
|
93
|
+
extensions: outputExts,
|
|
94
|
+
ignore: (name) => name.startsWith("-") || name.startsWith("old-") || name === ".DS_Store",
|
|
95
|
+
});
|
|
96
|
+
const regex = new RegExp(
|
|
97
|
+
"^" + basePattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
|
|
98
|
+
);
|
|
99
|
+
return files.filter((f) => regex.test(basename(f)));
|
|
100
|
+
}
|
|
101
|
+
const { collectFiles } = await loadWorkspaceDeps();
|
|
102
|
+
const distDir = join(workspaceRoot, "dist");
|
|
103
|
+
return collectFiles(distDir, {
|
|
104
|
+
extensions,
|
|
105
|
+
ignore: (name) => name.startsWith("-") || name.startsWith("old-") || name === ".DS_Store",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function runReleasePrepare(
|
|
110
|
+
input: ForgeCommandInput,
|
|
111
|
+
context: ForgeRuntimeContext,
|
|
112
|
+
): Promise<ForgeCommandResult<ForgeReleasePrepareResult>> {
|
|
113
|
+
const { workspaceRoot, logger } = context;
|
|
114
|
+
const { dryRun, profileIdOverride } = resolveLifecycleFlags(input, context);
|
|
115
|
+
|
|
116
|
+
const resolved = resolveActiveProfile(workspaceRoot, context.forgeRoot, profileIdOverride);
|
|
117
|
+
if (!resolved) {
|
|
118
|
+
return {
|
|
119
|
+
data: {
|
|
120
|
+
command: "forge.release.prepare",
|
|
121
|
+
profileId: "",
|
|
122
|
+
releaseDir: "",
|
|
123
|
+
manifest: {
|
|
124
|
+
schemaVersion: "1",
|
|
125
|
+
releaseId: "",
|
|
126
|
+
profileId: "",
|
|
127
|
+
version: "",
|
|
128
|
+
createdAt: "",
|
|
129
|
+
artifacts: [],
|
|
130
|
+
determinismChecked: false,
|
|
131
|
+
validationPassed: false,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
exitCode: 1,
|
|
135
|
+
summary: "No active profile found. Set `profile` in forge.yaml or use --profile <id>.",
|
|
136
|
+
nextSteps: [{ action: "Set profile in forge.yaml or use --profile <id>", kind: "required" }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { profile } = resolved;
|
|
141
|
+
|
|
142
|
+
if (!profile.release) {
|
|
143
|
+
return {
|
|
144
|
+
data: {
|
|
145
|
+
command: "forge.release.prepare",
|
|
146
|
+
profileId: profile.id,
|
|
147
|
+
releaseDir: "",
|
|
148
|
+
manifest: {
|
|
149
|
+
schemaVersion: "1",
|
|
150
|
+
releaseId: "",
|
|
151
|
+
profileId: profile.id,
|
|
152
|
+
version: "",
|
|
153
|
+
createdAt: "",
|
|
154
|
+
artifacts: [],
|
|
155
|
+
determinismChecked: false,
|
|
156
|
+
validationPassed: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
exitCode: 1,
|
|
160
|
+
summary: `Profile ${profile.id} does not declare a release configuration.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const releaseConfig: ProfileRelease = profile.release;
|
|
165
|
+
const releaseDir = join(workspaceRoot, releaseConfig.outputDir);
|
|
166
|
+
|
|
167
|
+
const artifacts = profile.artifacts ?? [];
|
|
168
|
+
const includeArtifacts = releaseConfig.includeArtifacts;
|
|
169
|
+
const filteredArtifacts = includeArtifacts
|
|
170
|
+
? artifacts.filter((a) => includeArtifacts.includes(a.id))
|
|
171
|
+
: artifacts.filter((a) => a.produce?.output);
|
|
172
|
+
|
|
173
|
+
const version = await readVersionFromPackageJson(workspaceRoot);
|
|
174
|
+
|
|
175
|
+
const manifestArtifacts: ReleaseManifest["artifacts"] = [];
|
|
176
|
+
let allHashes = "";
|
|
177
|
+
|
|
178
|
+
for (const artifact of filteredArtifacts) {
|
|
179
|
+
const builtFiles = await findBuiltArtifacts(
|
|
180
|
+
workspaceRoot,
|
|
181
|
+
artifact.extensions,
|
|
182
|
+
artifact.produce?.output,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (builtFiles.length === 0) {
|
|
186
|
+
return {
|
|
187
|
+
data: {
|
|
188
|
+
command: "forge.release.prepare",
|
|
189
|
+
profileId: profile.id,
|
|
190
|
+
releaseDir: "",
|
|
191
|
+
manifest: {
|
|
192
|
+
schemaVersion: "1",
|
|
193
|
+
releaseId: "",
|
|
194
|
+
profileId: profile.id,
|
|
195
|
+
version,
|
|
196
|
+
createdAt: "",
|
|
197
|
+
artifacts: [],
|
|
198
|
+
determinismChecked: false,
|
|
199
|
+
validationPassed: false,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
exitCode: 1,
|
|
203
|
+
summary: `Artifact ${artifact.id} has no built output — run \`forge build\` first.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const builtFile of builtFiles) {
|
|
208
|
+
const relPath = builtFile.replace(workspaceRoot + "/", "");
|
|
209
|
+
let hash = "";
|
|
210
|
+
let size = 0;
|
|
211
|
+
|
|
212
|
+
if (!dryRun) {
|
|
213
|
+
const { byteHashFile } = await loadWorkspaceDeps();
|
|
214
|
+
hash = await byteHashFile(builtFile);
|
|
215
|
+
const stats = await stat(builtFile);
|
|
216
|
+
size = stats.size;
|
|
217
|
+
allHashes += hash;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
manifestArtifacts.push({
|
|
221
|
+
artifactId: artifact.id,
|
|
222
|
+
path: relPath,
|
|
223
|
+
hash,
|
|
224
|
+
size,
|
|
225
|
+
deterministic: artifact.determinism?.hashable ?? false,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const shortHash = allHashes
|
|
231
|
+
? createHash("sha256").update(allHashes).digest("hex").slice(0, 8)
|
|
232
|
+
: "dryrun00";
|
|
233
|
+
const releaseId = generateReleaseId(profile.id, shortHash);
|
|
234
|
+
const createdAt = new Date().toISOString();
|
|
235
|
+
|
|
236
|
+
const manifest: ReleaseManifest = {
|
|
237
|
+
schemaVersion: "1",
|
|
238
|
+
releaseId,
|
|
239
|
+
profileId: profile.id,
|
|
240
|
+
version,
|
|
241
|
+
createdAt,
|
|
242
|
+
artifacts: manifestArtifacts,
|
|
243
|
+
determinismChecked: false,
|
|
244
|
+
validationPassed: true,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// G-3 note: validationPassed is set to true because forge.release.prepare
|
|
248
|
+
// does not run validation itself — the operator is expected to run
|
|
249
|
+
// `forge.validate` before `forge.release.prepare`. This mirrors the
|
|
250
|
+
// prepare-then-publish separation: prepare assumes prior validation.
|
|
251
|
+
|
|
252
|
+
if (dryRun) {
|
|
253
|
+
logger.info(`[dry-run] forge.release.prepare — profile: ${profile.id}`);
|
|
254
|
+
logger.info(` releaseId: ${releaseId}`);
|
|
255
|
+
logger.info(` version: ${version}`);
|
|
256
|
+
logger.info(` outputDir: ${releaseConfig.outputDir}`);
|
|
257
|
+
for (const a of manifestArtifacts) {
|
|
258
|
+
logger.info(` artifact: ${a.artifactId} — ${a.path}`);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
data: {
|
|
262
|
+
command: "forge.release.prepare",
|
|
263
|
+
profileId: profile.id,
|
|
264
|
+
releaseDir: releaseConfig.outputDir,
|
|
265
|
+
manifest,
|
|
266
|
+
},
|
|
267
|
+
summary: `[dry-run] Release prepared: ${manifestArtifacts.length} artifact(s), releaseId: ${releaseId}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await mkdir(releaseDir, { recursive: true });
|
|
272
|
+
|
|
273
|
+
for (const a of manifestArtifacts) {
|
|
274
|
+
const srcPath = join(workspaceRoot, a.path);
|
|
275
|
+
const dstPath = join(releaseDir, basename(a.path));
|
|
276
|
+
await copyFile(srcPath, dstPath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const manifestPath = join(releaseDir, releaseConfig.manifestName);
|
|
280
|
+
await writeFileIfChanged(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
281
|
+
|
|
282
|
+
logger.success(` Release prepared: ${manifestArtifacts.length} artifact(s)`);
|
|
283
|
+
logger.info(` Manifest: ${manifestPath}`);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
data: {
|
|
287
|
+
command: "forge.release.prepare",
|
|
288
|
+
profileId: profile.id,
|
|
289
|
+
releaseDir: releaseConfig.outputDir,
|
|
290
|
+
manifest,
|
|
291
|
+
},
|
|
292
|
+
summary: `Release prepared: ${manifestArtifacts.length} artifact(s), releaseId: ${releaseId}`,
|
|
293
|
+
};
|
|
294
|
+
}
|