@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.
@@ -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
- const root = path.resolve(projectRoot);
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
+ };