@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/CHANGELOG.md +32 -0
- package/README.md +15 -9
- package/README.zh-CN.md +16 -9
- package/docs/dv-command-reference.md +21 -3
- package/docs/execution-chain-migration.md +14 -3
- package/docs/maintainer-bootstrap.md +102 -0
- package/docs/pencil-rendering-workflow.md +1 -1
- package/docs/skill-usage.md +31 -0
- package/docs/workflow-overview.md +40 -5
- package/docs/zh-CN/dv-command-reference.md +19 -3
- package/docs/zh-CN/maintainer-bootstrap.md +101 -0
- package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
- package/docs/zh-CN/skill-usage.md +30 -0
- package/docs/zh-CN/workflow-overview.md +38 -5
- package/lib/audit.js +19 -0
- package/lib/cli/helpers.js +63 -2
- package/lib/cli.js +119 -2
- package/lib/gate-utils.js +56 -0
- package/lib/install.js +134 -6
- package/lib/isolated-worker-handoff.js +181 -0
- package/lib/lint-bindings.js +41 -28
- package/lib/lint-spec.js +403 -109
- package/lib/lint-tasks.js +571 -21
- package/lib/maintainer-readiness.js +317 -0
- package/lib/planning-parsers.js +190 -1
- package/lib/planning-quality-utils.js +81 -0
- package/lib/planning-signal-freshness.js +205 -0
- package/lib/scope-check.js +751 -82
- package/lib/sidecars.js +396 -1
- package/lib/supervisor-review.js +117 -6
- package/lib/task-execution.js +88 -16
- package/lib/task-review.js +14 -8
- package/lib/utils.js +15 -0
- package/lib/workflow-persisted-state.js +52 -32
- package/lib/workflow-state.js +1241 -249
- package/package.json +3 -2
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
|
|
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) => !
|
|
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(
|
|
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
|
+
};
|
package/lib/lint-bindings.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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 =
|
|
61
|
+
result.changeId = path.basename(changeDir);
|
|
55
62
|
|
|
56
|
-
const artifactPaths = readChangeArtifacts(projectRoot,
|
|
63
|
+
const artifactPaths = readChangeArtifacts(projectRoot, result.changeId);
|
|
57
64
|
const artifacts = readArtifactTexts(artifactPaths);
|
|
58
65
|
if (!artifacts.bindings) {
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
bindingsGate.compatibility.push(
|
|
90
101
|
`Unresolved implementation landing for "${mapping.implementation}" (allowed by explicit notes).`
|
|
91
102
|
);
|
|
92
103
|
} else {
|
|
93
|
-
|
|
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
|
-
|
|
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
|
}
|