@xenonbyte/da-vinci-workflow 0.2.7 → 0.2.9
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 +31 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/lib/async-offload.js +39 -2
- package/lib/cli/command-handlers-core.js +132 -0
- package/lib/cli/command-handlers-design.js +129 -0
- package/lib/cli/command-handlers-pen.js +231 -0
- package/lib/cli/command-handlers-workflow.js +221 -0
- package/lib/cli/command-handlers.js +49 -0
- package/lib/cli/helpers.js +62 -0
- package/lib/cli.js +98 -533
- package/lib/execution-signals.js +33 -0
- package/lib/fs-safety.js +1 -12
- package/lib/path-inside.js +17 -0
- package/lib/utils.js +2 -7
- package/lib/workflow-base-view.js +134 -0
- package/lib/workflow-decision-trace.js +335 -0
- package/lib/workflow-overlay.js +1033 -0
- package/lib/workflow-persisted-state.js +4 -0
- package/lib/workflow-stage.js +244 -0
- package/lib/workflow-state.js +414 -1708
- package/lib/workflow-task-groups.js +881 -0
- package/lib/worktree-preflight.js +31 -11
- package/package.json +1 -1
package/lib/execution-signals.js
CHANGED
|
@@ -118,6 +118,38 @@ function readExecutionSignals(projectRoot, options = {}) {
|
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function listExecutionSignalPathsForChange(projectRoot, options = {}) {
|
|
122
|
+
const changeId = options.changeId ? String(options.changeId).trim() : "";
|
|
123
|
+
const signalsDir = resolveSignalsDir(projectRoot);
|
|
124
|
+
if (!changeId || !pathExists(signalsDir)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const entries = fs
|
|
129
|
+
.readdirSync(signalsDir, { withFileTypes: true })
|
|
130
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"));
|
|
131
|
+
const paths = [];
|
|
132
|
+
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
const absolutePath = path.join(signalsDir, entry.name);
|
|
135
|
+
const parsedName = parseSignalFileName(entry.name);
|
|
136
|
+
let include = false;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const payload = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
|
|
140
|
+
include = String(payload.changeId || "") === changeId;
|
|
141
|
+
} catch (_error) {
|
|
142
|
+
include = String(parsedName.changeId || "").trim() === changeId;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (include) {
|
|
146
|
+
paths.push(absolutePath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return paths.sort((left, right) => left.localeCompare(right));
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
function summarizeSignalsBySurface(signals) {
|
|
122
154
|
const summary = {};
|
|
123
155
|
for (const signal of signals || []) {
|
|
@@ -148,6 +180,7 @@ function getLatestSignalBySurfacePrefix(signals, prefix) {
|
|
|
148
180
|
module.exports = {
|
|
149
181
|
writeExecutionSignal,
|
|
150
182
|
readExecutionSignals,
|
|
183
|
+
listExecutionSignalPathsForChange,
|
|
151
184
|
summarizeSignalsBySurface,
|
|
152
185
|
listSignalsBySurfacePrefix,
|
|
153
186
|
getLatestSignalBySurfacePrefix
|
package/lib/fs-safety.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { pathExists } = require("./utils");
|
|
4
|
+
const { isPathInside } = require("./path-inside");
|
|
4
5
|
|
|
5
6
|
const DEFAULT_MAX_DEPTH = 32;
|
|
6
7
|
const DEFAULT_MAX_ENTRIES = 20000;
|
|
@@ -19,18 +20,6 @@ function toPositiveInteger(value, fallback) {
|
|
|
19
20
|
return normalized;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function isPathInside(basePath, targetPath) {
|
|
23
|
-
const resolvedBase = path.resolve(basePath);
|
|
24
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
25
|
-
|
|
26
|
-
if (resolvedBase === resolvedTarget) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
31
|
-
return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
23
|
function listFilesRecursiveSafe(rootDir, options = {}) {
|
|
35
24
|
const maxDepth = toPositiveInteger(options.maxDepth, DEFAULT_MAX_DEPTH);
|
|
36
25
|
const maxEntries = toPositiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
function isPathInside(basePath, targetPath) {
|
|
4
|
+
const resolvedBase = path.resolve(basePath);
|
|
5
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
6
|
+
|
|
7
|
+
if (resolvedBase === resolvedTarget) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
12
|
+
return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
isPathInside
|
|
17
|
+
};
|
package/lib/utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const { isPathInside } = require("./path-inside");
|
|
3
4
|
|
|
4
5
|
const HAS_SYNC_WAIT =
|
|
5
6
|
typeof SharedArrayBuffer === "function" &&
|
|
@@ -36,13 +37,7 @@ function normalizeRelativePath(relativePath) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function pathWithinRoot(projectRoot, candidatePath) {
|
|
39
|
-
|
|
40
|
-
const candidate = path.resolve(candidatePath);
|
|
41
|
-
if (candidate === root) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
45
|
-
return candidate.startsWith(prefix);
|
|
40
|
+
return isPathInside(projectRoot, candidatePath);
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
function uniqueValues(values) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { getStageById } = require("./workflow-contract");
|
|
2
|
+
const {
|
|
3
|
+
deriveStageFromArtifacts,
|
|
4
|
+
buildBaseHandoffGates
|
|
5
|
+
} = require("./workflow-stage");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} WorkflowFindings
|
|
9
|
+
* @property {string[]} blockers
|
|
10
|
+
* @property {string[]} warnings
|
|
11
|
+
* @property {string[]} notes
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} WorkflowArtifactState
|
|
16
|
+
* @property {boolean} workflowRootReady
|
|
17
|
+
* @property {boolean} changeSelected
|
|
18
|
+
* @property {boolean} proposal
|
|
19
|
+
* @property {string[]} specFiles
|
|
20
|
+
* @property {boolean} design
|
|
21
|
+
* @property {boolean} pencilDesign
|
|
22
|
+
* @property {boolean} pencilBindings
|
|
23
|
+
* @property {boolean} tasks
|
|
24
|
+
* @property {boolean} verification
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object.<string, string>} WorkflowCheckpointStatusMap
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} WorkflowCompletionAudit
|
|
33
|
+
* @property {string} [status]
|
|
34
|
+
* @property {string[]} [failures]
|
|
35
|
+
* @property {string[]} [warnings]
|
|
36
|
+
* @property {string[]} [notes]
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} PersistedWorkflowRecord
|
|
41
|
+
* @property {string} [stage]
|
|
42
|
+
* @property {Object.<string, string>} [gates]
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} WorkflowBaseView
|
|
47
|
+
* @property {"persisted"|"derived"} source
|
|
48
|
+
* @property {string} stageId
|
|
49
|
+
* @property {WorkflowFindings} findings
|
|
50
|
+
* @property {Object.<string, string>} baseGates
|
|
51
|
+
* @property {WorkflowCompletionAudit | null} completionAudit
|
|
52
|
+
* @property {Array<object>} taskGroupSeed
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
function cloneFindings(findings) {
|
|
56
|
+
return {
|
|
57
|
+
blockers: Array.isArray(findings && findings.blockers) ? findings.blockers.slice() : [],
|
|
58
|
+
warnings: Array.isArray(findings && findings.warnings) ? findings.warnings.slice() : [],
|
|
59
|
+
notes: Array.isArray(findings && findings.notes) ? findings.notes.slice() : []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveCompletionAudit(stageId, resolver) {
|
|
64
|
+
if (typeof resolver !== "function") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return resolver(stageId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Normalize a trusted persisted workflow snapshot into the shared base-view shape.
|
|
72
|
+
*
|
|
73
|
+
* @param {{
|
|
74
|
+
* persistedRecord?: PersistedWorkflowRecord,
|
|
75
|
+
* findings?: WorkflowFindings,
|
|
76
|
+
* taskGroupSeed?: Array<object>,
|
|
77
|
+
* resolveCompletionAudit?: (stageId: string) => (WorkflowCompletionAudit | null)
|
|
78
|
+
* }} [options]
|
|
79
|
+
* @returns {WorkflowBaseView}
|
|
80
|
+
*/
|
|
81
|
+
function buildPersistedWorkflowBaseView(options = {}) {
|
|
82
|
+
const persistedRecord = options.persistedRecord || {};
|
|
83
|
+
const stage = getStageById(persistedRecord.stage) || getStageById("bootstrap");
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
source: "persisted",
|
|
87
|
+
stageId: stage.id,
|
|
88
|
+
findings: cloneFindings(options.findings),
|
|
89
|
+
baseGates:
|
|
90
|
+
persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
|
|
91
|
+
? { ...persistedRecord.gates }
|
|
92
|
+
: {},
|
|
93
|
+
completionAudit: resolveCompletionAudit(stage.id, options.resolveCompletionAudit),
|
|
94
|
+
taskGroupSeed: Array.isArray(options.taskGroupSeed) ? options.taskGroupSeed : []
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the artifact/checkpoint-backed base view before runtime overlays apply.
|
|
100
|
+
*
|
|
101
|
+
* @param {{
|
|
102
|
+
* artifactState?: WorkflowArtifactState,
|
|
103
|
+
* checkpointStatuses?: WorkflowCheckpointStatusMap,
|
|
104
|
+
* findings?: WorkflowFindings,
|
|
105
|
+
* taskGroupSeed?: Array<object>,
|
|
106
|
+
* resolveCompletionAudit?: (stageId: string) => (WorkflowCompletionAudit | null)
|
|
107
|
+
* }} [options]
|
|
108
|
+
* @returns {WorkflowBaseView}
|
|
109
|
+
*/
|
|
110
|
+
function buildDerivedWorkflowBaseView(options = {}) {
|
|
111
|
+
const findings = cloneFindings(options.findings);
|
|
112
|
+
const stageId = deriveStageFromArtifacts(
|
|
113
|
+
options.artifactState || {},
|
|
114
|
+
options.checkpointStatuses || {},
|
|
115
|
+
findings
|
|
116
|
+
);
|
|
117
|
+
const completionAudit = resolveCompletionAudit(stageId, options.resolveCompletionAudit);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
source: "derived",
|
|
121
|
+
stageId,
|
|
122
|
+
findings,
|
|
123
|
+
baseGates: buildBaseHandoffGates(options.artifactState || {}, options.checkpointStatuses || {}, {
|
|
124
|
+
completionAudit
|
|
125
|
+
}),
|
|
126
|
+
completionAudit,
|
|
127
|
+
taskGroupSeed: Array.isArray(options.taskGroupSeed) ? options.taskGroupSeed : []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
buildPersistedWorkflowBaseView,
|
|
133
|
+
buildDerivedWorkflowBaseView
|
|
134
|
+
};
|
|
@@ -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
|
+
};
|