@bastani/atomic 0.9.0-alpha.2 → 0.9.0-alpha.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/CHANGELOG.md +21 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +24 -0
- package/dist/builtin/workflows/README.md +12 -12
- package/dist/builtin/workflows/builtin/goal-ledger.ts +2 -0
- package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -0
- package/dist/builtin/workflows/builtin/goal-reports.ts +5 -0
- package/dist/builtin/workflows/builtin/goal-runner.ts +103 -4
- package/dist/builtin/workflows/builtin/goal-types.ts +4 -0
- package/dist/builtin/workflows/builtin/goal.d.ts +4 -0
- package/dist/builtin/workflows/builtin/goal.ts +14 -2
- package/dist/builtin/workflows/builtin/index.d.ts +8 -8
- package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +359 -0
- package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +254 -352
- package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +256 -414
- package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +272 -0
- package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +58 -68
- package/dist/builtin/workflows/builtin/open-claude-design.d.ts +5 -9
- package/dist/builtin/workflows/builtin/open-claude-design.ts +14 -26
- package/dist/builtin/workflows/builtin/prompt-refinement.ts +102 -0
- package/dist/builtin/workflows/builtin/ralph-core.ts +6 -4
- package/dist/builtin/workflows/builtin/ralph-runner.ts +22 -24
- package/dist/builtin/workflows/builtin/ralph.d.ts +2 -0
- package/dist/builtin/workflows/builtin/ralph.ts +3 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +14 -23
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +25 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +724 -29
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +219 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +57 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +750 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +648 -53
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +44 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +401 -46
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +6 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-args.mjs +42 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +21 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +835 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +180 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +185 -60
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +3369 -1026
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +133 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +42 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +4 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +21 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +205 -1269
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-target.mjs +30 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +69 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +73 -22
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +3 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +5 -5
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +0 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/docs/index.md +2 -2
- package/docs/quickstart.md +9 -9
- package/docs/workflows.md +816 -47
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +0 -126
- /package/dist/builtin/workflows/skills/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* open-claude-design feedback threading.
|
|
3
|
+
*
|
|
4
|
+
* The `user-feedback-*` stages capture Playwright annotation feedback (user
|
|
5
|
+
* notes + annotated snapshot) from the user. This module is the durable carrier
|
|
6
|
+
* for that feedback: it parses the feedback-stage output, persists it as a
|
|
7
|
+
* workflow artifact, and renders the user annotations that the next `generate-*`
|
|
8
|
+
* stage must honor. cross-ref: issue #1464.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { isAbsolute, dirname, join, resolve, sep } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** A single captured user-feedback round. */
|
|
15
|
+
export type PreviewFeedback = {
|
|
16
|
+
/** 1..N for generate/user-feedback loop iterations. */
|
|
17
|
+
readonly iteration: number;
|
|
18
|
+
/** Originating stage name, e.g. `user-feedback-1`. */
|
|
19
|
+
readonly stageName: string;
|
|
20
|
+
/** Full markdown result text emitted by the user-feedback stage. */
|
|
21
|
+
readonly text: string;
|
|
22
|
+
/** Extracted user annotation notes when the user actually annotated. */
|
|
23
|
+
readonly userNotes?: string;
|
|
24
|
+
/** Extracted annotated-snapshot artifact path when one was captured. */
|
|
25
|
+
readonly annotatedSnapshot?: string;
|
|
26
|
+
/** Extracted summary of the variants/edits the user accepted in the live QA session. */
|
|
27
|
+
readonly liveChanges?: string;
|
|
28
|
+
/** ISO timestamp when the feedback was captured. */
|
|
29
|
+
readonly capturedAt: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type PreviewResultLike = { readonly text?: string };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Field labels the user-feedback stages are instructed to emit, stored in
|
|
36
|
+
* canonical (alphanumeric-only, lowercase) form. Used to bound multi-line value
|
|
37
|
+
* extraction (a value ends when the next known field starts).
|
|
38
|
+
*/
|
|
39
|
+
const FIELD_LABELS = new Set<string>([
|
|
40
|
+
"displaymethod",
|
|
41
|
+
"previewpath",
|
|
42
|
+
"previewfileurl",
|
|
43
|
+
"annotatedsnapshot",
|
|
44
|
+
"usernotes",
|
|
45
|
+
"livechanges",
|
|
46
|
+
"nextactionhint",
|
|
47
|
+
"manualopeninstructions",
|
|
48
|
+
"specpath",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const PLACEHOLDER_TOKENS = new Set<string>([
|
|
52
|
+
"none",
|
|
53
|
+
"na",
|
|
54
|
+
"null",
|
|
55
|
+
"undefined",
|
|
56
|
+
"notavailable",
|
|
57
|
+
"unavailable",
|
|
58
|
+
"notcaptured",
|
|
59
|
+
"nonotes",
|
|
60
|
+
"nousernotes",
|
|
61
|
+
"nofeedback",
|
|
62
|
+
"noannotations",
|
|
63
|
+
"nonecaptured",
|
|
64
|
+
"tbd",
|
|
65
|
+
"pending",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
function isPlaceholderValue(value: string): boolean {
|
|
69
|
+
const compact = value
|
|
70
|
+
.replace(/\//g, "")
|
|
71
|
+
.replace(/[\s().,*_`~–—\-:]/g, "")
|
|
72
|
+
.toLowerCase();
|
|
73
|
+
if (compact.length === 0) return true;
|
|
74
|
+
return PLACEHOLDER_TOKENS.has(compact);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Canonicalize a label to lowercase alphanumerics so `user_notes`, `User Notes`,
|
|
78
|
+
* and `**user_notes**` all compare equal. */
|
|
79
|
+
function canonicalLabel(value: string): string {
|
|
80
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Normalize a candidate label line into a canonical key (or undefined). */
|
|
84
|
+
function labelOf(line: string): string | undefined {
|
|
85
|
+
const stripped = line
|
|
86
|
+
.replace(/^\s*#{1,6}\s+/, "")
|
|
87
|
+
.replace(/^\s*[-*+]\s+/, "")
|
|
88
|
+
.replace(/^\s*\d+\.\s+/, "");
|
|
89
|
+
const colonIdx = stripped.indexOf(":");
|
|
90
|
+
const candidate = colonIdx >= 0 ? stripped.slice(0, colonIdx) : stripped;
|
|
91
|
+
const key = canonicalLabel(candidate);
|
|
92
|
+
return key.length > 0 ? key : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Inline value following a `label:` on the same line. */
|
|
96
|
+
function inlineValueOf(line: string): string {
|
|
97
|
+
const stripped = line
|
|
98
|
+
.replace(/^\s*#{1,6}\s+/, "")
|
|
99
|
+
.replace(/^\s*[-*+]\s+/, "")
|
|
100
|
+
.replace(/^\s*\d+\.\s+/, "");
|
|
101
|
+
const colonIdx = stripped.indexOf(":");
|
|
102
|
+
if (colonIdx < 0) return "";
|
|
103
|
+
return stripped.slice(colonIdx + 1).replace(/[`*]/g, "").trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isHorizontalRule(line: string): boolean {
|
|
107
|
+
return /^\s*([-*_])(\s*\1){2,}\s*$/.test(line);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract the value of a labeled field (e.g. `user_notes`) from a user-feedback
|
|
112
|
+
* markdown blob, tolerating heading / bullet / bold / backtick label styles and
|
|
113
|
+
* multi-line values that run until the next known field label or a rule.
|
|
114
|
+
*/
|
|
115
|
+
export function extractField(text: string, field: string): string | undefined {
|
|
116
|
+
if (text.trim().length === 0) return undefined;
|
|
117
|
+
const target = canonicalLabel(field);
|
|
118
|
+
const lines = text.split(/\r?\n/);
|
|
119
|
+
let collecting = false;
|
|
120
|
+
const collected: string[] = [];
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (collecting) {
|
|
123
|
+
const label = labelOf(line);
|
|
124
|
+
if (label !== undefined && label !== target && FIELD_LABELS.has(label)) break;
|
|
125
|
+
if (isHorizontalRule(line)) break;
|
|
126
|
+
collected.push(line);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (labelOf(line) === target) {
|
|
130
|
+
const inline = inlineValueOf(line);
|
|
131
|
+
if (inline.length > 0) collected.push(inline);
|
|
132
|
+
collecting = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const value = collected.join("\n").trim();
|
|
136
|
+
if (value.length === 0 || isPlaceholderValue(value)) return undefined;
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function extractUserNotes(text: string): string | undefined {
|
|
141
|
+
return extractField(text, "user_notes");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function extractAnnotatedSnapshot(text: string): string | undefined {
|
|
145
|
+
return extractField(text, "annotated_snapshot");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function extractLiveChanges(text: string): string | undefined {
|
|
149
|
+
return extractField(text, "live_changes");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Build a PreviewFeedback record from a (possibly missing) stage result. */
|
|
153
|
+
export function toPreviewFeedback(input: {
|
|
154
|
+
readonly iteration: number;
|
|
155
|
+
readonly stageName: string;
|
|
156
|
+
readonly result: PreviewResultLike | undefined;
|
|
157
|
+
}): PreviewFeedback {
|
|
158
|
+
const text = (input.result?.text ?? "").trim();
|
|
159
|
+
const userNotes = extractUserNotes(text);
|
|
160
|
+
const annotatedSnapshot = extractAnnotatedSnapshot(text);
|
|
161
|
+
const liveChanges = extractLiveChanges(text);
|
|
162
|
+
return {
|
|
163
|
+
iteration: input.iteration,
|
|
164
|
+
stageName: input.stageName,
|
|
165
|
+
text,
|
|
166
|
+
capturedAt: new Date().toISOString(),
|
|
167
|
+
...(userNotes !== undefined ? { userNotes } : {}),
|
|
168
|
+
...(annotatedSnapshot !== undefined ? { annotatedSnapshot } : {}),
|
|
169
|
+
...(liveChanges !== undefined ? { liveChanges } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function hasMeaningfulUserNotes(feedback: PreviewFeedback): boolean {
|
|
174
|
+
return typeof feedback.userNotes === "string" && feedback.userNotes.length > 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function hasMeaningfulLiveChanges(feedback: PreviewFeedback): boolean {
|
|
178
|
+
return typeof feedback.liveChanges === "string" && feedback.liveChanges.length > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Whether a feedback round carries any meaningful user signal: typed notes or accepted live variants. */
|
|
182
|
+
export function hasMeaningfulFeedback(feedback: PreviewFeedback): boolean {
|
|
183
|
+
return hasMeaningfulUserNotes(feedback) || hasMeaningfulLiveChanges(feedback);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function feedbackLabel(feedback: PreviewFeedback): string {
|
|
187
|
+
return feedback.iteration === 0
|
|
188
|
+
? "the initial preview"
|
|
189
|
+
: `refinement iteration ${feedback.iteration}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Render the captured user annotations (latest first) as a markdown section.
|
|
194
|
+
* Returns "" when no iteration captured meaningful user notes.
|
|
195
|
+
*/
|
|
196
|
+
export function buildUserAnnotationsSection(history: readonly PreviewFeedback[]): string {
|
|
197
|
+
const withFeedback = history.filter(
|
|
198
|
+
(feedback) => hasMeaningfulUserNotes(feedback) || hasMeaningfulLiveChanges(feedback),
|
|
199
|
+
);
|
|
200
|
+
if (withFeedback.length === 0) return "";
|
|
201
|
+
return [...withFeedback]
|
|
202
|
+
.reverse()
|
|
203
|
+
.map((feedback) => {
|
|
204
|
+
const lines = [
|
|
205
|
+
`### User annotations from ${feedbackLabel(feedback)} (${feedback.stageName})`,
|
|
206
|
+
"",
|
|
207
|
+
];
|
|
208
|
+
if (hasMeaningfulUserNotes(feedback)) {
|
|
209
|
+
lines.push(feedback.userNotes ?? "");
|
|
210
|
+
}
|
|
211
|
+
if (hasMeaningfulLiveChanges(feedback)) {
|
|
212
|
+
lines.push("", "Accepted live variants/edits:", feedback.liveChanges ?? "");
|
|
213
|
+
}
|
|
214
|
+
if (feedback.annotatedSnapshot !== undefined) {
|
|
215
|
+
lines.push("", `Annotated snapshot: ${feedback.annotatedSnapshot}`);
|
|
216
|
+
}
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
})
|
|
219
|
+
.join("\n\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The user-annotations block injected into refinement prompts, plus whether any
|
|
224
|
+
* real annotations exist. When none exist, downstream stages are told to fall
|
|
225
|
+
* back to an impeccable critique rather than fabricating user feedback.
|
|
226
|
+
*/
|
|
227
|
+
export function userAnnotationsBlock(history: readonly PreviewFeedback[]): {
|
|
228
|
+
readonly hasNotes: boolean;
|
|
229
|
+
readonly text: string;
|
|
230
|
+
} {
|
|
231
|
+
const section = buildUserAnnotationsSection(history);
|
|
232
|
+
if (section.length === 0) {
|
|
233
|
+
return {
|
|
234
|
+
hasNotes: false,
|
|
235
|
+
text: "No interactive user annotations were captured in the user-feedback stage. There is no user feedback to honor for this iteration.",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return { hasNotes: true, text: section };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Guardrail: every captured user annotation must be present verbatim in the
|
|
243
|
+
* next generate prompt. If a `user-feedback-*` stage captured `user_notes` but
|
|
244
|
+
* they did not thread through, fail loudly instead of silently generating
|
|
245
|
+
* without user feedback. cross-ref: issue #1464 fix (6).
|
|
246
|
+
*/
|
|
247
|
+
export function assertUserAnnotationsThreaded(
|
|
248
|
+
prompt: string,
|
|
249
|
+
history: readonly PreviewFeedback[],
|
|
250
|
+
stageName: string,
|
|
251
|
+
): void {
|
|
252
|
+
for (const feedback of history) {
|
|
253
|
+
if (hasMeaningfulUserNotes(feedback)) {
|
|
254
|
+
const notes = (feedback.userNotes ?? "").trim();
|
|
255
|
+
if (notes.length > 0 && !prompt.includes(notes)) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`open-claude-design ${stageName}: user annotations captured in ${feedback.stageName} were not threaded into the refinement context. Refusing to refine without user feedback (see issue #1464).`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (hasMeaningfulLiveChanges(feedback)) {
|
|
262
|
+
const changes = (feedback.liveChanges ?? "").trim();
|
|
263
|
+
if (changes.length > 0 && !prompt.includes(changes)) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`open-claude-design ${stageName}: accepted live variants captured in ${feedback.stageName} were not threaded into the refinement context. Refusing to refine without user feedback.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Whether `childPath` resolves to `parentDir` itself or somewhere beneath it. */
|
|
273
|
+
function isWithin(childPath: string, parentDir: string): boolean {
|
|
274
|
+
const child = resolve(childPath);
|
|
275
|
+
const parent = resolve(parentDir);
|
|
276
|
+
return child === parent || child.startsWith(parent + sep);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function copyAnnotationArtifacts(
|
|
280
|
+
feedbackDir: string,
|
|
281
|
+
slug: string,
|
|
282
|
+
feedback: PreviewFeedback,
|
|
283
|
+
workflowCwd: string,
|
|
284
|
+
): void {
|
|
285
|
+
if (feedback.annotatedSnapshot === undefined) return;
|
|
286
|
+
const raw = feedback.annotatedSnapshot.trim();
|
|
287
|
+
if (raw.length === 0) return;
|
|
288
|
+
const source = isAbsolute(raw) ? raw : resolve(workflowCwd, raw);
|
|
289
|
+
// Constrain the model-supplied path to within the project or the run's
|
|
290
|
+
// artifact dir before copying, so an absolute path outside the project (e.g.
|
|
291
|
+
// an arbitrary file the model emitted) is never copied in.
|
|
292
|
+
const artifactDir = dirname(feedbackDir);
|
|
293
|
+
if (!isWithin(source, workflowCwd) && !isWithin(source, artifactDir)) return;
|
|
294
|
+
try {
|
|
295
|
+
if (!existsSync(source) || !statSync(source).isFile()) return;
|
|
296
|
+
} catch {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const extMatch = source.match(/\.[A-Za-z0-9]+$/);
|
|
300
|
+
const ext = extMatch ? extMatch[0] : ".png";
|
|
301
|
+
try {
|
|
302
|
+
copyFileSync(source, join(feedbackDir, `${slug}-annotations${ext}`));
|
|
303
|
+
} catch {
|
|
304
|
+
/* best-effort */
|
|
305
|
+
}
|
|
306
|
+
for (const yamlExt of [".yaml", ".yml"]) {
|
|
307
|
+
const sibling = source.replace(/\.[A-Za-z0-9]+$/, yamlExt);
|
|
308
|
+
try {
|
|
309
|
+
if (existsSync(sibling) && statSync(sibling).isFile()) {
|
|
310
|
+
copyFileSync(sibling, join(feedbackDir, `${slug}-annotations${yamlExt}`));
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
/* best-effort */
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Persist captured annotations as durable workflow artifacts under
|
|
321
|
+
* `<artifactDir>/feedback/`. Only writes when the user actually provided
|
|
322
|
+
* annotations (notes or an annotated snapshot). Best-effort: never throws.
|
|
323
|
+
* cross-ref: issue #1464 fix (5).
|
|
324
|
+
*/
|
|
325
|
+
export function persistPreviewFeedback(input: {
|
|
326
|
+
readonly artifactDir: string;
|
|
327
|
+
readonly workflowCwd: string;
|
|
328
|
+
readonly feedback: PreviewFeedback;
|
|
329
|
+
}): void {
|
|
330
|
+
const { feedback } = input;
|
|
331
|
+
if (
|
|
332
|
+
!hasMeaningfulUserNotes(feedback) &&
|
|
333
|
+
!hasMeaningfulLiveChanges(feedback) &&
|
|
334
|
+
feedback.annotatedSnapshot === undefined
|
|
335
|
+
) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const feedbackDir = join(input.artifactDir, "feedback");
|
|
340
|
+
mkdirSync(feedbackDir, { recursive: true });
|
|
341
|
+
const slug = `iteration-${feedback.iteration}`;
|
|
342
|
+
writeFileSync(join(feedbackDir, `${slug}.md`), `${feedback.text}\n`);
|
|
343
|
+
writeFileSync(
|
|
344
|
+
join(feedbackDir, `${slug}.json`),
|
|
345
|
+
`${JSON.stringify(
|
|
346
|
+
{
|
|
347
|
+
...feedback,
|
|
348
|
+
hasUserNotes: hasMeaningfulUserNotes(feedback),
|
|
349
|
+
hasLiveChanges: hasMeaningfulLiveChanges(feedback),
|
|
350
|
+
},
|
|
351
|
+
null,
|
|
352
|
+
2,
|
|
353
|
+
)}\n`,
|
|
354
|
+
);
|
|
355
|
+
copyAnnotationArtifacts(feedbackDir, slug, feedback, input.workflowCwd);
|
|
356
|
+
} catch {
|
|
357
|
+
/* best-effort durability; never block the workflow */
|
|
358
|
+
}
|
|
359
|
+
}
|