@ornexus/neocortex 4.60.22 → 4.60.24
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/sbom.cdx.json +24 -24
- package/install.js +67 -10
- package/install.ps1 +39 -8
- package/install.sh +18 -6
- package/package.json +9 -3
- package/packages/client/dist/adapters/adapter-registry.d.ts +2 -2
- package/packages/client/dist/adapters/adapter-registry.js +1 -1
- package/packages/client/dist/adapters/grok-adapter.d.ts +11 -0
- package/packages/client/dist/adapters/grok-adapter.js +2 -0
- package/packages/client/dist/adapters/index.d.ts +1 -0
- package/packages/client/dist/adapters/index.js +1 -1
- package/packages/client/dist/adapters/platform-detector.d.ts +1 -1
- package/packages/client/dist/adapters/platform-detector.js +1 -1
- package/packages/client/dist/agent/refresh-stubs.js +2 -2
- package/packages/client/dist/cli.js +21 -2
- package/packages/client/dist/commands/invoke.d.ts +39 -26
- package/packages/client/dist/commands/invoke.js +56 -50
- package/packages/client/dist/commands/story-checkpoint.d.ts +15 -0
- package/packages/client/dist/commands/story-checkpoint.js +1 -0
- package/packages/client/dist/config.js +1 -1
- package/packages/client/dist/continuity/invoke-hooks.js +1 -1
- package/packages/client/dist/fs/atomic-state-replace.d.ts +16 -0
- package/packages/client/dist/fs/atomic-state-replace.js +1 -0
- package/packages/client/dist/generated/command-registry.generated.d.ts +51 -51
- package/packages/client/dist/generated/command-registry.generated.js +1 -1
- package/packages/client/dist/git/direct-yolo-checkpoint.d.ts +67 -0
- package/packages/client/dist/git/direct-yolo-checkpoint.js +3 -0
- package/packages/client/dist/git/git-lifecycle-store.d.ts +198 -0
- package/packages/client/dist/git/git-lifecycle-store.js +4 -0
- package/packages/client/dist/git/secure-filesystem-boundary.d.ts +26 -0
- package/packages/client/dist/git/secure-filesystem-boundary.js +123 -0
- package/packages/client/dist/graph-retrieval/pre-command-hook.d.ts +11 -0
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -1
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -1
- package/packages/client/dist/index.d.ts +2 -0
- package/packages/client/dist/index.js +1 -1
- package/packages/client/dist/invocation/invoke-input.d.ts +9 -0
- package/packages/client/dist/invocation/invoke-input.js +1 -0
- package/packages/client/dist/memory/shared-project-memory-types.d.ts +29 -2
- package/packages/client/dist/memory/shared-project-memory-types.js +2 -2
- package/packages/client/dist/migrations/project-identity-capability.d.ts +13 -0
- package/packages/client/dist/migrations/project-identity-capability.js +1 -0
- package/packages/client/dist/migrations/project-identity.d.ts +41 -0
- package/packages/client/dist/migrations/project-identity.js +7 -0
- package/packages/client/dist/policy/subagent-approval-policy.js +1 -1
- package/packages/client/dist/runner/headless/continuous-story-workspace.d.ts +131 -0
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +58 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.d.ts +23 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -0
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.d.ts +15 -0
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +28 -0
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.d.ts +12 -0
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +8 -0
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.d.ts +134 -0
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +2 -0
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +1 -1
- package/packages/client/dist/runner/headless/run-yolo.js +1 -1
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +7 -2
- package/packages/client/dist/runner/headless/run-yoloop.js +8 -1
- package/packages/client/dist/runner/headless/types.d.ts +1 -1
- package/packages/client/dist/runner/headless/types.js +2 -2
- package/packages/client/dist/state/invocation-shape.d.ts +9 -0
- package/packages/client/dist/state/invocation-shape.js +1 -0
- package/packages/client/dist/state/project-state-snapshot.d.ts +9 -3
- package/packages/client/dist/state/project-state-snapshot.js +1 -1
- package/packages/client/dist/state/state-json-repair.d.ts +9 -0
- package/packages/client/dist/state/state-json-repair.js +3 -3
- package/packages/client/dist/state/structural-dag-projection.d.ts +24 -0
- package/packages/client/dist/state/structural-dag-projection.js +1 -0
- package/packages/client/dist/tier/tier-aware-client.js +1 -1
- package/packages/client/dist/yoloop/invoke-hooks.d.ts +13 -10
- package/packages/client/dist/yoloop/invoke-hooks.js +8 -5
- package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1
- package/packages/shared/dist/generated/command-registry.generated.js +64 -65
- package/packages/shared/dist/generated/invocation-schema.generated.js +16 -24
- package/packages/shared/dist/invocation/index.d.ts +6 -2
- package/packages/shared/dist/invocation/index.js +5 -0
- package/packages/shared/dist/invocation/input-envelope.d.ts +37 -0
- package/packages/shared/dist/invocation/input-envelope.js +98 -0
- package/packages/shared/dist/invocation/project-identity-migration.d.ts +28 -0
- package/packages/shared/dist/invocation/project-identity-migration.js +83 -0
- package/packages/shared/dist/invocation/public-diagnostics.d.ts +87 -0
- package/packages/shared/dist/invocation/public-diagnostics.js +181 -0
- package/packages/shared/dist/invocation/state-projection.d.ts +21 -0
- package/packages/shared/dist/invocation/state-projection.js +1 -0
- package/packages/shared/dist/invocation/strict-effect-receipt.d.ts +12 -0
- package/packages/shared/dist/invocation/strict-effect-receipt.js +43 -0
- package/packages/shared/dist/yoloop/epic-terminal-git.js +227 -0
- package/packages/shared/dist/yoloop/planning-graph.js +119 -0
- package/packages/shared/dist/yoloop/public-story-write-set.js +27 -0
- package/packages/shared/dist/yoloop/wave-decision-receipt.js +19 -0
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/neocortex.md +18 -0
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +21 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +20 -1
- package/targets-stubs/cursor/agent.md +20 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/gemini-cli/gemini.md +18 -0
- package/targets-stubs/grok/README.md +13 -0
- package/targets-stubs/grok/SKILL.md +50 -0
- package/targets-stubs/grok/config-mcp.toml +22 -0
- package/targets-stubs/grok/install-grok.sh +31 -0
- package/targets-stubs/kimi/neocortex.md +18 -0
- package/targets-stubs/openclaw/SKILL.md +18 -0
- package/targets-stubs/opencode/neocortex-root.md +20 -1
- package/targets-stubs/vscode/neocortex.agent.md +20 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { InvokeInputEvidenceV1 } from './input-envelope.js';
|
|
2
|
+
import type { InvocationShapeV2 } from './state-projection.js';
|
|
3
|
+
export declare const INVOCATION_PUBLIC_DIAGNOSTIC_VERSION: 1;
|
|
4
|
+
export declare const INVOCATION_DIGEST_PREFIX_LENGTH = 12;
|
|
5
|
+
export type InvocationPublicDiagnosticV1 = {
|
|
6
|
+
readonly version: 1;
|
|
7
|
+
readonly event: 'invoke.input';
|
|
8
|
+
readonly source: InvokeInputEvidenceV1['source'];
|
|
9
|
+
readonly bytesBucket: string;
|
|
10
|
+
readonly newlineStyle: InvokeInputEvidenceV1['newlineStyle'];
|
|
11
|
+
readonly digestPrefix: string;
|
|
12
|
+
readonly outcome: 'accepted' | 'rejected';
|
|
13
|
+
readonly reasonCode?: string;
|
|
14
|
+
readonly publicSafe: true;
|
|
15
|
+
} | {
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
readonly event: 'invoke.projection';
|
|
18
|
+
readonly shape: InvocationShapeV2;
|
|
19
|
+
readonly bytesBucket: string;
|
|
20
|
+
readonly includedEpics: number;
|
|
21
|
+
readonly includedStories: number;
|
|
22
|
+
readonly omittedEpics: number;
|
|
23
|
+
readonly omittedStories: number;
|
|
24
|
+
readonly nodes: number;
|
|
25
|
+
readonly edges: number;
|
|
26
|
+
readonly cycles: number;
|
|
27
|
+
readonly outcome: 'accepted' | 'rejected';
|
|
28
|
+
readonly reasonCode?: string;
|
|
29
|
+
readonly publicSafe: true;
|
|
30
|
+
} | {
|
|
31
|
+
readonly version: 1;
|
|
32
|
+
readonly event: 'invoke.project-identity';
|
|
33
|
+
readonly phase: 'prepare' | 'seed' | 'remote-acknowledged' | 'finalize' | 'reconcile';
|
|
34
|
+
readonly outcome: 'prepared' | 'seeded' | 'already-seeded' | 'finalized' | 'rejected' | 'retry';
|
|
35
|
+
readonly floorBucket: string;
|
|
36
|
+
readonly scopeDigestPrefix: string;
|
|
37
|
+
readonly capability: 'project-identity-migration-v1';
|
|
38
|
+
readonly publicSafe: true;
|
|
39
|
+
} | {
|
|
40
|
+
readonly version: 1;
|
|
41
|
+
readonly event: 'invoke.diagnose';
|
|
42
|
+
readonly authority: 'strict-read-only';
|
|
43
|
+
readonly receiptStatus: 'verified' | 'rejected';
|
|
44
|
+
readonly attemptedEffectCount: number;
|
|
45
|
+
readonly suppressedEffectCount: number;
|
|
46
|
+
readonly publicSafe: true;
|
|
47
|
+
} | {
|
|
48
|
+
readonly version: 1;
|
|
49
|
+
readonly event: 'invoke.trace';
|
|
50
|
+
readonly stages: readonly ('admission' | 'input' | 'shape' | 'projection' | 'size-guard' | 'send')[];
|
|
51
|
+
readonly outcome: 'accepted' | 'rejected';
|
|
52
|
+
readonly reasonCode?: string;
|
|
53
|
+
readonly publicSafe: true;
|
|
54
|
+
};
|
|
55
|
+
export declare function invocationByteBucket(bytes: number): string;
|
|
56
|
+
export declare function invocationFloorBucket(floor: number): string;
|
|
57
|
+
export declare function invocationDigestPrefix(domain: string, value: string): string;
|
|
58
|
+
export declare function buildInputPublicDiagnostic(evidence: InvokeInputEvidenceV1, outcome?: 'accepted' | 'rejected', reasonCode?: string): InvocationPublicDiagnosticV1;
|
|
59
|
+
export declare function buildProjectionPublicDiagnostic(input: {
|
|
60
|
+
readonly shape: InvocationShapeV2;
|
|
61
|
+
readonly bytes: number;
|
|
62
|
+
readonly includedEpics: number;
|
|
63
|
+
readonly includedStories: number;
|
|
64
|
+
readonly omittedEpics: number;
|
|
65
|
+
readonly omittedStories: number;
|
|
66
|
+
readonly nodes?: number;
|
|
67
|
+
readonly edges?: number;
|
|
68
|
+
readonly cycles?: number;
|
|
69
|
+
readonly outcome?: 'accepted' | 'rejected';
|
|
70
|
+
readonly reasonCode?: string;
|
|
71
|
+
}): InvocationPublicDiagnosticV1;
|
|
72
|
+
export declare function buildProjectIdentityPublicDiagnostic(input: {
|
|
73
|
+
readonly phase: Extract<InvocationPublicDiagnosticV1, {
|
|
74
|
+
event: 'invoke.project-identity';
|
|
75
|
+
}>['phase'];
|
|
76
|
+
readonly outcome: Extract<InvocationPublicDiagnosticV1, {
|
|
77
|
+
event: 'invoke.project-identity';
|
|
78
|
+
}>['outcome'];
|
|
79
|
+
readonly floor: number;
|
|
80
|
+
readonly licenseId: string;
|
|
81
|
+
readonly projectId: string;
|
|
82
|
+
}): InvocationPublicDiagnosticV1;
|
|
83
|
+
export declare function buildDiagnosePublicDiagnostic(receiptStatus: 'verified' | 'rejected', attemptedEffectCount: number, suppressedEffectCount: number): InvocationPublicDiagnosticV1;
|
|
84
|
+
export declare function buildInvokeTracePublicDiagnostic(stages: Extract<InvocationPublicDiagnosticV1, {
|
|
85
|
+
event: 'invoke.trace';
|
|
86
|
+
}>['stages'], outcome: 'accepted' | 'rejected', reasonCode?: string): InvocationPublicDiagnosticV1;
|
|
87
|
+
export declare function assertInvocationPublicDiagnosticV1(value: unknown): asserts value is InvocationPublicDiagnosticV1;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export const INVOCATION_PUBLIC_DIAGNOSTIC_VERSION = 1;
|
|
3
|
+
export const INVOCATION_DIGEST_PREFIX_LENGTH = 12;
|
|
4
|
+
const SAFE_REASON = /^[a-z0-9][a-z0-9._-]{0,79}$/;
|
|
5
|
+
const BYTE_BUCKETS = new Set(['0', '1-1023', '1k-16k', '16k-64k', '64k-128k', '128k-192k', '192k-256k', '256k+']);
|
|
6
|
+
const FLOOR_BUCKETS = new Set(['0-9', '10-99', '100-999', '1k-10k', '10k+']);
|
|
7
|
+
const INVOCATION_SHAPES = new Set([
|
|
8
|
+
'yolo-story', 'yolo-inline', 'yoloop-target', 'yoloop-session', 'yoloop-discovery',
|
|
9
|
+
]);
|
|
10
|
+
const TRACE_STAGES = new Set(['admission', 'input', 'shape', 'projection', 'size-guard', 'send']);
|
|
11
|
+
function boundedCount(value) {
|
|
12
|
+
return Number.isSafeInteger(value) && value >= 0 ? Math.min(value, 1_000_000) : 0;
|
|
13
|
+
}
|
|
14
|
+
export function invocationByteBucket(bytes) {
|
|
15
|
+
const safe = boundedCount(bytes);
|
|
16
|
+
if (safe === 0)
|
|
17
|
+
return '0';
|
|
18
|
+
if (safe < 1024)
|
|
19
|
+
return '1-1023';
|
|
20
|
+
if (safe < 16 * 1024)
|
|
21
|
+
return '1k-16k';
|
|
22
|
+
if (safe < 64 * 1024)
|
|
23
|
+
return '16k-64k';
|
|
24
|
+
if (safe < 128 * 1024)
|
|
25
|
+
return '64k-128k';
|
|
26
|
+
if (safe < 192 * 1024)
|
|
27
|
+
return '128k-192k';
|
|
28
|
+
if (safe < 256 * 1024)
|
|
29
|
+
return '192k-256k';
|
|
30
|
+
return '256k+';
|
|
31
|
+
}
|
|
32
|
+
export function invocationFloorBucket(floor) {
|
|
33
|
+
const safe = boundedCount(floor);
|
|
34
|
+
if (safe < 10)
|
|
35
|
+
return '0-9';
|
|
36
|
+
if (safe < 100)
|
|
37
|
+
return '10-99';
|
|
38
|
+
if (safe < 1_000)
|
|
39
|
+
return '100-999';
|
|
40
|
+
if (safe < 10_000)
|
|
41
|
+
return '1k-10k';
|
|
42
|
+
return '10k+';
|
|
43
|
+
}
|
|
44
|
+
export function invocationDigestPrefix(domain, value) {
|
|
45
|
+
return createHash('sha256').update(`${domain}\0${value}`, 'utf8').digest('hex').slice(0, INVOCATION_DIGEST_PREFIX_LENGTH);
|
|
46
|
+
}
|
|
47
|
+
export function buildInputPublicDiagnostic(evidence, outcome = 'accepted', reasonCode) {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
version: 1,
|
|
50
|
+
event: 'invoke.input',
|
|
51
|
+
source: evidence.source,
|
|
52
|
+
bytesBucket: invocationByteBucket(evidence.byteLength),
|
|
53
|
+
newlineStyle: evidence.newlineStyle,
|
|
54
|
+
digestPrefix: evidence.sha256.slice(0, INVOCATION_DIGEST_PREFIX_LENGTH),
|
|
55
|
+
outcome,
|
|
56
|
+
...(reasonCode && SAFE_REASON.test(reasonCode) ? { reasonCode } : {}),
|
|
57
|
+
publicSafe: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function buildProjectionPublicDiagnostic(input) {
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
version: 1,
|
|
63
|
+
event: 'invoke.projection',
|
|
64
|
+
shape: input.shape,
|
|
65
|
+
bytesBucket: invocationByteBucket(input.bytes),
|
|
66
|
+
includedEpics: boundedCount(input.includedEpics),
|
|
67
|
+
includedStories: boundedCount(input.includedStories),
|
|
68
|
+
omittedEpics: boundedCount(input.omittedEpics),
|
|
69
|
+
omittedStories: boundedCount(input.omittedStories),
|
|
70
|
+
nodes: boundedCount(input.nodes ?? input.includedStories),
|
|
71
|
+
edges: boundedCount(input.edges ?? 0),
|
|
72
|
+
cycles: boundedCount(input.cycles ?? 0),
|
|
73
|
+
outcome: input.outcome ?? 'accepted',
|
|
74
|
+
...(input.reasonCode && SAFE_REASON.test(input.reasonCode) ? { reasonCode: input.reasonCode } : {}),
|
|
75
|
+
publicSafe: true,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function buildProjectIdentityPublicDiagnostic(input) {
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
version: 1,
|
|
81
|
+
event: 'invoke.project-identity',
|
|
82
|
+
phase: input.phase,
|
|
83
|
+
outcome: input.outcome,
|
|
84
|
+
floorBucket: invocationFloorBucket(input.floor),
|
|
85
|
+
scopeDigestPrefix: invocationDigestPrefix('nx-project-scope-v1', `${input.licenseId}\0${input.projectId}`),
|
|
86
|
+
capability: 'project-identity-migration-v1',
|
|
87
|
+
publicSafe: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function buildDiagnosePublicDiagnostic(receiptStatus, attemptedEffectCount, suppressedEffectCount) {
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
version: 1,
|
|
93
|
+
event: 'invoke.diagnose',
|
|
94
|
+
authority: 'strict-read-only',
|
|
95
|
+
receiptStatus,
|
|
96
|
+
attemptedEffectCount: boundedCount(attemptedEffectCount),
|
|
97
|
+
suppressedEffectCount: boundedCount(suppressedEffectCount),
|
|
98
|
+
publicSafe: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function buildInvokeTracePublicDiagnostic(stages, outcome, reasonCode) {
|
|
102
|
+
const allowed = new Set(['admission', 'input', 'shape', 'projection', 'size-guard', 'send']);
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
version: 1,
|
|
105
|
+
event: 'invoke.trace',
|
|
106
|
+
stages: Object.freeze([...new Set(stages.filter((stage) => allowed.has(stage)))]),
|
|
107
|
+
outcome,
|
|
108
|
+
...(reasonCode && SAFE_REASON.test(reasonCode) ? { reasonCode } : {}),
|
|
109
|
+
publicSafe: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function assertInvocationPublicDiagnosticV1(value) {
|
|
113
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
114
|
+
throw new Error('INVOCATION_PUBLIC_DIAGNOSTIC_INVALID');
|
|
115
|
+
const record = value;
|
|
116
|
+
const serialized = JSON.stringify(value);
|
|
117
|
+
if (record.version !== 1 || record.publicSafe !== true || serialized.length > 4096
|
|
118
|
+
|| /(rawArgs|stateSnapshot|projectRoot|licenseId|projectId|payload|preview|secret|password|privateUrl)/i.test(serialized)) {
|
|
119
|
+
throw new Error('INVOCATION_PUBLIC_DIAGNOSTIC_INVALID');
|
|
120
|
+
}
|
|
121
|
+
const exactKeys = (required, optional = []) => {
|
|
122
|
+
const allowed = new Set([...required, ...optional]);
|
|
123
|
+
const keys = Object.keys(record);
|
|
124
|
+
return required.every((key) => keys.includes(key)) && keys.every((key) => allowed.has(key));
|
|
125
|
+
};
|
|
126
|
+
const validReason = record.reasonCode === undefined
|
|
127
|
+
|| (typeof record.reasonCode === 'string' && SAFE_REASON.test(record.reasonCode));
|
|
128
|
+
const validCount = (candidate) => Number.isSafeInteger(candidate)
|
|
129
|
+
&& candidate >= 0 && candidate <= 1_000_000;
|
|
130
|
+
const validOutcome = record.outcome === 'accepted' || record.outcome === 'rejected';
|
|
131
|
+
let valid = false;
|
|
132
|
+
if (record.event === 'invoke.input') {
|
|
133
|
+
valid = exactKeys(['version', 'event', 'source', 'bytesBucket', 'newlineStyle', 'digestPrefix', 'outcome', 'publicSafe'], ['reasonCode'])
|
|
134
|
+
&& ['args', 'stdin', 'args-file'].includes(String(record.source))
|
|
135
|
+
&& BYTE_BUCKETS.has(String(record.bytesBucket))
|
|
136
|
+
&& ['none', 'lf', 'crlf', 'mixed'].includes(String(record.newlineStyle))
|
|
137
|
+
&& typeof record.digestPrefix === 'string' && /^[a-f0-9]{12}$/.test(record.digestPrefix)
|
|
138
|
+
&& validOutcome && validReason;
|
|
139
|
+
}
|
|
140
|
+
else if (record.event === 'invoke.projection') {
|
|
141
|
+
valid = exactKeys([
|
|
142
|
+
'version', 'event', 'shape', 'bytesBucket', 'includedEpics', 'includedStories',
|
|
143
|
+
'omittedEpics', 'omittedStories', 'nodes', 'edges', 'cycles', 'outcome', 'publicSafe',
|
|
144
|
+
], ['reasonCode'])
|
|
145
|
+
&& INVOCATION_SHAPES.has(record.shape)
|
|
146
|
+
&& BYTE_BUCKETS.has(String(record.bytesBucket))
|
|
147
|
+
&& ['includedEpics', 'includedStories', 'omittedEpics', 'omittedStories', 'nodes', 'edges', 'cycles']
|
|
148
|
+
.every((key) => validCount(record[key]))
|
|
149
|
+
&& validOutcome && validReason;
|
|
150
|
+
}
|
|
151
|
+
else if (record.event === 'invoke.project-identity') {
|
|
152
|
+
valid = exactKeys([
|
|
153
|
+
'version', 'event', 'phase', 'outcome', 'floorBucket', 'scopeDigestPrefix', 'capability', 'publicSafe',
|
|
154
|
+
])
|
|
155
|
+
&& ['prepare', 'seed', 'remote-acknowledged', 'finalize', 'reconcile'].includes(String(record.phase))
|
|
156
|
+
&& ['prepared', 'seeded', 'already-seeded', 'finalized', 'rejected', 'retry'].includes(String(record.outcome))
|
|
157
|
+
&& FLOOR_BUCKETS.has(String(record.floorBucket))
|
|
158
|
+
&& typeof record.scopeDigestPrefix === 'string' && /^[a-f0-9]{12}$/.test(record.scopeDigestPrefix)
|
|
159
|
+
&& record.capability === 'project-identity-migration-v1';
|
|
160
|
+
}
|
|
161
|
+
else if (record.event === 'invoke.diagnose') {
|
|
162
|
+
valid = exactKeys([
|
|
163
|
+
'version', 'event', 'authority', 'receiptStatus', 'attemptedEffectCount',
|
|
164
|
+
'suppressedEffectCount', 'publicSafe',
|
|
165
|
+
])
|
|
166
|
+
&& record.authority === 'strict-read-only'
|
|
167
|
+
&& (record.receiptStatus === 'verified' || record.receiptStatus === 'rejected')
|
|
168
|
+
&& validCount(record.attemptedEffectCount)
|
|
169
|
+
&& validCount(record.suppressedEffectCount);
|
|
170
|
+
}
|
|
171
|
+
else if (record.event === 'invoke.trace') {
|
|
172
|
+
valid = exactKeys(['version', 'event', 'stages', 'outcome', 'publicSafe'], ['reasonCode'])
|
|
173
|
+
&& Array.isArray(record.stages)
|
|
174
|
+
&& record.stages.length <= TRACE_STAGES.size
|
|
175
|
+
&& new Set(record.stages).size === record.stages.length
|
|
176
|
+
&& record.stages.every((stage) => typeof stage === 'string' && TRACE_STAGES.has(stage))
|
|
177
|
+
&& validOutcome && validReason;
|
|
178
|
+
}
|
|
179
|
+
if (!valid)
|
|
180
|
+
throw new Error('INVOCATION_PUBLIC_DIAGNOSTIC_INVALID');
|
|
181
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const STATE_PROJECTION_VERSION: 2;
|
|
2
|
+
export type InvocationShapeV2 = 'yolo-story' | 'yolo-inline' | 'yoloop-target' | 'yoloop-session' | 'yoloop-discovery';
|
|
3
|
+
export type StructuralProjectionErrorCodeV2 = 'STRUCTURAL_TARGET_NOT_FOUND' | 'STRUCTURAL_DEPENDENCY_MISSING' | 'STRUCTURAL_DEPENDENCY_CYCLE' | 'STRUCTURAL_CONTEXT_TOO_LARGE';
|
|
4
|
+
export interface StateProjectionCountsV2 {
|
|
5
|
+
readonly epics: number;
|
|
6
|
+
readonly stories: number;
|
|
7
|
+
readonly edges: number;
|
|
8
|
+
}
|
|
9
|
+
export type StateProjectionResultV2<TSnapshot> = {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly version: typeof STATE_PROJECTION_VERSION;
|
|
12
|
+
readonly shape: InvocationShapeV2;
|
|
13
|
+
readonly snapshot: TSnapshot;
|
|
14
|
+
readonly included: StateProjectionCountsV2;
|
|
15
|
+
readonly omitted: Omit<StateProjectionCountsV2, 'edges'>;
|
|
16
|
+
readonly cycleCount: number;
|
|
17
|
+
} | {
|
|
18
|
+
readonly ok: false;
|
|
19
|
+
readonly code: StructuralProjectionErrorCodeV2;
|
|
20
|
+
readonly publicDetails: Readonly<Record<string, string | number>>;
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const STATE_PROJECTION_VERSION = 2;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const STRICT_DIAGNOSE_RECEIPT_VERSION: 1;
|
|
2
|
+
export interface StrictDiagnoseReceiptV1 {
|
|
3
|
+
readonly version: typeof STRICT_DIAGNOSE_RECEIPT_VERSION;
|
|
4
|
+
readonly authority: 'strict-read-only';
|
|
5
|
+
readonly projectBusinessMutation: false;
|
|
6
|
+
readonly attemptedEffects: readonly string[];
|
|
7
|
+
readonly suppressedEffects: readonly string[];
|
|
8
|
+
readonly inputDigest: string;
|
|
9
|
+
readonly responseDigest: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function assertStrictDiagnoseReceiptV1(value: unknown): asserts value is StrictDiagnoseReceiptV1;
|
|
12
|
+
export declare function verifyStrictDiagnoseReceiptV1(value: unknown, rawArgs: string, responseInstructions: string): asserts value is StrictDiagnoseReceiptV1;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SHA256_HEX_PATTERN } from './project-identity-migration.js';
|
|
2
|
+
import { invokeInputSha256 } from './input-envelope.js';
|
|
3
|
+
export const STRICT_DIAGNOSE_RECEIPT_VERSION = 1;
|
|
4
|
+
const validEffects = (value) => Array.isArray(value)
|
|
5
|
+
&& value.length <= 128
|
|
6
|
+
&& value.every((entry) => typeof entry === 'string' && entry.length > 0 && entry.length <= 128);
|
|
7
|
+
export function assertStrictDiagnoseReceiptV1(value) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
9
|
+
throw new Error('STRICT_DIAGNOSE_RECEIPT_INVALID');
|
|
10
|
+
}
|
|
11
|
+
const candidate = value;
|
|
12
|
+
if (candidate.version !== STRICT_DIAGNOSE_RECEIPT_VERSION) {
|
|
13
|
+
throw new Error('STRICT_DIAGNOSE_RECEIPT_VERSION_UNSUPPORTED');
|
|
14
|
+
}
|
|
15
|
+
if (candidate.projectBusinessMutation !== false) {
|
|
16
|
+
throw new Error('STRICT_DIAGNOSE_RECEIPT_MUTATION_CONFLICT');
|
|
17
|
+
}
|
|
18
|
+
const expectedKeys = [
|
|
19
|
+
'attemptedEffects', 'authority', 'inputDigest', 'projectBusinessMutation',
|
|
20
|
+
'responseDigest', 'suppressedEffects', 'version',
|
|
21
|
+
];
|
|
22
|
+
const actualKeys = Object.keys(candidate).sort();
|
|
23
|
+
if (actualKeys.length !== expectedKeys.length
|
|
24
|
+
|| actualKeys.some((key, index) => key !== expectedKeys[index])
|
|
25
|
+
|| candidate.authority !== 'strict-read-only'
|
|
26
|
+
|| !validEffects(candidate.attemptedEffects)
|
|
27
|
+
|| !validEffects(candidate.suppressedEffects)
|
|
28
|
+
|| typeof candidate.inputDigest !== 'string'
|
|
29
|
+
|| !SHA256_HEX_PATTERN.test(candidate.inputDigest)
|
|
30
|
+
|| typeof candidate.responseDigest !== 'string'
|
|
31
|
+
|| !SHA256_HEX_PATTERN.test(candidate.responseDigest)) {
|
|
32
|
+
throw new Error('STRICT_DIAGNOSE_RECEIPT_INVALID');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function verifyStrictDiagnoseReceiptV1(value, rawArgs, responseInstructions) {
|
|
36
|
+
assertStrictDiagnoseReceiptV1(value);
|
|
37
|
+
const receipt = value;
|
|
38
|
+
if (receipt.inputDigest !== invokeInputSha256(rawArgs)
|
|
39
|
+
|| receipt.responseDigest !== invokeInputSha256(responseInstructions)
|
|
40
|
+
|| JSON.stringify(receipt.attemptedEffects) !== JSON.stringify(receipt.suppressedEffects)) {
|
|
41
|
+
throw new Error('STRICT_DIAGNOSE_RECEIPT_BINDING_CONFLICT');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/** Public contracts and pure safety decisions for epic-owned terminal Git. */
|
|
2
|
+
export const EPIC_TERMINAL_GIT_REASON_CODES = Object.freeze({
|
|
3
|
+
READY: 'epic-terminal-git-ready',
|
|
4
|
+
ALREADY_COMPLETE: 'epic-terminal-git-already-complete',
|
|
5
|
+
ORIGIN_MISSING: 'epic-origin-branch-missing',
|
|
6
|
+
EXECUTION_MISSING: 'epic-execution-branch-missing',
|
|
7
|
+
SAME_BRANCH: 'epic-origin-equals-execution',
|
|
8
|
+
LOCAL_GATES_REQUIRED: 'epic-local-gates-required',
|
|
9
|
+
STORY_CHECKPOINTS_REQUIRED: 'epic-story-checkpoints-required',
|
|
10
|
+
LEASE_ACTIVE: 'epic-writer-lease-active',
|
|
11
|
+
HEAD_MISMATCH: 'epic-execution-head-mismatch',
|
|
12
|
+
GRAPH_REVISION_MISMATCH: 'epic-graph-revision-mismatch',
|
|
13
|
+
MERGE_CONFLICT: 'epic-terminal-merge-conflict',
|
|
14
|
+
REMOTE_POLICY_DENIED: 'epic-terminal-remote-policy-denied',
|
|
15
|
+
RETRY_ELIGIBLE: 'epic-terminal-retry-eligible',
|
|
16
|
+
RECOVERED_PHYSICAL_PUSH: 'epic-terminal-recovered-physical-push',
|
|
17
|
+
OWNERSHIP_UNPROVEN: 'git-resource-ownership-unproven',
|
|
18
|
+
RESOURCE_ACTIVE: 'git-resource-active',
|
|
19
|
+
RESOURCE_ORIGIN: 'git-resource-is-origin',
|
|
20
|
+
RESOURCE_CURRENT: 'git-resource-is-current',
|
|
21
|
+
RESOURCE_PROTECTED: 'git-resource-protected',
|
|
22
|
+
UNIQUE_COMMITS: 'git-resource-has-unique-commits',
|
|
23
|
+
INTEGRATION_UNPROVEN: 'git-resource-integration-unproven',
|
|
24
|
+
SAFE_TO_RECONCILE: 'git-resource-safe-to-reconcile',
|
|
25
|
+
ALREADY_RECONCILED: 'git-resource-already-reconciled',
|
|
26
|
+
AGGREGATE_QA_REQUIRED: 'epic-aggregate-qa-receipt-required',
|
|
27
|
+
AGGREGATE_QA_STALE: 'epic-aggregate-qa-receipt-stale',
|
|
28
|
+
ORIGIN_BINDING_REQUIRED: 'epic-origin-binding-required',
|
|
29
|
+
PROVIDER_TRUTH_UNKNOWN: 'git-resource-provider-truth-unknown',
|
|
30
|
+
WORKTREE_ACTIVE: 'git-resource-worktree-active',
|
|
31
|
+
RESOURCE_RACED: 'git-resource-expected-oid-raced',
|
|
32
|
+
REMOTE_DELETE_CAS_UNAVAILABLE: 'git-resource-remote-delete-cas-unavailable',
|
|
33
|
+
ORIGIN_CAPTURE_WINDOW_MISSED: 'epic-origin-binding-capture-window-missed',
|
|
34
|
+
ORIGIN_PHYSICAL_DRIFT: 'epic-origin-binding-physical-drift',
|
|
35
|
+
PHYSICAL_REVALIDATION_REQUIRED: 'epic-terminal-physical-revalidation-required',
|
|
36
|
+
PHYSICAL_ANCESTRY_UNPROVEN: 'epic-terminal-physical-ancestry-unproven',
|
|
37
|
+
PR_STRICT_BASE_GUARD_REQUIRED: 'epic-terminal-pr-strict-base-guard-required',
|
|
38
|
+
PR_BASE_GUARD_UNVERIFIABLE: 'epic-terminal-pr-base-guard-unverifiable',
|
|
39
|
+
PR_MERGE_QUEUE_LOCAL_GATE_REQUIRED: 'epic-terminal-pr-merge-queue-local-gate-required',
|
|
40
|
+
});
|
|
41
|
+
export function validateEpicTerminalGitDirective(directive, actual) {
|
|
42
|
+
const reasons = [];
|
|
43
|
+
if (!nonEmpty(directive.originBranch))
|
|
44
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.ORIGIN_MISSING);
|
|
45
|
+
if (!nonEmpty(directive.executionBranch))
|
|
46
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.EXECUTION_MISSING);
|
|
47
|
+
if (directive.originBranch === directive.executionBranch)
|
|
48
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.SAME_BRANCH);
|
|
49
|
+
if (directive.localGates?.passed !== true || directive.localGates.receiptRefs.length === 0) {
|
|
50
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.LOCAL_GATES_REQUIRED);
|
|
51
|
+
}
|
|
52
|
+
const aggregate = actual.aggregateQaReceipt;
|
|
53
|
+
const directedAggregate = directive.localGates?.aggregateQaReceipt;
|
|
54
|
+
if (!aggregate
|
|
55
|
+
|| aggregate.schemaVersion !== 1
|
|
56
|
+
|| aggregate.evidenceVersion !== 'p205-execution-v2'
|
|
57
|
+
|| aggregate.producer !== 'local-yolo-tdd'
|
|
58
|
+
|| aggregate.contract !== 'P205-CI-02'
|
|
59
|
+
|| aggregate.command !== 'node scripts/test-root.mjs --gate complete'
|
|
60
|
+
|| aggregate.verdict !== 'passed'
|
|
61
|
+
|| aggregate.executionHeadSha !== actual.executionHeadSha
|
|
62
|
+
|| aggregate.executionHeadSha !== directive.expectedExecutionHeadSha
|
|
63
|
+
|| aggregate.graphRevision !== directive.graphRevision
|
|
64
|
+
|| aggregate.receiptRef.length === 0
|
|
65
|
+
|| aggregate.gateIds.length === 0
|
|
66
|
+
|| !isHash(aggregate.manifestDigest)
|
|
67
|
+
|| !isHash(aggregate.resultDigest)
|
|
68
|
+
|| !directedAggregate
|
|
69
|
+
|| directedAggregate.executionHeadSha !== aggregate.executionHeadSha
|
|
70
|
+
|| directedAggregate.graphRevision !== aggregate.graphRevision
|
|
71
|
+
|| directedAggregate.checkedAt !== aggregate.checkedAt
|
|
72
|
+
|| directedAggregate.receiptRef !== aggregate.receiptRef
|
|
73
|
+
|| directedAggregate.manifestDigest !== aggregate.manifestDigest
|
|
74
|
+
|| directedAggregate.resultDigest !== aggregate.resultDigest
|
|
75
|
+
|| !sameSet(directedAggregate.gateIds, aggregate.gateIds)
|
|
76
|
+
|| !directive.localGates.receiptRefs.includes(aggregate.receiptRef)) {
|
|
77
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.AGGREGATE_QA_REQUIRED);
|
|
78
|
+
}
|
|
79
|
+
else if (!Number.isFinite(Date.parse(aggregate.checkedAt)) || actual.nowIso && (Date.parse(actual.nowIso) - Date.parse(aggregate.checkedAt) > 30 * 60 * 1000
|
|
80
|
+
|| Date.parse(aggregate.checkedAt) - Date.parse(actual.nowIso) > 5 * 60 * 1000)) {
|
|
81
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.AGGREGATE_QA_STALE);
|
|
82
|
+
}
|
|
83
|
+
const originBinding = actual.originBinding;
|
|
84
|
+
if (!originBinding
|
|
85
|
+
|| originBinding.schemaVersion !== 1
|
|
86
|
+
|| originBinding.capturedFromCurrentCheckout !== true
|
|
87
|
+
|| originBinding.originBranch !== directive.originBranch
|
|
88
|
+
|| originBinding.executionBranch !== directive.executionBranch
|
|
89
|
+
|| !isSha(originBinding.originHeadSha)
|
|
90
|
+
|| !Number.isFinite(Date.parse(originBinding.capturedAt))) {
|
|
91
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.ORIGIN_BINDING_REQUIRED);
|
|
92
|
+
}
|
|
93
|
+
if (actual.writerLeaseActive)
|
|
94
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.LEASE_ACTIVE);
|
|
95
|
+
if (!sameSet(directive.storyCheckpointIds, actual.acceptedCheckpointIds)) {
|
|
96
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.STORY_CHECKPOINTS_REQUIRED);
|
|
97
|
+
}
|
|
98
|
+
if (!isSha(directive.expectedExecutionHeadSha) || directive.expectedExecutionHeadSha !== actual.executionHeadSha) {
|
|
99
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.HEAD_MISMATCH);
|
|
100
|
+
}
|
|
101
|
+
if (directive.graphRevision && directive.graphRevision !== actual.graphRevision) {
|
|
102
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.GRAPH_REVISION_MISMATCH);
|
|
103
|
+
}
|
|
104
|
+
if (!/^[a-f0-9]{64}$/i.test(directive.graphRevision))
|
|
105
|
+
reasons.push(EPIC_TERMINAL_GIT_REASON_CODES.GRAPH_REVISION_MISMATCH);
|
|
106
|
+
if (actual.existingReceipt?.physicalStatus === 'pushed'
|
|
107
|
+
&& actual.existingReceipt.idempotencyKey === directive.idempotencyKey
|
|
108
|
+
&& actual.existingReceipt.executionHeadSha === directive.expectedExecutionHeadSha) {
|
|
109
|
+
return { ready: true, reasonCodes: [EPIC_TERMINAL_GIT_REASON_CODES.ALREADY_COMPLETE] };
|
|
110
|
+
}
|
|
111
|
+
return reasons.length === 0
|
|
112
|
+
? { ready: true, reasonCodes: [EPIC_TERMINAL_GIT_REASON_CODES.READY] }
|
|
113
|
+
: { ready: false, reasonCodes: unique(reasons) };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Fail-closed ownership and integration classifier. A canonical-looking name is
|
|
117
|
+
* never sufficient: state/receipt proof plus convention and provider metadata
|
|
118
|
+
* or PR head/base evidence are required before any destructive action.
|
|
119
|
+
*/
|
|
120
|
+
export function classifyGitReconciliationCandidate(candidate) {
|
|
121
|
+
const evidence = unique(candidate.ownershipEvidence);
|
|
122
|
+
const hasDurableProof = evidence.includes('state_reference') || evidence.includes('terminal_receipt');
|
|
123
|
+
const hasConvention = evidence.includes('canonical_convention');
|
|
124
|
+
const hasCorroboration = candidate.kind === 'pull_request'
|
|
125
|
+
? evidence.includes('pr_head_base_match')
|
|
126
|
+
: evidence.includes('managed_metadata');
|
|
127
|
+
const managed = hasDurableProof && hasConvention && hasCorroboration;
|
|
128
|
+
const preserve = (...reasonCodes) => Object.freeze({
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
resourceId: candidate.resourceId,
|
|
131
|
+
action: 'preserve',
|
|
132
|
+
managed,
|
|
133
|
+
reasonCodes: Object.freeze(unique(reasonCodes)),
|
|
134
|
+
evidence: Object.freeze(evidence),
|
|
135
|
+
publicSafe: true,
|
|
136
|
+
});
|
|
137
|
+
if (candidate.lifecycle === 'already_absent' && candidate.providerVerified) {
|
|
138
|
+
return Object.freeze({ schemaVersion: 1, resourceId: candidate.resourceId, action: 'no_op', managed, reasonCodes: [EPIC_TERMINAL_GIT_REASON_CODES.ALREADY_RECONCILED], evidence, publicSafe: true });
|
|
139
|
+
}
|
|
140
|
+
if (!managed)
|
|
141
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.OWNERSHIP_UNPROVEN);
|
|
142
|
+
if (!candidate.providerVerified)
|
|
143
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.PROVIDER_TRUTH_UNKNOWN);
|
|
144
|
+
if (candidate.origin)
|
|
145
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.RESOURCE_ORIGIN);
|
|
146
|
+
if (candidate.current)
|
|
147
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.RESOURCE_CURRENT);
|
|
148
|
+
if (candidate.worktreeActive)
|
|
149
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.WORKTREE_ACTIVE);
|
|
150
|
+
if (candidate.protected)
|
|
151
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.RESOURCE_PROTECTED);
|
|
152
|
+
if (candidate.lifecycle === 'active')
|
|
153
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.RESOURCE_ACTIVE);
|
|
154
|
+
if (candidate.hasExclusiveCommits)
|
|
155
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.UNIQUE_COMMITS);
|
|
156
|
+
if (candidate.previouslyReconciled)
|
|
157
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.RESOURCE_RACED);
|
|
158
|
+
if (!candidate.commitsReachable && !candidate.contentEquivalent) {
|
|
159
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.INTEGRATION_UNPROVEN);
|
|
160
|
+
}
|
|
161
|
+
// Generic Git deletion has no safe expected-OID guard without force flags.
|
|
162
|
+
// The GitHub capability guards both target and integration origin atomically.
|
|
163
|
+
if (candidate.kind === 'remote_branch' && candidate.remoteDeletionCas !== 'github-update-refs-v1') {
|
|
164
|
+
return preserve(EPIC_TERMINAL_GIT_REASON_CODES.REMOTE_DELETE_CAS_UNAVAILABLE);
|
|
165
|
+
}
|
|
166
|
+
const action = candidate.kind === 'local_branch'
|
|
167
|
+
? 'delete_local_branch'
|
|
168
|
+
: candidate.kind === 'remote_branch' ? 'delete_remote_branch' : 'close_pull_request';
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
resourceId: candidate.resourceId,
|
|
172
|
+
action,
|
|
173
|
+
managed: true,
|
|
174
|
+
reasonCodes: [EPIC_TERMINAL_GIT_REASON_CODES.SAFE_TO_RECONCILE],
|
|
175
|
+
evidence: Object.freeze(evidence),
|
|
176
|
+
publicSafe: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export function reconcileTerminalReceipt(current, requested) {
|
|
180
|
+
if (!current)
|
|
181
|
+
return { kind: 'accepted', receipt: requested };
|
|
182
|
+
if (current.idempotencyKey !== requested.idempotencyKey)
|
|
183
|
+
return { kind: 'accepted', receipt: requested };
|
|
184
|
+
if (current.epicId === requested.epicId
|
|
185
|
+
&& current.executionBranch === requested.executionBranch
|
|
186
|
+
&& current.originBranch === requested.originBranch
|
|
187
|
+
&& current.baseSha === requested.baseSha
|
|
188
|
+
&& current.executionHeadSha === requested.executionHeadSha
|
|
189
|
+
&& current.mergeSha === requested.mergeSha
|
|
190
|
+
&& current.pushedSha === requested.pushedSha
|
|
191
|
+
&& current.physicalStatus === requested.physicalStatus) {
|
|
192
|
+
if (requested.candidateQa && JSON.stringify(current.candidateQa) !== JSON.stringify(requested.candidateQa)) {
|
|
193
|
+
return { kind: 'accepted', receipt: requested };
|
|
194
|
+
}
|
|
195
|
+
if (requested.providerGuard && JSON.stringify(current.providerGuard) !== JSON.stringify(requested.providerGuard)) {
|
|
196
|
+
return { kind: 'accepted', receipt: requested };
|
|
197
|
+
}
|
|
198
|
+
return { kind: 'reused', receipt: current };
|
|
199
|
+
}
|
|
200
|
+
// A failed/prepared attempt is not integrated physical truth. The root has to
|
|
201
|
+
// revalidate a new candidate, but its successful retry must replace that debt.
|
|
202
|
+
// Never rewrite a pushed/merged receipt or move an idempotency key to a new owner.
|
|
203
|
+
if (current.retryEligible && !current.pushedSha
|
|
204
|
+
&& ['prepared', 'blocked_local_or_repo', 'denied_external_gate'].includes(current.physicalStatus)
|
|
205
|
+
&& current.epicId === requested.epicId
|
|
206
|
+
&& current.executionBranch === requested.executionBranch
|
|
207
|
+
&& current.originBranch === requested.originBranch
|
|
208
|
+
&& current.executionHeadSha === requested.executionHeadSha) {
|
|
209
|
+
return { kind: 'accepted', receipt: requested };
|
|
210
|
+
}
|
|
211
|
+
return { kind: 'blocked', reasonCode: 'epic-terminal-receipt-idempotency-conflict' };
|
|
212
|
+
}
|
|
213
|
+
function nonEmpty(value) {
|
|
214
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
215
|
+
}
|
|
216
|
+
function isSha(value) {
|
|
217
|
+
return /^[a-f0-9]{7,64}$/i.test(value);
|
|
218
|
+
}
|
|
219
|
+
function isHash(value) {
|
|
220
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
|
|
221
|
+
}
|
|
222
|
+
function unique(values) {
|
|
223
|
+
return [...new Set(values)];
|
|
224
|
+
}
|
|
225
|
+
function sameSet(left, right) {
|
|
226
|
+
return left.length === right.length && [...left].sort().every((item, index) => item === [...right].sort()[index]);
|
|
227
|
+
}
|