@xenonbyte/da-vinci-workflow 0.2.6 → 0.2.8
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/CHANGELOG.md +33 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/docs/dv-command-reference.md +3 -1
- package/docs/zh-CN/dv-command-reference.md +3 -1
- package/lib/cli.js +33 -5
- package/lib/isolated-worker-handoff.js +181 -0
- package/lib/supervisor-review.js +117 -6
- package/lib/task-execution.js +88 -16
- package/lib/task-review.js +12 -7
- package/lib/workflow-decision-trace.js +335 -0
- package/lib/workflow-state.js +462 -63
- package/package.json +3 -2
package/lib/task-execution.js
CHANGED
|
@@ -65,8 +65,37 @@ function normalizeTaskExecutionEnvelope(input = {}) {
|
|
|
65
65
|
}
|
|
66
66
|
const changedFiles = normalizeList(input.changedFiles);
|
|
67
67
|
const testEvidence = normalizeList(input.testEvidence);
|
|
68
|
+
const pendingTestEvidence = normalizeList(input.pendingTestEvidence);
|
|
69
|
+
const confirmTestEvidenceExecuted = input.confirmTestEvidenceExecuted === true;
|
|
68
70
|
const concerns = normalizeList(input.concerns);
|
|
69
71
|
const blockers = normalizeList(input.blockers);
|
|
72
|
+
const outOfScopeWrites = normalizeList(input.outOfScopeWrites);
|
|
73
|
+
const partial = input.partial === true;
|
|
74
|
+
|
|
75
|
+
const overlap = testEvidence.filter((item) => pendingTestEvidence.includes(item));
|
|
76
|
+
if (overlap.length > 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`task-execution cannot mark the same command as both executed and pending: ${overlap.join(", ")}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (testEvidence.length > 0 && !confirmTestEvidenceExecuted) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"`task-execution` requires explicit executed-evidence confirmation when `--test-evidence` is provided. Pass `--confirm-test-evidence-executed`."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (pendingTestEvidence.length > 0 && status === "DONE") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"`task-execution` cannot use status DONE when pending test evidence exists. Use DONE_WITH_CONCERNS, NEEDS_CONTEXT, or BLOCKED."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (partial && status === "DONE") {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"`task-execution` cannot use status DONE when --partial is set. Use DONE_WITH_CONCERNS, NEEDS_CONTEXT, or BLOCKED."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
70
99
|
|
|
71
100
|
return {
|
|
72
101
|
taskGroupId,
|
|
@@ -74,8 +103,12 @@ function normalizeTaskExecutionEnvelope(input = {}) {
|
|
|
74
103
|
summary,
|
|
75
104
|
changedFiles,
|
|
76
105
|
testEvidence,
|
|
106
|
+
pendingTestEvidence,
|
|
107
|
+
confirmTestEvidenceExecuted,
|
|
77
108
|
concerns,
|
|
78
109
|
blockers,
|
|
110
|
+
outOfScopeWrites,
|
|
111
|
+
partial,
|
|
79
112
|
recordedAt: new Date().toISOString()
|
|
80
113
|
};
|
|
81
114
|
}
|
|
@@ -93,35 +126,65 @@ function writeTaskExecutionEnvelope(projectPathInput, options = {}) {
|
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
const envelope = normalizeTaskExecutionEnvelope(options);
|
|
129
|
+
const envelopeWithIdentity = {
|
|
130
|
+
...envelope,
|
|
131
|
+
changeId: resolved.changeId
|
|
132
|
+
};
|
|
133
|
+
const signalStatus = mapImplementerStatusToSignal(envelopeWithIdentity.status);
|
|
134
|
+
const outOfScopeWriteWarnings = envelopeWithIdentity.outOfScopeWrites.map(
|
|
135
|
+
(item) => `out-of-scope write: ${item}`
|
|
136
|
+
);
|
|
137
|
+
const pendingTestWarnings = envelopeWithIdentity.pendingTestEvidence.map(
|
|
138
|
+
(item) => `test not executed: ${item}`
|
|
139
|
+
);
|
|
96
140
|
const signalPath = writeExecutionSignal(projectRoot, {
|
|
97
141
|
changeId: resolved.changeId,
|
|
98
|
-
surface: buildSurface(
|
|
99
|
-
status:
|
|
142
|
+
surface: buildSurface(envelopeWithIdentity.taskGroupId),
|
|
143
|
+
status: signalStatus,
|
|
100
144
|
advisory: false,
|
|
101
145
|
strict: true,
|
|
102
|
-
failures:
|
|
146
|
+
failures: envelopeWithIdentity.status === "BLOCKED" ? envelopeWithIdentity.blockers : [],
|
|
103
147
|
warnings:
|
|
104
|
-
|
|
105
|
-
?
|
|
106
|
-
: [],
|
|
107
|
-
notes: [
|
|
148
|
+
envelopeWithIdentity.status === "DONE_WITH_CONCERNS" || envelopeWithIdentity.status === "NEEDS_CONTEXT"
|
|
149
|
+
? unique([...envelopeWithIdentity.concerns, ...outOfScopeWriteWarnings, ...pendingTestWarnings])
|
|
150
|
+
: unique([...outOfScopeWriteWarnings, ...pendingTestWarnings]),
|
|
151
|
+
notes: [
|
|
152
|
+
envelopeWithIdentity.summary,
|
|
153
|
+
...envelopeWithIdentity.testEvidence.map((item) => `test: ${item}`)
|
|
154
|
+
],
|
|
108
155
|
details: {
|
|
109
156
|
type: "task_execution",
|
|
110
|
-
envelope
|
|
157
|
+
envelope: {
|
|
158
|
+
taskGroupId: envelopeWithIdentity.taskGroupId,
|
|
159
|
+
changeId: envelopeWithIdentity.changeId,
|
|
160
|
+
status: envelopeWithIdentity.status,
|
|
161
|
+
summary: envelopeWithIdentity.summary,
|
|
162
|
+
changedFiles: envelopeWithIdentity.changedFiles,
|
|
163
|
+
testEvidence: envelopeWithIdentity.testEvidence,
|
|
164
|
+
pendingTestEvidence: envelopeWithIdentity.pendingTestEvidence,
|
|
165
|
+
concerns: envelopeWithIdentity.concerns,
|
|
166
|
+
blockers: envelopeWithIdentity.blockers,
|
|
167
|
+
recordedAt: envelopeWithIdentity.recordedAt
|
|
168
|
+
},
|
|
169
|
+
outOfScopeWrites: envelopeWithIdentity.outOfScopeWrites,
|
|
170
|
+
partial: envelopeWithIdentity.partial
|
|
111
171
|
}
|
|
112
172
|
});
|
|
113
173
|
|
|
114
174
|
return {
|
|
115
|
-
status:
|
|
175
|
+
status: signalStatus,
|
|
116
176
|
projectRoot,
|
|
117
177
|
changeId: resolved.changeId,
|
|
118
|
-
taskGroupId:
|
|
119
|
-
implementerStatus:
|
|
120
|
-
summary:
|
|
121
|
-
changedFiles:
|
|
122
|
-
testEvidence:
|
|
123
|
-
|
|
124
|
-
|
|
178
|
+
taskGroupId: envelopeWithIdentity.taskGroupId,
|
|
179
|
+
implementerStatus: envelopeWithIdentity.status,
|
|
180
|
+
summary: envelopeWithIdentity.summary,
|
|
181
|
+
changedFiles: envelopeWithIdentity.changedFiles,
|
|
182
|
+
testEvidence: envelopeWithIdentity.testEvidence,
|
|
183
|
+
pendingTestEvidence: envelopeWithIdentity.pendingTestEvidence,
|
|
184
|
+
concerns: envelopeWithIdentity.concerns,
|
|
185
|
+
blockers: envelopeWithIdentity.blockers,
|
|
186
|
+
outOfScopeWrites: envelopeWithIdentity.outOfScopeWrites,
|
|
187
|
+
partial: envelopeWithIdentity.partial,
|
|
125
188
|
signalPath
|
|
126
189
|
};
|
|
127
190
|
}
|
|
@@ -143,12 +206,21 @@ function formatTaskExecutionReport(result) {
|
|
|
143
206
|
if (result.testEvidence.length > 0) {
|
|
144
207
|
lines.push(`Test evidence: ${result.testEvidence.join(", ")}`);
|
|
145
208
|
}
|
|
209
|
+
if (result.pendingTestEvidence.length > 0) {
|
|
210
|
+
lines.push(`Pending test evidence: ${result.pendingTestEvidence.join(", ")}`);
|
|
211
|
+
}
|
|
146
212
|
if (result.concerns.length > 0) {
|
|
147
213
|
lines.push(`Concerns: ${result.concerns.join(", ")}`);
|
|
148
214
|
}
|
|
149
215
|
if (result.blockers.length > 0) {
|
|
150
216
|
lines.push(`Blockers: ${result.blockers.join(", ")}`);
|
|
151
217
|
}
|
|
218
|
+
if (result.outOfScopeWrites.length > 0) {
|
|
219
|
+
lines.push(`Out-of-scope writes: ${result.outOfScopeWrites.join(", ")}`);
|
|
220
|
+
}
|
|
221
|
+
if (result.partial) {
|
|
222
|
+
lines.push("Partial: true");
|
|
223
|
+
}
|
|
152
224
|
return lines.join("\n");
|
|
153
225
|
}
|
|
154
226
|
|
package/lib/task-review.js
CHANGED
|
@@ -131,18 +131,23 @@ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
const envelopeWithIdentity = {
|
|
135
|
+
...envelope,
|
|
136
|
+
changeId: resolved.changeId
|
|
137
|
+
};
|
|
138
|
+
|
|
134
139
|
const signalPath = writeExecutionSignal(projectRoot, {
|
|
135
140
|
changeId: resolved.changeId,
|
|
136
|
-
surface: buildTaskReviewSurface(
|
|
137
|
-
status: mapReviewStatusToSignalStatus(
|
|
141
|
+
surface: buildTaskReviewSurface(envelopeWithIdentity.taskGroupId, envelopeWithIdentity.stage),
|
|
142
|
+
status: mapReviewStatusToSignalStatus(envelopeWithIdentity.status),
|
|
138
143
|
advisory: false,
|
|
139
144
|
strict: true,
|
|
140
|
-
failures:
|
|
141
|
-
warnings:
|
|
142
|
-
notes: [
|
|
145
|
+
failures: envelopeWithIdentity.status === "BLOCK" ? envelopeWithIdentity.issues : [],
|
|
146
|
+
warnings: envelopeWithIdentity.status === "WARN" ? envelopeWithIdentity.issues : [],
|
|
147
|
+
notes: [envelopeWithIdentity.summary, `reviewer: ${envelopeWithIdentity.reviewer}`],
|
|
143
148
|
details: {
|
|
144
149
|
type: "task_review",
|
|
145
|
-
envelope
|
|
150
|
+
envelope: envelopeWithIdentity
|
|
146
151
|
}
|
|
147
152
|
});
|
|
148
153
|
|
|
@@ -150,7 +155,7 @@ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
|
|
|
150
155
|
if (options.writeVerification === true) {
|
|
151
156
|
verificationPath = path.join(resolved.changeDir, "verification.md");
|
|
152
157
|
fs.mkdirSync(path.dirname(verificationPath), { recursive: true });
|
|
153
|
-
const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath),
|
|
158
|
+
const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelopeWithIdentity);
|
|
154
159
|
writeFileAtomic(verificationPath, nextVerification);
|
|
155
160
|
}
|
|
156
161
|
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { normalizeRelativePath } = require("./utils");
|
|
4
|
+
|
|
5
|
+
const TRACE_ENABLE_ENV = "DA_VINCI_TRACE_WORKFLOW_DECISIONS";
|
|
6
|
+
const ELIGIBLE_SURFACES = new Set(["workflow-status", "next-step"]);
|
|
7
|
+
const DECISION_FAMILIES = new Set([
|
|
8
|
+
"persisted_state_trust",
|
|
9
|
+
"task_group_seed_fallback",
|
|
10
|
+
"task_group_focus_resolution",
|
|
11
|
+
"planning_signal_freshness",
|
|
12
|
+
"verification_freshness_downgrade",
|
|
13
|
+
"worktree_isolation_downgrade"
|
|
14
|
+
]);
|
|
15
|
+
const OUTCOMES = new Set([
|
|
16
|
+
"accepted",
|
|
17
|
+
"fallback",
|
|
18
|
+
"selected_focus",
|
|
19
|
+
"rerun_required",
|
|
20
|
+
"downgraded",
|
|
21
|
+
"suppressed"
|
|
22
|
+
]);
|
|
23
|
+
const DECISION_KEYS_BY_FAMILY = Object.freeze({
|
|
24
|
+
persisted_state_trust: new Set([
|
|
25
|
+
"accepted_digest_match",
|
|
26
|
+
"accepted_age_advisory",
|
|
27
|
+
"fallback_missing",
|
|
28
|
+
"fallback_parse_error",
|
|
29
|
+
"fallback_version_mismatch",
|
|
30
|
+
"fallback_change_missing",
|
|
31
|
+
"fallback_fingerprint_mismatch"
|
|
32
|
+
]),
|
|
33
|
+
task_group_seed_fallback: new Set([
|
|
34
|
+
"seed_missing",
|
|
35
|
+
"seed_unreadable",
|
|
36
|
+
"seed_digest_mismatch",
|
|
37
|
+
"seed_legacy_embedded"
|
|
38
|
+
]),
|
|
39
|
+
task_group_focus_resolution: new Set([
|
|
40
|
+
"implementer_block",
|
|
41
|
+
"implementer_warn",
|
|
42
|
+
"spec_review_missing",
|
|
43
|
+
"spec_review_block",
|
|
44
|
+
"spec_review_warn",
|
|
45
|
+
"quality_review_missing",
|
|
46
|
+
"quality_review_block",
|
|
47
|
+
"quality_review_warn"
|
|
48
|
+
]),
|
|
49
|
+
planning_signal_freshness: new Set([
|
|
50
|
+
"stale_signal_rerun_required",
|
|
51
|
+
"stale_signal_strict_fallback"
|
|
52
|
+
]),
|
|
53
|
+
verification_freshness_downgrade: new Set(["verification_freshness_stale"]),
|
|
54
|
+
worktree_isolation_downgrade: new Set(["effective_serial_after_preflight"])
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const MAX_CONTEXT_ARRAY_LENGTH = 8;
|
|
58
|
+
const MAX_EVIDENCE_REFS = 8;
|
|
59
|
+
const MAX_TEXT_LENGTH = 240;
|
|
60
|
+
|
|
61
|
+
function truncateText(value, maxLength = MAX_TEXT_LENGTH) {
|
|
62
|
+
const text = String(value || "").trim();
|
|
63
|
+
if (!text) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
if (text.length <= maxLength) {
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatDateToken(date = new Date()) {
|
|
73
|
+
const year = date.getFullYear();
|
|
74
|
+
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
75
|
+
const day = `${date.getDate()}`.padStart(2, "0");
|
|
76
|
+
return `${year}-${month}-${day}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isWorkflowDecisionTracingEnabled(env = process.env) {
|
|
80
|
+
return String(env && env[TRACE_ENABLE_ENV] ? env[TRACE_ENABLE_ENV] : "").trim() === "1";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isEligibleWorkflowTraceSurface(surface) {
|
|
84
|
+
return ELIGIBLE_SURFACES.has(String(surface || "").trim());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldTraceWorkflowDecisions(options = {}) {
|
|
88
|
+
return (
|
|
89
|
+
isWorkflowDecisionTracingEnabled(options.env) &&
|
|
90
|
+
isEligibleWorkflowTraceSurface(options.surface)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveWorkflowDecisionTracePath(projectRoot, date = new Date()) {
|
|
95
|
+
return path.join(
|
|
96
|
+
path.resolve(projectRoot || process.cwd()),
|
|
97
|
+
".da-vinci",
|
|
98
|
+
"logs",
|
|
99
|
+
"workflow-decisions",
|
|
100
|
+
`${formatDateToken(date)}.ndjson`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatPathRef(projectRoot, candidatePath) {
|
|
105
|
+
const resolved = path.resolve(String(candidatePath || ""));
|
|
106
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
107
|
+
const relative = path.relative(root, resolved);
|
|
108
|
+
if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
109
|
+
return normalizeRelativePath(relative || ".");
|
|
110
|
+
}
|
|
111
|
+
return resolved.split(path.sep).join("/");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeContextValue(value) {
|
|
115
|
+
if (value === null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === "string") {
|
|
119
|
+
const text = truncateText(value);
|
|
120
|
+
return text || undefined;
|
|
121
|
+
}
|
|
122
|
+
if (typeof value === "number") {
|
|
123
|
+
return Number.isFinite(value) ? value : undefined;
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === "boolean") {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
const items = value
|
|
130
|
+
.map((item) => truncateText(item))
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.slice(0, MAX_CONTEXT_ARRAY_LENGTH);
|
|
133
|
+
return items.length > 0 ? items : undefined;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeContext(context) {
|
|
139
|
+
const normalized = {};
|
|
140
|
+
if (!context || typeof context !== "object" || Array.isArray(context)) {
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const [key, value] of Object.entries(context)) {
|
|
145
|
+
const normalizedValue = normalizeContextValue(value);
|
|
146
|
+
if (normalizedValue !== undefined) {
|
|
147
|
+
normalized[key] = normalizedValue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return normalized;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeEvidenceRefs(evidenceRefs) {
|
|
155
|
+
return Array.from(
|
|
156
|
+
new Set(
|
|
157
|
+
(Array.isArray(evidenceRefs) ? evidenceRefs : [])
|
|
158
|
+
.map((item) => truncateText(item))
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
)
|
|
161
|
+
).slice(0, MAX_EVIDENCE_REFS);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function inspectWorkflowDecisionRecord(record) {
|
|
165
|
+
if (!record || typeof record !== "object") {
|
|
166
|
+
return {
|
|
167
|
+
normalized: null,
|
|
168
|
+
rejection: "record must be an object"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const decisionFamily = String(record.decisionFamily || "").trim();
|
|
173
|
+
const decisionKey = String(record.decisionKey || "").trim();
|
|
174
|
+
const outcome = String(record.outcome || "").trim();
|
|
175
|
+
const reasonSummary = truncateText(record.reasonSummary);
|
|
176
|
+
|
|
177
|
+
if (!DECISION_FAMILIES.has(decisionFamily)) {
|
|
178
|
+
return {
|
|
179
|
+
normalized: null,
|
|
180
|
+
rejection: `unsupported decisionFamily: ${decisionFamily || "(empty)"}`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (!OUTCOMES.has(outcome)) {
|
|
184
|
+
return {
|
|
185
|
+
normalized: null,
|
|
186
|
+
rejection: `unsupported outcome for ${decisionFamily}: ${outcome || "(empty)"}`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const allowedKeys = DECISION_KEYS_BY_FAMILY[decisionFamily];
|
|
190
|
+
if (!allowedKeys || !allowedKeys.has(decisionKey)) {
|
|
191
|
+
return {
|
|
192
|
+
normalized: null,
|
|
193
|
+
rejection: `unsupported decisionKey for ${decisionFamily}: ${decisionKey || "(empty)"}`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (!reasonSummary) {
|
|
197
|
+
return {
|
|
198
|
+
normalized: null,
|
|
199
|
+
rejection: `reasonSummary is required for ${decisionFamily}/${decisionKey}`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
normalized: {
|
|
205
|
+
decisionFamily,
|
|
206
|
+
decisionKey,
|
|
207
|
+
outcome,
|
|
208
|
+
reasonSummary,
|
|
209
|
+
context: normalizeContext(record.context),
|
|
210
|
+
evidenceRefs: normalizeEvidenceRefs(record.evidenceRefs)
|
|
211
|
+
},
|
|
212
|
+
rejection: null
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function emitWorkflowDecisionTraces(options = {}) {
|
|
217
|
+
if (!shouldTraceWorkflowDecisions(options)) {
|
|
218
|
+
return {
|
|
219
|
+
enabled: false,
|
|
220
|
+
written: 0,
|
|
221
|
+
tracePath: null,
|
|
222
|
+
error: null,
|
|
223
|
+
rejectedCount: 0,
|
|
224
|
+
rejections: []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const changeId = String(options.changeId || "").trim();
|
|
229
|
+
const stage = String(options.stage || "").trim();
|
|
230
|
+
if (!changeId || !stage) {
|
|
231
|
+
return {
|
|
232
|
+
enabled: true,
|
|
233
|
+
written: 0,
|
|
234
|
+
tracePath: null,
|
|
235
|
+
error: null,
|
|
236
|
+
rejectedCount: 0,
|
|
237
|
+
rejections: []
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const rejections = [];
|
|
242
|
+
const normalizedRecords = [];
|
|
243
|
+
for (const record of Array.isArray(options.records) ? options.records : []) {
|
|
244
|
+
const inspected = inspectWorkflowDecisionRecord(record);
|
|
245
|
+
if (inspected.normalized) {
|
|
246
|
+
normalizedRecords.push(inspected.normalized);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inspected.rejection) {
|
|
250
|
+
rejections.push(inspected.rejection);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (normalizedRecords.length === 0) {
|
|
254
|
+
return {
|
|
255
|
+
enabled: true,
|
|
256
|
+
written: 0,
|
|
257
|
+
tracePath: null,
|
|
258
|
+
error: null,
|
|
259
|
+
rejectedCount: rejections.length,
|
|
260
|
+
rejections: rejections.slice(0, 3)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const now = options.now instanceof Date ? options.now : new Date();
|
|
265
|
+
const timestamp = now.toISOString();
|
|
266
|
+
const surface = String(options.surface || "").trim();
|
|
267
|
+
const tracePath = resolveWorkflowDecisionTracePath(options.projectRoot, now);
|
|
268
|
+
const lines = [];
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
|
|
271
|
+
for (const record of normalizedRecords) {
|
|
272
|
+
const payload = {
|
|
273
|
+
timestamp,
|
|
274
|
+
surface,
|
|
275
|
+
changeId,
|
|
276
|
+
stage,
|
|
277
|
+
decisionFamily: record.decisionFamily,
|
|
278
|
+
decisionKey: record.decisionKey,
|
|
279
|
+
outcome: record.outcome,
|
|
280
|
+
reasonSummary: record.reasonSummary,
|
|
281
|
+
context: record.context,
|
|
282
|
+
evidenceRefs: record.evidenceRefs
|
|
283
|
+
};
|
|
284
|
+
const serialized = JSON.stringify(payload);
|
|
285
|
+
if (seen.has(serialized)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
seen.add(serialized);
|
|
289
|
+
lines.push(serialized);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (lines.length === 0) {
|
|
293
|
+
return {
|
|
294
|
+
enabled: true,
|
|
295
|
+
written: 0,
|
|
296
|
+
tracePath: null,
|
|
297
|
+
error: null
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
fs.mkdirSync(path.dirname(tracePath), { recursive: true });
|
|
303
|
+
fs.appendFileSync(tracePath, `${lines.join("\n")}\n`, "utf8");
|
|
304
|
+
return {
|
|
305
|
+
enabled: true,
|
|
306
|
+
written: lines.length,
|
|
307
|
+
tracePath,
|
|
308
|
+
error: null,
|
|
309
|
+
rejectedCount: rejections.length,
|
|
310
|
+
rejections: rejections.slice(0, 3)
|
|
311
|
+
};
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return {
|
|
314
|
+
enabled: true,
|
|
315
|
+
written: 0,
|
|
316
|
+
tracePath,
|
|
317
|
+
error,
|
|
318
|
+
rejectedCount: rejections.length,
|
|
319
|
+
rejections: rejections.slice(0, 3)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
TRACE_ENABLE_ENV,
|
|
326
|
+
DECISION_FAMILIES,
|
|
327
|
+
DECISION_KEYS_BY_FAMILY,
|
|
328
|
+
OUTCOMES,
|
|
329
|
+
formatPathRef,
|
|
330
|
+
isWorkflowDecisionTracingEnabled,
|
|
331
|
+
isEligibleWorkflowTraceSurface,
|
|
332
|
+
shouldTraceWorkflowDecisions,
|
|
333
|
+
resolveWorkflowDecisionTracePath,
|
|
334
|
+
emitWorkflowDecisionTraces
|
|
335
|
+
};
|