forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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 +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const { stableStringify } = require('../kernel/evaluators');
|
|
6
|
+
|
|
7
|
+
const SURFACE_KINDS = new Set(['path', 'command', 'schema']);
|
|
8
|
+
|
|
9
|
+
function sha256(value) {
|
|
10
|
+
return `sha256:${crypto.createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hashManifest(manifest) {
|
|
14
|
+
const { manifest_hash: _manifestHash, ...payload } = manifest;
|
|
15
|
+
return sha256(stableStringify(payload));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assert(condition, message) {
|
|
19
|
+
if (!condition) throw new Error(`Invalid risk manifest: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertUniqueIds(entries, label) {
|
|
23
|
+
const ids = new Set();
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
assert(entry && typeof entry.id === 'string' && entry.id.length > 0, `${label} id is required`);
|
|
26
|
+
assert(!ids.has(entry.id), `duplicate ${label} id "${entry.id}"`);
|
|
27
|
+
ids.add(entry.id);
|
|
28
|
+
}
|
|
29
|
+
return ids;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePathValue(value) {
|
|
33
|
+
return value.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function matchesPathRoot(value, root) {
|
|
37
|
+
const normalizedValue = normalizePathValue(value);
|
|
38
|
+
const normalizedRoot = normalizePathValue(root).replace(/\/$/, '');
|
|
39
|
+
return normalizedValue === normalizedRoot || normalizedValue.startsWith(`${normalizedRoot}/`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pathSelectorsOverlap(left, right) {
|
|
43
|
+
const leftIsDirectory = left.endsWith('/');
|
|
44
|
+
const rightIsDirectory = right.endsWith('/');
|
|
45
|
+
if (!leftIsDirectory && !rightIsDirectory) return normalizePathValue(left) === normalizePathValue(right);
|
|
46
|
+
if (leftIsDirectory && matchesPathRoot(right.replace(/\/$/, ''), left)) return true;
|
|
47
|
+
return rightIsDirectory && matchesPathRoot(left.replace(/\/$/, ''), right);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function selectorsOverlap(left, right) {
|
|
51
|
+
if (left.kind !== right.kind) return false;
|
|
52
|
+
if (left.kind === 'path') return pathSelectorsOverlap(left.prefix, right.prefix);
|
|
53
|
+
return left.prefix.startsWith(right.prefix) || right.prefix.startsWith(left.prefix);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateCommandReferences(entries, label, commandIds) {
|
|
57
|
+
const ids = assertUniqueIds(entries, label);
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
assert(Array.isArray(entry.command_ids) && entry.command_ids.length > 0,
|
|
60
|
+
`${label} "${entry.id}" needs command_ids`);
|
|
61
|
+
assert(entry.command_ids.every((id) => commandIds.has(id)), `${label} "${entry.id}" references an unknown command`);
|
|
62
|
+
}
|
|
63
|
+
return ids;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateCommandRegistry(manifest) {
|
|
67
|
+
assert(Array.isArray(manifest.commands) && manifest.commands.length > 0, 'commands must be non-empty');
|
|
68
|
+
assert(Array.isArray(manifest.gates) && manifest.gates.length > 0, 'gates must be non-empty');
|
|
69
|
+
assert(Array.isArray(manifest.lanes) && manifest.lanes.length > 0, 'lanes must be non-empty');
|
|
70
|
+
const commandIds = assertUniqueIds(manifest.commands, 'command');
|
|
71
|
+
for (const command of manifest.commands) {
|
|
72
|
+
assert(typeof command.executable === 'string' && command.executable.length > 0,
|
|
73
|
+
`command "${command.id}" needs an executable`);
|
|
74
|
+
assert(Array.isArray(command.argv) && command.argv.every((argument) => typeof argument === 'string'),
|
|
75
|
+
`command "${command.id}" must use an argv array`);
|
|
76
|
+
assert(!Object.hasOwn(command, 'shell'), `command "${command.id}" cannot declare a shell string`);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
gateIds: validateCommandReferences(manifest.gates, 'gate', commandIds),
|
|
80
|
+
laneIds: validateCommandReferences(manifest.lanes, 'lane', commandIds),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateRisks(risks, gateIds) {
|
|
85
|
+
const riskIds = assertUniqueIds(risks, 'risk');
|
|
86
|
+
for (const risk of risks) {
|
|
87
|
+
assert(/^S[0-3]$/.test(risk.severity), `risk "${risk.id}" has invalid severity`);
|
|
88
|
+
assert(typeof risk.non_quarantinable === 'boolean', `risk "${risk.id}" must declare non_quarantinable`);
|
|
89
|
+
assert(Array.isArray(risk.gate_ids) && risk.gate_ids.length > 0,
|
|
90
|
+
`risk "${risk.id}" must own at least one gate`);
|
|
91
|
+
assert(risk.gate_ids.every((id) => gateIds.has(id)), `risk "${risk.id}" references an unknown gate`);
|
|
92
|
+
}
|
|
93
|
+
return riskIds;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateOwner(owner, riskIds, laneIds) {
|
|
97
|
+
assert(typeof owner.product === 'string' && owner.product.length > 0, `owner "${owner.id}" needs product`);
|
|
98
|
+
assert(typeof owner.package === 'string' && owner.package.length > 0, `owner "${owner.id}" needs package`);
|
|
99
|
+
assert(Array.isArray(owner.risk_ids) && owner.risk_ids.length > 0, `owner "${owner.id}" needs risk_ids`);
|
|
100
|
+
assert(owner.risk_ids.every((id) => riskIds.has(id)), `owner "${owner.id}" references an unknown risk`);
|
|
101
|
+
assert(Array.isArray(owner.canonical_test_ids) && owner.canonical_test_ids.length > 0,
|
|
102
|
+
`owner "${owner.id}" needs canonical_test_ids`);
|
|
103
|
+
assert(Array.isArray(owner.lanes) && owner.lanes.length > 0, `owner "${owner.id}" needs lanes`);
|
|
104
|
+
assert(owner.lanes.every((id) => laneIds.has(id)), `owner "${owner.id}" references an unknown lane`);
|
|
105
|
+
assert(Array.isArray(owner.dependent_routes), `owner "${owner.id}" needs dependent_routes`);
|
|
106
|
+
assert(Array.isArray(owner.platform_runtime_additions), `owner "${owner.id}" needs platform_runtime_additions`);
|
|
107
|
+
assert(Array.isArray(owner.selectors) && owner.selectors.length > 0, `owner "${owner.id}" needs selectors`);
|
|
108
|
+
assert(Array.isArray(owner.package_roots) && owner.package_roots.length > 0,
|
|
109
|
+
`owner "${owner.id}" needs package_roots`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectOwnershipMappings(owners, riskIds, laneIds) {
|
|
113
|
+
assertUniqueIds(owners, 'owner');
|
|
114
|
+
const selectors = [];
|
|
115
|
+
const packageRoots = [];
|
|
116
|
+
for (const owner of owners) {
|
|
117
|
+
validateOwner(owner, riskIds, laneIds);
|
|
118
|
+
for (const selector of owner.selectors) {
|
|
119
|
+
assert(SURFACE_KINDS.has(selector.kind), `owner "${owner.id}" has invalid selector kind`);
|
|
120
|
+
assert(typeof selector.prefix === 'string' && selector.prefix.length > 0,
|
|
121
|
+
`owner "${owner.id}" has an empty selector prefix`);
|
|
122
|
+
selectors.push({ ...selector, owner_id: owner.id });
|
|
123
|
+
}
|
|
124
|
+
for (const prefix of owner.package_roots) {
|
|
125
|
+
assert(typeof prefix === 'string' && prefix.length > 0, `owner "${owner.id}" has an empty package root`);
|
|
126
|
+
packageRoots.push({ prefix, owner_id: owner.id });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { selectors, packageRoots };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function assertOwnershipMappingsDoNotOverlap(selectors, packageRoots) {
|
|
133
|
+
for (let index = 0; index < selectors.length; index += 1) {
|
|
134
|
+
for (let peer = index + 1; peer < selectors.length; peer += 1) {
|
|
135
|
+
assert(!selectorsOverlap(selectors[index], selectors[peer]),
|
|
136
|
+
`selectors overlap between "${selectors[index].owner_id}" and "${selectors[peer].owner_id}"`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (let index = 0; index < packageRoots.length; index += 1) {
|
|
140
|
+
for (let peer = index + 1; peer < packageRoots.length; peer += 1) {
|
|
141
|
+
const left = packageRoots[index];
|
|
142
|
+
const right = packageRoots[peer];
|
|
143
|
+
assert(!(matchesPathRoot(left.prefix, right.prefix) || matchesPathRoot(right.prefix, left.prefix)),
|
|
144
|
+
`package roots overlap between "${left.owner_id}" and "${right.owner_id}"`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const REQUIRED_CONSERVATIVE_GATES = ['G0', 'G1', 'G3', 'G6'];
|
|
150
|
+
|
|
151
|
+
function validateFallback(manifest, laneIds, gateIds) {
|
|
152
|
+
assert(manifest.unknown_owner_fallback?.lane === 'repository-baseline',
|
|
153
|
+
'unknown_owner_fallback must select repository-baseline');
|
|
154
|
+
assert(laneIds.has(manifest.unknown_owner_fallback?.lane), 'unknown_owner_fallback references an unknown lane');
|
|
155
|
+
assert(laneIds.has('contract-baseline') && laneIds.has('affected-platform-baseline'),
|
|
156
|
+
'conservative fallback lanes must be executable');
|
|
157
|
+
assert(Array.isArray(manifest.unknown_owner_fallback.required_gates)
|
|
158
|
+
&& manifest.unknown_owner_fallback.required_gates.length > 0,
|
|
159
|
+
'unknown_owner_fallback must declare required_gates');
|
|
160
|
+
for (const gateId of REQUIRED_CONSERVATIVE_GATES) {
|
|
161
|
+
assert(gateIds.has(gateId), `conservative fallback references unknown gate ${gateId}`);
|
|
162
|
+
assert(manifest.unknown_owner_fallback.required_gates.includes(gateId),
|
|
163
|
+
`conservative fallback must require ${gateId}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateManifest(manifest, { verifyHash = true } = {}) {
|
|
168
|
+
assert(manifest && typeof manifest === 'object' && !Array.isArray(manifest), 'root must be an object');
|
|
169
|
+
assert(manifest.schema_id === 'forge.validation.risk-manifest.v1', 'unsupported schema_id');
|
|
170
|
+
assert(Number.isInteger(manifest.revision) && manifest.revision > 0, 'revision must be a positive integer');
|
|
171
|
+
assert(/^sha256:[a-f0-9]{64}$/.test(manifest.source_hash), 'source_hash must be a SHA-256 digest');
|
|
172
|
+
assert(typeof manifest.generator_version === 'string' && manifest.generator_version.length > 0,
|
|
173
|
+
'generator_version is required');
|
|
174
|
+
assert(Array.isArray(manifest.risks) && manifest.risks.length > 0, 'risks must be non-empty');
|
|
175
|
+
assert(Array.isArray(manifest.owners) && manifest.owners.length > 0, 'owners must be non-empty');
|
|
176
|
+
|
|
177
|
+
const { gateIds, laneIds } = validateCommandRegistry(manifest);
|
|
178
|
+
const riskIds = validateRisks(manifest.risks, gateIds);
|
|
179
|
+
const mappings = collectOwnershipMappings(manifest.owners, riskIds, laneIds);
|
|
180
|
+
assertOwnershipMappingsDoNotOverlap(mappings.selectors, mappings.packageRoots);
|
|
181
|
+
validateFallback(manifest, laneIds, gateIds);
|
|
182
|
+
|
|
183
|
+
if (verifyHash) {
|
|
184
|
+
assert(/^sha256:[a-f0-9]{64}$/.test(manifest.manifest_hash), 'manifest_hash must be a SHA-256 digest');
|
|
185
|
+
assert(manifest.manifest_hash === hashManifest(manifest), 'manifest hash mismatch');
|
|
186
|
+
}
|
|
187
|
+
return manifest;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function loadRiskManifest(input) {
|
|
191
|
+
const manifest = typeof input === 'string'
|
|
192
|
+
? JSON.parse(fs.readFileSync(input, 'utf8'))
|
|
193
|
+
: structuredClone(input);
|
|
194
|
+
return validateManifest(manifest);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeSurface(surface) {
|
|
198
|
+
assert(surface && SURFACE_KINDS.has(surface.kind), 'changed surface has an invalid kind');
|
|
199
|
+
assert(typeof surface.value === 'string' && surface.value.trim().length > 0,
|
|
200
|
+
'changed surface value is required');
|
|
201
|
+
const value = surface.kind === 'path'
|
|
202
|
+
? normalizePathValue(surface.value.trim())
|
|
203
|
+
: surface.value.trim();
|
|
204
|
+
return { kind: surface.kind, value };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function uniqueSorted(values) {
|
|
208
|
+
return [...new Set(values)].sort(compareText);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function compareText(left, right) {
|
|
212
|
+
if (left === right) return 0;
|
|
213
|
+
return left < right ? -1 : 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeChangedSurfaces(changedSurfaces) {
|
|
217
|
+
assert(Array.isArray(changedSurfaces), 'changedSurfaces must be an array');
|
|
218
|
+
const keyed = new Map();
|
|
219
|
+
for (const surface of changedSurfaces) {
|
|
220
|
+
const normalized = normalizeSurface(surface);
|
|
221
|
+
keyed.set(`${normalized.kind}\0${normalized.value}`, normalized);
|
|
222
|
+
}
|
|
223
|
+
return [...keyed.values()].sort((left, right) => compareText(left.kind, right.kind)
|
|
224
|
+
|| compareText(left.value, right.value));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function ownerForExactSurface(manifest, surface) {
|
|
228
|
+
for (const owner of manifest.owners) {
|
|
229
|
+
const selector = owner.selectors.find((candidate) => candidate.kind === surface.kind
|
|
230
|
+
&& (surface.kind === 'path'
|
|
231
|
+
? (candidate.prefix.endsWith('/')
|
|
232
|
+
? matchesPathRoot(surface.value, candidate.prefix)
|
|
233
|
+
: surface.value === normalizePathValue(candidate.prefix))
|
|
234
|
+
: surface.value.startsWith(candidate.prefix)));
|
|
235
|
+
if (selector) return { owner, selector };
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ownerForKnownPackage(manifest, surface) {
|
|
241
|
+
if (surface.kind !== 'path') return undefined;
|
|
242
|
+
return manifest.owners.find((owner) => owner.package_roots.some((root) => matchesPathRoot(surface.value, root)));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function ownerRisks(manifest, owners) {
|
|
246
|
+
const ids = new Set(owners.flatMap((owner) => owner.risk_ids));
|
|
247
|
+
return manifest.risks.filter((risk) => ids.has(risk.id));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveCommands(manifest, gateIds, laneIds) {
|
|
251
|
+
const selectedDefinitions = [
|
|
252
|
+
...manifest.gates.filter((gate) => gateIds.includes(gate.id)),
|
|
253
|
+
...manifest.lanes.filter((lane) => laneIds.includes(lane.id)),
|
|
254
|
+
];
|
|
255
|
+
const commandIds = uniqueSorted(selectedDefinitions.flatMap((definition) => definition.command_ids));
|
|
256
|
+
const commandsById = new Map(manifest.commands.map((command) => [command.id, command]));
|
|
257
|
+
return commandIds.map((id) => {
|
|
258
|
+
const command = commandsById.get(id);
|
|
259
|
+
return { id: command.id, executable: command.executable, argv: [...command.argv] };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function selectValidation({ manifest, changedSurfaces }) {
|
|
264
|
+
validateManifest(manifest);
|
|
265
|
+
const surfaces = normalizeChangedSurfaces(changedSurfaces);
|
|
266
|
+
const selections = surfaces.map((surface) => ({
|
|
267
|
+
surface,
|
|
268
|
+
exact: ownerForExactSurface(manifest, surface),
|
|
269
|
+
package: ownerForKnownPackage(manifest, surface),
|
|
270
|
+
}));
|
|
271
|
+
const unowned = selections.filter((selection) => !selection.exact && !selection.package)
|
|
272
|
+
.map((selection) => selection.surface);
|
|
273
|
+
const ambiguous = selections.filter((selection) => !selection.exact && selection.package);
|
|
274
|
+
const exactOwners = selections.map((selection) => selection.exact?.owner).filter(Boolean);
|
|
275
|
+
const packageOwners = ambiguous.map((selection) => selection.package);
|
|
276
|
+
const owners = [...new Map([...exactOwners, ...packageOwners].map((owner) => [owner.id, owner])).values()]
|
|
277
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
278
|
+
const risks = ownerRisks(manifest, owners);
|
|
279
|
+
|
|
280
|
+
let status = 'exact';
|
|
281
|
+
let targetedPassAllowed = surfaces.length > 0;
|
|
282
|
+
let lanes = owners.flatMap((owner) => owner.lanes);
|
|
283
|
+
let requiredGates = risks.flatMap((risk) => risk.gate_ids);
|
|
284
|
+
|
|
285
|
+
if (unowned.length > 0 || surfaces.length === 0) {
|
|
286
|
+
status = 'repository-baseline';
|
|
287
|
+
targetedPassAllowed = false;
|
|
288
|
+
lanes = [manifest.unknown_owner_fallback.lane];
|
|
289
|
+
requiredGates = manifest.unknown_owner_fallback.required_gates;
|
|
290
|
+
} else if (ambiguous.length > 0) {
|
|
291
|
+
status = 'conservative-package';
|
|
292
|
+
targetedPassAllowed = false;
|
|
293
|
+
lanes.push('contract-baseline', 'affected-platform-baseline');
|
|
294
|
+
requiredGates.push('G0', 'G1', 'G3', 'G6');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const selectedLanes = uniqueSorted(lanes);
|
|
298
|
+
const selectedGates = uniqueSorted(requiredGates);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
schema_id: 'forge.validation.selection.v1',
|
|
302
|
+
manifest_revision: manifest.revision,
|
|
303
|
+
manifest_digest: manifest.manifest_hash,
|
|
304
|
+
status,
|
|
305
|
+
targeted_pass_allowed: targetedPassAllowed,
|
|
306
|
+
changed_surfaces: surfaces,
|
|
307
|
+
unowned_surfaces: unowned,
|
|
308
|
+
matched_selectors: selections.filter((selection) => selection.exact).map((selection) => ({
|
|
309
|
+
surface: selection.surface,
|
|
310
|
+
owner_id: selection.exact.owner.id,
|
|
311
|
+
selector: selection.exact.selector,
|
|
312
|
+
})),
|
|
313
|
+
owner_ids: owners.map((owner) => owner.id),
|
|
314
|
+
owner_selections: owners.map((owner) => ({
|
|
315
|
+
owner_id: owner.id,
|
|
316
|
+
product: owner.product,
|
|
317
|
+
package: owner.package,
|
|
318
|
+
risk_ids: uniqueSorted(owner.risk_ids),
|
|
319
|
+
lanes: uniqueSorted(owner.lanes),
|
|
320
|
+
canonical_test_ids: uniqueSorted(owner.canonical_test_ids),
|
|
321
|
+
dependent_routes: uniqueSorted(owner.dependent_routes),
|
|
322
|
+
platform_runtime_additions: uniqueSorted(owner.platform_runtime_additions),
|
|
323
|
+
})),
|
|
324
|
+
risk_ids: uniqueSorted(risks.map((risk) => risk.id)),
|
|
325
|
+
required_gates: selectedGates,
|
|
326
|
+
lanes: selectedLanes,
|
|
327
|
+
dependent_routes: uniqueSorted(owners.flatMap((owner) => owner.dependent_routes)),
|
|
328
|
+
platform_runtime_additions: uniqueSorted(owners.flatMap((owner) => owner.platform_runtime_additions)),
|
|
329
|
+
test_ids: uniqueSorted(owners.flatMap((owner) => owner.canonical_test_ids)),
|
|
330
|
+
commands: resolveCommands(manifest, selectedGates, selectedLanes),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
hashManifest,
|
|
336
|
+
loadRiskManifest,
|
|
337
|
+
selectValidation,
|
|
338
|
+
validateManifest,
|
|
339
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const { execFileSync } = require('node:child_process');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { resolveWorktreeScope } = require('./protected-state-authority');
|
|
9
|
+
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
const RECEIPT_TTL_MS = 60 * 60 * 1000;
|
|
12
|
+
const FUTURE_SKEW_MS = 60 * 1000;
|
|
13
|
+
const RUNNER_ID = 'forge-full-suite-v1';
|
|
14
|
+
|
|
15
|
+
function git(projectRoot, args, deps = {}) {
|
|
16
|
+
return (deps.execFileSync || execFileSync)('git', args, {
|
|
17
|
+
cwd: projectRoot,
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
20
|
+
}).trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveGitRoot(projectRoot, deps = {}) {
|
|
24
|
+
return path.resolve(git(projectRoot, ['rev-parse', '--show-toplevel'], deps));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveReceiptPath(projectRoot, deps = {}) {
|
|
28
|
+
const root = resolveGitRoot(projectRoot, deps);
|
|
29
|
+
const gitPath = git(root, ['rev-parse', '--git-path', 'forge/validation-receipt.json'], deps);
|
|
30
|
+
return path.resolve(root, gitPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveKeyPath(deps = {}) {
|
|
34
|
+
return path.join(deps.homeDir || os.homedir(), '.forge', 'validation-receipt.key');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runtimeIdentity(deps = {}) {
|
|
38
|
+
return deps.runtimeIdentity
|
|
39
|
+
|| [process.platform, process.arch, process.versions.node, process.versions.bun || 'node'].join(':');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commandIdentity(command, projectRoot, env, run) {
|
|
43
|
+
const version = run(command, ['--version'], {
|
|
44
|
+
cwd: projectRoot,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
env,
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
}).trim();
|
|
49
|
+
const located = path.isAbsolute(command)
|
|
50
|
+
? fs.realpathSync.native(command)
|
|
51
|
+
: run(process.platform === 'win32' ? 'where.exe' : 'which', [command], {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
env,
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
}).trim().split(/\r?\n/)[0];
|
|
56
|
+
return `${process.platform === 'win32' ? located.toLowerCase() : located}:${version}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function testRuntimeIdentity(projectRoot, deps = {}) {
|
|
60
|
+
if (deps.testRuntimeIdentity) return deps.testRuntimeIdentity;
|
|
61
|
+
const env = deps.env || process.env;
|
|
62
|
+
const run = deps.runtimeExecFileSync || execFileSync;
|
|
63
|
+
return [env.BUN_EXE || 'bun', 'node']
|
|
64
|
+
.map(command => commandIdentity(command, projectRoot, env, run))
|
|
65
|
+
.join('|');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function captureState(projectRoot, deps = {}) {
|
|
69
|
+
const root = resolveGitRoot(projectRoot, deps);
|
|
70
|
+
return {
|
|
71
|
+
worktree: resolveWorktreeScope(root, deps),
|
|
72
|
+
head: git(root, ['rev-parse', 'HEAD'], deps),
|
|
73
|
+
clean: git(root, ['status', '--porcelain=v1', '--untracked-files=all'], deps) === '',
|
|
74
|
+
runtime: runtimeIdentity(deps),
|
|
75
|
+
testRuntime: testRuntimeIdentity(root, deps),
|
|
76
|
+
runner: RUNNER_ID,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sameState(left, right) {
|
|
81
|
+
return left?.clean === true
|
|
82
|
+
&& right?.clean === true
|
|
83
|
+
&& left.worktree === right.worktree
|
|
84
|
+
&& left.head === right.head
|
|
85
|
+
&& left.runtime === right.runtime
|
|
86
|
+
&& left.testRuntime === right.testRuntime
|
|
87
|
+
&& left.runner === right.runner;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validationIsComplete(result) {
|
|
91
|
+
const checks = result?.checks || {};
|
|
92
|
+
const tests = checks.tests || {};
|
|
93
|
+
return result?.success === true
|
|
94
|
+
&& checks.conflictMarkers?.success === true
|
|
95
|
+
&& checks.typeCheck?.success === true
|
|
96
|
+
&& (checks.typeCheck.skipped !== true || checks.typeCheck.notConfigured === true)
|
|
97
|
+
&& checks.lint?.success === true && checks.lint?.skipped !== true
|
|
98
|
+
&& checks.security?.success === true && checks.security?.skipped !== true
|
|
99
|
+
&& tests.success === true && tests.skipped !== true
|
|
100
|
+
&& tests.fullSuite === true && tests.testsFound === true
|
|
101
|
+
&& tests.total > 0 && tests.failed === 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readOrCreateKey(deps = {}) {
|
|
105
|
+
const keyPath = resolveKeyPath(deps);
|
|
106
|
+
fs.mkdirSync(path.dirname(keyPath), { recursive: true });
|
|
107
|
+
try {
|
|
108
|
+
fs.writeFileSync(keyPath, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== 'EEXIST') throw error;
|
|
111
|
+
}
|
|
112
|
+
return fs.readFileSync(keyPath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sign(payload, deps = {}) {
|
|
116
|
+
return crypto.createHmac('sha256', readOrCreateKey(deps))
|
|
117
|
+
.update(JSON.stringify(payload))
|
|
118
|
+
.digest('hex');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function signatureMatches(payload, signature, deps = {}) {
|
|
122
|
+
if (!/^[a-f0-9]{64}$/.test(signature || '')) return false;
|
|
123
|
+
const expected = Buffer.from(sign(payload, deps), 'hex');
|
|
124
|
+
const actual = Buffer.from(signature, 'hex');
|
|
125
|
+
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeAtomic(filePath, content) {
|
|
129
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
130
|
+
const temporary = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
131
|
+
try {
|
|
132
|
+
fs.writeFileSync(temporary, content, { flag: 'wx', mode: 0o600 });
|
|
133
|
+
fs.renameSync(temporary, filePath);
|
|
134
|
+
} finally {
|
|
135
|
+
fs.rmSync(temporary, { force: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function beginValidation(projectRoot, deps = {}) {
|
|
140
|
+
try {
|
|
141
|
+
fs.rmSync(resolveReceiptPath(projectRoot, deps), { force: true });
|
|
142
|
+
const state = captureState(projectRoot, deps);
|
|
143
|
+
return state.clean ? state : null;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function completeValidation(projectRoot, snapshot, result, deps = {}) {
|
|
150
|
+
try {
|
|
151
|
+
if (!snapshot || !validationIsComplete(result)) return false;
|
|
152
|
+
const current = captureState(projectRoot, deps);
|
|
153
|
+
if (!sameState(snapshot, current)) return false;
|
|
154
|
+
const issuedAt = (deps.now || Date.now)();
|
|
155
|
+
const payload = {
|
|
156
|
+
schemaVersion: SCHEMA_VERSION,
|
|
157
|
+
...current,
|
|
158
|
+
issuedAt,
|
|
159
|
+
expiresAt: issuedAt + RECEIPT_TTL_MS,
|
|
160
|
+
gates: ['conflictMarkers', 'typeCheck', 'lint', 'security', 'tests'],
|
|
161
|
+
};
|
|
162
|
+
writeAtomic(resolveReceiptPath(projectRoot, deps), JSON.stringify({ payload, signature: sign(payload, deps) }));
|
|
163
|
+
return true;
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function verifyValidationReceipt(projectRoot, deps = {}) {
|
|
170
|
+
try {
|
|
171
|
+
const receipt = JSON.parse(fs.readFileSync(resolveReceiptPath(projectRoot, deps), 'utf8'));
|
|
172
|
+
if (receipt?.payload?.schemaVersion !== SCHEMA_VERSION) return { valid: false, reason: 'schema' };
|
|
173
|
+
if (!signatureMatches(receipt.payload, receipt.signature, deps)) return { valid: false, reason: 'signature' };
|
|
174
|
+
const now = (deps.now || Date.now)();
|
|
175
|
+
if (receipt.payload.issuedAt > now + FUTURE_SKEW_MS) return { valid: false, reason: 'future' };
|
|
176
|
+
if (receipt.payload.expiresAt < now) return { valid: false, reason: 'expired' };
|
|
177
|
+
if (!sameState(receipt.payload, captureState(projectRoot, deps))) return { valid: false, reason: 'state' };
|
|
178
|
+
return { valid: true, head: receipt.payload.head };
|
|
179
|
+
} catch {
|
|
180
|
+
return { valid: false, reason: 'missing-or-invalid' };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
beginValidation,
|
|
186
|
+
completeValidation,
|
|
187
|
+
resolveReceiptPath,
|
|
188
|
+
validationIsComplete,
|
|
189
|
+
verifyValidationReceipt,
|
|
190
|
+
};
|
|
@@ -10,6 +10,11 @@ const {
|
|
|
10
10
|
readWorkflowState,
|
|
11
11
|
} = require('./state');
|
|
12
12
|
const { loadState, WORKFLOW_STATE_FILENAME } = require('./state-manager');
|
|
13
|
+
const {
|
|
14
|
+
findPlanWorkFolder,
|
|
15
|
+
readPlanSnapshot,
|
|
16
|
+
reconcilePlanAuthority,
|
|
17
|
+
} = require('./plan-authority');
|
|
13
18
|
|
|
14
19
|
const STATELESS_ENTRY_STAGES = new Set(['plan', 'dev', 'validate', 'verify']);
|
|
15
20
|
|
|
@@ -204,6 +209,42 @@ async function resolveKernelContext({ kernelDriver, activeIssueId, branch, proje
|
|
|
204
209
|
return { driver, issueId };
|
|
205
210
|
}
|
|
206
211
|
|
|
212
|
+
function linkedWorkFolder(driver, issueId, branch) {
|
|
213
|
+
if (!driver || typeof driver.listWorktrees !== 'function') return null;
|
|
214
|
+
try {
|
|
215
|
+
const row = (driver.listWorktrees() || []).find((entry) => entry?.issue_id === issueId
|
|
216
|
+
&& (!branch || entry.branch === branch) && (entry.state === 'active' || entry.state == null));
|
|
217
|
+
return row?.work_folder || null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function planRepairCommand(branch, issueId) {
|
|
224
|
+
const normalized = String(branch || '').replace(/^[^/]+\//, '').split('-').filter(Boolean).join(' ');
|
|
225
|
+
return `forge plan ${JSON.stringify(normalized || 'feature')} --issue ${issueId}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function enforcePlanAuthorityForDev({ driver, issueId, projectRoot, branch }) {
|
|
229
|
+
if (!projectRoot || !driver || typeof driver.loadPlanSnapshot !== 'function') return;
|
|
230
|
+
const snapshot = readPlanSnapshot(driver, issueId);
|
|
231
|
+
const slug = String(branch || '').replace(/^[^/]+\//, '');
|
|
232
|
+
const workFolder = linkedWorkFolder(driver, issueId, branch)
|
|
233
|
+
|| (!snapshot ? findPlanWorkFolder(projectRoot, slug) : null);
|
|
234
|
+
// Pre-0.1.0 issues without either a snapshot or a linked repository folder are
|
|
235
|
+
// intentionally unmanaged for compatibility. Once either side exists, the
|
|
236
|
+
// authority contract is strict and all missing/drift cases fail closed.
|
|
237
|
+
if (!snapshot && !workFolder) return;
|
|
238
|
+
reconcilePlanAuthority({
|
|
239
|
+
driver,
|
|
240
|
+
issueId,
|
|
241
|
+
projectRoot,
|
|
242
|
+
workFolder,
|
|
243
|
+
mode: 'dev',
|
|
244
|
+
repairCommand: planRepairCommand(branch, issueId),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
207
248
|
function getOverrideInput(flags = {}) {
|
|
208
249
|
if (Object.hasOwn(flags, 'overrideStage')) {
|
|
209
250
|
return flags.overrideStage;
|
|
@@ -416,6 +457,9 @@ async function enforceStageEntry({
|
|
|
416
457
|
// gate below requires the predecessor to be done, not merely entered).
|
|
417
458
|
const finish = (result) => {
|
|
418
459
|
if (kernelActive) {
|
|
460
|
+
if (stageId === 'dev') {
|
|
461
|
+
enforcePlanAuthorityForDev({ driver, issueId, projectRoot, branch: branch || detectBranchName(projectRoot) });
|
|
462
|
+
}
|
|
419
463
|
recordStageRunSafe(driver, issueId, stageId, 'start', warn);
|
|
420
464
|
invalidateDownstreamStages(driver, issueId, stageId, warn);
|
|
421
465
|
result.recordCompletion = () => recordStageRunSafe(driver, issueId, stageId, 'complete', warn);
|