gsd-pi 2.79.0-dev.5c910bb05 → 2.79.0-dev.bbb2f88ce
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +6 -1
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +6 -1
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → 3HYkAopiKls15zp5a8I9n}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → 3HYkAopiKls15zp5a8I9n}/_ssgManifest.js +0 -0
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// GSD2 UOK Contract Types and Versioning
|
|
2
|
+
|
|
3
|
+
export const CURRENT_UOK_CONTRACT_VERSION = "1" as const;
|
|
4
|
+
|
|
5
|
+
export type UokContractVersion = "0" | typeof CURRENT_UOK_CONTRACT_VERSION;
|
|
6
|
+
|
|
1
7
|
export type FailureClass =
|
|
2
8
|
| "none"
|
|
3
9
|
| "policy"
|
|
@@ -77,6 +83,7 @@ export interface TurnCloseoutRecord {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
export interface TurnResult {
|
|
86
|
+
version?: UokContractVersion;
|
|
80
87
|
traceId: string;
|
|
81
88
|
turnId: string;
|
|
82
89
|
iteration: number;
|
|
@@ -109,6 +116,7 @@ export interface DispatchExplanation {
|
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
export interface UokDispatchEnvelope {
|
|
119
|
+
version?: UokContractVersion;
|
|
112
120
|
action: "dispatch" | "stop" | "skip";
|
|
113
121
|
nodeKind?: UokNodeKind;
|
|
114
122
|
unitType?: string;
|
|
@@ -130,6 +138,7 @@ export interface UokDispatchEnvelope {
|
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
export interface AuditEventEnvelope {
|
|
141
|
+
version?: UokContractVersion;
|
|
133
142
|
eventId: string;
|
|
134
143
|
traceId: string;
|
|
135
144
|
turnId?: string;
|
|
@@ -199,3 +208,99 @@ export interface UokTurnObserver {
|
|
|
199
208
|
): void;
|
|
200
209
|
onTurnResult(result: TurnResult): void;
|
|
201
210
|
}
|
|
211
|
+
|
|
212
|
+
export interface ContractValidationIssue {
|
|
213
|
+
path: string;
|
|
214
|
+
message: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface ContractValidationResult<T> {
|
|
218
|
+
ok: boolean;
|
|
219
|
+
value: T;
|
|
220
|
+
issues: ContractValidationIssue[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
224
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeVersion(value: unknown): UokContractVersion {
|
|
228
|
+
return value === CURRENT_UOK_CONTRACT_VERSION ? CURRENT_UOK_CONTRACT_VERSION : "0";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function requireString(
|
|
232
|
+
value: Record<string, unknown>,
|
|
233
|
+
key: string,
|
|
234
|
+
issues: ContractValidationIssue[],
|
|
235
|
+
): void {
|
|
236
|
+
if (typeof value[key] !== "string" || value[key] === "") {
|
|
237
|
+
issues.push({ path: key, message: `${key} must be a non-empty string` });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function requireRecord(
|
|
242
|
+
value: Record<string, unknown>,
|
|
243
|
+
key: string,
|
|
244
|
+
issues: ContractValidationIssue[],
|
|
245
|
+
): void {
|
|
246
|
+
if (!isRecord(value[key])) {
|
|
247
|
+
issues.push({ path: key, message: `${key} must be an object` });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function normalizeTurnResult(value: TurnResult): TurnResult {
|
|
252
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function normalizeDispatchEnvelope(value: UokDispatchEnvelope): UokDispatchEnvelope {
|
|
256
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function normalizeAuditEvent(value: AuditEventEnvelope): AuditEventEnvelope {
|
|
260
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function validateTurnResult(value: TurnResult): ContractValidationResult<TurnResult> {
|
|
264
|
+
const normalized = normalizeTurnResult(value);
|
|
265
|
+
const record = normalized as unknown as Record<string, unknown>;
|
|
266
|
+
const issues: ContractValidationIssue[] = [];
|
|
267
|
+
requireString(record, "traceId", issues);
|
|
268
|
+
requireString(record, "turnId", issues);
|
|
269
|
+
if (!Number.isInteger(record.iteration)) {
|
|
270
|
+
issues.push({ path: "iteration", message: "iteration must be an integer" });
|
|
271
|
+
}
|
|
272
|
+
requireString(record, "status", issues);
|
|
273
|
+
requireString(record, "failureClass", issues);
|
|
274
|
+
if (!Array.isArray(record.phaseResults)) {
|
|
275
|
+
issues.push({ path: "phaseResults", message: "phaseResults must be an array" });
|
|
276
|
+
}
|
|
277
|
+
requireString(record, "startedAt", issues);
|
|
278
|
+
requireString(record, "finishedAt", issues);
|
|
279
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function validateDispatchEnvelope(value: UokDispatchEnvelope): ContractValidationResult<UokDispatchEnvelope> {
|
|
283
|
+
const normalized = normalizeDispatchEnvelope(value);
|
|
284
|
+
const record = normalized as unknown as Record<string, unknown>;
|
|
285
|
+
const issues: ContractValidationIssue[] = [];
|
|
286
|
+
requireString(record, "action", issues);
|
|
287
|
+
requireRecord(record, "reason", issues);
|
|
288
|
+
if (isRecord(record.reason)) {
|
|
289
|
+
requireString(record.reason, "reasonCode", issues);
|
|
290
|
+
requireString(record.reason, "summary", issues);
|
|
291
|
+
}
|
|
292
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function validateAuditEvent(value: AuditEventEnvelope): ContractValidationResult<AuditEventEnvelope> {
|
|
296
|
+
const normalized = normalizeAuditEvent(value);
|
|
297
|
+
const record = normalized as unknown as Record<string, unknown>;
|
|
298
|
+
const issues: ContractValidationIssue[] = [];
|
|
299
|
+
requireString(record, "eventId", issues);
|
|
300
|
+
requireString(record, "traceId", issues);
|
|
301
|
+
requireString(record, "category", issues);
|
|
302
|
+
requireString(record, "type", issues);
|
|
303
|
+
requireString(record, "ts", issues);
|
|
304
|
+
requireRecord(record, "payload", issues);
|
|
305
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
306
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// GSD2 UOK Dispatch Envelope Builder
|
|
2
|
+
|
|
1
3
|
import type {
|
|
2
4
|
DispatchExplanation,
|
|
3
5
|
DispatchReasonCode,
|
|
@@ -5,6 +7,7 @@ import type {
|
|
|
5
7
|
UokDispatchEnvelope,
|
|
6
8
|
UokGraphNode,
|
|
7
9
|
} from "./contracts.js";
|
|
10
|
+
import { CURRENT_UOK_CONTRACT_VERSION } from "./contracts.js";
|
|
8
11
|
|
|
9
12
|
export interface BuildDispatchEnvelopeInput {
|
|
10
13
|
action: UokDispatchEnvelope["action"];
|
|
@@ -22,6 +25,7 @@ export interface BuildDispatchEnvelopeInput {
|
|
|
22
25
|
|
|
23
26
|
export function buildDispatchEnvelope(input: BuildDispatchEnvelopeInput): UokDispatchEnvelope {
|
|
24
27
|
return {
|
|
28
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
25
29
|
action: input.action,
|
|
26
30
|
nodeKind: input.node?.kind,
|
|
27
31
|
unitType: input.unitType,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
// GSD2 UOK Turn Observer and DB-Backed Lifecycle Emission
|
|
2
|
+
|
|
1
3
|
import type {
|
|
2
4
|
TurnCloseoutRecord,
|
|
3
5
|
TurnContract,
|
|
4
6
|
TurnResult,
|
|
5
7
|
UokTurnObserver,
|
|
6
8
|
} from "./contracts.js";
|
|
9
|
+
import { CURRENT_UOK_CONTRACT_VERSION, validateTurnResult } from "./contracts.js";
|
|
7
10
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
|
|
8
11
|
import { writeTurnCloseoutGitRecord, writeTurnGitTransaction } from "./gitops.js";
|
|
9
12
|
import { acquireWriterToken, nextWriteRecord, releaseWriterToken } from "./writer.js";
|
|
@@ -142,56 +145,68 @@ export function createTurnObserver(options: CreateTurnObserverOptions): UokTurnO
|
|
|
142
145
|
},
|
|
143
146
|
|
|
144
147
|
onTurnResult(result): void {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
const cleanup = (): void => {
|
|
149
|
+
if (writerToken) {
|
|
150
|
+
releaseWriterToken(options.basePath, writerToken);
|
|
151
|
+
}
|
|
152
|
+
writerToken = null;
|
|
153
|
+
current = null;
|
|
154
|
+
phaseResults.length = 0;
|
|
148
155
|
};
|
|
149
156
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
turnId: merged.turnId,
|
|
156
|
-
category: "orchestration",
|
|
157
|
-
type: "turn-result",
|
|
158
|
-
payload: nextSequenceMetadata("audit", "append", {
|
|
159
|
-
unitType: merged.unitType,
|
|
160
|
-
unitId: merged.unitId,
|
|
161
|
-
status: merged.status,
|
|
162
|
-
failureClass: merged.failureClass,
|
|
163
|
-
error: merged.error,
|
|
164
|
-
phaseCount: merged.phaseResults.length,
|
|
165
|
-
}),
|
|
166
|
-
}),
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (options.enableGitops) {
|
|
171
|
-
const closeout: TurnCloseoutRecord = merged.closeout ?? {
|
|
172
|
-
traceId: merged.traceId,
|
|
173
|
-
turnId: merged.turnId,
|
|
174
|
-
unitType: merged.unitType,
|
|
175
|
-
unitId: merged.unitId,
|
|
176
|
-
status: merged.status,
|
|
177
|
-
failureClass: merged.failureClass,
|
|
178
|
-
gitAction: options.gitAction,
|
|
179
|
-
gitPushed: options.gitPush,
|
|
180
|
-
finishedAt: merged.finishedAt,
|
|
157
|
+
try {
|
|
158
|
+
const merged: TurnResult = {
|
|
159
|
+
...result,
|
|
160
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
161
|
+
phaseResults: Array.isArray(result.phaseResults) && result.phaseResults.length > 0 ? result.phaseResults : [...phaseResults],
|
|
181
162
|
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
);
|
|
187
|
-
}
|
|
163
|
+
const validation = validateTurnResult(merged);
|
|
164
|
+
if (!validation.ok) {
|
|
165
|
+
throw new Error(`Invalid UOK turn result: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
166
|
+
}
|
|
188
167
|
|
|
189
|
-
|
|
190
|
-
|
|
168
|
+
if (options.enableAudit) {
|
|
169
|
+
emitUokAuditEvent(
|
|
170
|
+
options.basePath,
|
|
171
|
+
buildAuditEnvelope({
|
|
172
|
+
traceId: validation.value.traceId,
|
|
173
|
+
turnId: validation.value.turnId,
|
|
174
|
+
category: "orchestration",
|
|
175
|
+
type: "turn-result",
|
|
176
|
+
payload: nextSequenceMetadata("audit", "append", {
|
|
177
|
+
contractVersion: validation.value.version,
|
|
178
|
+
unitType: validation.value.unitType,
|
|
179
|
+
unitId: validation.value.unitId,
|
|
180
|
+
status: validation.value.status,
|
|
181
|
+
failureClass: validation.value.failureClass,
|
|
182
|
+
error: validation.value.error,
|
|
183
|
+
phaseCount: validation.value.phaseResults.length,
|
|
184
|
+
}),
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (options.enableGitops) {
|
|
190
|
+
const closeout: TurnCloseoutRecord = merged.closeout ?? {
|
|
191
|
+
traceId: merged.traceId,
|
|
192
|
+
turnId: merged.turnId,
|
|
193
|
+
unitType: merged.unitType,
|
|
194
|
+
unitId: merged.unitId,
|
|
195
|
+
status: merged.status,
|
|
196
|
+
failureClass: merged.failureClass,
|
|
197
|
+
gitAction: options.gitAction,
|
|
198
|
+
gitPushed: options.gitPush,
|
|
199
|
+
finishedAt: merged.finishedAt,
|
|
200
|
+
};
|
|
201
|
+
writeTurnCloseoutGitRecord(
|
|
202
|
+
options.basePath,
|
|
203
|
+
closeout,
|
|
204
|
+
nextSequenceMetadata("gitops", "update", { action: "record" }),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
cleanup();
|
|
191
209
|
}
|
|
192
|
-
writerToken = null;
|
|
193
|
-
current = null;
|
|
194
|
-
phaseResults.length = 0;
|
|
195
210
|
},
|
|
196
211
|
};
|
|
197
212
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// GSD2 UOK Timeline Reconstruction from Authoritative DB Records
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { _getAdapter, isDbAvailable } from "../gsd-db.js";
|
|
7
|
+
import { gsdRoot } from "../paths.js";
|
|
8
|
+
|
|
9
|
+
export interface TurnTimelineFilter {
|
|
10
|
+
traceId?: string;
|
|
11
|
+
turnId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TurnTimelineEntry {
|
|
15
|
+
source: "audit_events" | "unit_dispatches" | "turn_git_transactions" | "audit_jsonl";
|
|
16
|
+
ts: string;
|
|
17
|
+
traceId?: string;
|
|
18
|
+
turnId?: string | null;
|
|
19
|
+
type: string;
|
|
20
|
+
payload: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TurnTimeline {
|
|
24
|
+
authoritative: "db" | "degraded-fallback";
|
|
25
|
+
degraded: boolean;
|
|
26
|
+
entries: TurnTimelineEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseJsonRecord(value: unknown): Record<string, unknown> {
|
|
30
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
31
|
+
return value as Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "string" || value.trim() === "") return {};
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(value) as unknown;
|
|
36
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
37
|
+
? parsed as Record<string, unknown>
|
|
38
|
+
: {};
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matchesFilter(entry: Pick<TurnTimelineEntry, "traceId" | "turnId">, filter: TurnTimelineFilter): boolean {
|
|
45
|
+
if (filter.traceId && entry.traceId !== filter.traceId) return false;
|
|
46
|
+
if (filter.turnId && entry.turnId !== filter.turnId) return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function byTimestamp(a: TurnTimelineEntry, b: TurnTimelineEntry): number {
|
|
51
|
+
return a.ts.localeCompare(b.ts);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readDbTimeline(filter: TurnTimelineFilter): TurnTimelineEntry[] {
|
|
55
|
+
const db = _getAdapter();
|
|
56
|
+
if (!db) return [];
|
|
57
|
+
const entries: TurnTimelineEntry[] = [];
|
|
58
|
+
const where: string[] = [];
|
|
59
|
+
const params: Record<string, string> = {};
|
|
60
|
+
if (filter.traceId) {
|
|
61
|
+
where.push("trace_id = :trace_id");
|
|
62
|
+
params[":trace_id"] = filter.traceId;
|
|
63
|
+
}
|
|
64
|
+
if (filter.turnId) {
|
|
65
|
+
where.push("turn_id = :turn_id");
|
|
66
|
+
params[":turn_id"] = filter.turnId;
|
|
67
|
+
}
|
|
68
|
+
const suffix = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
|
|
69
|
+
|
|
70
|
+
const auditRows = db.prepare(
|
|
71
|
+
`SELECT trace_id, turn_id, type, ts, payload_json FROM audit_events${suffix}`,
|
|
72
|
+
).all(params) as Array<{ trace_id: string; turn_id: string | null; type: string; ts: string; payload_json: string }>;
|
|
73
|
+
for (const row of auditRows) {
|
|
74
|
+
entries.push({
|
|
75
|
+
source: "audit_events",
|
|
76
|
+
ts: row.ts,
|
|
77
|
+
traceId: row.trace_id,
|
|
78
|
+
turnId: row.turn_id,
|
|
79
|
+
type: row.type,
|
|
80
|
+
payload: parseJsonRecord(row.payload_json),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const dispatchRows = db.prepare(
|
|
85
|
+
`SELECT trace_id, turn_id, unit_type, unit_id, status, started_at, ended_at, exit_reason,
|
|
86
|
+
error_summary, retry_after_ms, attempt_n, max_attempts
|
|
87
|
+
FROM unit_dispatches${suffix}`,
|
|
88
|
+
).all(params) as Array<Record<string, unknown>>;
|
|
89
|
+
for (const row of dispatchRows) {
|
|
90
|
+
entries.push({
|
|
91
|
+
source: "unit_dispatches",
|
|
92
|
+
ts: String(row.ended_at ?? row.started_at ?? ""),
|
|
93
|
+
traceId: String(row.trace_id ?? ""),
|
|
94
|
+
turnId: typeof row.turn_id === "string" ? row.turn_id : null,
|
|
95
|
+
type: `dispatch-${String(row.status ?? "unknown")}`,
|
|
96
|
+
payload: { ...row },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const gitRows = db.prepare(
|
|
101
|
+
`SELECT trace_id, turn_id, unit_type, unit_id, stage, action, push, status, error,
|
|
102
|
+
metadata_json, updated_at
|
|
103
|
+
FROM turn_git_transactions${suffix}`,
|
|
104
|
+
).all(params) as Array<Record<string, unknown>>;
|
|
105
|
+
for (const row of gitRows) {
|
|
106
|
+
entries.push({
|
|
107
|
+
source: "turn_git_transactions",
|
|
108
|
+
ts: String(row.updated_at ?? ""),
|
|
109
|
+
traceId: String(row.trace_id ?? ""),
|
|
110
|
+
turnId: typeof row.turn_id === "string" ? row.turn_id : null,
|
|
111
|
+
type: `gitops-${String(row.stage ?? "unknown")}`,
|
|
112
|
+
payload: {
|
|
113
|
+
...row,
|
|
114
|
+
metadata: parseJsonRecord(row.metadata_json),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return entries.filter((entry) => entry.ts !== "").sort(byTimestamp);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readJsonlTimeline(basePath: string, filter: TurnTimelineFilter): TurnTimelineEntry[] {
|
|
123
|
+
const path = join(gsdRoot(basePath), "audit", "events.jsonl");
|
|
124
|
+
if (!existsSync(path)) return [];
|
|
125
|
+
return readFileSync(path, "utf-8")
|
|
126
|
+
.split("\n")
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.map((line): TurnTimelineEntry | null => {
|
|
129
|
+
const event = parseJsonRecord(line);
|
|
130
|
+
const entry: TurnTimelineEntry = {
|
|
131
|
+
source: "audit_jsonl",
|
|
132
|
+
ts: String(event.ts ?? ""),
|
|
133
|
+
traceId: typeof event.traceId === "string" ? event.traceId : undefined,
|
|
134
|
+
turnId: typeof event.turnId === "string" ? event.turnId : null,
|
|
135
|
+
type: String(event.type ?? "audit"),
|
|
136
|
+
payload: parseJsonRecord(event.payload),
|
|
137
|
+
};
|
|
138
|
+
return entry.ts && matchesFilter(entry, filter) ? entry : null;
|
|
139
|
+
})
|
|
140
|
+
.filter((entry): entry is TurnTimelineEntry => entry !== null)
|
|
141
|
+
.sort(byTimestamp);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildTurnTimeline(basePath: string, filter: TurnTimelineFilter = {}): TurnTimeline {
|
|
145
|
+
if (isDbAvailable()) {
|
|
146
|
+
return {
|
|
147
|
+
authoritative: "db",
|
|
148
|
+
degraded: false,
|
|
149
|
+
entries: readDbTimeline(filter),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
authoritative: "degraded-fallback",
|
|
155
|
+
degraded: true,
|
|
156
|
+
entries: readJsonlTimeline(basePath, filter),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GSD2 Phase State — cross-extension coordination
|
|
3
3
|
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
4
4
|
*
|
|
5
5
|
* Lightweight module-level state that GSD auto-mode writes to and the
|
|
@@ -7,28 +7,81 @@
|
|
|
7
7
|
* a module variable is sufficient — no file I/O needed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { buildAuditEnvelope, emitUokAuditEvent } from "../gsd/uok/audit.js";
|
|
11
|
+
|
|
10
12
|
let _active = false;
|
|
11
13
|
let _currentPhase: string | null = null;
|
|
12
14
|
|
|
15
|
+
export interface GSDPhaseAuditContext {
|
|
16
|
+
basePath: string;
|
|
17
|
+
traceId: string;
|
|
18
|
+
turnId?: string;
|
|
19
|
+
causedBy?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let _auditContext: GSDPhaseAuditContext | null = null;
|
|
23
|
+
|
|
24
|
+
function emitPhaseChange(action: string, previousPhase: string | null, nextPhase: string | null): void {
|
|
25
|
+
if (!_auditContext) return;
|
|
26
|
+
emitUokAuditEvent(
|
|
27
|
+
_auditContext.basePath,
|
|
28
|
+
buildAuditEnvelope({
|
|
29
|
+
traceId: _auditContext.traceId,
|
|
30
|
+
turnId: _auditContext.turnId,
|
|
31
|
+
causedBy: _auditContext.causedBy,
|
|
32
|
+
category: "orchestration",
|
|
33
|
+
type: "phase_changed",
|
|
34
|
+
payload: {
|
|
35
|
+
action,
|
|
36
|
+
active: _active,
|
|
37
|
+
previousPhase,
|
|
38
|
+
nextPhase,
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function configureGSDPhaseAudit(context: GSDPhaseAuditContext | null): void {
|
|
45
|
+
_auditContext = context;
|
|
46
|
+
}
|
|
47
|
+
|
|
13
48
|
/** Mark GSD auto-mode as active. */
|
|
14
|
-
export function activateGSD(): void {
|
|
49
|
+
export function activateGSD(context?: GSDPhaseAuditContext): void {
|
|
50
|
+
if (context) _auditContext = context;
|
|
51
|
+
const previousPhase = _currentPhase;
|
|
15
52
|
_active = true;
|
|
53
|
+
emitPhaseChange("activate", previousPhase, _currentPhase);
|
|
16
54
|
}
|
|
17
55
|
|
|
18
56
|
/** Mark GSD auto-mode as inactive and clear the current phase. */
|
|
19
57
|
export function deactivateGSD(): void {
|
|
58
|
+
const previousPhase = _currentPhase;
|
|
20
59
|
_active = false;
|
|
21
60
|
_currentPhase = null;
|
|
61
|
+
emitPhaseChange("deactivate", previousPhase, _currentPhase);
|
|
62
|
+
_auditContext = null;
|
|
22
63
|
}
|
|
23
64
|
|
|
24
65
|
/** Set the currently dispatched GSD phase (e.g. "plan-milestone"). */
|
|
25
|
-
export function setCurrentPhase(phase: string):
|
|
66
|
+
export function setCurrentPhase(phase: string, context?: GSDPhaseAuditContext): boolean {
|
|
67
|
+
if (context) _auditContext = context;
|
|
68
|
+
if (!_active) {
|
|
69
|
+
process.emitWarning(`Ignoring GSD phase "${phase}" while GSD auto-mode is inactive`, {
|
|
70
|
+
code: "GSD_PHASE_INACTIVE",
|
|
71
|
+
});
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const previousPhase = _currentPhase;
|
|
26
75
|
_currentPhase = phase;
|
|
76
|
+
emitPhaseChange("set", previousPhase, _currentPhase);
|
|
77
|
+
return true;
|
|
27
78
|
}
|
|
28
79
|
|
|
29
80
|
/** Clear the current phase (unit completed or aborted). */
|
|
30
81
|
export function clearCurrentPhase(): void {
|
|
82
|
+
const previousPhase = _currentPhase;
|
|
31
83
|
_currentPhase = null;
|
|
84
|
+
emitPhaseChange("clear", previousPhase, _currentPhase);
|
|
32
85
|
}
|
|
33
86
|
|
|
34
87
|
/** Returns true if GSD auto-mode is currently active. */
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
// GSD2 Shared Phase State Coordination Tests
|
|
2
|
+
|
|
1
3
|
import { describe, it, beforeEach } from "node:test";
|
|
2
4
|
import assert from "node:assert/strict";
|
|
5
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
3
8
|
import {
|
|
4
9
|
activateGSD,
|
|
10
|
+
configureGSDPhaseAudit,
|
|
5
11
|
deactivateGSD,
|
|
6
12
|
setCurrentPhase,
|
|
7
13
|
clearCurrentPhase,
|
|
@@ -25,12 +31,18 @@ describe("gsd-phase-state", () => {
|
|
|
25
31
|
it("tracks the current phase when active", () => {
|
|
26
32
|
activateGSD();
|
|
27
33
|
assert.equal(getCurrentPhase(), null);
|
|
28
|
-
setCurrentPhase("plan-milestone");
|
|
34
|
+
assert.equal(setCurrentPhase("plan-milestone"), true);
|
|
29
35
|
assert.equal(getCurrentPhase(), "plan-milestone");
|
|
30
36
|
clearCurrentPhase();
|
|
31
37
|
assert.equal(getCurrentPhase(), null);
|
|
32
38
|
});
|
|
33
39
|
|
|
40
|
+
it("rejects phase changes while inactive", () => {
|
|
41
|
+
assert.equal(setCurrentPhase("plan-milestone"), false);
|
|
42
|
+
activateGSD();
|
|
43
|
+
assert.equal(getCurrentPhase(), null);
|
|
44
|
+
});
|
|
45
|
+
|
|
34
46
|
it("returns null phase when inactive even if phase was set", () => {
|
|
35
47
|
activateGSD();
|
|
36
48
|
setCurrentPhase("plan-milestone");
|
|
@@ -45,4 +57,34 @@ describe("gsd-phase-state", () => {
|
|
|
45
57
|
activateGSD();
|
|
46
58
|
assert.equal(getCurrentPhase(), null);
|
|
47
59
|
});
|
|
60
|
+
|
|
61
|
+
it("deactivation clears the audit context so later events do not carry stale trace data", () => {
|
|
62
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-phase-state-audit-"));
|
|
63
|
+
try {
|
|
64
|
+
activateGSD({ basePath, traceId: "stale-trace", causedBy: "test" });
|
|
65
|
+
setCurrentPhase("plan-milestone");
|
|
66
|
+
deactivateGSD();
|
|
67
|
+
|
|
68
|
+
// Re-activate WITHOUT a context. If deactivate did not clear the
|
|
69
|
+
// stored context, this setCurrentPhase would emit an audit event
|
|
70
|
+
// using "stale-trace".
|
|
71
|
+
activateGSD();
|
|
72
|
+
setCurrentPhase("execute-task");
|
|
73
|
+
|
|
74
|
+
const eventsPath = join(basePath, ".gsd", "audit", "events.jsonl");
|
|
75
|
+
if (existsSync(eventsPath)) {
|
|
76
|
+
const contents = readFileSync(eventsPath, "utf-8");
|
|
77
|
+
assert.equal(
|
|
78
|
+
contents.includes("stale-trace") &&
|
|
79
|
+
contents.split("\n").filter((line) => line.includes("stale-trace") && line.includes("execute-task")).length > 0,
|
|
80
|
+
false,
|
|
81
|
+
"execute-task phase change must not be emitted under the deactivated trace",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
configureGSDPhaseAudit(null);
|
|
86
|
+
deactivateGSD();
|
|
87
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
48
90
|
});
|
|
File without changes
|
|
File without changes
|