@xenonbyte/da-vinci-workflow 0.2.5 → 0.2.7

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/lib/install.js CHANGED
@@ -18,6 +18,7 @@ const REQUIRED_FILES = [
18
18
  ...listFiles(path.join(REPO_ROOT, "docs")).map((filePath) => path.relative(REPO_ROOT, filePath)),
19
19
  ...listFiles(path.join(REPO_ROOT, "examples")).map((filePath) => path.relative(REPO_ROOT, filePath))
20
20
  ];
21
+ const SUPPORTED_PLATFORMS = ["codex", "claude", "gemini"];
21
22
 
22
23
  const CODEX_PROMPT_TARGET_PAIRS = listFiles(path.join(REPO_ROOT, "commands", "codex", "prompts")).map(
23
24
  (filePath) => ({
@@ -90,7 +91,7 @@ function resolveHome(homeDir) {
90
91
 
91
92
  function parsePlatforms(raw) {
92
93
  if (!raw || raw === "all") {
93
- return ["codex", "claude", "gemini"];
94
+ return SUPPORTED_PLATFORMS.slice();
94
95
  }
95
96
 
96
97
  const platforms = raw
@@ -99,7 +100,7 @@ function parsePlatforms(raw) {
99
100
  .filter(Boolean);
100
101
 
101
102
  const unique = [...new Set(platforms)];
102
- const invalid = unique.filter((value) => !["codex", "claude", "gemini"].includes(value));
103
+ const invalid = unique.filter((value) => !SUPPORTED_PLATFORMS.includes(value));
103
104
 
104
105
  if (invalid.length > 0) {
105
106
  throw new Error(`Unsupported platform value: ${invalid.join(", ")}`);
@@ -108,6 +109,129 @@ function parsePlatforms(raw) {
108
109
  return unique;
109
110
  }
110
111
 
112
+ function summarizePlatformInstallConfidence(statusPayload) {
113
+ const status = statusPayload && typeof statusPayload === "object" ? statusPayload : {};
114
+ const codex = status.codex && typeof status.codex === "object" ? status.codex : {};
115
+ const claude = status.claude && typeof status.claude === "object" ? status.claude : {};
116
+ const gemini = status.gemini && typeof status.gemini === "object" ? status.gemini : {};
117
+
118
+ return {
119
+ codex: {
120
+ checks: {
121
+ prompt: codex.prompt === true,
122
+ skill: codex.skill === true
123
+ },
124
+ missing: [...(codex.promptMissing || []), ...(codex.skillMissing || [])],
125
+ mismatched: [...(codex.promptMismatched || []), ...(codex.skillMismatched || [])],
126
+ unreadable: [...(codex.promptUnreadable || []), ...(codex.skillUnreadable || [])]
127
+ },
128
+ claude: {
129
+ checks: {
130
+ command: claude.command === true,
131
+ actionSet: claude.actionSet === true
132
+ },
133
+ missing: [...(claude.commandMissing || []), ...(claude.actionSetMissing || [])],
134
+ mismatched: [...(claude.commandMismatched || []), ...(claude.actionSetMismatched || [])],
135
+ unreadable: [...(claude.commandUnreadable || []), ...(claude.actionSetUnreadable || [])]
136
+ },
137
+ gemini: {
138
+ checks: {
139
+ command: gemini.command === true,
140
+ actionSet: gemini.actionSet === true
141
+ },
142
+ missing: [...(gemini.commandMissing || []), ...(gemini.actionSetMissing || [])],
143
+ mismatched: [...(gemini.commandMismatched || []), ...(gemini.actionSetMismatched || [])],
144
+ unreadable: [...(gemini.commandUnreadable || []), ...(gemini.actionSetUnreadable || [])]
145
+ }
146
+ };
147
+ }
148
+
149
+ function verifyInstall(options = {}) {
150
+ const homeDir = resolveHome(options.homeDir);
151
+ const explicitScopeRaw = String(options.platforms || "").trim();
152
+ const scopeKnown = explicitScopeRaw.length > 0;
153
+ const selectedPlatforms = scopeKnown ? parsePlatforms(explicitScopeRaw) : [];
154
+ const status = getStatus({ homeDir });
155
+ const confidence = summarizePlatformInstallConfidence(status);
156
+ const platformCoverage = {};
157
+ const failures = [];
158
+ const warnings = [];
159
+ const notes = [];
160
+
161
+ for (const platform of SUPPORTED_PLATFORMS) {
162
+ const selected = selectedPlatforms.includes(platform);
163
+ const scope = scopeKnown ? (selected ? "selected" : "out_of_scope") : "unknown";
164
+ const platformConfidence = confidence[platform];
165
+ const failedChecks = Object.keys(platformConfidence.checks).filter(
166
+ (checkKey) => platformConfidence.checks[checkKey] !== true
167
+ );
168
+ const healthy = failedChecks.length === 0;
169
+ platformCoverage[platform] = {
170
+ scope,
171
+ healthy,
172
+ checks: platformConfidence.checks,
173
+ missing: platformConfidence.missing,
174
+ mismatched: platformConfidence.mismatched,
175
+ unreadable: platformConfidence.unreadable
176
+ };
177
+
178
+ if (scope === "selected" && !healthy) {
179
+ failures.push(
180
+ `${platform} install verification failed for selected scope (${failedChecks.join(
181
+ ", "
182
+ )}). Run \`da-vinci install --platform ${platform}\` and re-run verify-install.`
183
+ );
184
+ }
185
+ if (scope === "unknown" && !healthy) {
186
+ warnings.push(
187
+ `${platform} install verification is incomplete, but selected platform scope is unknown.`
188
+ );
189
+ }
190
+ if (scope === "out_of_scope" && !healthy) {
191
+ notes.push(
192
+ `${platform} is out-of-scope for this verify-install run and is reported as degraded coverage.`
193
+ );
194
+ }
195
+ }
196
+
197
+ if (!scopeKnown) {
198
+ warnings.push(
199
+ "Selected platform scope is unknown; pass `--platform <value>` so verify-install can evaluate selected targets explicitly."
200
+ );
201
+ }
202
+
203
+ const statusToken = failures.length > 0 ? "BLOCK" : warnings.length > 0 ? "WARN" : "PASS";
204
+ const nextSteps = [];
205
+ if (failures.length > 0) {
206
+ nextSteps.push("Fix selected-platform install failures, then rerun `da-vinci verify-install`.");
207
+ }
208
+ if (!scopeKnown) {
209
+ nextSteps.push(
210
+ "Rerun with explicit scope, for example `da-vinci verify-install --platform codex`, to avoid unknown-scope coverage."
211
+ );
212
+ }
213
+ if (statusToken === "PASS") {
214
+ nextSteps.push(
215
+ "Continue with repository readiness checks (for example `da-vinci maintainer-readiness`)."
216
+ );
217
+ }
218
+
219
+ return {
220
+ status: statusToken,
221
+ homeDir,
222
+ scope: {
223
+ known: scopeKnown,
224
+ selectedPlatforms,
225
+ supportedPlatforms: SUPPORTED_PLATFORMS.slice()
226
+ },
227
+ platformCoverage,
228
+ failures: failures,
229
+ warnings: warnings,
230
+ notes: notes,
231
+ nextSteps
232
+ };
233
+ }
234
+
111
235
  function ensureDir(targetPath) {
112
236
  fs.mkdirSync(targetPath, { recursive: true });
113
237
  }
@@ -407,25 +531,29 @@ function getStatus(options = {}) {
407
531
  };
408
532
  }
409
533
 
410
- function validateAssets() {
534
+ function validateAssets(options = {}) {
535
+ const repoRoot = options.repoRoot ? path.resolve(String(options.repoRoot)) : REPO_ROOT;
411
536
  const missing = REQUIRED_FILES.filter((relativePath) => {
412
- return !fs.existsSync(path.join(REPO_ROOT, relativePath));
537
+ return !fs.existsSync(path.join(repoRoot, relativePath));
413
538
  });
414
539
 
415
540
  if (missing.length > 0) {
416
- throw new Error(`Missing required assets:\n${missing.join("\n")}`);
541
+ throw new Error(`Missing required assets under ${repoRoot}:\n${missing.join("\n")}`);
417
542
  }
418
543
 
419
544
  return {
420
545
  version: VERSION,
421
- requiredAssets: REQUIRED_FILES.length
546
+ requiredAssets: REQUIRED_FILES.length,
547
+ repoRoot
422
548
  };
423
549
  }
424
550
 
425
551
  module.exports = {
426
552
  VERSION,
553
+ SUPPORTED_PLATFORMS,
427
554
  installPlatforms,
428
555
  uninstallPlatforms,
429
556
  getStatus,
557
+ verifyInstall,
430
558
  validateAssets
431
559
  };
@@ -0,0 +1,181 @@
1
+ const VALID_IMPLEMENTER_RESULT_STATUSES = new Set([
2
+ "DONE",
3
+ "DONE_WITH_CONCERNS",
4
+ "NEEDS_CONTEXT",
5
+ "BLOCKED"
6
+ ]);
7
+
8
+ const IMPLEMENTER_INPUT_REQUIRED_FIELDS = Object.freeze([
9
+ "changeId",
10
+ "taskGroupId",
11
+ "title",
12
+ "executionIntent",
13
+ "targetFiles",
14
+ "fileReferences",
15
+ "reviewIntent",
16
+ "verificationActions",
17
+ "verificationCommands",
18
+ "canonicalProjectRoot",
19
+ "isolatedWorkspaceRoot"
20
+ ]);
21
+
22
+ const IMPLEMENTER_RESULT_REQUIRED_FIELDS = Object.freeze([
23
+ "changeId",
24
+ "taskGroupId",
25
+ "status",
26
+ "summary",
27
+ "changedFiles",
28
+ "testEvidence",
29
+ "concerns",
30
+ "blockers",
31
+ "outOfScopeWrites",
32
+ "recordedAt"
33
+ ]);
34
+
35
+ const IMPLEMENTER_PROGRESS_REQUIRED_FIELDS = Object.freeze([
36
+ ...IMPLEMENTER_RESULT_REQUIRED_FIELDS,
37
+ "partial"
38
+ ]);
39
+
40
+ function normalizeString(value) {
41
+ return String(value || "").trim();
42
+ }
43
+
44
+ function normalizeList(value) {
45
+ const source = Array.isArray(value)
46
+ ? value
47
+ : String(value || "")
48
+ .split(/[,\n;]/)
49
+ .map((item) => item.trim());
50
+ return Array.from(
51
+ new Set(
52
+ source
53
+ .map((item) => String(item || "").trim())
54
+ .filter(Boolean)
55
+ )
56
+ );
57
+ }
58
+
59
+ function assertRequiredFields(payload, requiredFields, label) {
60
+ const missing = requiredFields.filter((field) => !Object.prototype.hasOwnProperty.call(payload || {}, field));
61
+ if (missing.length > 0) {
62
+ throw new Error(`${label} is missing required fields: ${missing.join(", ")}`);
63
+ }
64
+ }
65
+
66
+ function assertNoUnknownFields(payload, allowedFields, label) {
67
+ const allowed = new Set(allowedFields);
68
+ const unknown = Object.keys(payload || {}).filter((field) => !allowed.has(field));
69
+ if (unknown.length > 0) {
70
+ throw new Error(`${label} contains unsupported fields: ${unknown.join(", ")}`);
71
+ }
72
+ }
73
+
74
+ function normalizeStatus(status) {
75
+ const normalized = normalizeString(status).toUpperCase();
76
+ if (!VALID_IMPLEMENTER_RESULT_STATUSES.has(normalized)) {
77
+ throw new Error(
78
+ `isolated implementer result status must be one of ${Array.from(VALID_IMPLEMENTER_RESULT_STATUSES).join(", ")}.`
79
+ );
80
+ }
81
+ return normalized;
82
+ }
83
+
84
+ function normalizeRecordedAt(value, label) {
85
+ const normalized = normalizeString(value);
86
+ if (!normalized) {
87
+ throw new Error(`${label} requires recordedAt.`);
88
+ }
89
+ const parsed = Date.parse(normalized);
90
+ if (!Number.isFinite(parsed)) {
91
+ throw new Error(`${label} recordedAt must be a valid ISO-8601 timestamp.`);
92
+ }
93
+ return new Date(parsed).toISOString();
94
+ }
95
+
96
+ function normalizeIsolatedImplementerInputPayload(payload = {}) {
97
+ assertRequiredFields(payload, IMPLEMENTER_INPUT_REQUIRED_FIELDS, "isolated implementer input payload");
98
+ assertNoUnknownFields(payload, IMPLEMENTER_INPUT_REQUIRED_FIELDS, "isolated implementer input payload");
99
+
100
+ const changeId = normalizeString(payload.changeId);
101
+ const taskGroupId = normalizeString(payload.taskGroupId);
102
+ const title = normalizeString(payload.title);
103
+ const canonicalProjectRoot = normalizeString(payload.canonicalProjectRoot);
104
+ const isolatedWorkspaceRoot = normalizeString(payload.isolatedWorkspaceRoot);
105
+ if (!changeId || !taskGroupId || !title || !canonicalProjectRoot || !isolatedWorkspaceRoot) {
106
+ throw new Error(
107
+ "isolated implementer input payload requires non-empty changeId, taskGroupId, title, canonicalProjectRoot, and isolatedWorkspaceRoot."
108
+ );
109
+ }
110
+
111
+ return {
112
+ changeId,
113
+ taskGroupId,
114
+ title,
115
+ executionIntent: normalizeList(payload.executionIntent),
116
+ targetFiles: normalizeList(payload.targetFiles),
117
+ fileReferences: normalizeList(payload.fileReferences),
118
+ reviewIntent: payload.reviewIntent === true,
119
+ verificationActions: normalizeList(payload.verificationActions),
120
+ verificationCommands: normalizeList(payload.verificationCommands),
121
+ canonicalProjectRoot,
122
+ isolatedWorkspaceRoot
123
+ };
124
+ }
125
+
126
+ function normalizeIsolatedImplementerResultPayload(payload = {}) {
127
+ assertRequiredFields(payload, IMPLEMENTER_RESULT_REQUIRED_FIELDS, "isolated implementer result payload");
128
+ assertNoUnknownFields(payload, IMPLEMENTER_RESULT_REQUIRED_FIELDS, "isolated implementer result payload");
129
+
130
+ const changeId = normalizeString(payload.changeId);
131
+ const taskGroupId = normalizeString(payload.taskGroupId);
132
+ const summary = normalizeString(payload.summary);
133
+ if (!changeId || !taskGroupId || !summary) {
134
+ throw new Error("isolated implementer result payload requires non-empty changeId, taskGroupId, and summary.");
135
+ }
136
+
137
+ return {
138
+ changeId,
139
+ taskGroupId,
140
+ status: normalizeStatus(payload.status),
141
+ summary,
142
+ changedFiles: normalizeList(payload.changedFiles),
143
+ testEvidence: normalizeList(payload.testEvidence),
144
+ concerns: normalizeList(payload.concerns),
145
+ blockers: normalizeList(payload.blockers),
146
+ outOfScopeWrites: normalizeList(payload.outOfScopeWrites),
147
+ recordedAt: normalizeRecordedAt(payload.recordedAt, "isolated implementer result payload")
148
+ };
149
+ }
150
+
151
+ function normalizeIsolatedImplementerProgressPayload(payload = {}) {
152
+ assertRequiredFields(payload, IMPLEMENTER_PROGRESS_REQUIRED_FIELDS, "isolated implementer progress payload");
153
+ assertNoUnknownFields(payload, IMPLEMENTER_PROGRESS_REQUIRED_FIELDS, "isolated implementer progress payload");
154
+ if (payload.partial !== true) {
155
+ throw new Error("isolated implementer progress payload requires partial=true.");
156
+ }
157
+
158
+ const {
159
+ partial: _partial,
160
+ ...resultShape
161
+ } = payload;
162
+ const normalizedResult = normalizeIsolatedImplementerResultPayload(resultShape);
163
+ if (normalizedResult.status === "DONE") {
164
+ throw new Error("isolated implementer progress payload cannot use status DONE because partial snapshots are non-final.");
165
+ }
166
+
167
+ return {
168
+ ...normalizedResult,
169
+ partial: true
170
+ };
171
+ }
172
+
173
+ module.exports = {
174
+ VALID_IMPLEMENTER_RESULT_STATUSES,
175
+ IMPLEMENTER_INPUT_REQUIRED_FIELDS,
176
+ IMPLEMENTER_RESULT_REQUIRED_FIELDS,
177
+ IMPLEMENTER_PROGRESS_REQUIRED_FIELDS,
178
+ normalizeIsolatedImplementerInputPayload,
179
+ normalizeIsolatedImplementerResultPayload,
180
+ normalizeIsolatedImplementerProgressPayload
181
+ };
@@ -1,23 +1,24 @@
1
1
  const path = require("path");
2
- const { STATUS } = require("./workflow-contract");
3
2
  const {
4
3
  unique,
5
4
  resolveImplementationLanding,
6
- resolveChangeDir,
7
5
  parseBindingsArtifact,
8
6
  readChangeArtifacts,
9
7
  readArtifactTexts
10
8
  } = require("./planning-parsers");
9
+ const { buildGateEnvelope, finalizeGateEnvelope } = require("./gate-utils");
10
+ const {
11
+ buildBasePlanningResultEnvelope,
12
+ finalizePlanningResult,
13
+ resolveChangeWithFindings
14
+ } = require("./planning-quality-utils");
11
15
 
12
16
  function buildEnvelope(projectRoot, strict) {
13
17
  return {
14
- status: STATUS.PASS,
15
- failures: [],
16
- warnings: [],
17
- notes: [],
18
- projectRoot,
19
- changeId: null,
20
- strict,
18
+ ...buildBasePlanningResultEnvelope(projectRoot, strict),
19
+ gates: {
20
+ bindingsHealth: null
21
+ },
21
22
  summary: {
22
23
  mappings: 0,
23
24
  malformed: 0
@@ -29,13 +30,19 @@ function finalize(result) {
29
30
  result.failures = unique(result.failures);
30
31
  result.warnings = unique(result.warnings);
31
32
  result.notes = unique(result.notes);
32
- const hasFindings = result.failures.length > 0 || result.warnings.length > 0;
33
- if (!hasFindings) {
34
- result.status = STATUS.PASS;
35
- return result;
33
+ return finalizePlanningResult(result);
34
+ }
35
+
36
+ function attachBindingsGateFindings(result, gate) {
37
+ for (const message of gate.blocking || []) {
38
+ result.failures.push(`[gate:bindingsHealth] ${message}`);
39
+ }
40
+ for (const message of gate.advisory || []) {
41
+ result.warnings.push(`[gate:bindingsHealth] ${message}`);
42
+ }
43
+ for (const message of gate.compatibility || []) {
44
+ result.notes.push(`[gate:bindingsHealth] ${message}`);
36
45
  }
37
- result.status = result.strict ? STATUS.BLOCK : STATUS.WARN;
38
- return result;
39
46
  }
40
47
 
41
48
  function lintBindings(projectPathInput, options = {}) {
@@ -43,20 +50,22 @@ function lintBindings(projectPathInput, options = {}) {
43
50
  const strict = options.strict === true;
44
51
  const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
45
52
  const result = buildEnvelope(projectRoot, strict);
53
+ const bindingsGate = buildGateEnvelope("bindingsHealth");
46
54
 
47
- const resolved = resolveChangeDir(projectRoot, requestedChangeId);
48
- result.failures.push(...resolved.failures);
49
- result.notes.push(...resolved.notes);
50
- if (!resolved.changeDir) {
55
+ const changeDir = resolveChangeWithFindings(projectRoot, requestedChangeId, result.failures, result.notes);
56
+ if (!changeDir) {
57
+ result.gates.bindingsHealth = finalizeGateEnvelope(bindingsGate, { strict });
51
58
  result.notes.push("lint-bindings defaults to advisory mode; pass `--strict` to block on findings.");
52
59
  return finalize(result);
53
60
  }
54
- result.changeId = resolved.changeId;
61
+ result.changeId = path.basename(changeDir);
55
62
 
56
- const artifactPaths = readChangeArtifacts(projectRoot, resolved.changeId);
63
+ const artifactPaths = readChangeArtifacts(projectRoot, result.changeId);
57
64
  const artifacts = readArtifactTexts(artifactPaths);
58
65
  if (!artifacts.bindings) {
59
- result.failures.push("Missing `pencil-bindings.md` for lint-bindings.");
66
+ bindingsGate.blocking.push("Missing `pencil-bindings.md` for lint-bindings.");
67
+ result.gates.bindingsHealth = finalizeGateEnvelope(bindingsGate, { strict });
68
+ attachBindingsGateFindings(result, result.gates.bindingsHealth);
60
69
  result.notes.push("lint-bindings defaults to advisory mode; pass `--strict` to block on findings.");
61
70
  return finalize(result);
62
71
  }
@@ -66,17 +75,19 @@ function lintBindings(projectPathInput, options = {}) {
66
75
  result.summary.malformed = parsed.malformed.length;
67
76
 
68
77
  if (parsed.mappings.length === 0) {
69
- result.failures.push("No implementation-to-Pencil mappings were parsed from `pencil-bindings.md`.");
78
+ bindingsGate.blocking.push(
79
+ "No implementation-to-Pencil mappings were parsed from `pencil-bindings.md`."
80
+ );
70
81
  }
71
82
  if (parsed.malformed.length > 0) {
72
83
  for (const malformed of parsed.malformed) {
73
- result.warnings.push(`Malformed binding mapping entry: "${malformed}".`);
84
+ bindingsGate.advisory.push(`Malformed binding mapping entry: "${malformed}".`);
74
85
  }
75
86
  }
76
87
 
77
88
  for (const mapping of parsed.mappings) {
78
89
  if (!mapping.implementation || !mapping.designPage) {
79
- result.warnings.push(`Malformed binding mapping entry: "${mapping.raw}".`);
90
+ bindingsGate.advisory.push(`Malformed binding mapping entry: "${mapping.raw}".`);
80
91
  continue;
81
92
  }
82
93
 
@@ -86,21 +97,23 @@ function lintBindings(projectPathInput, options = {}) {
86
97
  /missing|todo|gap|pending|temporary/i.test(note)
87
98
  );
88
99
  if (noteContainsIntentionalGap) {
89
- result.warnings.push(
100
+ bindingsGate.compatibility.push(
90
101
  `Unresolved implementation landing for "${mapping.implementation}" (allowed by explicit notes).`
91
102
  );
92
103
  } else {
93
- result.warnings.push(`Unresolved implementation landing for "${mapping.implementation}".`);
104
+ bindingsGate.advisory.push(`Unresolved implementation landing for "${mapping.implementation}".`);
94
105
  }
95
106
  }
96
107
 
97
108
  if (mapping.designSource && !String(mapping.designSource).includes(".pen")) {
98
- result.warnings.push(
109
+ bindingsGate.advisory.push(
99
110
  `Binding source for "${mapping.implementation}" does not look like a .pen path: "${mapping.designSource}".`
100
111
  );
101
112
  }
102
113
  }
103
114
 
115
+ result.gates.bindingsHealth = finalizeGateEnvelope(bindingsGate, { strict });
116
+ attachBindingsGateFindings(result, result.gates.bindingsHealth);
104
117
  result.notes.push("lint-bindings defaults to advisory mode; pass `--strict` to block on findings.");
105
118
  return finalize(result);
106
119
  }