@captain_z/zsk 1.8.3 → 1.8.4
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/dist/commands/add-flow.d.ts +3 -7
- package/dist/commands/add-flow.js +7 -59
- package/dist/commands/add-flow.js.map +1 -1
- package/dist/commands/add.js +25 -104
- package/dist/commands/add.js.map +1 -1
- package/dist/core/prepare-sync.d.ts +2 -31
- package/dist/core/prepare-sync.js +96 -128
- package/dist/core/prepare-sync.js.map +1 -1
- package/dist/core/profile-bundle-installation.d.ts +55 -0
- package/dist/core/profile-bundle-installation.js +170 -0
- package/dist/core/profile-bundle-installation.js.map +1 -0
- package/dist/core/source-snapshot-adapters.d.ts +59 -0
- package/dist/core/source-snapshot-adapters.js +82 -0
- package/dist/core/source-snapshot-adapters.js.map +1 -0
- package/dist/core/staffing-plan.js +52 -18
- package/dist/core/staffing-plan.js.map +1 -1
- package/dist/core/stage-clarity-verification.d.ts +31 -0
- package/dist/core/stage-clarity-verification.js +313 -0
- package/dist/core/stage-clarity-verification.js.map +1 -0
- package/dist/core/stage-quality-artifacts.d.ts +15 -0
- package/dist/core/stage-quality-artifacts.js +421 -0
- package/dist/core/stage-quality-artifacts.js.map +1 -0
- package/dist/core/stage-quality-contracts.d.ts +86 -0
- package/dist/core/stage-quality-contracts.js +2 -0
- package/dist/core/stage-quality-contracts.js.map +1 -0
- package/dist/core/stage-quality-criteria.d.ts +9 -0
- package/dist/core/stage-quality-criteria.js +323 -0
- package/dist/core/stage-quality-criteria.js.map +1 -0
- package/dist/core/stage-quality-rendering.d.ts +13 -0
- package/dist/core/stage-quality-rendering.js +122 -0
- package/dist/core/stage-quality-rendering.js.map +1 -0
- package/dist/core/stage-quality.d.ts +4 -59
- package/dist/core/stage-quality.js +39 -791
- package/dist/core/stage-quality.js.map +1 -1
- package/package.json +2 -2
- package/templates/module/frontend-module/design.md +10 -0
- package/templates/module/frontend-module/proposal.md +8 -0
- package/templates/module/frontend-module/spec.md +7 -0
- package/templates/project-init/.zsk/config.yaml +33 -0
- package/templates/project-init/.zsk/docs/PROJECT-CONFIG.md +43 -0
- package/templates/project-init/.zsk/docs/SYSTEM-SPEC.md +22 -2
- package/templates/project-init/.zsk/raws/index.md +16 -0
- package/templates/project-init/.zsk/raws/prepare/design/index.md +18 -0
- package/templates/project-init/.zsk/raws/prepare/index.md +33 -0
- package/templates/project-init/.zsk/raws/prepare/ux/index.md +19 -0
- package/templates/project-init/.zsk/roles.yaml +3 -3
|
@@ -1,309 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { listStageQualityInputStages, normalizeReviewTargetPath, readStageQualityArtifactContents, readStageQualityArtifacts, resolveStageQualityModuleId, reviewModeForReviewTarget, } from "./stage-quality-artifacts.js";
|
|
5
|
+
import { DEFAULT_STAGE_QUALITY_THRESHOLD } from "./stage-quality-contracts.js";
|
|
6
|
+
import { evaluateStageQualityCriteria } from "./stage-quality-criteria.js";
|
|
7
|
+
import { renderGateAssessmentMarkdown, renderWaiverMarkdown, summarizeStageQualityDecision, } from "./stage-quality-rendering.js";
|
|
5
8
|
import { getWorkspacePath } from "./workspace-layout.js";
|
|
6
|
-
|
|
7
|
-
const MODULE_STAGES = new Set(["proposal", "spec", "design", "task", "coding", "fix", "smoke", "review", "ready", "verify", "acceptance", "archive"]);
|
|
8
|
-
const SPLITTABLE_MODULE_STAGES = new Set(["proposal", "spec", "design", "tasks"]);
|
|
9
|
-
const SKIPPED_REVIEW_TARGET_DIRS = new Set([
|
|
10
|
-
".auth",
|
|
11
|
-
".cache",
|
|
12
|
-
".git",
|
|
13
|
-
".next",
|
|
14
|
-
".turbo",
|
|
15
|
-
"coverage",
|
|
16
|
-
"dist",
|
|
17
|
-
"node_modules",
|
|
18
|
-
]);
|
|
19
|
-
const STAGE_INPUTS = {
|
|
20
|
-
prepare: [
|
|
21
|
-
{ key: "project-config", path: (config) => getWorkspacePath(config, "projectConfig") },
|
|
22
|
-
{ key: "system-spec", path: (config) => getWorkspacePath(config, "systemSpec") },
|
|
23
|
-
],
|
|
24
|
-
preproposal: [],
|
|
25
|
-
proposal: [
|
|
26
|
-
{ key: "project-config", path: (config) => getWorkspacePath(config, "projectConfig") },
|
|
27
|
-
{ key: "system-spec", path: (config) => getWorkspacePath(config, "systemSpec") },
|
|
28
|
-
{ key: "raw-manifest", path: (config) => getWorkspacePath(config, "rawsManifest") },
|
|
29
|
-
],
|
|
30
|
-
spec: [
|
|
31
|
-
{ key: "project-config", path: (config) => getWorkspacePath(config, "projectConfig") },
|
|
32
|
-
{ key: "system-spec", path: (config) => getWorkspacePath(config, "systemSpec") },
|
|
33
|
-
{ key: "proposal", moduleStage: "proposal" },
|
|
34
|
-
],
|
|
35
|
-
design: [
|
|
36
|
-
{ key: "project-config", path: (config) => getWorkspacePath(config, "projectConfig") },
|
|
37
|
-
{ key: "system-spec", path: (config) => getWorkspacePath(config, "systemSpec") },
|
|
38
|
-
{ key: "spec", moduleStage: "spec" },
|
|
39
|
-
],
|
|
40
|
-
task: [
|
|
41
|
-
{ key: "project-config", path: (config) => getWorkspacePath(config, "projectConfig") },
|
|
42
|
-
{ key: "system-spec", path: (config) => getWorkspacePath(config, "systemSpec") },
|
|
43
|
-
{ key: "proposal", moduleStage: "proposal" },
|
|
44
|
-
{ key: "spec", moduleStage: "spec" },
|
|
45
|
-
{ key: "design", moduleStage: "design" },
|
|
46
|
-
],
|
|
47
|
-
coding: [
|
|
48
|
-
{ key: "tasks", moduleStage: "tasks" },
|
|
49
|
-
],
|
|
50
|
-
fix: [],
|
|
51
|
-
smoke: [
|
|
52
|
-
{ key: "tasks", moduleStage: "tasks" },
|
|
53
|
-
],
|
|
54
|
-
review: [
|
|
55
|
-
{ key: "spec", moduleStage: "spec" },
|
|
56
|
-
{ key: "design", moduleStage: "design" },
|
|
57
|
-
{ key: "tasks", moduleStage: "tasks" },
|
|
58
|
-
{ key: "smoke", moduleStage: "smoke" },
|
|
59
|
-
],
|
|
60
|
-
ready: [
|
|
61
|
-
{ key: "review", moduleStage: "review" },
|
|
62
|
-
],
|
|
63
|
-
verify: [
|
|
64
|
-
{ key: "ready", moduleStage: "ready" },
|
|
65
|
-
],
|
|
66
|
-
acceptance: [
|
|
67
|
-
{ key: "verify", moduleStage: "verify" },
|
|
68
|
-
],
|
|
69
|
-
archive: [
|
|
70
|
-
{ key: "acceptance", moduleStage: "acceptance" },
|
|
71
|
-
],
|
|
72
|
-
};
|
|
73
|
-
const COMMON_CRITERIA = [
|
|
74
|
-
{
|
|
75
|
-
id: "GATE-INPUT-01",
|
|
76
|
-
label: "Required upstream artifacts exist and are non-empty.",
|
|
77
|
-
weight: 2,
|
|
78
|
-
required: true,
|
|
79
|
-
test: (ctx) => ctx.artifacts.every((artifact) => artifact.exists && artifact.bytes > 0),
|
|
80
|
-
evidence: (ctx) => ctx.artifacts.map((artifact) => `${artifact.key}: ${artifact.exists ? `${artifact.bytes} bytes` : "missing"} (${artifact.path})`),
|
|
81
|
-
gap: "One or more required upstream artifacts are missing or empty.",
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: "GATE-RULES-01",
|
|
85
|
-
label: "PROJECT-CONFIG.md and SYSTEM-SPEC.md constraints are available or explicitly not required for this stage.",
|
|
86
|
-
weight: 1,
|
|
87
|
-
required: true,
|
|
88
|
-
test: (ctx) => {
|
|
89
|
-
if (["preproposal", "coding", "fix", "smoke", "review", "ready", "verify", "acceptance", "archive"].includes(ctx.stage))
|
|
90
|
-
return true;
|
|
91
|
-
return hasContent(ctx, "project-config") && hasContent(ctx, "system-spec");
|
|
92
|
-
},
|
|
93
|
-
evidence: (ctx) => ["project-config", "system-spec"].map((key) => `${key}: ${hasContent(ctx, key) ? "present" : "missing"}`),
|
|
94
|
-
gap: "Project/system rules are not available for a stage that must obey them before handoff.",
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: "GATE-TEMPLATE-01",
|
|
98
|
-
label: "Upstream content is not placeholder-only or template filler.",
|
|
99
|
-
weight: 1,
|
|
100
|
-
required: true,
|
|
101
|
-
test: (ctx) => allPrimaryContent(ctx, (content) => {
|
|
102
|
-
const text = content.toLowerCase();
|
|
103
|
-
const placeholders = ["<fill-me>", "<project", "<module", "todo:", "tbd", "lorem ipsum"];
|
|
104
|
-
return !placeholders.some((placeholder) => text.includes(placeholder));
|
|
105
|
-
}),
|
|
106
|
-
evidence: (ctx) => primaryArtifacts(ctx).map((artifact) => `${artifact.key}: ${artifact.bytes} bytes`),
|
|
107
|
-
gap: "At least one upstream artifact still looks like a placeholder or template filler.",
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: "GATE-CONTENT-01",
|
|
111
|
-
label: "Upstream content has enough detail for reliable handoff.",
|
|
112
|
-
weight: 1,
|
|
113
|
-
test: (ctx) => {
|
|
114
|
-
const artifacts = primaryArtifacts(ctx);
|
|
115
|
-
if (artifacts.length === 0)
|
|
116
|
-
return true;
|
|
117
|
-
return artifacts.every((artifact) => wordCount(ctx.contentByKey.get(artifact.key) ?? "") >= 40);
|
|
118
|
-
},
|
|
119
|
-
evidence: (ctx) => primaryArtifacts(ctx).map((artifact) => `${artifact.key}: ${wordCount(ctx.contentByKey.get(artifact.key) ?? "")} words`),
|
|
120
|
-
gap: "At least one upstream artifact is very short; treat this as a quality gap unless the project explicitly accepts the risk.",
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
id: "GATE-TRACE-01",
|
|
124
|
-
label: "Claims trace to source, requirement, acceptance, design, task, issue, or evidence identifiers.",
|
|
125
|
-
weight: 1,
|
|
126
|
-
test: (ctx) => combinedContent(ctx).match(/\b(FR|NFR|AC|SPEC|DES|TASK|ISSUE|EVIDENCE|ADR|REQ)-?\d+\b/i) !== null ||
|
|
127
|
-
(ctx.stage === "preproposal" && ctx.artifacts.length === 0) ||
|
|
128
|
-
/traceability|source|evidence|acceptance|scenario/i.test(combinedContent(ctx)),
|
|
129
|
-
evidence: (ctx) => [`trace terms found: ${/traceability|source|evidence|acceptance|scenario/i.test(combinedContent(ctx)) ? "yes" : "no"}`],
|
|
130
|
-
gap: "Traceability is weak; the next stage may rely on chat memory instead of source-backed IDs or evidence.",
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: "GATE-UNCERTAINTY-01",
|
|
134
|
-
label: "Open questions, assumptions, risks, or blockers are explicit instead of silently resolved.",
|
|
135
|
-
weight: 1,
|
|
136
|
-
test: (ctx) => (ctx.stage === "preproposal" && ctx.artifacts.length === 0) ||
|
|
137
|
-
/open questions?|assumptions?|risks?|blockers?|constraints?|unknowns?|n\/a|none/i.test(combinedContent(ctx)),
|
|
138
|
-
evidence: (ctx) => [`uncertainty section marker found: ${/open questions?|assumptions?|risks?|blockers?|constraints?|unknowns?/i.test(combinedContent(ctx)) ? "yes" : "n/a or none marker required"}`],
|
|
139
|
-
gap: "Uncertainty handling is not explicit enough for a downstream stage to decide whether to continue or ask.",
|
|
140
|
-
},
|
|
141
|
-
];
|
|
142
|
-
const STAGE_CRITERIA = {
|
|
143
|
-
design: [
|
|
144
|
-
{
|
|
145
|
-
id: "DESIGN-ENTRY-SPEC-01",
|
|
146
|
-
label: "Spec contains observable acceptance criteria.",
|
|
147
|
-
weight: 1,
|
|
148
|
-
required: true,
|
|
149
|
-
artifactKeys: ["spec"],
|
|
150
|
-
test: (ctx) => hasPattern(ctx, ["spec"], /acceptance criteria|验收|given\s+.*when\s+.*then|AC-?\d+/is),
|
|
151
|
-
evidence: () => ["spec acceptance marker required"],
|
|
152
|
-
gap: "Design cannot safely start until the spec exposes observable acceptance criteria.",
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
id: "DESIGN-ENTRY-SPEC-02",
|
|
156
|
-
label: "Spec covers edge, negative, boundary, or error scenarios.",
|
|
157
|
-
weight: 1,
|
|
158
|
-
artifactKeys: ["spec"],
|
|
159
|
-
test: (ctx) => hasPattern(ctx, ["spec"], /edge cases?|negative|boundary|error|failure|异常|边界/is),
|
|
160
|
-
evidence: () => ["spec edge/negative/boundary marker required"],
|
|
161
|
-
gap: "Spec lacks explicit non-happy-path coverage.",
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
id: "DESIGN-ENTRY-SPEC-03",
|
|
165
|
-
label: "Spec names NFR or quality attributes, with missing thresholds marked as 未指定 when needed.",
|
|
166
|
-
weight: 1,
|
|
167
|
-
artifactKeys: ["spec"],
|
|
168
|
-
test: (ctx) => hasPattern(ctx, ["spec"], /NFR|non-functional|performance|security|reliability|accessibility|observability|未指定|质量/is),
|
|
169
|
-
evidence: () => ["spec NFR/quality marker required"],
|
|
170
|
-
gap: "Spec does not expose quality attributes enough for design tradeoffs.",
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
id: "DESIGN-ENTRY-CONTROL-01",
|
|
174
|
-
label: "Permission/auth/privacy/data-access behavior has a behavior-level control matrix before design starts.",
|
|
175
|
-
weight: 2,
|
|
176
|
-
required: true,
|
|
177
|
-
artifactKeys: ["spec"],
|
|
178
|
-
test: (ctx) => !touchesControlConcern(ctx, ["spec"]) || hasControlMatrix(ctx, ["spec"]),
|
|
179
|
-
evidence: (ctx) => [`control concern: ${touchesControlConcern(ctx, ["spec"]) ? "triggered" : "not triggered"}`],
|
|
180
|
-
gap: "Spec touches a control concern but lacks a behavior-level control matrix.",
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
task: [
|
|
184
|
-
{
|
|
185
|
-
id: "TASK-ENTRY-DES-01",
|
|
186
|
-
label: "Design includes useful diagrams or explicitly states why diagrams are N/A.",
|
|
187
|
-
weight: 1,
|
|
188
|
-
artifactKeys: ["design"],
|
|
189
|
-
test: (ctx) => hasPattern(ctx, ["design"], /```mermaid|diagram|C4|sequence|state|data flow|flowchart|图|N\/A/is),
|
|
190
|
-
evidence: () => ["design diagram or N/A rationale marker required"],
|
|
191
|
-
gap: "Task breakdown needs a reviewable design view or explicit diagram N/A rationale.",
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
id: "TASK-ENTRY-DES-02",
|
|
195
|
-
label: "Design records ADR/decision-record entries for task planning.",
|
|
196
|
-
weight: 1,
|
|
197
|
-
required: true,
|
|
198
|
-
artifactKeys: ["design"],
|
|
199
|
-
test: (ctx) => hasPattern(ctx, ["design"], /ADR-?\d+|Architecture Decision Record|Decision Record|ADR\s*\/|##\s*ADR|决策记录/is),
|
|
200
|
-
evidence: () => ["design ADR/decision-record marker required"],
|
|
201
|
-
gap: "Design lacks ADR/decision-record entries required for task planning.",
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
id: "TASK-ENTRY-CONTROL-01",
|
|
205
|
-
label: "Triggered permission/auth/privacy/data-access behavior has a design control traceability matrix.",
|
|
206
|
-
weight: 2,
|
|
207
|
-
required: true,
|
|
208
|
-
artifactKeys: ["spec", "design"],
|
|
209
|
-
test: (ctx) => !touchesControlConcern(ctx, ["spec", "design"]) || hasControlMatrix(ctx, ["design"]),
|
|
210
|
-
evidence: (ctx) => [`control concern: ${touchesControlConcern(ctx, ["spec", "design"]) ? "triggered" : "not triggered"}`],
|
|
211
|
-
gap: "Spec or design touches a control concern but design lacks a control traceability matrix.",
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
coding: [
|
|
215
|
-
{
|
|
216
|
-
id: "CODING-ENTRY-TASK-01",
|
|
217
|
-
label: "Tasks use Kiro-style nested checkbox groups and subtasks.",
|
|
218
|
-
weight: 2,
|
|
219
|
-
required: true,
|
|
220
|
-
artifactKeys: ["tasks"],
|
|
221
|
-
test: (ctx) => hasPattern(ctx, ["tasks"], /^- \[ \] \d+\. .+$/m) && hasPattern(ctx, ["tasks"], /^\s+- \[ \] \d+\.\d+ .+$/m),
|
|
222
|
-
evidence: () => ["task list must contain '- [ ] 1.' and indented '- [ ] 1.1' items"],
|
|
223
|
-
gap: "Coding should not start from flat or vague tasks; use Kiro-style groups and subtasks.",
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
id: "CODING-ENTRY-TASK-02",
|
|
227
|
-
label: "Each implementation slice has verification or evidence hooks.",
|
|
228
|
-
weight: 1,
|
|
229
|
-
required: true,
|
|
230
|
-
artifactKeys: ["tasks"],
|
|
231
|
-
test: (ctx) => hasPattern(ctx, ["tasks"], /verification|evidence|test command|pnpm|npm|pytest|vitest|playwright|验证|证据/is),
|
|
232
|
-
evidence: () => ["task verification/evidence marker required"],
|
|
233
|
-
gap: "Tasks do not define how each slice proves completion.",
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
fix: [
|
|
237
|
-
{
|
|
238
|
-
id: "FIX-ENTRY-ISSUE-01",
|
|
239
|
-
label: "Issue anchor exposes expected behavior, actual behavior, and evidence.",
|
|
240
|
-
weight: 2,
|
|
241
|
-
required: true,
|
|
242
|
-
artifactKeys: ["issue-record"],
|
|
243
|
-
test: (ctx) => hasPattern(ctx, ["issue-record"], /expected|actual|evidence|repro|steps|root cause|fix route|预期|实际|证据|复现/is),
|
|
244
|
-
evidence: () => ["issue record must expose expected/actual/evidence markers"],
|
|
245
|
-
gap: "Fix should not start without an issue anchor that records expected/actual behavior and evidence.",
|
|
246
|
-
},
|
|
247
|
-
],
|
|
248
|
-
smoke: [
|
|
249
|
-
{
|
|
250
|
-
id: "SMOKE-ENTRY-TEST-01",
|
|
251
|
-
label: "Test basis is traceable before local smoke claims pass.",
|
|
252
|
-
weight: 2,
|
|
253
|
-
required: true,
|
|
254
|
-
artifactKeys: ["tasks"],
|
|
255
|
-
test: (ctx) => hasPattern(ctx, ["tasks"], /test basis|acceptance|AC-?\d+|FR-?\d+|scenario|regression|验收|回归/is),
|
|
256
|
-
evidence: () => ["tasks must link tests to acceptance/scenario/regression basis"],
|
|
257
|
-
gap: "Smoke cannot treat passing commands as meaningful without a traceable test basis.",
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
review: [
|
|
261
|
-
{
|
|
262
|
-
id: "REVIEW-TARGET-01",
|
|
263
|
-
label: "Explicit review target exists and is treated as the review contract when supplied.",
|
|
264
|
-
weight: 2,
|
|
265
|
-
required: true,
|
|
266
|
-
artifactKeys: ["review-target"],
|
|
267
|
-
test: (ctx) => !ctx.reviewTarget || hasContent(ctx, "review-target"),
|
|
268
|
-
evidence: (ctx) => [
|
|
269
|
-
`review target: ${ctx.reviewTarget ?? "module implementation review"}`,
|
|
270
|
-
`review mode: ${ctx.reviewMode ?? "implementation"}`,
|
|
271
|
-
],
|
|
272
|
-
gap: "The explicit review target is missing or has no reviewable content.",
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
id: "REVIEW-ENTRY-TEST-01",
|
|
276
|
-
label: "Smoke evidence distinguishes passed, failed, skipped, and unrun checks.",
|
|
277
|
-
weight: 1,
|
|
278
|
-
required: true,
|
|
279
|
-
artifactKeys: ["smoke"],
|
|
280
|
-
test: (ctx) => ctx.reviewMode !== "implementation" || hasPattern(ctx, ["smoke"], /passed|failed|skipped|unrun|exit status|command|evidence|通过|失败|跳过/is),
|
|
281
|
-
evidence: (ctx) => [ctx.reviewMode === "implementation" ? "smoke pass/fail/skipped/unrun markers required" : "explicit target review does not require module smoke input"],
|
|
282
|
-
gap: "Review needs precise test result status, not only a generic all-passed claim.",
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
id: "REVIEW-PREPROPOSAL-01",
|
|
286
|
-
label: "Preproposal/raw-source reviews check product, roadmap, UX, assumptions, or readiness evidence instead of module spec artifacts.",
|
|
287
|
-
weight: 1,
|
|
288
|
-
artifactKeys: ["review-target"],
|
|
289
|
-
test: (ctx) => !["preproposal", "raw-source"].includes(ctx.reviewMode ?? "implementation") ||
|
|
290
|
-
hasPattern(ctx, ["review-target"], /product|problem|user|persona|journey|roadmap|mvp|phase|ux|flow|assumption|risk|blocker|source|evidence|产品|用户|路线图|阶段|流程|假设|风险|证据/is),
|
|
291
|
-
evidence: (ctx) => [`target-aware raw review: ${["preproposal", "raw-source"].includes(ctx.reviewMode ?? "implementation") ? "required" : "n/a"}`],
|
|
292
|
-
gap: "Raw/preproposal review target lacks product, roadmap, UX, assumption, risk, source, or evidence markers.",
|
|
293
|
-
},
|
|
294
|
-
],
|
|
295
|
-
};
|
|
296
|
-
function unknownStageCriterion(stage) {
|
|
297
|
-
return {
|
|
298
|
-
id: "GATE-STAGE-00",
|
|
299
|
-
label: `Stage '${stage}' is known to the gate assessment contract.`,
|
|
300
|
-
weight: 2,
|
|
301
|
-
required: true,
|
|
302
|
-
test: () => false,
|
|
303
|
-
evidence: () => [`known stages: ${Object.keys(STAGE_INPUTS).join(", ")}`],
|
|
304
|
-
gap: "Unknown stage; define its required inputs before using gate assessment.",
|
|
305
|
-
};
|
|
306
|
-
}
|
|
9
|
+
export { normalizeReviewTargetPath, reviewModeForReviewTarget };
|
|
307
10
|
export async function writeGateAssessment(target, config, opts) {
|
|
308
11
|
const bundle = await buildGateAssessment(target, config, opts);
|
|
309
12
|
await mkdir(bundle.artifacts.dir, { recursive: true });
|
|
@@ -325,57 +28,28 @@ export function gateThresholdForStage(config, stage) {
|
|
|
325
28
|
export async function buildGateAssessment(target, config, opts) {
|
|
326
29
|
const stage = safeSlug(opts.stage || "design");
|
|
327
30
|
const threshold = normalizeThreshold(opts.threshold);
|
|
328
|
-
const moduleId = await
|
|
329
|
-
const reviewTarget =
|
|
330
|
-
const reviewMode =
|
|
31
|
+
const moduleId = await resolveStageQualityModuleId(target, config, stage, opts.module);
|
|
32
|
+
const reviewTarget = normalizeReviewTargetPath(target, opts.reviewTarget);
|
|
33
|
+
const reviewMode = reviewModeForReviewTarget(reviewTarget);
|
|
331
34
|
const runId = createGateRunId();
|
|
332
|
-
const artifacts = await
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const ctx = { stage, ...(reviewTarget ? { reviewTarget, reviewMode } : {}), artifacts, contentByKey };
|
|
340
|
-
const specs = [
|
|
341
|
-
...(!STAGE_INPUTS[stage] ? [unknownStageCriterion(stage)] : []),
|
|
342
|
-
...COMMON_CRITERIA,
|
|
343
|
-
...(STAGE_CRITERIA[stage] ?? []),
|
|
344
|
-
];
|
|
345
|
-
const criteria = specs.map((spec) => {
|
|
346
|
-
const passed = spec.test(ctx);
|
|
347
|
-
return {
|
|
348
|
-
id: spec.id,
|
|
349
|
-
label: spec.label,
|
|
350
|
-
weight: spec.weight,
|
|
351
|
-
passed,
|
|
352
|
-
required: spec.required === true,
|
|
353
|
-
evidence: spec.evidence(ctx),
|
|
354
|
-
...(passed ? {} : { gap: spec.gap }),
|
|
355
|
-
};
|
|
35
|
+
const artifacts = await readStageQualityArtifacts({
|
|
36
|
+
target,
|
|
37
|
+
config,
|
|
38
|
+
stage,
|
|
39
|
+
moduleId,
|
|
40
|
+
issue: opts.issue,
|
|
41
|
+
reviewTarget,
|
|
356
42
|
});
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
? "BLOCKED"
|
|
368
|
-
: waiverAllowed
|
|
369
|
-
? "WAIVED"
|
|
370
|
-
: belowThreshold
|
|
371
|
-
? "NEEDS_CONFIRMATION"
|
|
372
|
-
: "READY";
|
|
373
|
-
const decision = status === "READY" || status === "WAIVED"
|
|
374
|
-
? "proceed"
|
|
375
|
-
: status === "NEEDS_CONFIRMATION"
|
|
376
|
-
? "human-confirmation-required"
|
|
377
|
-
: "blocked";
|
|
378
|
-
const waiver = waiverAllowed ? { reason: opts.acceptRisk, appliedAt: new Date().toISOString() } : undefined;
|
|
43
|
+
const contentByKey = await readStageQualityArtifactContents(target, artifacts);
|
|
44
|
+
const criteria = evaluateStageQualityCriteria({
|
|
45
|
+
stage,
|
|
46
|
+
reviewTarget,
|
|
47
|
+
reviewMode,
|
|
48
|
+
artifacts,
|
|
49
|
+
contentByKey,
|
|
50
|
+
knownStages: listStageQualityInputStages(),
|
|
51
|
+
});
|
|
52
|
+
const summary = summarizeStageQualityDecision(criteria, threshold, opts.acceptRisk);
|
|
379
53
|
const assessment = {
|
|
380
54
|
version: 1,
|
|
381
55
|
runId,
|
|
@@ -383,18 +57,18 @@ export async function buildGateAssessment(target, config, opts) {
|
|
|
383
57
|
...(moduleId ? { module: moduleId } : {}),
|
|
384
58
|
...(reviewTarget ? { reviewTarget, reviewMode } : {}),
|
|
385
59
|
threshold,
|
|
386
|
-
score,
|
|
387
|
-
status,
|
|
388
|
-
decision,
|
|
60
|
+
score: summary.score,
|
|
61
|
+
status: summary.status,
|
|
62
|
+
decision: summary.decision,
|
|
389
63
|
artifacts,
|
|
390
64
|
criteria,
|
|
391
|
-
blockers,
|
|
392
|
-
gaps,
|
|
393
|
-
...(waiver ? { waiver } : {}),
|
|
394
|
-
nextAction:
|
|
65
|
+
blockers: summary.blockers,
|
|
66
|
+
gaps: summary.gaps,
|
|
67
|
+
...(summary.waiver ? { waiver: summary.waiver } : {}),
|
|
68
|
+
nextAction: summary.nextAction,
|
|
395
69
|
};
|
|
396
70
|
const dir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "gates", runId);
|
|
397
|
-
const waiverPath = waiver ? join(dir, "risk-acceptance.md") : undefined;
|
|
71
|
+
const waiverPath = summary.waiver ? join(dir, "risk-acceptance.md") : undefined;
|
|
398
72
|
return {
|
|
399
73
|
assessment,
|
|
400
74
|
artifacts: {
|
|
@@ -404,443 +78,17 @@ export async function buildGateAssessment(target, config, opts) {
|
|
|
404
78
|
...(waiverPath ? { waiverPath } : {}),
|
|
405
79
|
},
|
|
406
80
|
markdown: renderGateAssessmentMarkdown(assessment),
|
|
407
|
-
...(waiver ? { waiverMarkdown: renderWaiverMarkdown(assessment) } : {}),
|
|
81
|
+
...(summary.waiver ? { waiverMarkdown: renderWaiverMarkdown(assessment) } : {}),
|
|
408
82
|
};
|
|
409
83
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return undefined;
|
|
413
|
-
if (requested)
|
|
414
|
-
return requested;
|
|
415
|
-
const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
|
|
416
|
-
try {
|
|
417
|
-
const entries = (await readdir(modulesRoot, { withFileTypes: true }))
|
|
418
|
-
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("_"))
|
|
419
|
-
.map((entry) => entry.name);
|
|
420
|
-
if (entries.length === 1)
|
|
421
|
-
return entries[0];
|
|
422
|
-
}
|
|
423
|
-
catch {
|
|
424
|
-
// Missing modules root is handled as missing artifacts by the assessment.
|
|
425
|
-
}
|
|
426
|
-
return undefined;
|
|
427
|
-
}
|
|
428
|
-
async function readGateArtifacts(target, config, stage, moduleId, issue, reviewTarget) {
|
|
429
|
-
if (stage === "fix")
|
|
430
|
-
return readFixGateArtifacts(target, config, moduleId, issue);
|
|
431
|
-
if (stage === "review" && reviewTarget)
|
|
432
|
-
return [await readPathArtifact(target, "review-target", reviewTarget)];
|
|
433
|
-
const specs = STAGE_INPUTS[stage] ?? [];
|
|
434
|
-
const artifacts = [];
|
|
435
|
-
for (const spec of specs) {
|
|
436
|
-
if (spec.moduleStage) {
|
|
437
|
-
artifacts.push(await readModuleStageArtifact(target, config, spec.key, spec.moduleStage, moduleId));
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
const path = spec.path?.(config, moduleId);
|
|
441
|
-
if (!path) {
|
|
442
|
-
artifacts.push({ key: spec.key, path: "<module-required>", exists: false, bytes: 0 });
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
const abs = resolve(target, path);
|
|
446
|
-
const exists = existsSync(abs);
|
|
447
|
-
artifacts.push({
|
|
448
|
-
key: spec.key,
|
|
449
|
-
path,
|
|
450
|
-
exists,
|
|
451
|
-
bytes: exists ? Buffer.byteLength(await readFile(abs, "utf8")) : 0,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return artifacts;
|
|
455
|
-
}
|
|
456
|
-
async function readPathArtifact(target, key, path) {
|
|
457
|
-
const boundary = resolve(target);
|
|
458
|
-
const abs = resolve(boundary, path);
|
|
459
|
-
if (!isInsidePath(boundary, abs))
|
|
460
|
-
throw new Error(`review target must stay under project root: ${path}`);
|
|
461
|
-
try {
|
|
462
|
-
const info = await stat(abs);
|
|
463
|
-
if (info.isFile()) {
|
|
464
|
-
return { key, path, exists: true, bytes: Buffer.byteLength(await readFile(abs, "utf8")) };
|
|
465
|
-
}
|
|
466
|
-
if (info.isDirectory()) {
|
|
467
|
-
const content = await readReviewTargetDirectoryContent(abs);
|
|
468
|
-
return { key, path, exists: true, bytes: Buffer.byteLength(content) };
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
catch {
|
|
472
|
-
// Missing paths are reported as missing artifacts by the assessment.
|
|
473
|
-
}
|
|
474
|
-
return { key, path, exists: false, bytes: 0 };
|
|
475
|
-
}
|
|
476
|
-
async function readModuleStageArtifact(target, config, key, stage, moduleId) {
|
|
477
|
-
const path = moduleStagePath(config, stage, moduleId);
|
|
478
|
-
if (!path)
|
|
479
|
-
return { key, path: "<module-required>", exists: false, bytes: 0 };
|
|
480
|
-
const abs = resolve(target, path);
|
|
481
|
-
try {
|
|
482
|
-
const info = await stat(abs);
|
|
483
|
-
if (info.isFile()) {
|
|
484
|
-
return { key, path, exists: true, bytes: Buffer.byteLength(await readFile(abs, "utf8")) };
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
catch {
|
|
488
|
-
// Fall through to stage-directory lookup for splittable artifacts.
|
|
489
|
-
}
|
|
490
|
-
if (!SPLITTABLE_MODULE_STAGES.has(stage)) {
|
|
491
|
-
return { key, path, exists: false, bytes: 0 };
|
|
492
|
-
}
|
|
493
|
-
const indexPath = moduleStageIndexPath(config, stage, moduleId);
|
|
494
|
-
if (!indexPath)
|
|
495
|
-
return { key, path, exists: false, bytes: 0 };
|
|
496
|
-
const indexAbs = resolve(target, indexPath);
|
|
497
|
-
try {
|
|
498
|
-
const info = await stat(indexAbs);
|
|
499
|
-
if (!info.isFile())
|
|
500
|
-
return { key, path: indexPath, exists: false, bytes: 0 };
|
|
501
|
-
const content = await readStageDirectoryContent(indexAbs);
|
|
502
|
-
return { key, path: indexPath, exists: true, bytes: Buffer.byteLength(content) };
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
return { key, path, exists: false, bytes: 0 };
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
async function readFixGateArtifacts(target, config, moduleId, issue) {
|
|
509
|
-
const path = await resolveIssueArtifactPath(target, config, moduleId, issue);
|
|
510
|
-
if (!path)
|
|
511
|
-
return [{ key: "issue-record", path: "<issue-required>", exists: false, bytes: 0 }];
|
|
512
|
-
const abs = resolve(target, path);
|
|
513
|
-
const exists = existsSync(abs);
|
|
514
|
-
return [{
|
|
515
|
-
key: "issue-record",
|
|
516
|
-
path,
|
|
517
|
-
exists,
|
|
518
|
-
bytes: exists ? Buffer.byteLength(await readFile(abs, "utf8")) : 0,
|
|
519
|
-
}];
|
|
520
|
-
}
|
|
521
|
-
async function resolveIssueArtifactPath(target, config, moduleId, issue) {
|
|
522
|
-
const value = issue?.trim();
|
|
523
|
-
if (!value)
|
|
524
|
-
return undefined;
|
|
525
|
-
if (looksLikeIssuePath(value))
|
|
526
|
-
return normalizeIssuePath(target, value);
|
|
527
|
-
const candidates = await collectIssueRecordCandidates(target, config, moduleId);
|
|
528
|
-
return candidates.find((candidate) => issueKey(candidate) === value) ??
|
|
529
|
-
candidates.find((candidate) => issueKey(candidate).startsWith(`${value}-`));
|
|
530
|
-
}
|
|
531
|
-
async function normalizeIssuePath(target, value) {
|
|
532
|
-
const abs = resolve(target, value);
|
|
533
|
-
try {
|
|
534
|
-
const info = await stat(abs);
|
|
535
|
-
if (info.isDirectory())
|
|
536
|
-
return toProjectPath(target, join(abs, "issue.md"));
|
|
537
|
-
}
|
|
538
|
-
catch {
|
|
539
|
-
// Missing issue paths are returned as-is so the gate can report the missing artifact.
|
|
540
|
-
}
|
|
541
|
-
if (!extname(value))
|
|
542
|
-
return toProjectPath(target, join(abs, "issue.md"));
|
|
543
|
-
return toProjectPath(target, abs);
|
|
544
|
-
}
|
|
545
|
-
async function collectIssueRecordCandidates(target, config, moduleId) {
|
|
546
|
-
const roots = [
|
|
547
|
-
...(moduleId ? [resolve(target, getWorkspacePath(config, "moduleIssues", { module: moduleId }))] : []),
|
|
548
|
-
resolve(target, getWorkspacePath(config, "issuesRoot")),
|
|
549
|
-
resolve(target, getWorkspacePath(config, "issuesRoot"), "global"),
|
|
550
|
-
resolve(target, getWorkspacePath(config, "issuesRoot"), "shared"),
|
|
551
|
-
resolve(target, getWorkspacePath(config, "issuesRoot"), "public"),
|
|
552
|
-
];
|
|
553
|
-
const files = [];
|
|
554
|
-
for (const root of roots)
|
|
555
|
-
files.push(...(await collectIssueFiles(root)));
|
|
556
|
-
return [...new Set(files.map((file) => toProjectPath(target, file)))].sort();
|
|
557
|
-
}
|
|
558
|
-
async function collectIssueFiles(root) {
|
|
559
|
-
try {
|
|
560
|
-
const entries = await readdir(root, { withFileTypes: true });
|
|
561
|
-
const files = [];
|
|
562
|
-
for (const entry of entries) {
|
|
563
|
-
const path = join(root, entry.name);
|
|
564
|
-
if (entry.isDirectory())
|
|
565
|
-
files.push(...(await collectIssueFiles(path)));
|
|
566
|
-
if (entry.isFile() && entry.name !== "index.md" && entry.name.endsWith(".md"))
|
|
567
|
-
files.push(path);
|
|
568
|
-
}
|
|
569
|
-
return files;
|
|
570
|
-
}
|
|
571
|
-
catch {
|
|
572
|
-
return [];
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
function looksLikeIssuePath(value) {
|
|
576
|
-
return value.includes("/") || value.endsWith(".md") || value.startsWith(".");
|
|
577
|
-
}
|
|
578
|
-
function issueKey(path) {
|
|
579
|
-
const name = basename(path);
|
|
580
|
-
return name === "issue.md" ? basename(dirname(path)) : name.replace(/\.md$/i, "");
|
|
581
|
-
}
|
|
582
|
-
function toProjectPath(target, path) {
|
|
583
|
-
return relative(target, path).split("\\").join("/");
|
|
584
|
-
}
|
|
585
|
-
function moduleStagePath(config, stage, moduleId) {
|
|
586
|
-
if (!moduleId)
|
|
587
|
-
return undefined;
|
|
588
|
-
return join(getWorkspacePath(config, "modulesRoot"), moduleId, `${stage}.md`);
|
|
589
|
-
}
|
|
590
|
-
function moduleStageIndexPath(config, stage, moduleId) {
|
|
591
|
-
if (!moduleId)
|
|
592
|
-
return undefined;
|
|
593
|
-
return join(getWorkspacePath(config, "modulesRoot"), moduleId, stage, "index.md");
|
|
594
|
-
}
|
|
595
|
-
async function readGateArtifactContent(target, artifact) {
|
|
596
|
-
const boundary = resolve(target);
|
|
597
|
-
const abs = resolve(boundary, artifact.path);
|
|
598
|
-
if (!isInsidePath(boundary, abs))
|
|
599
|
-
throw new Error(`gate artifact must stay under project root: ${artifact.path}`);
|
|
600
|
-
if (basename(abs) === "index.md" && SPLITTABLE_MODULE_STAGES.has(basename(dirname(abs)))) {
|
|
601
|
-
return readStageDirectoryContent(abs);
|
|
602
|
-
}
|
|
603
|
-
try {
|
|
604
|
-
const info = await stat(abs);
|
|
605
|
-
if (info.isDirectory())
|
|
606
|
-
return readReviewTargetDirectoryContent(abs);
|
|
607
|
-
}
|
|
608
|
-
catch {
|
|
609
|
-
// The caller only asks for content after the artifact exists; fall through
|
|
610
|
-
// to the file read so the original error remains visible if the path races.
|
|
611
|
-
}
|
|
612
|
-
return readFile(abs, "utf8");
|
|
613
|
-
}
|
|
614
|
-
async function readReviewTargetDirectoryContent(dir) {
|
|
615
|
-
const files = await collectReviewableFiles(dir, dir);
|
|
616
|
-
const contents = [];
|
|
617
|
-
for (const file of files) {
|
|
618
|
-
try {
|
|
619
|
-
contents.push(await readFile(file, "utf8"));
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
// Ignore unreadable children; the target can still be reviewed from the
|
|
623
|
-
// accessible files and the missing child remains a review finding.
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
return contents.join("\n\n");
|
|
627
|
-
}
|
|
628
|
-
async function collectReviewableFiles(root, boundary) {
|
|
629
|
-
try {
|
|
630
|
-
const entries = await readdir(root, { withFileTypes: true });
|
|
631
|
-
const files = [];
|
|
632
|
-
for (const entry of entries) {
|
|
633
|
-
if (shouldSkipReviewTargetEntry(entry.name))
|
|
634
|
-
continue;
|
|
635
|
-
const path = join(root, entry.name);
|
|
636
|
-
if (!isInsidePath(boundary, path))
|
|
637
|
-
continue;
|
|
638
|
-
if (entry.isDirectory()) {
|
|
639
|
-
files.push(...(await collectReviewableFiles(path, boundary)));
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
if (entry.isFile() && /\.(md|mdx|ya?ml|json|txt)$/i.test(entry.name))
|
|
643
|
-
files.push(path);
|
|
644
|
-
}
|
|
645
|
-
return files.sort();
|
|
646
|
-
}
|
|
647
|
-
catch {
|
|
648
|
-
return [];
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
function normalizeReviewTarget(target, value) {
|
|
652
|
-
return normalizeReviewTargetPath(target, value);
|
|
653
|
-
}
|
|
654
|
-
export function normalizeReviewTargetPath(target, value) {
|
|
655
|
-
const trimmed = value?.trim();
|
|
656
|
-
if (!trimmed)
|
|
657
|
-
return undefined;
|
|
658
|
-
const boundary = resolve(target);
|
|
659
|
-
const abs = resolve(boundary, trimmed);
|
|
660
|
-
if (isInsidePath(boundary, abs))
|
|
661
|
-
return toProjectPath(boundary, abs) || ".";
|
|
662
|
-
throw new Error(`review target must stay under project root: ${trimmed}`);
|
|
663
|
-
}
|
|
664
|
-
function reviewModeFor(reviewTarget) {
|
|
665
|
-
return reviewModeForReviewTarget(reviewTarget);
|
|
666
|
-
}
|
|
667
|
-
export function reviewModeForReviewTarget(reviewTarget) {
|
|
668
|
-
if (!reviewTarget)
|
|
669
|
-
return undefined;
|
|
670
|
-
const path = reviewTarget.replace(/\\/g, "/");
|
|
671
|
-
if (path === ".zsk" || path.startsWith(".zsk/")) {
|
|
672
|
-
if (path.startsWith(".zsk/raws/prepare/product") ||
|
|
673
|
-
path.startsWith(".zsk/raws/prepare/roadmap") ||
|
|
674
|
-
path.startsWith(".zsk/raws/prepare/ux") ||
|
|
675
|
-
path.startsWith(".zsk/raws/prepare/design") ||
|
|
676
|
-
path.startsWith(".zsk/evidence/preproposal"))
|
|
677
|
-
return "preproposal";
|
|
678
|
-
if (path.startsWith(".zsk/raws/design/") || path.startsWith(".zsk/raws/visual/") || path.includes("/design-source"))
|
|
679
|
-
return "design-source";
|
|
680
|
-
if (path.startsWith(".zsk/raws/"))
|
|
681
|
-
return "raw-source";
|
|
682
|
-
if (/^\.zsk\/modules\/[^/]+\/(?:proposal|spec|design|tasks?)(?:\.md|\/|$)/.test(path))
|
|
683
|
-
return "stage-doc";
|
|
684
|
-
return "zsk-workspace";
|
|
685
|
-
}
|
|
686
|
-
if (/\.(?:cjs|cts|js|jsx|mjs|mts|ts|tsx|vue|svelte|css|scss|go|rs|java|kt|swift|py|rb|php|cs|cpp|c|h|hpp)$/i.test(path))
|
|
687
|
-
return "code-target";
|
|
688
|
-
if (/(?:design|figma|prototype|wireframe|mockup|screen|asset)/i.test(path))
|
|
689
|
-
return "design-source";
|
|
690
|
-
return "target-path";
|
|
691
|
-
}
|
|
692
|
-
function shouldSkipReviewTargetEntry(name) {
|
|
693
|
-
if (SKIPPED_REVIEW_TARGET_DIRS.has(name))
|
|
694
|
-
return true;
|
|
695
|
-
return /(?:storage-state|storagestate|cookies?|tokens?|secrets?|credentials?)\.(?:json|ya?ml|txt)$/i.test(name);
|
|
696
|
-
}
|
|
697
|
-
async function readStageDirectoryContent(indexPath) {
|
|
698
|
-
const indexContent = await readFile(indexPath, "utf8");
|
|
699
|
-
const dir = dirname(indexPath);
|
|
700
|
-
const linkedChildren = linkedMarkdownChildren(indexContent, dir);
|
|
701
|
-
const childContents = [];
|
|
702
|
-
for (const child of linkedChildren) {
|
|
703
|
-
try {
|
|
704
|
-
const info = await stat(child);
|
|
705
|
-
if (info.isFile())
|
|
706
|
-
childContents.push(await readFile(child, "utf8"));
|
|
707
|
-
}
|
|
708
|
-
catch {
|
|
709
|
-
// Missing linked children are handled by zsk check; gate content still
|
|
710
|
-
// relies on the index so the artifact cannot pass from hidden files.
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return [indexContent, ...childContents].join("\n\n");
|
|
714
|
-
}
|
|
715
|
-
function linkedMarkdownChildren(content, dir) {
|
|
716
|
-
const files = new Set();
|
|
717
|
-
for (const match of content.matchAll(/\[[^\]]+\]\(([^)]+?\.md)(?:#[^)]+)?\)/gi)) {
|
|
718
|
-
const raw = match[1]?.trim();
|
|
719
|
-
if (!raw || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("/"))
|
|
720
|
-
continue;
|
|
721
|
-
const resolved = resolve(dir, raw);
|
|
722
|
-
if (!isInsidePath(dir, resolved) || resolved === resolve(dir, "index.md"))
|
|
723
|
-
continue;
|
|
724
|
-
files.add(resolved);
|
|
725
|
-
}
|
|
726
|
-
return [...files].sort();
|
|
727
|
-
}
|
|
728
|
-
function isInsidePath(parent, child) {
|
|
729
|
-
const rel = relative(parent, child);
|
|
730
|
-
return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/") && !rel.includes("..\\"));
|
|
731
|
-
}
|
|
732
|
-
function hasContent(ctx, key) {
|
|
733
|
-
return (ctx.contentByKey.get(key)?.trim().length ?? 0) > 0;
|
|
734
|
-
}
|
|
735
|
-
function primaryArtifacts(ctx) {
|
|
736
|
-
return ctx.artifacts.filter((artifact) => !["project-config", "system-spec", "raw-manifest"].includes(artifact.key));
|
|
737
|
-
}
|
|
738
|
-
function allPrimaryContent(ctx, test) {
|
|
739
|
-
const artifacts = primaryArtifacts(ctx);
|
|
740
|
-
if (artifacts.length === 0)
|
|
741
|
-
return ctx.artifacts.every((artifact) => artifact.exists && artifact.bytes > 0);
|
|
742
|
-
return artifacts.every((artifact) => {
|
|
743
|
-
const content = ctx.contentByKey.get(artifact.key);
|
|
744
|
-
return typeof content === "string" && test(content);
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
function combinedContent(ctx) {
|
|
748
|
-
return [...ctx.contentByKey.values()].join("\n\n");
|
|
749
|
-
}
|
|
750
|
-
function hasPattern(ctx, keys, pattern) {
|
|
751
|
-
return keys.some((key) => pattern.test(ctx.contentByKey.get(key) ?? ""));
|
|
752
|
-
}
|
|
753
|
-
function touchesControlConcern(ctx, keys) {
|
|
754
|
-
return hasPattern(ctx, keys, /auth(?:entication|orization)?|permission|access control|role|tenant|privacy|data access|compliance|audit|credential|PII|RBAC|ABAC|权限|认证|授权|角色|租户|隐私|审计|合规|凭证/is);
|
|
755
|
-
}
|
|
756
|
-
function hasControlMatrix(ctx, keys) {
|
|
757
|
-
return hasPattern(ctx, keys, /control (?:traceability )?matrix|control matrix|permission matrix|access control matrix|权限(?:追踪|控制)?矩阵|Source Rule|Subject \/ Actor|Action \/ Capability|Resource \/ Data|Enforcement Point/is);
|
|
758
|
-
}
|
|
759
|
-
function wordCount(content) {
|
|
760
|
-
return content.split(/\s+/).filter(Boolean).length;
|
|
84
|
+
export function createGateRunId(now = new Date()) {
|
|
85
|
+
return `${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 12)}`;
|
|
761
86
|
}
|
|
762
87
|
function normalizeThreshold(value) {
|
|
763
88
|
if (typeof value !== "number" || Number.isNaN(value))
|
|
764
|
-
return
|
|
89
|
+
return DEFAULT_STAGE_QUALITY_THRESHOLD;
|
|
765
90
|
return Math.min(10, Math.max(0, value));
|
|
766
91
|
}
|
|
767
|
-
function roundScore(value) {
|
|
768
|
-
return Math.round(value * 10) / 10;
|
|
769
|
-
}
|
|
770
|
-
function nextActionFor(status) {
|
|
771
|
-
if (status === "READY")
|
|
772
|
-
return "Proceed to the requested stage with the recorded assessment evidence.";
|
|
773
|
-
if (status === "WAIVED")
|
|
774
|
-
return "Proceed with the accepted risk and carry the gaps into the next stage output.";
|
|
775
|
-
if (status === "NEEDS_CONFIRMATION")
|
|
776
|
-
return "Ask for human confirmation, improve upstream artifacts, or rerun with --accept-risk and a concrete reason.";
|
|
777
|
-
return "Do not start the stage; fix missing required artifacts or required criteria first.";
|
|
778
|
-
}
|
|
779
|
-
function renderGateAssessmentMarkdown(assessment) {
|
|
780
|
-
return [
|
|
781
|
-
"# Stage Gate Assessment",
|
|
782
|
-
"",
|
|
783
|
-
`- Stage: \`${assessment.stage}\``,
|
|
784
|
-
...(assessment.module ? [`- Module: \`${assessment.module}\``] : []),
|
|
785
|
-
...(assessment.reviewTarget ? [`- Review target: \`${assessment.reviewTarget}\``] : []),
|
|
786
|
-
...(assessment.reviewMode ? [`- Review mode: \`${assessment.reviewMode}\``] : []),
|
|
787
|
-
`- Score: ${assessment.score}/10`,
|
|
788
|
-
`- Threshold: ${assessment.threshold}/10`,
|
|
789
|
-
`- Status: \`${assessment.status}\``,
|
|
790
|
-
`- Decision: \`${assessment.decision}\``,
|
|
791
|
-
`- Next action: ${assessment.nextAction}`,
|
|
792
|
-
"",
|
|
793
|
-
"## Artifacts",
|
|
794
|
-
"",
|
|
795
|
-
"| Key | Path | Status | Bytes |",
|
|
796
|
-
"| --- | --- | --- | --- |",
|
|
797
|
-
...assessment.artifacts.map((artifact) => `| ${artifact.key} | \`${artifact.path}\` | ${artifact.exists ? "present" : "missing"} | ${artifact.bytes} |`),
|
|
798
|
-
"",
|
|
799
|
-
"## Criteria",
|
|
800
|
-
"",
|
|
801
|
-
"| ID | Result | Required | Weight | Gap | Evidence |",
|
|
802
|
-
"| --- | --- | --- | --- | --- | --- |",
|
|
803
|
-
...assessment.criteria.map((criterion) => `| ${criterion.id} | ${criterion.passed ? "PASS" : "FAIL"} | ${criterion.required ? "yes" : "no"} | ${criterion.weight} | ${criterion.gap ?? ""} | ${criterion.evidence.join("<br>")} |`),
|
|
804
|
-
"",
|
|
805
|
-
"## Blockers",
|
|
806
|
-
"",
|
|
807
|
-
...(assessment.blockers.length > 0 ? assessment.blockers.map((blocker) => `- ${blocker}`) : ["- none"]),
|
|
808
|
-
"",
|
|
809
|
-
"## Gaps",
|
|
810
|
-
"",
|
|
811
|
-
...(assessment.gaps.length > 0 ? assessment.gaps.map((gap) => `- ${gap}`) : ["- none"]),
|
|
812
|
-
...(assessment.waiver ? [
|
|
813
|
-
"",
|
|
814
|
-
"## Risk Acceptance",
|
|
815
|
-
"",
|
|
816
|
-
`- Reason: ${assessment.waiver.reason}`,
|
|
817
|
-
`- Applied at: ${assessment.waiver.appliedAt}`,
|
|
818
|
-
] : []),
|
|
819
|
-
"",
|
|
820
|
-
].join("\n");
|
|
821
|
-
}
|
|
822
|
-
function renderWaiverMarkdown(assessment) {
|
|
823
|
-
return [
|
|
824
|
-
"# Risk Acceptance",
|
|
825
|
-
"",
|
|
826
|
-
`- Stage: \`${assessment.stage}\``,
|
|
827
|
-
...(assessment.module ? [`- Module: \`${assessment.module}\``] : []),
|
|
828
|
-
`- Score: ${assessment.score}/10`,
|
|
829
|
-
`- Threshold: ${assessment.threshold}/10`,
|
|
830
|
-
`- Reason: ${assessment.waiver?.reason ?? ""}`,
|
|
831
|
-
`- Applied at: ${assessment.waiver?.appliedAt ?? ""}`,
|
|
832
|
-
"",
|
|
833
|
-
"## Accepted Gaps",
|
|
834
|
-
"",
|
|
835
|
-
...(assessment.gaps.length > 0 ? assessment.gaps.map((gap) => `- ${gap}`) : ["- none"]),
|
|
836
|
-
"",
|
|
837
|
-
"This waiver does not convert missing required artifacts into PASS. It only records an explicit human risk decision for quality gaps that are below the configured threshold.",
|
|
838
|
-
"",
|
|
839
|
-
].join("\n");
|
|
840
|
-
}
|
|
841
|
-
export function createGateRunId(now = new Date()) {
|
|
842
|
-
return `${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 12)}`;
|
|
843
|
-
}
|
|
844
92
|
function safeSlug(value) {
|
|
845
93
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
846
94
|
}
|