@captain_z/zsk 1.8.1 → 1.8.2

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.
Files changed (78) hide show
  1. package/dist/bin.js +180 -1
  2. package/dist/bin.js.map +1 -1
  3. package/dist/commands/check.js +75 -4
  4. package/dist/commands/check.js.map +1 -1
  5. package/dist/commands/config.d.ts +11 -0
  6. package/dist/commands/config.js +132 -3
  7. package/dist/commands/config.js.map +1 -1
  8. package/dist/commands/demo.js +2 -2
  9. package/dist/commands/demo.js.map +1 -1
  10. package/dist/commands/dispatch.d.ts +11 -0
  11. package/dist/commands/dispatch.js +69 -0
  12. package/dist/commands/dispatch.js.map +1 -0
  13. package/dist/commands/gate.d.ts +9 -0
  14. package/dist/commands/gate.js +48 -0
  15. package/dist/commands/gate.js.map +1 -0
  16. package/dist/commands/prep.d.ts +2 -4
  17. package/dist/commands/prep.js +1 -131
  18. package/dist/commands/prep.js.map +1 -1
  19. package/dist/commands/prepare.d.ts +17 -0
  20. package/dist/commands/prepare.js +259 -0
  21. package/dist/commands/prepare.js.map +1 -0
  22. package/dist/commands/project-init.d.ts +1 -0
  23. package/dist/commands/project-init.js +12 -10
  24. package/dist/commands/project-init.js.map +1 -1
  25. package/dist/commands/template.d.ts +2 -0
  26. package/dist/commands/template.js +34 -0
  27. package/dist/commands/template.js.map +1 -0
  28. package/dist/core/config.d.ts +85 -1
  29. package/dist/core/config.js +141 -7
  30. package/dist/core/config.js.map +1 -1
  31. package/dist/core/origin-detection.d.ts +10 -0
  32. package/dist/core/origin-detection.js +135 -0
  33. package/dist/core/origin-detection.js.map +1 -0
  34. package/dist/core/prepare-lifecycle.d.ts +54 -0
  35. package/dist/core/prepare-lifecycle.js +302 -0
  36. package/dist/core/prepare-lifecycle.js.map +1 -0
  37. package/dist/core/prepare-sync.d.ts +82 -0
  38. package/dist/core/prepare-sync.js +1499 -0
  39. package/dist/core/prepare-sync.js.map +1 -0
  40. package/dist/core/raw-manifest.d.ts +10 -0
  41. package/dist/core/raw-manifest.js +58 -4
  42. package/dist/core/raw-manifest.js.map +1 -1
  43. package/dist/core/source-draft.d.ts +14 -0
  44. package/dist/core/source-draft.js +251 -0
  45. package/dist/core/source-draft.js.map +1 -0
  46. package/dist/core/staffing-plan.d.ts +206 -0
  47. package/dist/core/staffing-plan.js +1115 -0
  48. package/dist/core/staffing-plan.js.map +1 -0
  49. package/dist/core/stage-quality.d.ts +56 -0
  50. package/dist/core/stage-quality.js +487 -0
  51. package/dist/core/stage-quality.js.map +1 -0
  52. package/dist/core/template-registry.d.ts +29 -0
  53. package/dist/core/template-registry.js +289 -0
  54. package/dist/core/template-registry.js.map +1 -0
  55. package/dist/core/workspace-layout.d.ts +3 -0
  56. package/dist/core/workspace-layout.js +14 -1
  57. package/dist/core/workspace-layout.js.map +1 -1
  58. package/package.json +2 -2
  59. package/schemas/zsk-config.schema.json +233 -196
  60. package/templates/module/frontend-module/design.md +71 -0
  61. package/templates/module/frontend-module/proposal.md +17 -0
  62. package/templates/module/frontend-module/spec.md +17 -0
  63. package/templates/module/frontend-module/tasks.md +36 -6
  64. package/templates/project-init/.zsk/config.yaml +8 -96
  65. package/templates/project-init/.zsk/raws/index.md +8 -0
  66. package/templates/project-init/.zsk/raws/prepare/backend/index.md +4 -0
  67. package/templates/project-init/.zsk/raws/prepare/design/index.md +3 -0
  68. package/templates/project-init/.zsk/raws/prepare/index.md +4 -0
  69. package/templates/project-init/.zsk/raws/prepare/product/index.md +4 -0
  70. package/templates/project-init/.zsk/raws/prepare/qa/index.md +4 -0
  71. package/templates/project-init/.zsk/raws/prepare/ux/index.md +3 -0
  72. package/templates/project-init/.zsk/roles.yaml +129 -0
  73. package/templates/project-init/.zsk/raws/backend/index.md +0 -3
  74. package/templates/project-init/.zsk/raws/jira/index.md +0 -3
  75. package/templates/project-init/.zsk/raws/manual/index.md +0 -3
  76. package/templates/project-init/.zsk/raws/product/index.md +0 -3
  77. package/templates/project-init/.zsk/raws/qa/index.md +0 -3
  78. package/templates/project-init/.zsk/raws/ue/index.md +0 -3
