gsd-pi 2.79.0-dev.5c910bb05 → 2.79.0-dev.ece5fd8ba
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/bootstrap/register-hooks.js +8 -8
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +45 -4
- 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 +15 -15
- 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/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 +15 -15
- 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/package.json +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +13 -2
- 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/bootstrap/register-hooks.ts +11 -8
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- 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/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- 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/execute-summary-save-empty-project.test.ts +109 -0
- 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/tests/workflow-tool-executors.test.ts +36 -7
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +47 -4
- 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 → TzEVJ1Lh8vbez4n4Q9TqQ}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → TzEVJ1Lh8vbez4n4Q9TqQ}/_ssgManifest.js +0 -0
|
@@ -692,7 +692,18 @@ test("executeSummarySave supports root-level deep planning artifacts", async ()
|
|
|
692
692
|
|
|
693
693
|
const project = await inProjectDir(base, () => executeSummarySave({
|
|
694
694
|
artifact_type: "PROJECT",
|
|
695
|
-
content:
|
|
695
|
+
content: [
|
|
696
|
+
"# Project",
|
|
697
|
+
"",
|
|
698
|
+
"## What This Is",
|
|
699
|
+
"",
|
|
700
|
+
"A root project artifact.",
|
|
701
|
+
"",
|
|
702
|
+
"## Milestone Sequence",
|
|
703
|
+
"",
|
|
704
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
705
|
+
"",
|
|
706
|
+
].join("\n"),
|
|
696
707
|
}, base));
|
|
697
708
|
assert.equal(project.isError, undefined);
|
|
698
709
|
assert.equal(project.details.path, "PROJECT.md");
|
|
@@ -794,7 +805,7 @@ test("executeSummarySave registers PROJECT milestone sequence for the next run",
|
|
|
794
805
|
}
|
|
795
806
|
});
|
|
796
807
|
|
|
797
|
-
test("executeSummarySave
|
|
808
|
+
test("executeSummarySave hard-fails when milestone registration throws so silent No-Active-Milestone is impossible", async () => {
|
|
798
809
|
const base = makeTmpBase();
|
|
799
810
|
try {
|
|
800
811
|
openTestDb(base);
|
|
@@ -824,10 +835,15 @@ test("executeSummarySave keeps PROJECT artifact save successful if milestone reg
|
|
|
824
835
|
].join("\n"),
|
|
825
836
|
}, base));
|
|
826
837
|
|
|
827
|
-
|
|
838
|
+
// The artifact is persisted before registration runs, but registration must
|
|
839
|
+
// surface as isError so the LLM retries (INSERT OR IGNORE makes it idempotent)
|
|
840
|
+
// instead of announcing "ready" while the DB has zero milestone rows.
|
|
841
|
+
assert.equal(result.isError, true);
|
|
828
842
|
assert.equal(result.details.path, "PROJECT.md");
|
|
829
|
-
assert.equal(result.details.
|
|
830
|
-
assert.match(String(result.details.
|
|
843
|
+
assert.equal(result.details.error, "milestone_registration_threw");
|
|
844
|
+
assert.match(String(result.details.registration_error), /simulated milestone registration failure/);
|
|
845
|
+
assert.match(result.content[0].text, /milestone registration failed/);
|
|
846
|
+
assert.match(result.content[0].text, /idempotent/);
|
|
831
847
|
assert.ok(existsSync(join(base, ".gsd", "PROJECT.md")));
|
|
832
848
|
const artifact = originalPrepare("SELECT path FROM artifacts WHERE path = ?").get("PROJECT.md");
|
|
833
849
|
assert.equal(artifact?.path, "PROJECT.md");
|
|
@@ -872,9 +888,22 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
|
|
|
872
888
|
writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\nplanning_depth: deep\n---\n");
|
|
873
889
|
openTestDb(base);
|
|
874
890
|
|
|
891
|
+
const projectFixture = [
|
|
892
|
+
"# Project",
|
|
893
|
+
"",
|
|
894
|
+
"## What This Is",
|
|
895
|
+
"",
|
|
896
|
+
"A root project artifact.",
|
|
897
|
+
"",
|
|
898
|
+
"## Milestone Sequence",
|
|
899
|
+
"",
|
|
900
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
901
|
+
"",
|
|
902
|
+
].join("\n");
|
|
903
|
+
|
|
875
904
|
const blocked = await inProjectDir(base, () => executeSummarySave({
|
|
876
905
|
artifact_type: "PROJECT",
|
|
877
|
-
content:
|
|
906
|
+
content: projectFixture,
|
|
878
907
|
}, base));
|
|
879
908
|
|
|
880
909
|
assert.equal(blocked.isError, true);
|
|
@@ -886,7 +915,7 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
|
|
|
886
915
|
|
|
887
916
|
const unblocked = await inProjectDir(base, () => executeSummarySave({
|
|
888
917
|
artifact_type: "PROJECT",
|
|
889
|
-
content:
|
|
918
|
+
content: projectFixture,
|
|
890
919
|
}, base));
|
|
891
920
|
|
|
892
921
|
assert.equal(unblocked.isError, undefined);
|
|
@@ -190,19 +190,63 @@ export async function executeSummarySave(
|
|
|
190
190
|
);
|
|
191
191
|
|
|
192
192
|
let registeredMilestones: string[] = [];
|
|
193
|
-
let registrationWarning: string | undefined;
|
|
194
193
|
if (params.artifact_type === "PROJECT") {
|
|
195
194
|
try {
|
|
196
195
|
registeredMilestones = registerProjectMilestoneSequence(contentToSave);
|
|
197
196
|
if (registeredMilestones.length > 0) invalidateStateCache();
|
|
198
197
|
} catch (err) {
|
|
199
198
|
const msg = err instanceof Error ? err.message : String(err);
|
|
200
|
-
|
|
201
|
-
logWarning("tool", registrationWarning, {
|
|
199
|
+
logError("tool", `gsd_summary_save: PROJECT artifact persisted but milestone registration threw: ${msg}`, {
|
|
202
200
|
tool: "gsd_summary_save",
|
|
203
201
|
error: String(err),
|
|
204
202
|
stack: err instanceof Error ? err.stack ?? "" : "",
|
|
205
203
|
});
|
|
204
|
+
// PROJECT.md was persisted by saveArtifactToDb above; the artifacts row
|
|
205
|
+
// changed even though no milestones registered. Invalidate so subsequent
|
|
206
|
+
// /gsd reads see the persisted artifact instead of the pre-save cache.
|
|
207
|
+
invalidateStateCache();
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: "text",
|
|
211
|
+
text:
|
|
212
|
+
`Error: PROJECT.md was saved to ${relativePath} but milestone registration failed: ${msg}. ` +
|
|
213
|
+
`The DB has no milestone rows for this project, so /gsd will report "No Active Milestone". ` +
|
|
214
|
+
`Re-call gsd_summary_save(PROJECT) once the underlying error is resolved — INSERT OR IGNORE makes registration idempotent.`,
|
|
215
|
+
}],
|
|
216
|
+
details: {
|
|
217
|
+
operation: "save_summary",
|
|
218
|
+
path: relativePath,
|
|
219
|
+
artifact_type: params.artifact_type,
|
|
220
|
+
error: "milestone_registration_threw",
|
|
221
|
+
registration_error: msg,
|
|
222
|
+
},
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (registeredMilestones.length === 0) {
|
|
227
|
+
logError("tool", `gsd_summary_save: PROJECT.md saved to ${relativePath} but parsed zero milestones — registration produced no DB rows`, {
|
|
228
|
+
tool: "gsd_summary_save",
|
|
229
|
+
});
|
|
230
|
+
// PROJECT.md was persisted; invalidate so subsequent reads see the new
|
|
231
|
+
// artifacts row even though no milestones registered.
|
|
232
|
+
invalidateStateCache();
|
|
233
|
+
return {
|
|
234
|
+
content: [{
|
|
235
|
+
type: "text",
|
|
236
|
+
text:
|
|
237
|
+
`Error: PROJECT.md was saved to ${relativePath} but contains zero parseable milestone lines, ` +
|
|
238
|
+
`so no milestones were registered in the DB. /gsd will report "No Active Milestone". ` +
|
|
239
|
+
`Rewrite PROJECT.md so the "Milestone Sequence" section uses canonical lines: ` +
|
|
240
|
+
`\`- [ ] M001: <Title> — <One-liner>\` (em-dash, double-dash \`--\`, or single-dash \`-\` separator), then re-call gsd_summary_save(PROJECT).`,
|
|
241
|
+
}],
|
|
242
|
+
details: {
|
|
243
|
+
operation: "save_summary",
|
|
244
|
+
path: relativePath,
|
|
245
|
+
artifact_type: params.artifact_type,
|
|
246
|
+
error: "milestone_registration_empty_parse",
|
|
247
|
+
},
|
|
248
|
+
isError: true,
|
|
249
|
+
};
|
|
206
250
|
}
|
|
207
251
|
}
|
|
208
252
|
|
|
@@ -225,7 +269,6 @@ export async function executeSummarySave(
|
|
|
225
269
|
artifact_type: params.artifact_type,
|
|
226
270
|
content_source: contentSource,
|
|
227
271
|
...(registeredMilestones.length > 0 ? { registeredMilestones } : {}),
|
|
228
|
-
...(registrationWarning ? { warning: registrationWarning } : {}),
|
|
229
272
|
},
|
|
230
273
|
};
|
|
231
274
|
} catch (err) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// GSD2 UOK Audit Events and DB-First Projection Writes
|
|
2
|
+
|
|
1
3
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -6,7 +8,7 @@ import { isStaleWrite } from "../auto/turn-epoch.js";
|
|
|
6
8
|
import { withFileLockSync } from "../file-lock.js";
|
|
7
9
|
import { gsdRoot } from "../paths.js";
|
|
8
10
|
import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
|
|
9
|
-
import type
|
|
11
|
+
import { CURRENT_UOK_CONTRACT_VERSION, validateAuditEvent, type AuditEventEnvelope } from "./contracts.js";
|
|
10
12
|
|
|
11
13
|
function auditLogPath(basePath: string): string {
|
|
12
14
|
return join(gsdRoot(basePath), "audit", "events.jsonl");
|
|
@@ -25,6 +27,7 @@ export function buildAuditEnvelope(args: {
|
|
|
25
27
|
payload?: Record<string, unknown>;
|
|
26
28
|
}): AuditEventEnvelope {
|
|
27
29
|
return {
|
|
30
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
28
31
|
eventId: randomUUID(),
|
|
29
32
|
traceId: args.traceId,
|
|
30
33
|
turnId: args.turnId,
|
|
@@ -39,6 +42,26 @@ export function buildAuditEnvelope(args: {
|
|
|
39
42
|
export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope): void {
|
|
40
43
|
// Drop writes from a turn superseded by timeout recovery / cancellation.
|
|
41
44
|
if (isStaleWrite("uok-audit")) return;
|
|
45
|
+
const validation = validateAuditEvent(event);
|
|
46
|
+
if (!validation.ok) {
|
|
47
|
+
throw new Error(`Invalid UOK audit event: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
48
|
+
}
|
|
49
|
+
const canonical = validation.value;
|
|
50
|
+
|
|
51
|
+
if (isDbAvailable()) {
|
|
52
|
+
try {
|
|
53
|
+
insertAuditEvent({
|
|
54
|
+
...canonical,
|
|
55
|
+
payload: {
|
|
56
|
+
...canonical.payload,
|
|
57
|
+
contractVersion: canonical.version ?? CURRENT_UOK_CONTRACT_VERSION,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw new Error(`DB authoritative audit write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
try {
|
|
43
66
|
ensureAuditDir(basePath);
|
|
44
67
|
const path = auditLogPath(basePath);
|
|
@@ -52,18 +75,11 @@ export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope):
|
|
|
52
75
|
withFileLockSync(
|
|
53
76
|
path,
|
|
54
77
|
() => {
|
|
55
|
-
appendFileSync(path, `${JSON.stringify(
|
|
78
|
+
appendFileSync(path, `${JSON.stringify(canonical)}\n`, "utf-8");
|
|
56
79
|
},
|
|
57
80
|
{ onLocked: "skip" },
|
|
58
81
|
);
|
|
59
82
|
} catch {
|
|
60
83
|
// Best-effort: audit writes must never break orchestration.
|
|
61
84
|
}
|
|
62
|
-
|
|
63
|
-
if (!isDbAvailable()) return;
|
|
64
|
-
try {
|
|
65
|
-
insertAuditEvent(event);
|
|
66
|
-
} catch {
|
|
67
|
-
// Projection failures are non-fatal while legacy readers are still active.
|
|
68
|
-
}
|
|
69
85
|
}
|
|
@@ -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
|
+
}
|