@@ -0,0 +1,1499 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createRequire } from "node:module";
3
+ import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { DESIGN_LOCAL_FILE_ORIGIN_KEYS, JIRA_LOCAL_FILE_ORIGIN_KEYS, flattenProjectSources, resolveSourceSnapshot, } from "./config.js";
6
+ import { inferSourceOrigin } from "./origin-detection.js";
7
+ import { describeResource, updateRawIndexes, updateRawManifest } from "./raw-manifest.js";
8
+ import { getWorkspacePath } from "./workspace-layout.js";
9
+ export function resolvePrepareSyncArtifacts(target, config, runId) {
10
+ const dir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "prepare", runId);
11
+ const authDir = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
12
+ return {
13
+ runId,
14
+ dir,
15
+ adapterResultsDir: join(dir, "adapter-results"),
16
+ authCheckPath: join(dir, "auth-check.json"),
17
+ downstreamImpactPath: join(dir, "downstream-impact.md"),
18
+ authScriptPath: join(authDir, "login.mjs"),
19
+ authStatePath: join(authDir, "user_data.json"),
20
+ migrationPlanPath: join(dir, "raws-migration-plan.md"),
21
+ };
22
+ }
23
+ export async function syncPrepareSources(target, config, opts = {}) {
24
+ const runId = opts.runId ?? createSyncRunId();
25
+ const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
26
+ const entries = flattenProjectSources(config.sources);
27
+ const selected = new Set(selectSourceEntries(entries, opts));
28
+ const results = [];
29
+ await mkdir(artifacts.adapterResultsDir, { recursive: true });
30
+ for (const entry of entries) {
31
+ if (!selected.has(entry))
32
+ continue;
33
+ const result = await syncEntry(target, config, entry, opts);
34
+ results.push(result);
35
+ await writeFile(join(artifacts.adapterResultsDir, `${safeFileName(entry.id)}.json`), `${JSON.stringify(result, null, 2)}\n`, "utf8");
36
+ }
37
+ if (!opts.dryRun) {
38
+ const records = [];
39
+ for (const entry of entries) {
40
+ records.push(await describeResource(target, entry.provider, { ...entry.source, snapshot: resolveSourceSnapshot(config, entry) }, { role: entry.rawLane, sourcePath: entry.path }));
41
+ }
42
+ await updateRawManifest(resolve(target, getWorkspacePath(config, "resourcesManifest")), records);
43
+ await updateRawIndexes(target, getWorkspacePath(config, "resourcesRoot"), records);
44
+ }
45
+ const downstreamImpact = renderDownstreamImpact(results);
46
+ await mkdir(dirname(artifacts.downstreamImpactPath), { recursive: true });
47
+ await writeFile(artifacts.downstreamImpactPath, downstreamImpact, "utf8");
48
+ return {
49
+ artifacts,
50
+ results,
51
+ downstreamImpact,
52
+ };
53
+ }
54
+ export async function writeAuthLoginHelper(target, config, opts = {}) {
55
+ const artifacts = resolvePrepareSyncArtifacts(target, config, opts.runId ?? createSyncRunId());
56
+ const statePath = opts.out ? resolve(target, opts.out) : resolveAuthProfilePath(target, config, opts.profile);
57
+ const authRoot = dirname(artifacts.authScriptPath);
58
+ if (!isInside(authRoot, statePath)) {
59
+ throw new Error(`Playwright storageState output must stay under ${authRoot}`);
60
+ }
61
+ const script = [
62
+ `import { chromium } from "playwright";`,
63
+ ``,
64
+ `const targetUrl = process.env.ZSK_PREPARE_AUTH_URL ?? ${JSON.stringify(opts.url ?? "about:blank")};`,
65
+ `const storageStatePath = process.env.ZSK_PLAYWRIGHT_STORAGE_STATE ?? ${JSON.stringify(statePath)};`,
66
+ `const browser = await chromium.launch({ headless: false });`,
67
+ `const context = await browser.newContext();`,
68
+ `const page = await context.newPage();`,
69
+ `await page.goto(targetUrl, { waitUntil: "domcontentloaded" });`,
70
+ `console.log("Complete login in the opened browser, then press Enter here to save storageState.");`,
71
+ `await new Promise((resolve) => process.stdin.once("data", resolve));`,
72
+ `await context.storageState({ path: storageStatePath });`,
73
+ `console.log(\`Saved Playwright storageState to ${"${storageStatePath}"}\`);`,
74
+ `await browser.close();`,
75
+ ``,
76
+ ].join("\n");
77
+ await mkdir(dirname(artifacts.authScriptPath), { recursive: true });
78
+ await writeFile(artifacts.authScriptPath, script, "utf8");
79
+ return { ...artifacts, authStatePath: statePath };
80
+ }
81
+ export async function checkPrepareAuth(target, config, opts = {}) {
82
+ const runId = opts.runId ?? createSyncRunId();
83
+ const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
84
+ const entries = selectSourceEntries(flattenProjectSources(config.sources), opts);
85
+ const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
86
+ const results = [];
87
+ for (const entry of entries) {
88
+ results.push(await checkAuthEntry(entry, authStatePath, opts));
89
+ }
90
+ await mkdir(dirname(artifacts.authCheckPath), { recursive: true });
91
+ await writeFile(artifacts.authCheckPath, `${JSON.stringify({ runId, results }, null, 2)}\n`, "utf8");
92
+ return { artifacts, results };
93
+ }
94
+ export async function buildRawMigrationPlan(target, config, runId = createSyncRunId()) {
95
+ const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
96
+ const prepareRoot = resolve(target, getWorkspacePath(config, "resourcesRoot"), "prepare");
97
+ const staleNames = new Set([
98
+ "qa-engineer",
99
+ "test-engineer",
100
+ "backend-engineer",
101
+ "frontend-engineer",
102
+ "product-manager",
103
+ "designer",
104
+ "jira",
105
+ "confluence",
106
+ "figma",
107
+ "modao",
108
+ "manual",
109
+ "provider",
110
+ "version",
111
+ ]);
112
+ const entries = await safeReadDir(prepareRoot);
113
+ const findings = entries
114
+ .filter((entry) => entry.isDirectory() && isMigrationLaneCandidate(entry.name, staleNames))
115
+ .map((entry) => ({
116
+ path: join(".zsk/raws/prepare", entry.name),
117
+ action: "dry-run only; keep readable, require explicit migration before rewriting references",
118
+ }));
119
+ const content = [
120
+ "# Raw Migration Plan",
121
+ "",
122
+ "Dry-run report only. No files were moved, deleted, or rewritten.",
123
+ "",
124
+ findings.length === 0
125
+ ? "No stale provider/method/version prepare lanes were found."
126
+ : findings.map((item) => `- \`${item.path}\`: ${item.action}`).join("\n"),
127
+ "",
128
+ ].join("\n");
129
+ await mkdir(dirname(artifacts.migrationPlanPath), { recursive: true });
130
+ await writeFile(artifacts.migrationPlanPath, content, "utf8");
131
+ return { artifacts, content };
132
+ }
133
+ function selectSourceEntries(entries, opts) {
134
+ if (opts.all || !opts.source)
135
+ return entries;
136
+ const selector = opts.source;
137
+ const exact = entries.filter((entry) => entry.id === selector || entry.path === selector || entry.segments.join(".") === selector);
138
+ if (exact.length > 0)
139
+ return exact;
140
+ const alias = entries.filter((entry) => entry.provider === selector);
141
+ if (alias.length > 1) {
142
+ const matches = alias.map((entry) => `${entry.path} (${entry.id})`).join(", ");
143
+ throw new Error(`source selector "${selector}" matched multiple configured sources: ${matches}. Use a source path or path-scoped id.`);
144
+ }
145
+ return alias;
146
+ }
147
+ async function checkAuthEntry(entry, authStatePath, opts) {
148
+ const origin = inferSourceOrigin(entry.source);
149
+ const base = {
150
+ sourceKey: entry.id,
151
+ sourcePath: entry.path,
152
+ rawLane: entry.rawLane,
153
+ provider: origin.provider,
154
+ origin: origin.ref,
155
+ method: origin.method,
156
+ };
157
+ const url = origin.ref;
158
+ if (!url || origin.method !== "url") {
159
+ return {
160
+ ...base,
161
+ status: "source-gap",
162
+ authSources: [],
163
+ directAuthAvailable: false,
164
+ networkAttempted: false,
165
+ reason: "auth-check only validates URL origins; configure origin.url before checking runtime credentials",
166
+ validation: { urlOrigin: "fail", secretValuesHidden: "pass" },
167
+ };
168
+ }
169
+ const { headers, authSources } = await headersForSource(entry.source, url, authStatePath);
170
+ const directAuthAvailable = Boolean(headers.authorization || headers.cookie);
171
+ if (!opts.allowNetwork) {
172
+ return {
173
+ ...base,
174
+ status: directAuthAvailable ? "ready" : "missing-auth",
175
+ authSources,
176
+ directAuthAvailable,
177
+ networkAttempted: false,
178
+ reason: directAuthAvailable
179
+ ? "runtime credential source is available; rerun with --allow-network to validate reachability without writing snapshots"
180
+ : "no source-bound runtime credential was found; configure origin.auth env names with exported values or create a Playwright auth profile",
181
+ validation: {
182
+ runtimeAuth: directAuthAvailable ? "pass" : "fail",
183
+ networkSkipped: "pass",
184
+ secretValuesHidden: "pass",
185
+ },
186
+ };
187
+ }
188
+ try {
189
+ const response = await fetch(url, { headers });
190
+ const contentType = response.headers.get("content-type") ?? "";
191
+ const body = await response.text();
192
+ if (!response.ok) {
193
+ return {
194
+ ...base,
195
+ status: response.status === 401 || response.status === 403 ? "blocked-auth" : "failed",
196
+ authSources,
197
+ directAuthAvailable,
198
+ networkAttempted: true,
199
+ httpStatus: response.status,
200
+ contentType,
201
+ reason: `auth-check fetch returned HTTP ${response.status}`,
202
+ validation: { httpOk: "fail", secretValuesHidden: "pass" },
203
+ };
204
+ }
205
+ if (looksLikeLoginPage(body, response.url)) {
206
+ return {
207
+ ...base,
208
+ status: "blocked-auth",
209
+ authSources,
210
+ directAuthAvailable,
211
+ networkAttempted: true,
212
+ httpStatus: response.status,
213
+ contentType,
214
+ reason: "auth-check fetch resolved to login/chrome content",
215
+ validation: { httpOk: "pass", loginPageRejected: "fail", secretValuesHidden: "pass" },
216
+ };
217
+ }
218
+ const content = extractRemoteContent(contentType, body);
219
+ if (!hasMaterialContent(content)) {
220
+ return {
221
+ ...base,
222
+ status: "source-gap",
223
+ authSources,
224
+ directAuthAvailable,
225
+ networkAttempted: true,
226
+ httpStatus: response.status,
227
+ contentType,
228
+ reason: "auth-check fetch produced no material body content",
229
+ validation: { httpOk: "pass", bodyContent: "fail", secretValuesHidden: "pass" },
230
+ };
231
+ }
232
+ return {
233
+ ...base,
234
+ status: "reachable",
235
+ authSources,
236
+ directAuthAvailable,
237
+ networkAttempted: true,
238
+ httpStatus: response.status,
239
+ contentType,
240
+ reason: "auth-check reached material content without writing a snapshot",
241
+ validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", secretValuesHidden: "pass" },
242
+ };
243
+ }
244
+ catch (error) {
245
+ return {
246
+ ...base,
247
+ status: "failed",
248
+ authSources,
249
+ directAuthAvailable,
250
+ networkAttempted: true,
251
+ reason: `auth-check fetch failed: ${error instanceof Error ? error.message : String(error)}`,
252
+ validation: { httpOk: "fail", secretValuesHidden: "pass" },
253
+ };
254
+ }
255
+ }
256
+ async function syncEntry(target, config, entry, opts) {
257
+ const source = entry.source;
258
+ const origin = inferSourceOrigin(source);
259
+ const snapshot = resolveSourceSnapshot(config, entry);
260
+ const snapshotPath = resolve(target, snapshot);
261
+ const previousSnapshotHash = await hashIfExists(snapshotPath);
262
+ const base = {
263
+ envelopeVersion: 1,
264
+ sourceKey: entry.id,
265
+ sourcePath: entry.path,
266
+ rawLane: entry.rawLane,
267
+ provider: origin.provider,
268
+ origin: origin.ref,
269
+ snapshot,
270
+ previousSnapshotHash,
271
+ };
272
+ if (opts.dryRun) {
273
+ return {
274
+ ...base,
275
+ strategy: chooseStrategy(origin.method),
276
+ status: "skipped",
277
+ changed: false,
278
+ reason: "dry-run requested; no snapshot was written",
279
+ validation: { dryRun: "pass" },
280
+ };
281
+ }
282
+ if (origin.method === "local") {
283
+ const sourcePath = source.origin?.path ?? source.path;
284
+ if (!sourcePath) {
285
+ return sourceGap(base, "configured local origin has no path");
286
+ }
287
+ const originPath = resolve(target, sourcePath);
288
+ if (!(await exists(originPath))) {
289
+ return sourceGap(base, "configured local origin path does not exist");
290
+ }
291
+ await mkdir(dirname(snapshotPath), { recursive: true });
292
+ if (shouldPreserveMachineReadable(source, originPath, snapshotPath)) {
293
+ await copyFile(originPath, snapshotPath);
294
+ }
295
+ else {
296
+ await writeFile(snapshotPath, await renderStructuredLocalMarkdown(target, entry, originPath), "utf8");
297
+ }
298
+ const snapshotHash = await sha256(snapshotPath);
299
+ return {
300
+ ...base,
301
+ strategy: "local-copy-structured-markdown",
302
+ status: "materialized",
303
+ snapshotHash,
304
+ changed: previousSnapshotHash !== snapshotHash,
305
+ reason: "local origin copied or materialized into configured snapshot",
306
+ validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
307
+ };
308
+ }
309
+ const providerAdapter = selectProviderAdapter(source, origin);
310
+ if (providerAdapter) {
311
+ const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
312
+ const adapterResult = await runProviderAdapter(providerAdapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
313
+ if (adapterResult) {
314
+ if (adapterResult.status === "blocked-auth" && opts.browser && origin.method === "url") {
315
+ const browserResult = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
316
+ return {
317
+ ...browserResult,
318
+ adapter: `${providerAdapter}-browser-fallback`,
319
+ metadata: cleanMetadata({ providerAdapter, fallbackFrom: adapterResult.strategy }),
320
+ };
321
+ }
322
+ return adapterResult;
323
+ }
324
+ }
325
+ if (origin.method === "repository") {
326
+ await mkdir(dirname(snapshotPath), { recursive: true });
327
+ const content = renderRepositoryMetadata(entry, origin.ref ?? "");
328
+ await writeFile(snapshotPath, content, "utf8");
329
+ const snapshotHash = await sha256(snapshotPath);
330
+ return {
331
+ ...base,
332
+ strategy: "repository-metadata-only",
333
+ status: "metadata-only",
334
+ snapshotHash,
335
+ changed: previousSnapshotHash !== snapshotHash,
336
+ reason: "repository origin recorded as metadata; configure contract paths or checkout policy before broad acquisition",
337
+ validation: { repositoryNotCrawled: "pass", snapshotWritten: "pass" },
338
+ };
339
+ }
340
+ if (origin.method === "url") {
341
+ if (!opts.allowNetwork) {
342
+ return {
343
+ ...base,
344
+ strategy: "playwright-auth-or-direct-fetch",
345
+ status: "blocked-auth",
346
+ changed: false,
347
+ reason: "remote URL requires --allow-network or a materialized snapshot; Playwright auth helper can create reusable storageState first",
348
+ validation: { networkAllowed: "fail", previousSnapshotPreserved: previousSnapshotHash ? "pass" : "skipped" },
349
+ };
350
+ }
351
+ const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
352
+ if (opts.browser) {
353
+ if (origin.ref && await hasDirectRuntimeAuthForSource(source, origin.ref, authStatePath)) {
354
+ const direct = await fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
355
+ if (direct.status === "materialized")
356
+ return direct;
357
+ }
358
+ return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
359
+ }
360
+ return fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
361
+ }
362
+ return {
363
+ ...base,
364
+ strategy: "confirm-acquisition-method",
365
+ status: "blocked-auth",
366
+ changed: false,
367
+ reason: "provider-managed origin needs an explicit acquisition method or exported snapshot",
368
+ validation: { acquisitionMethodConfirmed: "fail", previousSnapshotPreserved: previousSnapshotHash ? "pass" : "skipped" },
369
+ };
370
+ }
371
+ function selectProviderAdapter(source, origin) {
372
+ const keys = [
373
+ origin.provider,
374
+ origin.kind,
375
+ source.origin?.provider,
376
+ source.origin?.kind,
377
+ source.kind,
378
+ source.type,
379
+ ].map((value) => normalizeAdapterKey(typeof value === "string" ? value : undefined));
380
+ if (keys.some((value) => value.includes("confluence")))
381
+ return "confluence";
382
+ if (keys.some((value) => value.includes("jira")))
383
+ return "jira";
384
+ if (keys.some((value) => value.includes("gitlab")))
385
+ return "gitlab";
386
+ if (keys.some((value) => ["figma", "modao", "mastergo", "design", "design-asset", "design-source"].includes(value))) {
387
+ return "design-source";
388
+ }
389
+ return undefined;
390
+ }
391
+ async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
392
+ if (adapter === "confluence") {
393
+ return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
394
+ }
395
+ if (adapter === "jira") {
396
+ return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
397
+ }
398
+ if (adapter === "gitlab") {
399
+ return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
400
+ }
401
+ return runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
402
+ }
403
+ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
404
+ const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "confluence");
405
+ const url = firstOriginString(entry.source, "apiUrl", "restUrl", "url") ?? base.origin;
406
+ const strategy = "confluence-storage-rest";
407
+ if (!url)
408
+ return sourceGap(base, "confluence adapter needs origin.url, origin.apiUrl, or origin.restUrl");
409
+ if (!opts.allowNetwork) {
410
+ return blockedAdapter(base, strategy, "confluence source requires --allow-network and optional Playwright storageState before acquisition");
411
+ }
412
+ const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
413
+ if (!fetched.ok)
414
+ return blockedAdapter(base, strategy, fetched.reason);
415
+ if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
416
+ return blockedAdapter(base, strategy, "confluence adapter resolved to login/chrome content; previous snapshot was preserved");
417
+ }
418
+ const parsed = parseJsonObject(fetched.body);
419
+ const title = parsed ? stringValue(parsed, "title") : undefined;
420
+ const pageId = parsed ? firstStringValue(parsed, "id", "pageId", "contentId") : undefined;
421
+ const version = parsed ? versionValue(parsed) : undefined;
422
+ const storageHtml = parsed ? confluenceBodyValue(parsed) : undefined;
423
+ const content = storageHtml
424
+ ? htmlToStructuredMarkdown(storageHtml)
425
+ : fetched.contentType.includes("html")
426
+ ? htmlToStructuredMarkdown(fetched.body)
427
+ : fencedBody(fetched.body, fetched.contentType);
428
+ if (!hasMaterialContent(content)) {
429
+ return blockedAdapter(base, strategy, "confluence adapter produced no material body content; previous snapshot was preserved");
430
+ }
431
+ return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
432
+ adapter: "confluence",
433
+ strategy,
434
+ resolvedOrigin: fetched.resolvedOrigin,
435
+ contentType: fetched.contentType,
436
+ content,
437
+ reason: "confluence source fetched and normalized into structured Markdown",
438
+ metadata: cleanMetadata({ title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
439
+ validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
440
+ });
441
+ }
442
+ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
443
+ const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "jira");
444
+ const csvPath = firstOriginString(entry.source, ...JIRA_LOCAL_FILE_ORIGIN_KEYS);
445
+ if (csvPath) {
446
+ const path = resolveAdapterLocalFilePath(target, csvPath);
447
+ if (!path)
448
+ return failed(base, "jira CSV fallback path must stay inside the target project");
449
+ if (!(await exists(path)))
450
+ return sourceGap(base, "jira CSV fallback path does not exist");
451
+ const csv = await readFile(path, "utf8");
452
+ const rows = parseCsvRows(csv);
453
+ const content = renderJiraCsvMarkdown(rows, csvPath);
454
+ if (!hasMaterialContent(content)) {
455
+ return blockedAdapter(base, "jira-csv-import", "jira CSV fallback produced no material issue content; previous snapshot was preserved");
456
+ }
457
+ return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
458
+ adapter: "jira",
459
+ strategy: "jira-csv-import",
460
+ resolvedOrigin: csvPath,
461
+ contentType: "text/csv",
462
+ content,
463
+ reason: "jira CSV export imported and normalized into structured Markdown",
464
+ metadata: cleanMetadata({ issueCount: rows.length, fallback: "csv" }),
465
+ validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
466
+ });
467
+ }
468
+ const url = firstOriginString(entry.source, "apiUrl", "searchUrl", "url") ?? (base.origin?.startsWith("http") || base.origin?.startsWith("data:") ? base.origin : undefined);
469
+ if (!url)
470
+ return null;
471
+ const strategy = "jira-rest-search";
472
+ if (!opts.allowNetwork) {
473
+ return blockedAdapter(base, strategy, "jira REST source requires --allow-network and optional Playwright storageState before acquisition");
474
+ }
475
+ const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
476
+ if (!fetched.ok)
477
+ return blockedAdapter(base, strategy, fetched.reason);
478
+ if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
479
+ return blockedAdapter(base, strategy, "jira adapter resolved to login/chrome content; previous snapshot was preserved");
480
+ }
481
+ const parsed = parseJsonObject(fetched.body);
482
+ const content = parsed ? renderJiraRestMarkdown(parsed) : fencedBody(fetched.body, fetched.contentType);
483
+ if (!hasMaterialContent(content)) {
484
+ return blockedAdapter(base, strategy, "jira adapter produced no material issue content; previous snapshot was preserved");
485
+ }
486
+ return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
487
+ adapter: "jira",
488
+ strategy,
489
+ resolvedOrigin: fetched.resolvedOrigin,
490
+ contentType: fetched.contentType,
491
+ content,
492
+ reason: "jira REST source fetched and normalized into structured Markdown",
493
+ metadata: cleanMetadata({ issueCount: jiraIssueCount(parsed), runtimeAuth: fetched.authSources.join(", ") || undefined }),
494
+ validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
495
+ });
496
+ }
497
+ async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
498
+ const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "gitlab");
499
+ const url = firstOriginString(entry.source, "rawUrl", "apiUrl", "url");
500
+ if (!url)
501
+ return null;
502
+ const strategy = "gitlab-raw-or-file-api";
503
+ if (!opts.allowNetwork) {
504
+ return blockedAdapter(base, strategy, "gitlab source requires --allow-network and optional token/storageState before acquisition");
505
+ }
506
+ const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
507
+ if (!fetched.ok)
508
+ return blockedAdapter(base, strategy, fetched.reason);
509
+ if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
510
+ return blockedAdapter(base, strategy, "gitlab adapter resolved to login/chrome content; previous snapshot was preserved");
511
+ }
512
+ const parsed = parseJsonObject(fetched.body);
513
+ const fileContent = parsed ? gitLabFileContent(parsed) : undefined;
514
+ const content = fileContent
515
+ ? fencedBody(fileContent.content, fileContent.contentType)
516
+ : fetched.contentType.includes("html")
517
+ ? htmlToStructuredMarkdown(fetched.body)
518
+ : fencedBody(fetched.body, fetched.contentType);
519
+ if (!hasMaterialContent(content)) {
520
+ return blockedAdapter(base, strategy, "gitlab adapter produced no material file content; previous snapshot was preserved");
521
+ }
522
+ return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
523
+ adapter: "gitlab",
524
+ strategy,
525
+ resolvedOrigin: fetched.resolvedOrigin,
526
+ contentType: fileContent?.contentType ?? fetched.contentType,
527
+ content,
528
+ reason: "gitlab file source fetched and normalized into structured Markdown",
529
+ metadata: cleanMetadata({
530
+ filePath: parsed ? firstStringValue(parsed, "file_path", "path", "name") : undefined,
531
+ ref: parsed ? firstStringValue(parsed, "ref", "branch") : undefined,
532
+ runtimeAuth: fetched.authSources.join(", ") || undefined,
533
+ }),
534
+ validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
535
+ });
536
+ }
537
+ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
538
+ const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "design-source");
539
+ const exportPath = firstOriginString(entry.source, ...DESIGN_LOCAL_FILE_ORIGIN_KEYS);
540
+ if (exportPath) {
541
+ const path = resolveAdapterLocalFilePath(target, exportPath);
542
+ if (!path)
543
+ return failed(base, "design export path must stay inside the target project");
544
+ if (!(await exists(path)))
545
+ return sourceGap(base, "design export path does not exist");
546
+ const body = await readFile(path, "utf8");
547
+ const content = renderDesignMarkdown(body, `file:${exportPath}`, contentTypeForPath(exportPath));
548
+ if (!hasMaterialContent(content)) {
549
+ return blockedAdapter(base, "design-export-import", "design export produced no material content; previous snapshot was preserved");
550
+ }
551
+ return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
552
+ adapter: "design-source",
553
+ strategy: "design-export-import",
554
+ resolvedOrigin: exportPath,
555
+ contentType: contentTypeForPath(exportPath),
556
+ content,
557
+ reason: "design export imported and normalized into structured Markdown",
558
+ metadata: cleanMetadata({ provider: base.provider, fallback: "export" }),
559
+ validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
560
+ });
561
+ }
562
+ const url = firstOriginString(entry.source, "apiUrl", "exportUrl", "url") ?? base.origin;
563
+ if (!url)
564
+ return null;
565
+ if (!opts.allowNetwork) {
566
+ return blockedAdapter(base, opts.browser ? "design-browser-render" : "design-api-or-browser", "design source requires --allow-network with token/export URL or --browser storageState acquisition");
567
+ }
568
+ const directRuntimeAuth = await hasDirectRuntimeAuthForSource(entry.source, url, authStatePath);
569
+ if (opts.browser && !directRuntimeAuth) {
570
+ const rendered = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
571
+ return {
572
+ ...rendered,
573
+ adapter: "design-source",
574
+ metadata: cleanMetadata({ provider: base.provider, acquisition: "browser" }),
575
+ };
576
+ }
577
+ const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
578
+ if (!fetched.ok) {
579
+ if (opts.browser)
580
+ return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
581
+ return blockedAdapter(base, "design-api-or-export", fetched.reason);
582
+ }
583
+ if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
584
+ if (opts.browser)
585
+ return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
586
+ return blockedAdapter(base, "design-api-or-export", "design adapter resolved to login/chrome content; previous snapshot was preserved");
587
+ }
588
+ const content = renderDesignMarkdown(fetched.body, fetched.resolvedOrigin, fetched.contentType);
589
+ if (!hasMaterialContent(content)) {
590
+ if (opts.browser)
591
+ return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
592
+ return blockedAdapter(base, "design-api-or-export", "design adapter produced no material design content; previous snapshot was preserved");
593
+ }
594
+ return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
595
+ adapter: "design-source",
596
+ strategy: "design-api-or-export",
597
+ resolvedOrigin: fetched.resolvedOrigin,
598
+ contentType: fetched.contentType,
599
+ content,
600
+ reason: "design source fetched and normalized into structured Markdown",
601
+ metadata: cleanMetadata({ provider: base.provider, runtimeAuth: fetched.authSources.join(", ") || undefined }),
602
+ validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
603
+ });
604
+ }
605
+ function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter) {
606
+ const origin = inferSourceOrigin(entry.source);
607
+ return {
608
+ envelopeVersion: 1,
609
+ sourceKey: entry.id,
610
+ sourcePath: entry.path,
611
+ rawLane: entry.rawLane,
612
+ provider: origin.provider,
613
+ adapter,
614
+ origin: origin.ref,
615
+ snapshot: relative(target, snapshotPath),
616
+ previousSnapshotHash,
617
+ };
618
+ }
619
+ async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result) {
620
+ const origin = inferSourceOrigin(entry.source);
621
+ const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata);
622
+ await mkdir(dirname(snapshotPath), { recursive: true });
623
+ await writeFile(snapshotPath, markdown, "utf8");
624
+ const snapshotHash = await sha256(snapshotPath);
625
+ return {
626
+ envelopeVersion: 1,
627
+ sourceKey: entry.id,
628
+ sourcePath: entry.path,
629
+ rawLane: entry.rawLane,
630
+ provider: origin.provider,
631
+ adapter: result.adapter,
632
+ strategy: result.strategy,
633
+ status: "materialized",
634
+ origin: origin.ref,
635
+ resolvedOrigin: result.resolvedOrigin,
636
+ contentType: result.contentType,
637
+ snapshot: relative(target, snapshotPath),
638
+ snapshotHash,
639
+ previousSnapshotHash,
640
+ metadata: result.metadata,
641
+ changed: previousSnapshotHash !== snapshotHash,
642
+ reason: result.reason,
643
+ validation: result.validation,
644
+ };
645
+ }
646
+ async function fetchAdapterBody(source, url, authStatePath) {
647
+ const { headers, cookieUsed, authSources } = await headersForSource(source, url, authStatePath);
648
+ try {
649
+ const response = await fetch(url, { headers });
650
+ const contentType = response.headers.get("content-type") ?? "";
651
+ const body = await response.text();
652
+ if (!response.ok)
653
+ return { ok: false, reason: `adapter fetch failed with HTTP ${response.status}` };
654
+ return { ok: true, resolvedOrigin: response.url, contentType, body, cookieUsed, authSources };
655
+ }
656
+ catch (error) {
657
+ return { ok: false, reason: `adapter fetch failed: ${error instanceof Error ? error.message : String(error)}` };
658
+ }
659
+ }
660
+ async function headersForSource(source, url, authStatePath) {
661
+ const headers = {};
662
+ const authSources = [];
663
+ const token = envValue(authStringList(source, "tokenEnv", "accessTokenEnv", "apiTokenEnv"));
664
+ if (token.value) {
665
+ const headerName = firstAuthString(source, "tokenHeader", "headerName") ?? "authorization";
666
+ const tokenScheme = firstAuthString(source, "tokenScheme", "scheme") ?? "Bearer";
667
+ headers[headerName] = headerName.toLowerCase() === "authorization" && tokenScheme
668
+ ? `${tokenScheme} ${token.value}`
669
+ : token.value;
670
+ authSources.push(`env:${token.name}`);
671
+ }
672
+ const envCookie = cookieHeaderFromEnvironment(source);
673
+ if (envCookie.source)
674
+ authSources.push(`env:${envCookie.source}`);
675
+ const storageCookie = authStatePath ? await cookieHeaderFromStorageState(authStatePath, url) : "";
676
+ if (storageCookie)
677
+ authSources.push("playwright-storage-state");
678
+ const cookie = mergeCookieHeaders(envCookie.value, storageCookie);
679
+ if (cookie)
680
+ headers.cookie = cookie;
681
+ return { headers, cookieUsed: Boolean(cookie), authSources };
682
+ }
683
+ async function hasDirectRuntimeAuthForSource(source, url, authStatePath) {
684
+ const { headers } = await headersForSource(source, url, authStatePath);
685
+ return Boolean(headers.authorization || headers.cookie);
686
+ }
687
+ async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath) {
688
+ const origin = inferSourceOrigin(entry.source);
689
+ const url = origin.ref;
690
+ const base = {
691
+ envelopeVersion: 1,
692
+ sourceKey: entry.id,
693
+ sourcePath: entry.path,
694
+ rawLane: entry.rawLane,
695
+ provider: origin.provider,
696
+ adapter: "generic-http",
697
+ origin: url,
698
+ snapshot: relative(target, snapshotPath),
699
+ previousSnapshotHash,
700
+ };
701
+ if (!url) {
702
+ return sourceGap(base, "remote URL origin is missing");
703
+ }
704
+ const { headers, authSources } = await headersForSource(entry.source, url, authStatePath);
705
+ try {
706
+ const response = await fetch(url, { headers });
707
+ const contentType = response.headers.get("content-type") ?? "";
708
+ const body = await response.text();
709
+ if (!response.ok) {
710
+ return blocked(base, `remote fetch failed with HTTP ${response.status}`);
711
+ }
712
+ if (looksLikeLoginPage(body, response.url)) {
713
+ return blocked(base, "remote fetch resolved to login/chrome content; previous snapshot was preserved");
714
+ }
715
+ const content = extractRemoteContent(contentType, body);
716
+ if (!hasMaterialContent(content)) {
717
+ return blocked(base, "remote fetch produced no material body content; previous snapshot was preserved");
718
+ }
719
+ const markdown = renderRemoteMarkdown(entry, response.url, contentType, content);
720
+ await mkdir(dirname(snapshotPath), { recursive: true });
721
+ await writeFile(snapshotPath, markdown, "utf8");
722
+ const snapshotHash = await sha256(snapshotPath);
723
+ return {
724
+ ...base,
725
+ strategy: authSources.length > 0 ? "runtime-auth-direct-fetch" : "direct-fetch-structured-markdown",
726
+ status: "materialized",
727
+ resolvedOrigin: response.url,
728
+ contentType,
729
+ snapshotHash,
730
+ changed: previousSnapshotHash !== snapshotHash,
731
+ reason: "remote URL fetched and normalized into structured Markdown",
732
+ metadata: cleanMetadata({ runtimeAuth: authSources.join(", ") || undefined }),
733
+ validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
734
+ };
735
+ }
736
+ catch (error) {
737
+ return blocked(base, `remote fetch failed: ${error instanceof Error ? error.message : String(error)}`);
738
+ }
739
+ }
740
+ async function fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath) {
741
+ const origin = inferSourceOrigin(entry.source);
742
+ const url = origin.ref;
743
+ const base = {
744
+ envelopeVersion: 1,
745
+ sourceKey: entry.id,
746
+ sourcePath: entry.path,
747
+ rawLane: entry.rawLane,
748
+ provider: origin.provider,
749
+ adapter: "generic-browser",
750
+ origin: url,
751
+ snapshot: relative(target, snapshotPath),
752
+ previousSnapshotHash,
753
+ };
754
+ if (!url) {
755
+ return sourceGap(base, "remote URL origin is missing");
756
+ }
757
+ let browser = null;
758
+ try {
759
+ const playwright = await loadProjectPlaywright(target);
760
+ browser = await playwright.chromium.launch({ headless: true });
761
+ const context = await browser.newContext(authStatePath ? { storageState: authStatePath } : {});
762
+ try {
763
+ const page = await context.newPage();
764
+ await page.goto(url, { waitUntil: "networkidle", timeout: 60_000 });
765
+ const resolvedUrl = page.url();
766
+ const body = await page.content();
767
+ if (looksLikeLoginPage(body, resolvedUrl)) {
768
+ return blocked(base, "browser acquisition resolved to login/chrome content; previous snapshot was preserved");
769
+ }
770
+ const content = htmlToStructuredMarkdown(body);
771
+ if (!hasMaterialContent(content)) {
772
+ return blocked(base, "browser acquisition produced no material body content; previous snapshot was preserved");
773
+ }
774
+ const markdown = renderRemoteMarkdown(entry, resolvedUrl, "text/html; rendered=playwright", content, "playwright-headless-browser");
775
+ await mkdir(dirname(snapshotPath), { recursive: true });
776
+ await writeFile(snapshotPath, markdown, "utf8");
777
+ const snapshotHash = await sha256(snapshotPath);
778
+ return {
779
+ ...base,
780
+ strategy: "playwright-headless-browser",
781
+ status: "materialized",
782
+ resolvedOrigin: resolvedUrl,
783
+ contentType: "text/html; rendered=playwright",
784
+ snapshotHash,
785
+ changed: previousSnapshotHash !== snapshotHash,
786
+ reason: "remote URL rendered through Playwright and normalized into structured Markdown",
787
+ validation: { browserRendered: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
788
+ };
789
+ }
790
+ finally {
791
+ await context.close().catch(() => undefined);
792
+ }
793
+ }
794
+ catch (error) {
795
+ return failed(base, `browser acquisition failed: ${error instanceof Error ? error.message : String(error)}`);
796
+ }
797
+ finally {
798
+ await browser?.close().catch(() => undefined);
799
+ }
800
+ }
801
+ async function renderStructuredLocalMarkdown(target, entry, originPath) {
802
+ const raw = await readFile(originPath, "utf8");
803
+ return [
804
+ "---",
805
+ `sourceKey: ${JSON.stringify(entry.id)}`,
806
+ `sourcePath: ${JSON.stringify(entry.path)}`,
807
+ entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
808
+ `origin: ${JSON.stringify(relative(target, originPath))}`,
809
+ `extractionMethod: "local-file-copy"`,
810
+ `extractedAt: ${JSON.stringify(new Date().toISOString())}`,
811
+ `snapshotStatus: "materialized-page"`,
812
+ "---",
813
+ "",
814
+ "# Source Snapshot",
815
+ "",
816
+ "## Provenance",
817
+ "",
818
+ `- Source key: \`${entry.id}\``,
819
+ entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
820
+ `- Origin: \`${relative(target, originPath)}\``,
821
+ "- Method: local-file-copy",
822
+ "",
823
+ "## Content",
824
+ "",
825
+ raw.trimEnd(),
826
+ "",
827
+ ].filter((line) => typeof line === "string").join("\n");
828
+ }
829
+ function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extractionMethod = "direct-fetch") {
830
+ return [
831
+ "---",
832
+ `sourceKey: ${JSON.stringify(entry.id)}`,
833
+ `sourcePath: ${JSON.stringify(entry.path)}`,
834
+ entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
835
+ `origin: ${JSON.stringify(resolvedUrl)}`,
836
+ `contentType: ${JSON.stringify(contentType)}`,
837
+ `extractionMethod: ${JSON.stringify(extractionMethod)}`,
838
+ `extractedAt: ${JSON.stringify(new Date().toISOString())}`,
839
+ `snapshotStatus: "materialized-page"`,
840
+ "---",
841
+ "",
842
+ "# Source Snapshot",
843
+ "",
844
+ "## Provenance",
845
+ "",
846
+ `- Source key: \`${entry.id}\``,
847
+ entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
848
+ `- Resolved URL: \`${resolvedUrl}\``,
849
+ `- Method: ${extractionMethod}`,
850
+ "",
851
+ "## Content",
852
+ "",
853
+ content.trimEnd(),
854
+ "",
855
+ ].filter((line) => typeof line === "string").join("\n");
856
+ }
857
+ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extractionMethod, adapter, metadata) {
858
+ const metadataLines = metadata && Object.keys(metadata).length > 0
859
+ ? [
860
+ "## Adapter Metadata",
861
+ "",
862
+ ...Object.entries(metadata).map(([key, value]) => `- ${key}: \`${String(value)}\``),
863
+ "",
864
+ ]
865
+ : [];
866
+ return [
867
+ "---",
868
+ `sourceKey: ${JSON.stringify(entry.id)}`,
869
+ `sourcePath: ${JSON.stringify(entry.path)}`,
870
+ entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
871
+ `origin: ${JSON.stringify(resolvedOrigin)}`,
872
+ `contentType: ${JSON.stringify(contentType)}`,
873
+ `adapter: ${JSON.stringify(adapter)}`,
874
+ `extractionMethod: ${JSON.stringify(extractionMethod)}`,
875
+ `extractedAt: ${JSON.stringify(new Date().toISOString())}`,
876
+ `snapshotStatus: "materialized-page"`,
877
+ "---",
878
+ "",
879
+ "# Source Snapshot",
880
+ "",
881
+ "## Provenance",
882
+ "",
883
+ `- Source key: \`${entry.id}\``,
884
+ entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
885
+ `- Resolved origin: \`${resolvedOrigin}\``,
886
+ `- Adapter: ${adapter}`,
887
+ `- Method: ${extractionMethod}`,
888
+ "",
889
+ ...metadataLines,
890
+ "## Content",
891
+ "",
892
+ content.trimEnd(),
893
+ "",
894
+ ].filter((line) => typeof line === "string").join("\n");
895
+ }
896
+ function extractRemoteContent(contentType, body) {
897
+ return contentType.includes("html") ? htmlToStructuredMarkdown(body) : fencedBody(body, contentType);
898
+ }
899
+ function confluenceBodyValue(value) {
900
+ const body = recordValue(value, "body");
901
+ const storage = recordValue(body, "storage");
902
+ const view = recordValue(body, "view");
903
+ return stringValue(storage, "value") ?? stringValue(view, "value") ?? stringValue(value, "content") ?? stringValue(value, "value");
904
+ }
905
+ function versionValue(value) {
906
+ const version = recordValue(value, "version");
907
+ const number = version?.number;
908
+ if (typeof number === "number" || typeof number === "string")
909
+ return String(number);
910
+ return firstStringValue(value, "version", "versionNumber");
911
+ }
912
+ function renderJiraCsvMarkdown(rows, origin) {
913
+ const issues = rows.map((row) => ({
914
+ key: firstCsvValue(row, "Key", "Issue key", "Issue Key", "key") ?? "",
915
+ summary: firstCsvValue(row, "Summary", "summary", "Title", "title") ?? "",
916
+ status: firstCsvValue(row, "Status", "status") ?? "",
917
+ assignee: firstCsvValue(row, "Assignee", "assignee") ?? "",
918
+ updated: firstCsvValue(row, "Updated", "updated", "Updated date") ?? "",
919
+ }));
920
+ return [
921
+ "# Jira Issue Export",
922
+ "",
923
+ "## Provenance",
924
+ "",
925
+ `- Origin: \`${origin}\``,
926
+ "- Method: jira-csv-import",
927
+ `- Issues: ${issues.length}`,
928
+ "",
929
+ "## Issues",
930
+ "",
931
+ "| Key | Status | Assignee | Updated | Summary |",
932
+ "| --- | --- | --- | --- | --- |",
933
+ ...issues.map((issue) => `| ${escapeTable(issue.key)} | ${escapeTable(issue.status)} | ${escapeTable(issue.assignee)} | ${escapeTable(issue.updated)} | ${escapeTable(issue.summary)} |`),
934
+ "",
935
+ ].join("\n");
936
+ }
937
+ function renderJiraRestMarkdown(value) {
938
+ const issues = jiraIssues(value);
939
+ if (issues.length === 0 && stringValue(value, "key")) {
940
+ issues.push(value);
941
+ }
942
+ return [
943
+ "# Jira Issue Snapshot",
944
+ "",
945
+ `Issue count: ${issues.length}`,
946
+ "",
947
+ ...issues.flatMap((issue) => renderJiraIssue(issue)),
948
+ issues.length === 0 ? fencedBody(JSON.stringify(value, null, 2), "application/json") : "",
949
+ "",
950
+ ].join("\n");
951
+ }
952
+ function renderJiraIssue(issue) {
953
+ const fields = recordValue(issue, "fields");
954
+ const status = recordValue(fields, "status");
955
+ const assignee = recordValue(fields, "assignee");
956
+ const key = stringValue(issue, "key") ?? "(unknown)";
957
+ const summary = stringValue(fields, "summary") ?? stringValue(issue, "summary") ?? "";
958
+ const description = jiraDescription(fields?.description);
959
+ return [
960
+ `## ${key}`,
961
+ "",
962
+ summary ? `- Summary: ${summary}` : undefined,
963
+ stringValue(status, "name") ? `- Status: ${stringValue(status, "name")}` : undefined,
964
+ stringValue(assignee, "displayName") ? `- Assignee: ${stringValue(assignee, "displayName")}` : undefined,
965
+ stringValue(fields, "updated") ? `- Updated: ${stringValue(fields, "updated")}` : undefined,
966
+ description ? "" : undefined,
967
+ description ? "### Description" : undefined,
968
+ description ? "" : undefined,
969
+ description,
970
+ "",
971
+ ].filter((line) => typeof line === "string");
972
+ }
973
+ function jiraIssues(value) {
974
+ const issues = value.issues;
975
+ return Array.isArray(issues) ? issues.filter(isRecord) : [];
976
+ }
977
+ function jiraIssueCount(value) {
978
+ if (!value)
979
+ return undefined;
980
+ const issues = jiraIssues(value);
981
+ if (issues.length > 0)
982
+ return issues.length;
983
+ return stringValue(value, "key") ? 1 : undefined;
984
+ }
985
+ function jiraDescription(value) {
986
+ if (typeof value === "string")
987
+ return value;
988
+ if (!value)
989
+ return undefined;
990
+ return JSON.stringify(value, null, 2);
991
+ }
992
+ function gitLabFileContent(value) {
993
+ const content = stringValue(value, "content");
994
+ if (!content)
995
+ return undefined;
996
+ const encoding = stringValue(value, "encoding");
997
+ if (encoding === "base64") {
998
+ return { content: Buffer.from(content, "base64").toString("utf8"), contentType: contentTypeForPath(firstStringValue(value, "file_path", "path", "name") ?? "") };
999
+ }
1000
+ return { content, contentType: contentTypeForPath(firstStringValue(value, "file_path", "path", "name") ?? "") };
1001
+ }
1002
+ function renderDesignMarkdown(body, origin, contentType) {
1003
+ const parsed = parseJsonObject(body);
1004
+ if (!parsed) {
1005
+ return contentType.includes("html") ? htmlToStructuredMarkdown(body) : fencedBody(body, contentType);
1006
+ }
1007
+ const name = firstStringValue(parsed, "name", "title", "fileName") ?? "Design Source";
1008
+ const modified = firstStringValue(parsed, "lastModified", "updatedAt", "modifiedAt");
1009
+ const nodes = designNodeNames(parsed).slice(0, 80);
1010
+ return [
1011
+ "# Design Source",
1012
+ "",
1013
+ `- Origin: \`${origin}\``,
1014
+ `- Name: ${name}`,
1015
+ modified ? `- Last modified: ${modified}` : undefined,
1016
+ `- Indexed nodes: ${nodes.length}`,
1017
+ "",
1018
+ nodes.length > 0 ? "## Nodes" : undefined,
1019
+ nodes.length > 0 ? "" : undefined,
1020
+ ...nodes.map((node) => `- ${node}`),
1021
+ "",
1022
+ "## Raw Metadata",
1023
+ "",
1024
+ fencedBody(JSON.stringify(parsed, null, 2), "application/json"),
1025
+ "",
1026
+ ].filter((line) => typeof line === "string").join("\n");
1027
+ }
1028
+ function designNodeNames(value) {
1029
+ if (Array.isArray(value))
1030
+ return value.flatMap(designNodeNames);
1031
+ if (!isRecord(value))
1032
+ return [];
1033
+ const current = stringValue(value, "name") ?? stringValue(value, "title");
1034
+ const children = value.children;
1035
+ const nested = Array.isArray(children) ? children.flatMap(designNodeNames) : [];
1036
+ return current ? [current, ...nested] : nested;
1037
+ }
1038
+ function parseCsvRows(value) {
1039
+ const rows = parseCsv(value).filter((row) => row.some((cell) => cell.trim().length > 0));
1040
+ const headers = rows.shift() ?? [];
1041
+ return rows.map((row) => {
1042
+ const record = {};
1043
+ headers.forEach((header, index) => {
1044
+ if (header.trim())
1045
+ record[header.trim()] = row[index]?.trim() ?? "";
1046
+ });
1047
+ return record;
1048
+ });
1049
+ }
1050
+ function parseCsv(value) {
1051
+ const rows = [];
1052
+ let row = [];
1053
+ let cell = "";
1054
+ let quoted = false;
1055
+ for (let index = 0; index < value.length; index += 1) {
1056
+ const char = value[index];
1057
+ const next = value[index + 1];
1058
+ if (char === "\"" && quoted && next === "\"") {
1059
+ cell += "\"";
1060
+ index += 1;
1061
+ continue;
1062
+ }
1063
+ if (char === "\"") {
1064
+ quoted = !quoted;
1065
+ continue;
1066
+ }
1067
+ if (char === "," && !quoted) {
1068
+ row.push(cell);
1069
+ cell = "";
1070
+ continue;
1071
+ }
1072
+ if ((char === "\n" || char === "\r") && !quoted) {
1073
+ if (char === "\r" && next === "\n")
1074
+ index += 1;
1075
+ row.push(cell);
1076
+ rows.push(row);
1077
+ row = [];
1078
+ cell = "";
1079
+ continue;
1080
+ }
1081
+ cell += char ?? "";
1082
+ }
1083
+ row.push(cell);
1084
+ rows.push(row);
1085
+ return rows;
1086
+ }
1087
+ function parseJsonObject(value) {
1088
+ try {
1089
+ const parsed = JSON.parse(value);
1090
+ return isRecord(parsed) ? parsed : undefined;
1091
+ }
1092
+ catch {
1093
+ return undefined;
1094
+ }
1095
+ }
1096
+ function recordValue(value, key) {
1097
+ if (!isRecord(value))
1098
+ return undefined;
1099
+ const item = value[key];
1100
+ return isRecord(item) ? item : undefined;
1101
+ }
1102
+ function stringValue(value, key) {
1103
+ if (!isRecord(value))
1104
+ return undefined;
1105
+ const item = value[key];
1106
+ return typeof item === "string" && item.trim().length > 0 ? item : undefined;
1107
+ }
1108
+ function firstStringValue(value, ...keys) {
1109
+ for (const key of keys) {
1110
+ const item = value[key];
1111
+ if (typeof item === "string" && item.trim().length > 0)
1112
+ return item;
1113
+ if (typeof item === "number")
1114
+ return String(item);
1115
+ }
1116
+ return undefined;
1117
+ }
1118
+ function firstCsvValue(row, ...keys) {
1119
+ for (const key of keys) {
1120
+ const value = row[key];
1121
+ if (value && value.trim().length > 0)
1122
+ return value.trim();
1123
+ }
1124
+ return undefined;
1125
+ }
1126
+ function firstOriginString(source, ...keys) {
1127
+ const origin = source.origin;
1128
+ if (!origin)
1129
+ return undefined;
1130
+ for (const key of keys) {
1131
+ const value = origin[key];
1132
+ if (typeof value === "string" && value.trim().length > 0)
1133
+ return value.trim();
1134
+ }
1135
+ return undefined;
1136
+ }
1137
+ function firstAuthString(source, ...keys) {
1138
+ const auth = authConfig(source);
1139
+ for (const key of keys) {
1140
+ const value = auth?.[key];
1141
+ if (typeof value === "string" && value.trim().length > 0)
1142
+ return value.trim();
1143
+ }
1144
+ return firstOriginString(source, ...keys);
1145
+ }
1146
+ function authStringList(source, ...keys) {
1147
+ const values = [];
1148
+ const auth = authConfig(source);
1149
+ for (const key of keys) {
1150
+ values.push(...stringListValue(auth?.[key]));
1151
+ }
1152
+ for (const key of keys) {
1153
+ values.push(...stringListValue(source.origin?.[key]));
1154
+ }
1155
+ return Array.from(new Set(values));
1156
+ }
1157
+ function authCookieMap(source) {
1158
+ const cookies = recordValue(authConfig(source), "cookies");
1159
+ if (!cookies)
1160
+ return {};
1161
+ const result = {};
1162
+ for (const [cookieName, envName] of Object.entries(cookies)) {
1163
+ if (typeof envName === "string" && envName.trim().length > 0) {
1164
+ result[cookieName] = envName.trim();
1165
+ }
1166
+ }
1167
+ return result;
1168
+ }
1169
+ function authConfig(source) {
1170
+ const auth = source.origin?.auth;
1171
+ return isRecord(auth) ? auth : undefined;
1172
+ }
1173
+ function stringListValue(value) {
1174
+ if (typeof value === "string" && value.trim().length > 0)
1175
+ return [value.trim()];
1176
+ if (Array.isArray(value)) {
1177
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
1178
+ }
1179
+ return [];
1180
+ }
1181
+ function envValue(names) {
1182
+ for (const name of names) {
1183
+ const value = process.env[name];
1184
+ if (value && value.trim().length > 0)
1185
+ return { name, value };
1186
+ }
1187
+ return {};
1188
+ }
1189
+ function cookieHeaderFromEnvironment(source) {
1190
+ const parts = [];
1191
+ const sources = [];
1192
+ const rawCookie = envValue(authStringList(source, "cookieEnv", "sessionCookieEnv"));
1193
+ if (rawCookie.value) {
1194
+ parts.push(rawCookie.value);
1195
+ if (rawCookie.name)
1196
+ sources.push(rawCookie.name);
1197
+ }
1198
+ for (const [cookieName, envName] of Object.entries(authCookieMap(source))) {
1199
+ const cookieValue = envValue([envName]);
1200
+ if (!cookieValue.value)
1201
+ continue;
1202
+ parts.push(`${cookieName}=${encodeURIComponent(cookieValue.value)}`);
1203
+ sources.push(envName);
1204
+ }
1205
+ return {
1206
+ source: sources.join(", ") || undefined,
1207
+ value: parts.join("; "),
1208
+ };
1209
+ }
1210
+ function mergeCookieHeaders(...headers) {
1211
+ const merged = new Map();
1212
+ for (const header of headers) {
1213
+ for (const part of header.split(";")) {
1214
+ const trimmed = part.trim();
1215
+ if (!trimmed)
1216
+ continue;
1217
+ const index = trimmed.indexOf("=");
1218
+ if (index <= 0)
1219
+ continue;
1220
+ merged.set(trimmed.slice(0, index), trimmed.slice(index + 1));
1221
+ }
1222
+ }
1223
+ return Array.from(merged, ([key, value]) => `${key}=${value}`).join("; ");
1224
+ }
1225
+ function cleanMetadata(value) {
1226
+ const entries = Object.entries(value).filter(([, item]) => item !== undefined && item !== "");
1227
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
1228
+ }
1229
+ function contentTypeForPath(path) {
1230
+ const ext = extname(path).toLowerCase();
1231
+ if (ext === ".json")
1232
+ return "application/json";
1233
+ if (ext === ".md" || ext === ".markdown")
1234
+ return "text/markdown";
1235
+ if (ext === ".yaml" || ext === ".yml")
1236
+ return "application/yaml";
1237
+ if (ext === ".csv")
1238
+ return "text/csv";
1239
+ if (ext === ".html" || ext === ".htm")
1240
+ return "text/html";
1241
+ return "text/plain";
1242
+ }
1243
+ function escapeTable(value) {
1244
+ return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
1245
+ }
1246
+ function isRecord(value) {
1247
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1248
+ }
1249
+ function normalizeAdapterKey(value) {
1250
+ return value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ?? "";
1251
+ }
1252
+ function renderRepositoryMetadata(entry, repository) {
1253
+ return [
1254
+ "---",
1255
+ `sourceKey: ${JSON.stringify(entry.id)}`,
1256
+ `sourcePath: ${JSON.stringify(entry.path)}`,
1257
+ entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
1258
+ `origin: ${JSON.stringify(repository)}`,
1259
+ `extractionMethod: "repository-metadata-only"`,
1260
+ `extractedAt: ${JSON.stringify(new Date().toISOString())}`,
1261
+ `snapshotStatus: "metadata-only"`,
1262
+ "---",
1263
+ "",
1264
+ "# Repository Source",
1265
+ "",
1266
+ "## Provenance",
1267
+ "",
1268
+ `- Source key: \`${entry.id}\``,
1269
+ entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
1270
+ `- Repository: \`${repository}\``,
1271
+ "- Method: repository-metadata-only",
1272
+ "",
1273
+ "## Acquisition Boundary",
1274
+ "",
1275
+ "Repository origins are not crawled or converted wholesale. Configure contract paths, refs, or a checkout policy before materializing repository content.",
1276
+ "",
1277
+ ].filter((line) => typeof line === "string").join("\n");
1278
+ }
1279
+ function htmlToStructuredMarkdown(value) {
1280
+ const body = (value.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? value)
1281
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
1282
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
1283
+ .replace(/<(nav|header|footer|aside)[^>]*>[\s\S]*?<\/\1>/gi, "");
1284
+ return body
1285
+ .replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
1286
+ .replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
1287
+ .replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
1288
+ .replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, "\n#### $1\n")
1289
+ .replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "\n- $1")
1290
+ .replace(/<br\s*\/?>/gi, "\n")
1291
+ .replace(/<\/(p|div|section|article|tr)>/gi, "\n")
1292
+ .replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
1293
+ .replace(/<[^>]+>/g, "")
1294
+ .replace(/&nbsp;/g, " ")
1295
+ .replace(/&amp;/g, "&")
1296
+ .replace(/&lt;/g, "<")
1297
+ .replace(/&gt;/g, ">")
1298
+ .replace(/&quot;/g, "\"")
1299
+ .split("\n")
1300
+ .map((line) => line.trim())
1301
+ .filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
1302
+ .join("\n");
1303
+ }
1304
+ function fencedBody(body, contentType) {
1305
+ const info = contentType.includes("json")
1306
+ ? "json"
1307
+ : contentType.includes("yaml") || contentType.includes("yml")
1308
+ ? "yaml"
1309
+ : "";
1310
+ return ["```" + info, body.trimEnd(), "```"].join("\n");
1311
+ }
1312
+ function shouldPreserveMachineReadable(source, originPath, snapshotPath) {
1313
+ const ext = extname(originPath).toLowerCase();
1314
+ const snapshotExt = extname(snapshotPath).toLowerCase();
1315
+ return (source.type === "api_contract" ||
1316
+ ["backend_repo", "design_asset"].includes(source.type ?? "") ||
1317
+ [".json", ".yaml", ".yml"].includes(ext) ||
1318
+ [".json", ".yaml", ".yml"].includes(snapshotExt));
1319
+ }
1320
+ function chooseStrategy(method) {
1321
+ if (method === "local")
1322
+ return "local-copy-structured-markdown";
1323
+ if (method === "repository")
1324
+ return "repository-metadata-only";
1325
+ if (method === "url")
1326
+ return "playwright-auth-or-direct-fetch";
1327
+ return "confirm-acquisition-method";
1328
+ }
1329
+ function renderDownstreamImpact(results) {
1330
+ const changed = results.filter((result) => result.changed);
1331
+ const blocked = results.filter((result) => ["blocked-auth", "source-gap", "failed"].includes(result.status));
1332
+ return [
1333
+ "# Downstream Impact",
1334
+ "",
1335
+ `Changed sources: ${changed.length}`,
1336
+ `Blocked sources: ${blocked.length}`,
1337
+ "",
1338
+ changed.length === 0
1339
+ ? "No downstream refresh is recommended from this sync run."
1340
+ : changed.map((result) => `- \`${result.sourcePath}\`: snapshot changed; review dependent proposal/spec/design/tasks/tests before claiming freshness.`).join("\n"),
1341
+ "",
1342
+ blocked.length === 0
1343
+ ? "No blocked sources."
1344
+ : blocked.map((result) => `- \`${result.sourcePath}\` [${result.status}]: ${result.reason}`).join("\n"),
1345
+ "",
1346
+ ].join("\n");
1347
+ }
1348
+ async function cookieHeaderFromStorageState(storageStatePath, url) {
1349
+ try {
1350
+ const parsed = JSON.parse(await readFile(storageStatePath, "utf8"));
1351
+ const host = new URL(url).hostname;
1352
+ return (parsed.cookies ?? [])
1353
+ .filter((cookie) => cookie.name && cookie.value && domainMatches(host, cookie.domain ?? ""))
1354
+ .map((cookie) => `${cookie.name}=${cookie.value}`)
1355
+ .join("; ");
1356
+ }
1357
+ catch {
1358
+ return "";
1359
+ }
1360
+ }
1361
+ function domainMatches(host, domain) {
1362
+ const normalized = domain.replace(/^\./, "");
1363
+ return host === normalized || host.endsWith(`.${normalized}`);
1364
+ }
1365
+ function looksLikeLoginPage(body, url) {
1366
+ const lower = `${url}\n${body.slice(0, 5000)}`.toLowerCase();
1367
+ return (lower.includes("login") ||
1368
+ lower.includes("sign in") ||
1369
+ lower.includes("signin") ||
1370
+ lower.includes("log in") ||
1371
+ lower.includes("password") ||
1372
+ lower.includes("sso")) && !lower.includes("logout");
1373
+ }
1374
+ function hasMaterialContent(value) {
1375
+ const body = value.replace(/^---[\s\S]*?---/, "").replace(/[#*\-[\]()`\s]/g, "");
1376
+ return body.length >= 20;
1377
+ }
1378
+ function sourceGap(base, reason) {
1379
+ return {
1380
+ ...base,
1381
+ strategy: "source-gap",
1382
+ status: "source-gap",
1383
+ changed: false,
1384
+ reason,
1385
+ validation: { sourceAvailable: "fail" },
1386
+ };
1387
+ }
1388
+ function blocked(base, reason) {
1389
+ return {
1390
+ ...base,
1391
+ strategy: "direct-fetch-structured-markdown",
1392
+ status: "blocked-auth",
1393
+ changed: false,
1394
+ reason,
1395
+ validation: { bodyContent: "fail", previousSnapshotPreserved: base.previousSnapshotHash ? "pass" : "skipped" },
1396
+ };
1397
+ }
1398
+ function blockedAdapter(base, strategy, reason) {
1399
+ return {
1400
+ ...base,
1401
+ strategy,
1402
+ status: "blocked-auth",
1403
+ changed: false,
1404
+ reason,
1405
+ validation: { bodyContent: "fail", previousSnapshotPreserved: base.previousSnapshotHash ? "pass" : "skipped" },
1406
+ };
1407
+ }
1408
+ function failed(base, reason) {
1409
+ return {
1410
+ ...base,
1411
+ strategy: "playwright-headless-browser",
1412
+ status: "failed",
1413
+ changed: false,
1414
+ reason,
1415
+ validation: { browserRendered: "fail", previousSnapshotPreserved: base.previousSnapshotHash ? "pass" : "skipped" },
1416
+ };
1417
+ }
1418
+ async function loadProjectPlaywright(target) {
1419
+ const requireFromTarget = createRequire(resolve(target, "package.json"));
1420
+ let resolved;
1421
+ try {
1422
+ resolved = requireFromTarget.resolve("playwright");
1423
+ }
1424
+ catch {
1425
+ throw new Error("Playwright browser acquisition requires the target project to install the `playwright` package");
1426
+ }
1427
+ return await import(resolved);
1428
+ }
1429
+ async function hashIfExists(path) {
1430
+ try {
1431
+ return await sha256(path);
1432
+ }
1433
+ catch {
1434
+ return undefined;
1435
+ }
1436
+ }
1437
+ async function sha256(path) {
1438
+ const content = await readFile(path);
1439
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
1440
+ }
1441
+ async function exists(path) {
1442
+ try {
1443
+ await stat(path);
1444
+ return true;
1445
+ }
1446
+ catch {
1447
+ return false;
1448
+ }
1449
+ }
1450
+ async function safeReadDir(path) {
1451
+ try {
1452
+ return await import("node:fs/promises").then((fs) => fs.readdir(path, { withFileTypes: true }));
1453
+ }
1454
+ catch {
1455
+ return [];
1456
+ }
1457
+ }
1458
+ function safeFileName(value) {
1459
+ return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "source";
1460
+ }
1461
+ function isMigrationLaneCandidate(value, staleNames) {
1462
+ return (staleNames.has(value) ||
1463
+ /^v?\d+(?:[._-]\d+)*$/i.test(value) ||
1464
+ /^(sprint|iteration|release)[-_]?\d*/i.test(value));
1465
+ }
1466
+ function resolveAuthStatePath(target, config, authState) {
1467
+ const statePath = resolve(target, authState);
1468
+ const sharedAuthRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
1469
+ const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
1470
+ const pathParts = statePath.split(/[\\/]/);
1471
+ const modulePrivate = isInside(modulesRoot, statePath) && pathParts.includes("_playwright") && pathParts.includes(".auth");
1472
+ if (isInside(sharedAuthRoot, statePath) || modulePrivate)
1473
+ return statePath;
1474
+ throw new Error(`Playwright storageState input must stay under ${sharedAuthRoot} or a module _playwright/.auth directory`);
1475
+ }
1476
+ function resolveSelectedAuthStatePath(target, config, opts) {
1477
+ if (opts.authState)
1478
+ return resolveAuthStatePath(target, config, opts.authState);
1479
+ if (opts.authProfile)
1480
+ return resolveAuthProfilePath(target, config, opts.authProfile);
1481
+ return undefined;
1482
+ }
1483
+ function resolveAuthProfilePath(target, config, profile) {
1484
+ const authRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
1485
+ const fileName = profile ? `${safeFileName(profile)}.json` : "user_data.json";
1486
+ return join(authRoot, fileName);
1487
+ }
1488
+ function resolveAdapterLocalFilePath(target, value) {
1489
+ const path = resolve(target, value);
1490
+ return isInside(resolve(target), path) ? path : undefined;
1491
+ }
1492
+ function isInside(root, target) {
1493
+ const rel = relative(root, target);
1494
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
1495
+ }
1496
+ export function createSyncRunId(now = new Date()) {
1497
+ return now.toISOString().replace(/[:.]/g, "-");
1498
+ }
1499
+ //# sourceMappingURL=prepare-sync.js.map