@captain_z/zsk 1.8.3 → 1.8.5

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 (143) hide show
  1. package/dist/bin.js +13 -0
  2. package/dist/bin.js.map +1 -1
  3. package/dist/commands/add-flow.d.ts +3 -7
  4. package/dist/commands/add-flow.js +7 -59
  5. package/dist/commands/add-flow.js.map +1 -1
  6. package/dist/commands/add.js +25 -104
  7. package/dist/commands/add.js.map +1 -1
  8. package/dist/commands/check.js +14 -575
  9. package/dist/commands/check.js.map +1 -1
  10. package/dist/commands/config.js +1 -1
  11. package/dist/commands/config.js.map +1 -1
  12. package/dist/commands/demo.d.ts +5 -0
  13. package/dist/commands/demo.js +70 -297
  14. package/dist/commands/demo.js.map +1 -1
  15. package/dist/commands/doctor.js +9 -4
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/gate.d.ts +1 -0
  18. package/dist/commands/gate.js +8 -2
  19. package/dist/commands/gate.js.map +1 -1
  20. package/dist/commands/prepare.js +7 -1
  21. package/dist/commands/prepare.js.map +1 -1
  22. package/dist/commands/project-init.js +30 -8
  23. package/dist/commands/project-init.js.map +1 -1
  24. package/dist/core/config.d.ts +68 -0
  25. package/dist/core/config.js +198 -15
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/demo-auth.d.ts +30 -0
  28. package/dist/core/demo-auth.js +213 -0
  29. package/dist/core/demo-auth.js.map +1 -0
  30. package/dist/core/demo-scenarios.d.ts +62 -0
  31. package/dist/core/demo-scenarios.js +276 -0
  32. package/dist/core/demo-scenarios.js.map +1 -0
  33. package/dist/core/demo-sources.d.ts +37 -0
  34. package/dist/core/demo-sources.js +198 -0
  35. package/dist/core/demo-sources.js.map +1 -0
  36. package/dist/core/mcp-registry-discovery.d.ts +16 -0
  37. package/dist/core/mcp-registry-discovery.js +187 -0
  38. package/dist/core/mcp-registry-discovery.js.map +1 -0
  39. package/dist/core/origin-detection.js +1 -1
  40. package/dist/core/origin-detection.js.map +1 -1
  41. package/dist/core/prepare-artifacts.d.ts +16 -0
  42. package/dist/core/prepare-artifacts.js +25 -0
  43. package/dist/core/prepare-artifacts.js.map +1 -0
  44. package/dist/core/prepare-auth-helper.d.ts +8 -0
  45. package/dist/core/prepare-auth-helper.js +32 -0
  46. package/dist/core/prepare-auth-helper.js.map +1 -0
  47. package/dist/core/prepare-materialization.d.ts +8 -0
  48. package/dist/core/prepare-materialization.js +26 -0
  49. package/dist/core/prepare-materialization.js.map +1 -0
  50. package/dist/core/prepare-migration.d.ts +6 -0
  51. package/dist/core/prepare-migration.js +57 -0
  52. package/dist/core/prepare-migration.js.map +1 -0
  53. package/dist/core/prepare-reporting.d.ts +5 -0
  54. package/dist/core/prepare-reporting.js +106 -0
  55. package/dist/core/prepare-reporting.js.map +1 -0
  56. package/dist/core/prepare-routing.d.ts +12 -0
  57. package/dist/core/prepare-routing.js +182 -0
  58. package/dist/core/prepare-routing.js.map +1 -0
  59. package/dist/core/prepare-sync.d.ts +13 -53
  60. package/dist/core/prepare-sync.js +878 -359
  61. package/dist/core/prepare-sync.js.map +1 -1
  62. package/dist/core/prepare-utils.d.ts +6 -0
  63. package/dist/core/prepare-utils.js +35 -0
  64. package/dist/core/prepare-utils.js.map +1 -0
  65. package/dist/core/profile-bundle-installation.d.ts +55 -0
  66. package/dist/core/profile-bundle-installation.js +170 -0
  67. package/dist/core/profile-bundle-installation.js.map +1 -0
  68. package/dist/core/provider-policy.d.ts +26 -0
  69. package/dist/core/provider-policy.js +180 -0
  70. package/dist/core/provider-policy.js.map +1 -0
  71. package/dist/core/provider-readiness.d.ts +39 -0
  72. package/dist/core/provider-readiness.js +78 -0
  73. package/dist/core/provider-readiness.js.map +1 -0
  74. package/dist/core/source-adapter-normalization.d.ts +31 -0
  75. package/dist/core/source-adapter-normalization.js +235 -0
  76. package/dist/core/source-adapter-normalization.js.map +1 -0
  77. package/dist/core/source-snapshot-adapters.d.ts +59 -0
  78. package/dist/core/source-snapshot-adapters.js +60 -0
  79. package/dist/core/source-snapshot-adapters.js.map +1 -0
  80. package/dist/core/staffing-plan.d.ts +1 -0
  81. package/dist/core/staffing-plan.js +113 -21
  82. package/dist/core/staffing-plan.js.map +1 -1
  83. package/dist/core/stage-clarity-verification.d.ts +31 -0
  84. package/dist/core/stage-clarity-verification.js +316 -0
  85. package/dist/core/stage-clarity-verification.js.map +1 -0
  86. package/dist/core/stage-output-quality.d.ts +3 -0
  87. package/dist/core/stage-output-quality.js +122 -0
  88. package/dist/core/stage-output-quality.js.map +1 -0
  89. package/dist/core/stage-quality-artifacts.d.ts +15 -0
  90. package/dist/core/stage-quality-artifacts.js +421 -0
  91. package/dist/core/stage-quality-artifacts.js.map +1 -0
  92. package/dist/core/stage-quality-contracts.d.ts +105 -0
  93. package/dist/core/stage-quality-contracts.js +2 -0
  94. package/dist/core/stage-quality-contracts.js.map +1 -0
  95. package/dist/core/stage-quality-criteria.d.ts +9 -0
  96. package/dist/core/stage-quality-criteria.js +286 -0
  97. package/dist/core/stage-quality-criteria.js.map +1 -0
  98. package/dist/core/stage-quality-rendering.d.ts +15 -0
  99. package/dist/core/stage-quality-rendering.js +240 -0
  100. package/dist/core/stage-quality-rendering.js.map +1 -0
  101. package/dist/core/stage-quality.d.ts +4 -59
  102. package/dist/core/stage-quality.js +54 -795
  103. package/dist/core/stage-quality.js.map +1 -1
  104. package/dist/core/template-registry.js +12 -15
  105. package/dist/core/template-registry.js.map +1 -1
  106. package/dist/core/workspace-conformance.d.ts +39 -0
  107. package/dist/core/workspace-conformance.js +603 -0
  108. package/dist/core/workspace-conformance.js.map +1 -0
  109. package/package.json +2 -2
  110. package/schemas/providers.schema.json +74 -0
  111. package/schemas/zsk-config.schema.json +417 -1
  112. package/templates/module/frontend-module/design.md +10 -0
  113. package/templates/module/frontend-module/proposal.md +8 -0
  114. package/templates/module/frontend-module/spec.md +7 -0
  115. package/templates/project-init/.zsk/README.md +48 -0
  116. package/templates/project-init/.zsk/config.yaml +37 -0
  117. package/templates/project-init/.zsk/docs/CONFIG-SCHEMA.md +127 -0
  118. package/templates/project-init/.zsk/docs/PROJECT-CONFIG.md +53 -5
  119. package/templates/project-init/.zsk/docs/README.md +10 -0
  120. package/templates/project-init/.zsk/docs/SECURITY.md +34 -0
  121. package/templates/project-init/.zsk/docs/SYSTEM-SPEC.md +39 -7
  122. package/templates/project-init/.zsk/evidence/README.md +15 -0
  123. package/templates/project-init/.zsk/issues/README.md +10 -0
  124. package/templates/project-init/.zsk/modules/README.md +19 -0
  125. package/templates/project-init/.zsk/modules/index.md +9 -5
  126. package/templates/project-init/.zsk/raws/README.md +34 -0
  127. package/templates/project-init/.zsk/roles.yaml +36 -105
  128. package/templates/project-init/.zsk/templates/config.examples.yaml +83 -0
  129. package/templates/project-init/.zsk/templates/issue-card.md +23 -0
  130. package/templates/project-init/.zsk/templates/module/README.md +13 -0
  131. package/templates/project-init/.zsk/templates/module/design.md +22 -0
  132. package/templates/project-init/.zsk/templates/module/module.yaml +15 -0
  133. package/templates/project-init/.zsk/templates/module/proposal.md +20 -0
  134. package/templates/project-init/.zsk/templates/module/spec.md +22 -0
  135. package/templates/project-init/.zsk/templates/module/tasks.md +16 -0
  136. package/templates/project-init/.zsk/raws/index.md +0 -18
  137. package/templates/project-init/.zsk/raws/manifest.json +0 -4
  138. package/templates/project-init/.zsk/raws/prepare/backend/index.md +0 -4
  139. package/templates/project-init/.zsk/raws/prepare/design/index.md +0 -3
  140. package/templates/project-init/.zsk/raws/prepare/index.md +0 -4
  141. package/templates/project-init/.zsk/raws/prepare/product/index.md +0 -4
  142. package/templates/project-init/.zsk/raws/prepare/qa/index.md +0 -4
  143. package/templates/project-init/.zsk/raws/prepare/ux/index.md +0 -3
@@ -1,39 +1,47 @@
1
- import { createHash } from "node:crypto";
1
+ import { Buffer } from "node:buffer";
2
+ import { execFile } from "node:child_process";
2
3
  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";
4
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
5
+ import { dirname, extname, join, relative, resolve } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { DESIGN_LOCAL_FILE_ORIGIN_KEYS, JIRA_LOCAL_FILE_ORIGIN_KEYS, flattenPrepareEntries, resolveSourceSnapshot, } from "./config.js";
6
8
  import { inferSourceOrigin } from "./origin-detection.js";
9
+ import { discoverMcpRegistries } from "./mcp-registry-discovery.js";
10
+ import { loadProviderPolicy } from "./provider-policy.js";
11
+ import { buildProviderReadinessEvidence } from "./provider-readiness.js";
7
12
  import { describeResource, updateRawIndexes, updateRawManifest } from "./raw-manifest.js";
13
+ import { createSyncRunId, resolvePrepareSyncArtifacts, } from "./prepare-artifacts.js";
14
+ import { hashIfExists, sha256, writePreparedSnapshot } from "./prepare-materialization.js";
15
+ import { renderDownstreamImpact, renderPreparationReport } from "./prepare-reporting.js";
16
+ import { routePreparedResources } from "./prepare-routing.js";
17
+ import { escapeTable, exists, isRecord, safeFileName } from "./prepare-utils.js";
18
+ import { interpretSourceAdapter } from "./source-adapter-normalization.js";
19
+ import { acquireSourceSnapshot, chooseSourceSnapshotStrategy, createSourceSnapshotContext, } from "./source-snapshot-adapters.js";
20
+ import { isPathInside, resolvePlaywrightAuthProfilePath, resolvePlaywrightAuthStatePath, } from "./workspace-conformance.js";
8
21
  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
- }
22
+ export { createSyncRunId, resolvePrepareSyncArtifacts } from "./prepare-artifacts.js";
23
+ export { writeAuthLoginHelper } from "./prepare-auth-helper.js";
24
+ export { buildRawMigrationPlan } from "./prepare-migration.js";
25
+ const execFileAsync = promisify(execFile);
26
+ const DEFAULT_ADAPTER_COMMAND_TIMEOUT_MS = 60_000;
27
+ const DEFAULT_ADAPTER_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
23
28
  export async function syncPrepareSources(target, config, opts = {}) {
24
29
  const runId = opts.runId ?? createSyncRunId();
25
30
  const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
26
- const entries = flattenProjectSources(config.sources);
31
+ const entries = flattenPrepareEntries(config);
27
32
  const selected = new Set(selectSourceEntries(entries, opts));
28
33
  const results = [];
29
34
  await mkdir(artifacts.adapterResultsDir, { recursive: true });
35
+ await mkdir(artifacts.providerReadinessDir, { recursive: true });
36
+ const providerReadiness = await resolveProviderReadiness(target, artifacts, [...selected]);
30
37
  for (const entry of entries) {
31
38
  if (!selected.has(entry))
32
39
  continue;
33
- const result = await syncEntry(target, config, entry, opts);
40
+ const result = annotateProviderReadiness(target, entry, await syncEntry(target, config, entry, opts, entries), providerReadiness.byProvider);
34
41
  results.push(result);
35
42
  await writeFile(join(artifacts.adapterResultsDir, `${safeFileName(entry.id)}.json`), `${JSON.stringify(result, null, 2)}\n`, "utf8");
36
43
  }
44
+ const routes = opts.dryRun ? [] : await routePreparedResources(target, config, results);
37
45
  if (!opts.dryRun) {
38
46
  const records = [];
39
47
  for (const entry of entries) {
@@ -45,43 +53,22 @@ export async function syncPrepareSources(target, config, opts = {}) {
45
53
  const downstreamImpact = renderDownstreamImpact(results);
46
54
  await mkdir(dirname(artifacts.downstreamImpactPath), { recursive: true });
47
55
  await writeFile(artifacts.downstreamImpactPath, downstreamImpact, "utf8");
56
+ const syncReport = renderPreparationReport(entries, results, routes);
57
+ await mkdir(dirname(artifacts.syncReportPath), { recursive: true });
58
+ await writeFile(artifacts.syncReportPath, syncReport, "utf8");
48
59
  return {
49
60
  artifacts,
50
61
  results,
62
+ routes,
63
+ providerReadiness: [...providerReadiness.byProvider.values()].map((item) => item.evidence),
51
64
  downstreamImpact,
65
+ syncReport,
52
66
  };
53
67
  }
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
68
  export async function checkPrepareAuth(target, config, opts = {}) {
82
69
  const runId = opts.runId ?? createSyncRunId();
83
70
  const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
84
- const entries = selectSourceEntries(flattenProjectSources(config.sources), opts);
71
+ const entries = selectSourceEntries(flattenPrepareEntries(config), opts);
85
72
  const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
86
73
  const results = [];
87
74
  for (const entry of entries) {
@@ -91,45 +78,6 @@ export async function checkPrepareAuth(target, config, opts = {}) {
91
78
  await writeFile(artifacts.authCheckPath, `${JSON.stringify({ runId, results }, null, 2)}\n`, "utf8");
92
79
  return { artifacts, results };
93
80
  }
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
81
  function selectSourceEntries(entries, opts) {
134
82
  if (opts.all || !opts.source)
135
83
  return entries;
@@ -144,6 +92,52 @@ function selectSourceEntries(entries, opts) {
144
92
  }
145
93
  return alias;
146
94
  }
95
+ async function resolveProviderReadiness(target, artifacts, entries) {
96
+ const names = new Map();
97
+ for (const entry of entries) {
98
+ const normalized = interpretSourceAdapter(entry.source);
99
+ if (!normalized.mcpProviderName)
100
+ continue;
101
+ names.set(normalized.mcpProviderName, normalized);
102
+ }
103
+ if (names.size === 0)
104
+ return { byProvider: new Map() };
105
+ const policy = await loadProviderPolicy(target);
106
+ const registry = await discoverMcpRegistries(target);
107
+ const byProvider = new Map();
108
+ for (const [providerName, normalized] of names) {
109
+ const evidence = buildProviderReadinessEvidence(target, providerName, normalized.adapterId, policy, registry.registrations);
110
+ evidence.warnings.push(...registry.warnings);
111
+ const path = join(artifacts.providerReadinessDir, `${safeFileName(providerName)}.json`);
112
+ await writeFile(path, `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
113
+ byProvider.set(providerName, { path, evidence });
114
+ }
115
+ return { byProvider };
116
+ }
117
+ function annotateProviderReadiness(target, entry, result, readiness) {
118
+ const normalized = interpretSourceAdapter(entry.source);
119
+ const providerName = normalized.mcpProviderName;
120
+ if (!providerName)
121
+ return result;
122
+ const item = readiness.get(providerName);
123
+ if (!item)
124
+ return result;
125
+ return {
126
+ ...result,
127
+ metadata: cleanMetadata({
128
+ ...(result.metadata ?? {}),
129
+ adapterId: normalized.adapterId,
130
+ mcpProvider: providerName,
131
+ providerReadiness: relative(target, item.path),
132
+ providerReadinessStatus: item.evidence.summary,
133
+ providerPolicyStatus: item.evidence.policy.status,
134
+ providerRegistryStatus: item.evidence.registry.status,
135
+ providerConnectionStatus: item.evidence.connection.status,
136
+ capabilitiesMatched: item.evidence.capabilities.matched.join(", ") || undefined,
137
+ capabilitiesEvaluation: item.evidence.capabilities.evaluation,
138
+ }),
139
+ };
140
+ }
147
141
  async function checkAuthEntry(entry, authStatePath, opts) {
148
142
  const origin = inferSourceOrigin(entry.source);
149
143
  const base = {
@@ -154,20 +148,20 @@ async function checkAuthEntry(entry, authStatePath, opts) {
154
148
  origin: origin.ref,
155
149
  method: origin.method,
156
150
  };
157
- const url = origin.ref;
158
- if (!url || origin.method !== "url") {
151
+ const url = authCheckUrlForSource(entry.source, origin);
152
+ if (!url) {
159
153
  return {
160
154
  ...base,
161
155
  status: "source-gap",
162
156
  authSources: [],
163
157
  directAuthAvailable: false,
164
158
  networkAttempted: false,
165
- reason: "auth-check only validates URL origins; configure origin.url before checking runtime credentials",
159
+ reason: "auth-check needs origin.url or provider fields that derive a REST URL before checking runtime credentials",
166
160
  validation: { urlOrigin: "fail", secretValuesHidden: "pass" },
167
161
  };
168
162
  }
169
163
  const { headers, authSources } = await headersForSource(entry.source, url, authStatePath);
170
- const directAuthAvailable = Boolean(headers.authorization || headers.cookie);
164
+ const directAuthAvailable = hasRuntimeAuthHeaders(headers);
171
165
  if (!opts.allowNetwork) {
172
166
  return {
173
167
  ...base,
@@ -253,161 +247,193 @@ async function checkAuthEntry(entry, authStatePath, opts) {
253
247
  };
254
248
  }
255
249
  }
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,
250
+ function authCheckUrlForSource(source, origin) {
251
+ if (origin.ref && origin.method === "url")
252
+ return origin.ref;
253
+ const provider = sourceProviderToken(source);
254
+ if (provider?.includes("confluence")) {
255
+ return firstOriginString(source, "apiUrl", "restUrl") ?? deriveConfluenceRestUrl(source);
256
+ }
257
+ if (provider?.includes("jira")) {
258
+ return firstOriginString(source, "apiUrl", "searchUrl") ?? deriveJiraRestUrl(source);
259
+ }
260
+ return undefined;
261
+ }
262
+ async function syncEntry(target, config, entry, opts, entries = [entry]) {
263
+ const context = await createSourceSnapshotContext(target, config, entry, optsWithSourceFallback(entry.source, opts), hashIfExists);
264
+ const sharedSnapshot = sharedSnapshotProvenance(target, config, entry, entries);
265
+ return acquireSourceSnapshot(context, {
266
+ dryRun: skippedDryRunSnapshot,
267
+ local: syncLocalSnapshot,
268
+ provider: (snapshotContext, providerAdapter) => syncProviderSnapshot(snapshotContext, providerAdapter, sharedSnapshot),
269
+ repository: syncRepositorySnapshot,
270
+ url: syncUrlSnapshot,
271
+ providerManaged: blockedProviderManagedSnapshot,
272
+ });
273
+ }
274
+ function optsWithSourceFallback(source, opts) {
275
+ const interpretation = interpretSourceAdapter(source);
276
+ if (interpretation.hasBrowserFallback) {
277
+ return { ...opts, browser: true };
278
+ }
279
+ return opts;
280
+ }
281
+ function skippedDryRunSnapshot(context) {
282
+ return {
283
+ ...context.base,
284
+ strategy: chooseSourceSnapshotStrategy(context.origin.method),
285
+ status: "skipped",
286
+ changed: false,
287
+ reason: "dry-run requested; no snapshot was written",
288
+ validation: { dryRun: "pass" },
271
289
  };
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
- };
290
+ }
291
+ async function syncLocalSnapshot(context) {
292
+ const { base, entry, source, snapshotPath, previousSnapshotHash, target, opts } = context;
293
+ const sourcePath = source.origin?.path ?? source.path;
294
+ if (!sourcePath) {
295
+ return sourceGap(base, "configured local origin has no path");
281
296
  }
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
- }
297
+ const originPath = resolve(target, sourcePath);
298
+ if (!(await exists(originPath)) && opts.allowNetwork && isGitSubmoduleResource(source)) {
299
+ const update = await updateGitSubmodule(target, sourcePath);
300
+ if (!update.ok)
301
+ return failed(base, update.reason);
302
+ }
303
+ const info = await safeStat(originPath);
304
+ if (!info) {
305
+ return sourceGap(base, "configured local origin path does not exist");
306
+ }
307
+ let written;
308
+ if (info.isDirectory) {
309
+ written = await writePreparedSnapshot(target, snapshotPath, await renderStructuredLocalDirectoryMarkdown(target, entry, originPath), previousSnapshotHash);
310
+ }
311
+ else if (shouldPreserveMachineReadable(source, originPath, snapshotPath)) {
291
312
  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
- }
313
+ await copyFile(originPath, snapshotPath);
298
314
  const snapshotHash = await sha256(snapshotPath);
299
- return {
300
- ...base,
301
- strategy: "local-copy-structured-markdown",
302
- status: "materialized",
315
+ written = {
303
316
  snapshotHash,
304
317
  changed: previousSnapshotHash !== snapshotHash,
305
- reason: "local origin copied or materialized into configured snapshot",
306
- validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
307
318
  };
308
319
  }
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
- }
320
+ else {
321
+ written = await writePreparedSnapshot(target, snapshotPath, await renderStructuredLocalMarkdown(target, entry, originPath), previousSnapshotHash);
322
+ }
323
+ return {
324
+ ...base,
325
+ strategy: "local-copy-structured-markdown",
326
+ status: "materialized",
327
+ snapshotHash: written.snapshotHash,
328
+ changed: written.changed,
329
+ reason: info.isDirectory
330
+ ? "local directory origin summarized into configured snapshot"
331
+ : "local origin copied or materialized into configured snapshot",
332
+ validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
333
+ };
334
+ }
335
+ async function syncProviderSnapshot(context, providerAdapter, sharedSnapshot) {
336
+ const { target, config, entry, snapshotPath, previousSnapshotHash, opts, origin } = context;
337
+ const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
338
+ const adapterResult = await runProviderAdapter(providerAdapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
339
+ if (!adapterResult)
340
+ return null;
341
+ if (adapterResult.status === "source-gap" && origin.method === "url") {
342
+ if (adapterResult.metadata?.providerGap === "adapter-ambiguous") {
322
343
  return adapterResult;
323
344
  }
345
+ const fallback = await syncUrlSnapshot(context);
346
+ return annotateFallbackResult(fallback, providerAdapter, adapterResult);
324
347
  }
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);
348
+ if (adapterResult.status === "blocked-auth" && opts.browser && origin.method === "url") {
349
+ const browserResult = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
350
+ return annotateFallbackResult(browserResult, providerAdapter, adapterResult, `${providerAdapter}-browser-fallback`);
351
+ }
352
+ return adapterResult;
353
+ }
354
+ function annotateFallbackResult(fallback, providerAdapter, adapterResult, adapter = `${providerAdapter}-generic-fallback`) {
355
+ return {
356
+ ...fallback,
357
+ adapter,
358
+ metadata: cleanMetadata({
359
+ ...(fallback.metadata ?? {}),
360
+ providerAdapter,
361
+ fallbackFrom: adapterResult.strategy,
362
+ fallbackReason: adapterResult.reason,
363
+ fallbackStatus: adapterResult.status,
364
+ }),
365
+ };
366
+ }
367
+ async function syncRepositorySnapshot(context) {
368
+ const { base, entry, origin, snapshotPath, previousSnapshotHash } = context;
369
+ const content = renderRepositoryMetadata(entry, origin.ref ?? "");
370
+ const written = await writePreparedSnapshot(context.target, snapshotPath, content, previousSnapshotHash);
371
+ return {
372
+ ...base,
373
+ strategy: "repository-metadata-only",
374
+ status: "metadata-only",
375
+ snapshotHash: written.snapshotHash,
376
+ changed: written.changed,
377
+ reason: "repository origin recorded as metadata; configure contract paths or checkout policy before broad acquisition",
378
+ validation: { repositoryNotCrawled: "pass", snapshotWritten: "pass" },
379
+ };
380
+ }
381
+ async function syncUrlSnapshot(context) {
382
+ const { base, entry, source, origin, target, config, opts, snapshotPath, previousSnapshotHash } = context;
383
+ if (!opts.allowNetwork) {
330
384
  return {
331
385
  ...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" },
386
+ strategy: "playwright-auth-or-direct-fetch",
387
+ status: "blocked-auth",
388
+ changed: false,
389
+ reason: "remote URL requires --allow-network or a materialized snapshot; Playwright auth helper can create reusable storageState first",
390
+ validation: { networkAllowed: "fail", previousSnapshotPreserved: previousSnapshotHash ? "pass" : "skipped" },
338
391
  };
339
392
  }
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);
393
+ const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
394
+ if (opts.browser) {
395
+ if (origin.ref && await hasDirectRuntimeAuthForSource(source, origin.ref, authStatePath)) {
396
+ const direct = await fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
397
+ if (direct.status === "materialized")
398
+ return direct;
359
399
  }
360
- return fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
400
+ return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
361
401
  }
402
+ return fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
403
+ }
404
+ function blockedProviderManagedSnapshot(context) {
362
405
  return {
363
- ...base,
406
+ ...context.base,
364
407
  strategy: "confirm-acquisition-method",
365
408
  status: "blocked-auth",
366
409
  changed: false,
367
410
  reason: "provider-managed origin needs an explicit acquisition method or exported snapshot",
368
- validation: { acquisitionMethodConfirmed: "fail", previousSnapshotPreserved: previousSnapshotHash ? "pass" : "skipped" },
411
+ validation: { acquisitionMethodConfirmed: "fail", previousSnapshotPreserved: context.previousSnapshotHash ? "pass" : "skipped" },
369
412
  };
370
413
  }
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) {
414
+ async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
392
415
  if (adapter === "confluence") {
393
- return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
416
+ return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
394
417
  }
395
418
  if (adapter === "jira") {
396
- return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
419
+ return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
397
420
  }
398
421
  if (adapter === "gitlab") {
399
- return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
422
+ return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
400
423
  }
401
- return runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
424
+ return runDesignAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
402
425
  }
403
- async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
426
+ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
404
427
  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";
428
+ const exportWordUrl = firstOriginString(entry.source, "exportWordUrl", "wordExportUrl", "exportUrl");
429
+ const restUrl = firstOriginString(entry.source, "apiUrl", "restUrl") ?? deriveConfluenceRestUrl(entry.source);
430
+ const pageUrl = firstOriginString(entry.source, "url") ?? base.origin;
431
+ const url = exportWordUrl ?? restUrl ?? pageUrl;
432
+ const strategy = exportWordUrl ? "confluence-word-export" : "confluence-storage-rest";
407
433
  if (!url)
408
- return sourceGap(base, "confluence adapter needs origin.url, origin.apiUrl, or origin.restUrl");
434
+ return sourceGap(base, "confluence adapter needs origin.url, origin.apiUrl, origin.restUrl, or origin.exportWordUrl");
409
435
  if (!opts.allowNetwork) {
410
- return blockedAdapter(base, strategy, "confluence source requires --allow-network and optional Playwright storageState before acquisition");
436
+ return blockedAdapter(base, strategy, "confluence source requires --allow-network and optional env auth or Playwright storageState before acquisition", cleanMetadata({ derivedUrl: restUrl }));
411
437
  }
412
438
  const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
413
439
  if (!fetched.ok)
@@ -416,15 +442,18 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
416
442
  return blockedAdapter(base, strategy, "confluence adapter resolved to login/chrome content; previous snapshot was preserved");
417
443
  }
418
444
  const parsed = parseJsonObject(fetched.body);
445
+ const wordExport = parseConfluenceWordExport(fetched.body, fetched.contentType);
419
446
  const title = parsed ? stringValue(parsed, "title") : undefined;
420
447
  const pageId = parsed ? firstStringValue(parsed, "id", "pageId", "contentId") : undefined;
421
448
  const version = parsed ? versionValue(parsed) : undefined;
422
449
  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);
450
+ const content = wordExport?.html
451
+ ? htmlToStructuredMarkdown(wordExport.html)
452
+ : storageHtml
453
+ ? htmlToStructuredMarkdown(storageHtml)
454
+ : fetched.contentType.includes("html")
455
+ ? htmlToStructuredMarkdown(fetched.body)
456
+ : fencedBody(fetched.body, fetched.contentType);
428
457
  if (!hasMaterialContent(content)) {
429
458
  return blockedAdapter(base, strategy, "confluence adapter produced no material body content; previous snapshot was preserved");
430
459
  }
@@ -435,11 +464,11 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
435
464
  contentType: fetched.contentType,
436
465
  content,
437
466
  reason: "confluence source fetched and normalized into structured Markdown",
438
- metadata: cleanMetadata({ title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
467
+ metadata: cleanMetadata({ title: wordExport?.title ?? title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
439
468
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
440
- });
469
+ }, sharedSnapshot);
441
470
  }
442
- async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
471
+ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
443
472
  const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "jira");
444
473
  const csvPath = firstOriginString(entry.source, ...JIRA_LOCAL_FILE_ORIGIN_KEYS);
445
474
  if (csvPath) {
@@ -463,14 +492,15 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
463
492
  reason: "jira CSV export imported and normalized into structured Markdown",
464
493
  metadata: cleanMetadata({ issueCount: rows.length, fallback: "csv" }),
465
494
  validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
466
- });
495
+ }, sharedSnapshot);
467
496
  }
468
- const url = firstOriginString(entry.source, "apiUrl", "searchUrl", "url") ?? (base.origin?.startsWith("http") || base.origin?.startsWith("data:") ? base.origin : undefined);
497
+ const derivedUrl = deriveJiraRestUrl(entry.source);
498
+ const url = firstOriginString(entry.source, "apiUrl", "searchUrl") ?? derivedUrl ?? firstOriginString(entry.source, "url") ?? (base.origin?.startsWith("http") || base.origin?.startsWith("data:") ? base.origin : undefined);
469
499
  if (!url)
470
500
  return null;
471
501
  const strategy = "jira-rest-search";
472
502
  if (!opts.allowNetwork) {
473
- return blockedAdapter(base, strategy, "jira REST source requires --allow-network and optional Playwright storageState before acquisition");
503
+ return blockedAdapter(base, strategy, "jira REST source requires --allow-network and optional env auth or Playwright storageState before acquisition", cleanMetadata({ derivedUrl }));
474
504
  }
475
505
  const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
476
506
  if (!fetched.ok)
@@ -492,9 +522,9 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
492
522
  reason: "jira REST source fetched and normalized into structured Markdown",
493
523
  metadata: cleanMetadata({ issueCount: jiraIssueCount(parsed), runtimeAuth: fetched.authSources.join(", ") || undefined }),
494
524
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
495
- });
525
+ }, sharedSnapshot);
496
526
  }
497
- async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
527
+ async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
498
528
  const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "gitlab");
499
529
  const url = firstOriginString(entry.source, "rawUrl", "apiUrl", "url");
500
530
  if (!url)
@@ -532,10 +562,11 @@ async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHas
532
562
  runtimeAuth: fetched.authSources.join(", ") || undefined,
533
563
  }),
534
564
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
535
- });
565
+ }, sharedSnapshot);
536
566
  }
537
- async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
538
- const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "design-source");
567
+ async function runDesignAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
568
+ const normalized = interpretSourceAdapter(entry.source);
569
+ const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter);
539
570
  const exportPath = firstOriginString(entry.source, ...DESIGN_LOCAL_FILE_ORIGIN_KEYS);
540
571
  if (exportPath) {
541
572
  const path = resolveAdapterLocalFilePath(target, exportPath);
@@ -549,7 +580,7 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
549
580
  return blockedAdapter(base, "design-export-import", "design export produced no material content; previous snapshot was preserved");
550
581
  }
551
582
  return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
552
- adapter: "design-source",
583
+ adapter,
553
584
  strategy: "design-export-import",
554
585
  resolvedOrigin: exportPath,
555
586
  contentType: contentTypeForPath(exportPath),
@@ -557,21 +588,56 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
557
588
  reason: "design export imported and normalized into structured Markdown",
558
589
  metadata: cleanMetadata({ provider: base.provider, fallback: "export" }),
559
590
  validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
560
- });
591
+ }, sharedSnapshot);
592
+ }
593
+ if (adapter === "design-source" && normalized.ambiguous && !exportPath) {
594
+ return sourceGap(base, "design source needs explicit adapter before provider-backed acquisition; configure source adapter, for example adapter: figma", designProviderGapMetadata("adapter-ambiguous", entry.source, undefined));
561
595
  }
562
- const url = firstOriginString(entry.source, "apiUrl", "exportUrl", "url") ?? base.origin;
596
+ const figmaLocator = figmaLocatorForSource(entry.source);
597
+ const command = figmaCommandForSource(entry.source, target, relative(target, snapshotPath), figmaLocator);
598
+ if (command) {
599
+ if (!opts.allowNetwork) {
600
+ return blockedAdapter(base, "figma-mcp-command", "figma MCP command acquisition requires --allow-network because it may contact the local MCP bridge or Figma desktop plugin", designProviderGapMetadata("network-not-allowed", entry.source, figmaLocator));
601
+ }
602
+ const commandResult = await runDesignCommand(target, command);
603
+ if (!commandResult.ok) {
604
+ return blockedAdapter(base, "figma-mcp-command", commandResult.reason, designProviderGapMetadata("mcp-command-failed", entry.source, figmaLocator));
605
+ }
606
+ const content = renderDesignMarkdown(commandResult.body, commandResult.origin, commandResult.contentType);
607
+ if (!hasMaterialContent(content)) {
608
+ return blockedAdapter(base, "figma-mcp-command", "figma MCP command produced no material design content; previous snapshot was preserved", designProviderGapMetadata("mcp-command-empty", entry.source, figmaLocator));
609
+ }
610
+ return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
611
+ adapter,
612
+ strategy: "figma-mcp-command",
613
+ resolvedOrigin: commandResult.origin,
614
+ contentType: commandResult.contentType,
615
+ content,
616
+ reason: "design source captured through configured Figma MCP command and normalized into structured Markdown",
617
+ metadata: cleanMetadata({
618
+ provider: base.provider,
619
+ mcpProvider: command.provider,
620
+ command: command.safeLabel,
621
+ fileKey: figmaLocator?.fileKey,
622
+ nodeId: figmaLocator?.nodeId,
623
+ }),
624
+ validation: { commandExited: "pass", bodyContent: "pass", snapshotWritten: "pass", secretValuesHidden: "pass" },
625
+ }, sharedSnapshot);
626
+ }
627
+ const derivedApiUrl = figmaLocator?.apiUrl;
628
+ const url = firstOriginString(entry.source, "apiUrl", "exportUrl") ?? derivedApiUrl ?? firstOriginString(entry.source, "url") ?? base.origin;
563
629
  if (!url) {
564
- return sourceGap(base, "design source needs origin.url, origin.apiUrl, origin.exportUrl, or a local exportPath/assetPath/filePath before acquisition", designProviderGapMetadata("missing-provider-locator"));
630
+ return sourceGap(base, "design source needs origin.url, origin.apiUrl, origin.exportUrl, or a local exportPath/assetPath/filePath before acquisition", designProviderGapMetadata("missing-provider-locator", entry.source, figmaLocator));
565
631
  }
566
632
  if (!opts.allowNetwork) {
567
- 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", designProviderGapMetadata("network-not-allowed"));
633
+ 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", designProviderGapMetadata("network-not-allowed", entry.source, figmaLocator));
568
634
  }
569
635
  const directRuntimeAuth = await hasDirectRuntimeAuthForSource(entry.source, url, authStatePath);
570
636
  if (opts.browser && !directRuntimeAuth) {
571
637
  const rendered = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
572
638
  return {
573
639
  ...rendered,
574
- adapter: "design-source",
640
+ adapter,
575
641
  metadata: cleanMetadata({ provider: base.provider, acquisition: "browser" }),
576
642
  };
577
643
  }
@@ -579,29 +645,35 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
579
645
  if (!fetched.ok) {
580
646
  if (opts.browser)
581
647
  return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
582
- return blockedAdapter(base, "design-api-or-export", fetched.reason, designProviderGapMetadata("provider-fetch-failed"));
648
+ return blockedAdapter(base, "design-api-or-export", fetched.reason, designProviderGapMetadata("provider-fetch-failed", entry.source, figmaLocator));
583
649
  }
584
650
  if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
585
651
  if (opts.browser)
586
652
  return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
587
- return blockedAdapter(base, "design-api-or-export", "design adapter resolved to login/chrome content; previous snapshot was preserved", designProviderGapMetadata("provider-auth-blocked"));
653
+ return blockedAdapter(base, "design-api-or-export", "design adapter resolved to login/chrome content; previous snapshot was preserved", designProviderGapMetadata("provider-auth-blocked", entry.source, figmaLocator));
588
654
  }
589
655
  const content = renderDesignMarkdown(fetched.body, fetched.resolvedOrigin, fetched.contentType);
590
656
  if (!hasMaterialContent(content)) {
591
657
  if (opts.browser)
592
658
  return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
593
- return blockedAdapter(base, "design-api-or-export", "design adapter produced no material design content; previous snapshot was preserved", designProviderGapMetadata("provider-content-incomplete"));
659
+ return blockedAdapter(base, "design-api-or-export", "design adapter produced no material design content; previous snapshot was preserved", designProviderGapMetadata("provider-content-incomplete", entry.source, figmaLocator));
594
660
  }
595
661
  return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
596
- adapter: "design-source",
662
+ adapter,
597
663
  strategy: "design-api-or-export",
598
664
  resolvedOrigin: fetched.resolvedOrigin,
599
665
  contentType: fetched.contentType,
600
666
  content,
601
667
  reason: "design source fetched and normalized into structured Markdown",
602
- metadata: cleanMetadata({ provider: base.provider, runtimeAuth: fetched.authSources.join(", ") || undefined }),
668
+ metadata: cleanMetadata({
669
+ provider: base.provider,
670
+ runtimeAuth: fetched.authSources.join(", ") || undefined,
671
+ derivedApiUrl,
672
+ fileKey: figmaLocator?.fileKey,
673
+ nodeId: figmaLocator?.nodeId,
674
+ }),
603
675
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
604
- });
676
+ }, sharedSnapshot);
605
677
  }
606
678
  function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter) {
607
679
  const origin = inferSourceOrigin(entry.source);
@@ -617,12 +689,10 @@ function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter)
617
689
  previousSnapshotHash,
618
690
  };
619
691
  }
620
- async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result) {
692
+ async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result, sharedSnapshot) {
621
693
  const origin = inferSourceOrigin(entry.source);
622
- const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata);
623
- await mkdir(dirname(snapshotPath), { recursive: true });
624
- await writeFile(snapshotPath, markdown, "utf8");
625
- const snapshotHash = await sha256(snapshotPath);
694
+ const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata, sharedSnapshot);
695
+ const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
626
696
  return {
627
697
  envelopeVersion: 1,
628
698
  sourceKey: entry.id,
@@ -635,11 +705,11 @@ async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapsho
635
705
  origin: origin.ref,
636
706
  resolvedOrigin: result.resolvedOrigin,
637
707
  contentType: result.contentType,
638
- snapshot: relative(target, snapshotPath),
639
- snapshotHash,
708
+ snapshot: written.snapshot,
709
+ snapshotHash: written.snapshotHash,
640
710
  previousSnapshotHash,
641
711
  metadata: result.metadata,
642
- changed: previousSnapshotHash !== snapshotHash,
712
+ changed: written.changed,
643
713
  reason: result.reason,
644
714
  validation: result.validation,
645
715
  };
@@ -663,13 +733,18 @@ async function headersForSource(source, url, authStatePath) {
663
733
  const authSources = [];
664
734
  const token = envValue(authStringList(source, "tokenEnv", "accessTokenEnv", "apiTokenEnv"));
665
735
  if (token.value) {
666
- const headerName = firstAuthString(source, "tokenHeader", "headerName") ?? "authorization";
667
- const tokenScheme = firstAuthString(source, "tokenScheme", "scheme") ?? "Bearer";
736
+ const headerName = firstAuthString(source, "tokenHeader", "headerName") ?? defaultTokenHeader(source);
737
+ const tokenScheme = firstAuthString(source, "tokenScheme", "scheme") ?? defaultTokenScheme(source, headerName);
668
738
  headers[headerName] = headerName.toLowerCase() === "authorization" && tokenScheme
669
739
  ? `${tokenScheme} ${token.value}`
670
740
  : token.value;
671
741
  authSources.push(`env:${token.name}`);
672
742
  }
743
+ const basic = basicAuthHeaderFromEnvironment(source);
744
+ if (!headers.authorization && basic.value) {
745
+ headers.authorization = basic.value;
746
+ authSources.push(...basic.sources.map((name) => `env:${name}`));
747
+ }
673
748
  const envCookie = cookieHeaderFromEnvironment(source);
674
749
  if (envCookie.source)
675
750
  authSources.push(`env:${envCookie.source}`);
@@ -683,7 +758,10 @@ async function headersForSource(source, url, authStatePath) {
683
758
  }
684
759
  async function hasDirectRuntimeAuthForSource(source, url, authStatePath) {
685
760
  const { headers } = await headersForSource(source, url, authStatePath);
686
- return Boolean(headers.authorization || headers.cookie);
761
+ return hasRuntimeAuthHeaders(headers);
762
+ }
763
+ function hasRuntimeAuthHeaders(headers) {
764
+ return Object.keys(headers).length > 0;
687
765
  }
688
766
  async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath) {
689
767
  const origin = inferSourceOrigin(entry.source);
@@ -718,17 +796,15 @@ async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHas
718
796
  return blocked(base, "remote fetch produced no material body content; previous snapshot was preserved");
719
797
  }
720
798
  const markdown = renderRemoteMarkdown(entry, response.url, contentType, content);
721
- await mkdir(dirname(snapshotPath), { recursive: true });
722
- await writeFile(snapshotPath, markdown, "utf8");
723
- const snapshotHash = await sha256(snapshotPath);
799
+ const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
724
800
  return {
725
801
  ...base,
726
802
  strategy: authSources.length > 0 ? "runtime-auth-direct-fetch" : "direct-fetch-structured-markdown",
727
803
  status: "materialized",
728
804
  resolvedOrigin: response.url,
729
805
  contentType,
730
- snapshotHash,
731
- changed: previousSnapshotHash !== snapshotHash,
806
+ snapshotHash: written.snapshotHash,
807
+ changed: written.changed,
732
808
  reason: "remote URL fetched and normalized into structured Markdown",
733
809
  metadata: cleanMetadata({ runtimeAuth: authSources.join(", ") || undefined }),
734
810
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
@@ -773,17 +849,15 @@ async function fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previous
773
849
  return blocked(base, "browser acquisition produced no material body content; previous snapshot was preserved");
774
850
  }
775
851
  const markdown = renderRemoteMarkdown(entry, resolvedUrl, "text/html; rendered=playwright", content, "playwright-headless-browser");
776
- await mkdir(dirname(snapshotPath), { recursive: true });
777
- await writeFile(snapshotPath, markdown, "utf8");
778
- const snapshotHash = await sha256(snapshotPath);
852
+ const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
779
853
  return {
780
854
  ...base,
781
855
  strategy: "playwright-headless-browser",
782
856
  status: "materialized",
783
857
  resolvedOrigin: resolvedUrl,
784
858
  contentType: "text/html; rendered=playwright",
785
- snapshotHash,
786
- changed: previousSnapshotHash !== snapshotHash,
859
+ snapshotHash: written.snapshotHash,
860
+ changed: written.changed,
787
861
  reason: "remote URL rendered through Playwright and normalized into structured Markdown",
788
862
  validation: { browserRendered: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
789
863
  };
@@ -827,6 +901,63 @@ async function renderStructuredLocalMarkdown(target, entry, originPath) {
827
901
  "",
828
902
  ].filter((line) => typeof line === "string").join("\n");
829
903
  }
904
+ async function renderStructuredLocalDirectoryMarkdown(target, entry, originPath) {
905
+ const entries = await readdir(originPath, { withFileTypes: true }).catch(() => []);
906
+ const git = await readGitDirectoryMetadata(originPath);
907
+ return [
908
+ "---",
909
+ `sourceKey: ${JSON.stringify(entry.id)}`,
910
+ `sourcePath: ${JSON.stringify(entry.path)}`,
911
+ entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
912
+ `origin: ${JSON.stringify(relative(target, originPath))}`,
913
+ `extractionMethod: "local-directory-summary"`,
914
+ `extractedAt: ${JSON.stringify(new Date().toISOString())}`,
915
+ `snapshotStatus: "metadata-only"`,
916
+ git.head ? `gitHead: ${JSON.stringify(git.head)}` : undefined,
917
+ git.status ? `gitStatus: ${JSON.stringify(git.status)}` : undefined,
918
+ "---",
919
+ "",
920
+ "# Directory Resource Snapshot",
921
+ "",
922
+ "## Provenance",
923
+ "",
924
+ `- Source key: \`${entry.id}\``,
925
+ entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
926
+ `- Origin: \`${relative(target, originPath)}\``,
927
+ "- Method: local-directory-summary",
928
+ git.head ? `- Git HEAD: \`${git.head}\`` : undefined,
929
+ git.status ? `- Git status: ${git.status}` : undefined,
930
+ "",
931
+ "## Top-Level Entries",
932
+ "",
933
+ entries.length === 0
934
+ ? "- Empty directory."
935
+ : entries
936
+ .slice(0, 200)
937
+ .map((entry) => `- ${entry.isDirectory() ? "dir" : "file"}: \`${entry.name}\``)
938
+ .join("\n"),
939
+ entries.length > 200 ? `- ... ${entries.length - 200} more entries omitted.` : undefined,
940
+ "",
941
+ ].filter((line) => typeof line === "string").join("\n");
942
+ }
943
+ async function readGitDirectoryMetadata(originPath) {
944
+ const head = await gitOutput(originPath, ["rev-parse", "HEAD"]);
945
+ const status = await gitOutput(originPath, ["status", "--short"]);
946
+ return {
947
+ head,
948
+ status: status ? "dirty" : head ? "clean" : undefined,
949
+ };
950
+ }
951
+ async function gitOutput(cwd, args) {
952
+ try {
953
+ const { stdout } = await execFileAsync("git", args, { cwd, timeout: 10_000, maxBuffer: 1024 * 1024 });
954
+ const value = stdout.trim();
955
+ return value || undefined;
956
+ }
957
+ catch {
958
+ return undefined;
959
+ }
960
+ }
830
961
  function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extractionMethod = "direct-fetch") {
831
962
  return [
832
963
  "---",
@@ -855,7 +986,33 @@ function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extracti
855
986
  "",
856
987
  ].filter((line) => typeof line === "string").join("\n");
857
988
  }
858
- function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extractionMethod, adapter, metadata) {
989
+ function sharedSnapshotProvenance(target, config, entry, entries) {
990
+ const snapshotPath = resolve(target, resolveSourceSnapshot(config, entry));
991
+ const shared = entries.filter((candidate) => resolve(target, resolveSourceSnapshot(config, candidate)) === snapshotPath);
992
+ if (shared.length <= 1)
993
+ return undefined;
994
+ return {
995
+ sourceKeys: shared.map((candidate) => candidate.id),
996
+ sourcePaths: shared.map((candidate) => candidate.path),
997
+ sourceAliases: uniqueStrings(shared.map((candidate) => candidate.provider).filter(Boolean)),
998
+ sourceTypes: uniqueStrings(shared.map(sourceType).filter((value) => Boolean(value))),
999
+ };
1000
+ }
1001
+ function sourceType(entry) {
1002
+ return typeof entry.source.type === "string" && entry.source.type.trim() ? entry.source.type : undefined;
1003
+ }
1004
+ function uniqueStrings(values) {
1005
+ return Array.from(new Set(values));
1006
+ }
1007
+ function yamlStringList(key, values) {
1008
+ if (values.length === 0)
1009
+ return [];
1010
+ return [
1011
+ `${key}:`,
1012
+ ...values.map((value) => ` - ${JSON.stringify(value)}`),
1013
+ ];
1014
+ }
1015
+ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extractionMethod, adapter, metadata, sharedSnapshot) {
859
1016
  const metadataLines = metadata && Object.keys(metadata).length > 0
860
1017
  ? [
861
1018
  "## Adapter Metadata",
@@ -864,11 +1021,28 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
864
1021
  "",
865
1022
  ]
866
1023
  : [];
1024
+ const sharedFrontmatter = sharedSnapshot
1025
+ ? [
1026
+ `snapshotProvenance: "shared-config-snapshot"`,
1027
+ ...yamlStringList("sourceKeys", sharedSnapshot.sourceKeys),
1028
+ ...yamlStringList("sourcePaths", sharedSnapshot.sourcePaths),
1029
+ ...yamlStringList("sourceAliases", sharedSnapshot.sourceAliases),
1030
+ ...yamlStringList("sourceTypes", sharedSnapshot.sourceTypes),
1031
+ ]
1032
+ : [];
1033
+ const sharedProvenance = sharedSnapshot
1034
+ ? [
1035
+ `- Shared snapshot: ${sharedSnapshot.sourceKeys.length} configured sources`,
1036
+ `- Source keys: ${sharedSnapshot.sourceKeys.map((key) => `\`${key}\``).join(", ")}`,
1037
+ `- Source aliases: ${sharedSnapshot.sourceAliases.map((alias) => `\`${alias}\``).join(", ")}`,
1038
+ ]
1039
+ : [];
867
1040
  return [
868
1041
  "---",
869
1042
  `sourceKey: ${JSON.stringify(entry.id)}`,
870
1043
  `sourcePath: ${JSON.stringify(entry.path)}`,
871
1044
  entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
1045
+ ...sharedFrontmatter,
872
1046
  `origin: ${JSON.stringify(resolvedOrigin)}`,
873
1047
  `contentType: ${JSON.stringify(contentType)}`,
874
1048
  `adapter: ${JSON.stringify(adapter)}`,
@@ -883,6 +1057,7 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
883
1057
  "",
884
1058
  `- Source key: \`${entry.id}\``,
885
1059
  entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
1060
+ ...sharedProvenance,
886
1061
  `- Resolved origin: \`${resolvedOrigin}\``,
887
1062
  `- Adapter: ${adapter}`,
888
1063
  `- Method: ${extractionMethod}`,
@@ -903,6 +1078,75 @@ function confluenceBodyValue(value) {
903
1078
  const view = recordValue(body, "view");
904
1079
  return stringValue(storage, "value") ?? stringValue(view, "value") ?? stringValue(value, "content") ?? stringValue(value, "value");
905
1080
  }
1081
+ function parseConfluenceWordExport(body, contentType) {
1082
+ if (!/multipart\/related|application\/vnd\.ms-word|MIME-Version:/i.test(`${contentType}\n${body.slice(0, 512)}`)) {
1083
+ return undefined;
1084
+ }
1085
+ const rootHeadersEnd = body.search(/\r?\n\r?\n/);
1086
+ if (rootHeadersEnd === -1)
1087
+ return undefined;
1088
+ const rootHeaders = parseMimeHeaders(body.slice(0, rootHeadersEnd));
1089
+ const boundary = /boundary="?([^";\r\n]+)"?/i.exec(rootHeaders["content-type"] ?? "")?.[1];
1090
+ if (!boundary)
1091
+ return undefined;
1092
+ const chunks = body.split(`--${boundary}`).slice(1);
1093
+ for (const chunk of chunks) {
1094
+ if (chunk.startsWith("--"))
1095
+ continue;
1096
+ const trimmed = chunk.replace(/^\r?\n/, "");
1097
+ const headerEnd = trimmed.search(/\r?\n\r?\n/);
1098
+ if (headerEnd === -1)
1099
+ continue;
1100
+ const headers = parseMimeHeaders(trimmed.slice(0, headerEnd));
1101
+ if (!/text\/html/i.test(headers["content-type"] ?? ""))
1102
+ continue;
1103
+ const html = decodeMimePartBody(trimmed.slice(headerEnd).replace(/^\r?\n\r?\n?/, ""), headers);
1104
+ return {
1105
+ html,
1106
+ title: decodeHtmlEntities(stripHtml(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "")) || undefined,
1107
+ };
1108
+ }
1109
+ return undefined;
1110
+ }
1111
+ function parseMimeHeaders(text) {
1112
+ const headers = {};
1113
+ let current;
1114
+ for (const line of text.split(/\r?\n/)) {
1115
+ if (/^\s/.test(line) && current) {
1116
+ headers[current] += ` ${line.trim()}`;
1117
+ continue;
1118
+ }
1119
+ const index = line.indexOf(":");
1120
+ if (index === -1)
1121
+ continue;
1122
+ current = line.slice(0, index).toLowerCase();
1123
+ headers[current] = line.slice(index + 1).trim();
1124
+ }
1125
+ return headers;
1126
+ }
1127
+ function decodeMimePartBody(body, headers) {
1128
+ const encoding = (headers["content-transfer-encoding"] ?? "").toLowerCase();
1129
+ if (encoding.includes("quoted-printable"))
1130
+ return decodeQuotedPrintable(body);
1131
+ if (encoding.includes("base64"))
1132
+ return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8");
1133
+ return body;
1134
+ }
1135
+ function decodeQuotedPrintable(input) {
1136
+ const normalized = input.replace(/=\r?\n/g, "");
1137
+ const bytes = [];
1138
+ for (let i = 0; i < normalized.length; i += 1) {
1139
+ if (normalized[i] === "=" && /^[0-9a-fA-F]{2}$/.test(normalized.slice(i + 1, i + 3))) {
1140
+ bytes.push(parseInt(normalized.slice(i + 1, i + 3), 16));
1141
+ i += 2;
1142
+ continue;
1143
+ }
1144
+ const char = normalized[i];
1145
+ if (char)
1146
+ bytes.push(...Buffer.from(char, "utf8"));
1147
+ }
1148
+ return Buffer.from(bytes).toString("utf8");
1149
+ }
906
1150
  function versionValue(value) {
907
1151
  const version = recordValue(value, "version");
908
1152
  const number = version?.number;
@@ -1041,13 +1285,230 @@ function designNodeNames(value) {
1041
1285
  const nested = Array.isArray(children) ? children.flatMap(designNodeNames) : [];
1042
1286
  return current ? [current, ...nested] : nested;
1043
1287
  }
1044
- function designProviderGapMetadata(reason) {
1288
+ function designProviderGapMetadata(reason, source, figma) {
1289
+ const mcpProvider = source ? interpretSourceAdapter(source).mcpProviderName : undefined;
1045
1290
  return {
1046
1291
  providerGap: reason,
1047
1292
  fallbackEvidenceRequired: true,
1293
+ mcpProvider,
1294
+ derivedApiUrl: figma?.apiUrl,
1295
+ fileKey: figma?.fileKey,
1296
+ nodeId: figma?.nodeId,
1297
+ nextActions: [
1298
+ "configure a local exportPath/assetPath/filePath",
1299
+ "configure origin.mcpCommand + origin.mcpArgs for a Figma MCP capture command",
1300
+ "use Playwright/browser acquisition with --browser when HTML rendering is sufficient",
1301
+ "capture Computer Use or human visual evidence outside the CLI and attach it as a raw source",
1302
+ ].join("; "),
1048
1303
  reconstructionRisk: "semantic UE reconstruction needs provider raw payload, assets/raw files, reconstruction.json, and known gaps before downstream design use",
1049
1304
  };
1050
1305
  }
1306
+ function deriveConfluenceRestUrl(source) {
1307
+ const pageId = firstOriginString(source, "pageId", "contentId", "id") ?? confluencePageIdFromUrl(firstOriginString(source, "url"));
1308
+ if (!pageId)
1309
+ return undefined;
1310
+ const base = confluenceBaseUrl(source);
1311
+ if (!base)
1312
+ return undefined;
1313
+ return `${base}/rest/api/content/${encodeURIComponent(pageId)}?expand=body.storage,version`;
1314
+ }
1315
+ function confluencePageIdFromUrl(value) {
1316
+ if (!value)
1317
+ return undefined;
1318
+ try {
1319
+ const url = new URL(value);
1320
+ return /\/pages\/(\d+)/.exec(url.pathname)?.[1] ?? undefined;
1321
+ }
1322
+ catch {
1323
+ return /\/pages\/(\d+)/.exec(value)?.[1] ?? undefined;
1324
+ }
1325
+ }
1326
+ function confluenceBaseUrl(source) {
1327
+ const explicit = firstOriginString(source, "baseUrl", "siteUrl", "host");
1328
+ if (explicit)
1329
+ return trimTrailingSlash(explicit);
1330
+ const pageUrl = firstOriginString(source, "url");
1331
+ if (!pageUrl)
1332
+ return undefined;
1333
+ try {
1334
+ const url = new URL(pageUrl);
1335
+ const wikiPrefix = url.pathname.startsWith("/wiki/") ? "/wiki" : "";
1336
+ return `${url.protocol}//${url.host}${wikiPrefix}`;
1337
+ }
1338
+ catch {
1339
+ return undefined;
1340
+ }
1341
+ }
1342
+ function deriveJiraRestUrl(source) {
1343
+ const explicitIssue = firstOriginString(source, "issueKey", "key", "id") ?? jiraIssueKeyFromUrl(firstOriginString(source, "url"));
1344
+ const query = firstOriginString(source, "jql", "query");
1345
+ const base = jiraBaseUrl(source);
1346
+ if (!base)
1347
+ return undefined;
1348
+ if (explicitIssue)
1349
+ return `${base}/rest/api/3/issue/${encodeURIComponent(explicitIssue)}`;
1350
+ if (query)
1351
+ return `${base}/rest/api/3/search?jql=${encodeURIComponent(query)}`;
1352
+ return undefined;
1353
+ }
1354
+ function jiraIssueKeyFromUrl(value) {
1355
+ if (!value)
1356
+ return undefined;
1357
+ try {
1358
+ const url = new URL(value);
1359
+ return /\/browse\/([A-Z][A-Z0-9_]+-\d+)/i.exec(url.pathname)?.[1] ?? undefined;
1360
+ }
1361
+ catch {
1362
+ return /\/browse\/([A-Z][A-Z0-9_]+-\d+)/i.exec(value)?.[1] ?? undefined;
1363
+ }
1364
+ }
1365
+ function jiraBaseUrl(source) {
1366
+ const explicit = firstOriginString(source, "baseUrl", "siteUrl", "host");
1367
+ if (explicit)
1368
+ return trimTrailingSlash(explicit);
1369
+ const urlValue = firstOriginString(source, "url");
1370
+ if (!urlValue)
1371
+ return undefined;
1372
+ try {
1373
+ const url = new URL(urlValue);
1374
+ return `${url.protocol}//${url.host}`;
1375
+ }
1376
+ catch {
1377
+ return undefined;
1378
+ }
1379
+ }
1380
+ function figmaLocatorForSource(source) {
1381
+ if (!isFigmaSource(source))
1382
+ return undefined;
1383
+ const sourceUrl = firstOriginString(source, "url");
1384
+ const fileKey = firstOriginString(source, "fileKey", "file_key", "fileId", "file_id", "id") ?? figmaFileKeyFromUrl(sourceUrl);
1385
+ const nodeId = normalizeFigmaNodeId(firstOriginString(source, "nodeId", "node_id") ?? figmaNodeIdFromUrl(sourceUrl));
1386
+ const explicitApiUrl = firstOriginString(source, "apiUrl", "exportUrl");
1387
+ const apiUrl = explicitApiUrl ?? (fileKey ? figmaApiUrl(fileKey, nodeId) : undefined);
1388
+ return cleanMetadata({ sourceUrl, fileKey, nodeId, apiUrl });
1389
+ }
1390
+ function isFigmaSource(source) {
1391
+ return [
1392
+ source.origin?.kind,
1393
+ source.origin?.provider,
1394
+ source.kind,
1395
+ source.type,
1396
+ firstOriginString(source, "mcpProvider", "mcpServer"),
1397
+ firstOriginString(source, "url"),
1398
+ ]
1399
+ .filter((value) => typeof value === "string")
1400
+ .some((value) => value.toLowerCase().includes("figma"));
1401
+ }
1402
+ function figmaFileKeyFromUrl(value) {
1403
+ if (!value)
1404
+ return undefined;
1405
+ try {
1406
+ const url = new URL(value);
1407
+ return /\/(?:file|design)\/([^/?#]+)/i.exec(url.pathname)?.[1] ?? undefined;
1408
+ }
1409
+ catch {
1410
+ return /\/(?:file|design)\/([^/?#]+)/i.exec(value)?.[1] ?? undefined;
1411
+ }
1412
+ }
1413
+ function figmaNodeIdFromUrl(value) {
1414
+ if (!value)
1415
+ return undefined;
1416
+ try {
1417
+ return new URL(value).searchParams.get("node-id") ?? undefined;
1418
+ }
1419
+ catch {
1420
+ return /[?&]node-id=([^&#]+)/i.exec(value)?.[1] ?? undefined;
1421
+ }
1422
+ }
1423
+ function normalizeFigmaNodeId(value) {
1424
+ if (!value)
1425
+ return undefined;
1426
+ const decoded = decodeURIComponent(value.trim());
1427
+ return decoded.includes(":") ? decoded : decoded.replace("-", ":");
1428
+ }
1429
+ function figmaApiUrl(fileKey, nodeId) {
1430
+ const encodedFileKey = encodeURIComponent(fileKey);
1431
+ if (nodeId)
1432
+ return `https://api.figma.com/v1/files/${encodedFileKey}/nodes?ids=${encodeURIComponent(nodeId)}`;
1433
+ return `https://api.figma.com/v1/files/${encodedFileKey}`;
1434
+ }
1435
+ function figmaCommandForSource(source, target, snapshot, figma) {
1436
+ const command = firstOriginString(source, "mcpCommand", "command", "exportCommand");
1437
+ if (!command)
1438
+ return undefined;
1439
+ const args = originStringList(source, "mcpArgs", "args", "commandArgs")
1440
+ .map((arg) => renderCommandTemplate(arg, source, snapshot, figma));
1441
+ const provider = interpretSourceAdapter(source).mcpProviderName;
1442
+ const contentType = firstOriginString(source, "commandContentType", "contentType") ?? "application/json";
1443
+ const timeoutMs = numberOriginValue(source, "timeoutMs") ?? DEFAULT_ADAPTER_COMMAND_TIMEOUT_MS;
1444
+ const resolvedCommand = resolveCommandForTarget(target, command);
1445
+ return {
1446
+ command: resolvedCommand,
1447
+ args,
1448
+ provider,
1449
+ safeLabel: `${command} (${args.length} args)`,
1450
+ contentType,
1451
+ timeoutMs,
1452
+ };
1453
+ }
1454
+ async function runDesignCommand(target, command) {
1455
+ try {
1456
+ const { stdout } = await execFileAsync(command.command, command.args, {
1457
+ cwd: target,
1458
+ env: process.env,
1459
+ timeout: command.timeoutMs,
1460
+ maxBuffer: DEFAULT_ADAPTER_COMMAND_MAX_BUFFER,
1461
+ });
1462
+ const body = stdout.trim();
1463
+ if (!body)
1464
+ return { ok: false, reason: "configured design command exited without stdout content" };
1465
+ return {
1466
+ ok: true,
1467
+ body,
1468
+ contentType: command.contentType,
1469
+ origin: `command:${command.safeLabel}`,
1470
+ };
1471
+ }
1472
+ catch (error) {
1473
+ const code = isRecord(error) && (typeof error.code === "number" || typeof error.code === "string") ? String(error.code) : "unknown";
1474
+ return {
1475
+ ok: false,
1476
+ reason: `configured design command failed without exposing stdout/stderr content (code: ${code})`,
1477
+ };
1478
+ }
1479
+ }
1480
+ function originStringList(source, ...keys) {
1481
+ const origin = source.origin;
1482
+ const values = [];
1483
+ for (const key of keys) {
1484
+ values.push(...stringListValue(origin?.[key]));
1485
+ }
1486
+ return values;
1487
+ }
1488
+ function numberOriginValue(source, key) {
1489
+ const value = source.origin?.[key];
1490
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
1491
+ }
1492
+ function renderCommandTemplate(value, source, snapshot, figma) {
1493
+ const replacements = {
1494
+ url: firstOriginString(source, "url") ?? "",
1495
+ fileKey: figma?.fileKey ?? "",
1496
+ file_key: figma?.fileKey ?? "",
1497
+ nodeId: figma?.nodeId ?? "",
1498
+ node_id: figma?.nodeId ?? "",
1499
+ apiUrl: figma?.apiUrl ?? "",
1500
+ snapshot,
1501
+ };
1502
+ return value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_, key) => replacements[key] ?? "");
1503
+ }
1504
+ function resolveCommandForTarget(target, command) {
1505
+ if (command.includes("/") || command.includes("\\"))
1506
+ return resolve(target, command);
1507
+ return command;
1508
+ }
1509
+ function trimTrailingSlash(value) {
1510
+ return value.replace(/\/+$/, "");
1511
+ }
1051
1512
  function parseCsvRows(value) {
1052
1513
  const rows = parseCsv(value).filter((row) => row.some((cell) => cell.trim().length > 0));
1053
1514
  const headers = rows.shift() ?? [];
@@ -1138,10 +1599,11 @@ function firstCsvValue(row, ...keys) {
1138
1599
  }
1139
1600
  function firstOriginString(source, ...keys) {
1140
1601
  const origin = source.origin;
1141
- if (!origin)
1142
- return undefined;
1143
1602
  for (const key of keys) {
1144
- const value = origin[key];
1603
+ const sourceValue = source[key];
1604
+ if (typeof sourceValue === "string" && sourceValue.trim().length > 0)
1605
+ return sourceValue.trim();
1606
+ const value = origin?.[key];
1145
1607
  if (typeof value === "string" && value.trim().length > 0)
1146
1608
  return value.trim();
1147
1609
  }
@@ -1162,9 +1624,13 @@ function authStringList(source, ...keys) {
1162
1624
  for (const key of keys) {
1163
1625
  values.push(...stringListValue(auth?.[key]));
1164
1626
  }
1627
+ if (keys.some((key) => ["tokenEnv", "accessTokenEnv", "apiTokenEnv"].includes(key))) {
1628
+ values.push(...stringListValue(auth?.env));
1629
+ }
1165
1630
  for (const key of keys) {
1166
1631
  values.push(...stringListValue(source.origin?.[key]));
1167
1632
  }
1633
+ values.push(...defaultAuthEnvNames(source, keys));
1168
1634
  return Array.from(new Set(values));
1169
1635
  }
1170
1636
  function authCookieMap(source) {
@@ -1199,6 +1665,77 @@ function envValue(names) {
1199
1665
  }
1200
1666
  return {};
1201
1667
  }
1668
+ function basicAuthHeaderFromEnvironment(source) {
1669
+ const username = envValue(authStringList(source, "usernameEnv", "userEnv", "basicUsernameEnv", "basicUserEnv"));
1670
+ const password = envValue(authStringList(source, "passwordEnv", "passEnv", "basicPasswordEnv", "basicPassEnv"));
1671
+ if (!username.value || !password.value)
1672
+ return { sources: [] };
1673
+ return {
1674
+ sources: [username.name, password.name].filter((name) => Boolean(name)),
1675
+ value: `Basic ${Buffer.from(`${username.value}:${password.value}`).toString("base64")}`,
1676
+ };
1677
+ }
1678
+ function defaultAuthEnvNames(source, keys) {
1679
+ const provider = sourceProviderToken(source);
1680
+ if (!provider)
1681
+ return [];
1682
+ if (keys.some((key) => ["tokenEnv", "accessTokenEnv", "apiTokenEnv"].includes(key))) {
1683
+ if (provider.includes("figma"))
1684
+ return ["FIGMA_ACCESS_TOKEN", "FIGMA_TOKEN", "FIGMA_API_TOKEN"];
1685
+ if (provider.includes("gitlab"))
1686
+ return ["GITLAB_TOKEN", "GITLAB_ACCESS_TOKEN", "GITLAB_PRIVATE_TOKEN"];
1687
+ if (provider.includes("jira"))
1688
+ return ["ACCESS_TOKEN", "JIRA_ACCESS_TOKEN", "JIRA_TOKEN"];
1689
+ if (provider.includes("confluence"))
1690
+ return ["CONFLUENCE_ACCESS_TOKEN", "CONFLUENCE_TOKEN"];
1691
+ }
1692
+ if (keys.some((key) => ["usernameEnv", "userEnv", "basicUsernameEnv", "basicUserEnv"].includes(key))) {
1693
+ if (provider.includes("jira"))
1694
+ return ["JIRA_USER", "JIRA_USERNAME", "ATLASSIAN_EMAIL", "ATLASSIAN_USER"];
1695
+ if (provider.includes("confluence"))
1696
+ return ["CONFLUENCE_USER", "CONFLUENCE_USERNAME", "ATLASSIAN_EMAIL", "ATLASSIAN_USER"];
1697
+ }
1698
+ if (keys.some((key) => ["passwordEnv", "passEnv", "basicPasswordEnv", "basicPassEnv"].includes(key))) {
1699
+ if (provider.includes("jira"))
1700
+ return ["JIRA_API_TOKEN", "JIRA_PASSWORD", "ATLASSIAN_API_TOKEN", "ATLASSIAN_TOKEN"];
1701
+ if (provider.includes("confluence"))
1702
+ return ["CONFLUENCE_API_TOKEN", "CONFLUENCE_PASSWORD", "ATLASSIAN_API_TOKEN", "ATLASSIAN_TOKEN"];
1703
+ }
1704
+ return [];
1705
+ }
1706
+ function defaultTokenHeader(source) {
1707
+ const provider = sourceProviderToken(source);
1708
+ if (provider?.includes("figma"))
1709
+ return "X-Figma-Token";
1710
+ if (provider?.includes("gitlab"))
1711
+ return "PRIVATE-TOKEN";
1712
+ return "authorization";
1713
+ }
1714
+ function defaultTokenScheme(source, headerName) {
1715
+ const provider = sourceProviderToken(source);
1716
+ if (headerName.toLowerCase() !== "authorization")
1717
+ return "";
1718
+ if (provider?.includes("figma") || provider?.includes("gitlab"))
1719
+ return "";
1720
+ return "Bearer";
1721
+ }
1722
+ function sourceProviderToken(source) {
1723
+ const origin = inferSourceOrigin(source);
1724
+ return [
1725
+ origin.provider,
1726
+ origin.kind,
1727
+ source.origin?.provider,
1728
+ source.origin?.kind,
1729
+ source.kind,
1730
+ source.type,
1731
+ ]
1732
+ .filter((value) => typeof value === "string" && value.trim().length > 0)
1733
+ .map((value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""))
1734
+ .find((value) => value.includes("figma") ||
1735
+ value.includes("jira") ||
1736
+ value.includes("confluence") ||
1737
+ value.includes("gitlab"));
1738
+ }
1202
1739
  function cookieHeaderFromEnvironment(source) {
1203
1740
  const parts = [];
1204
1741
  const sources = [];
@@ -1253,15 +1790,6 @@ function contentTypeForPath(path) {
1253
1790
  return "text/html";
1254
1791
  return "text/plain";
1255
1792
  }
1256
- function escapeTable(value) {
1257
- return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
1258
- }
1259
- function isRecord(value) {
1260
- return typeof value === "object" && value !== null && !Array.isArray(value);
1261
- }
1262
- function normalizeAdapterKey(value) {
1263
- return value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ?? "";
1264
- }
1265
1793
  function renderRepositoryMetadata(entry, repository) {
1266
1794
  return [
1267
1795
  "---",
@@ -1295,6 +1823,7 @@ function htmlToStructuredMarkdown(value) {
1295
1823
  .replace(/<style[\s\S]*?<\/style>/gi, "")
1296
1824
  .replace(/<(nav|header|footer|aside)[^>]*>[\s\S]*?<\/\1>/gi, "");
1297
1825
  return body
1826
+ .replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, (table) => `\n${htmlTableToMarkdown(table)}\n`)
1298
1827
  .replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
1299
1828
  .replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
1300
1829
  .replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
@@ -1305,14 +1834,60 @@ function htmlToStructuredMarkdown(value) {
1305
1834
  .replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
1306
1835
  .replace(/<[^>]+>/g, "")
1307
1836
  .replace(/&nbsp;/g, " ")
1837
+ .split("\n")
1838
+ .map((line) => decodeHtmlEntities(line).trim())
1839
+ .filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
1840
+ .join("\n");
1841
+ }
1842
+ function htmlTableToMarkdown(table) {
1843
+ const rows = Array.from(table.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi))
1844
+ .map((row) => Array.from((row[1] ?? "").matchAll(/<(td|th)\b[^>]*>([\s\S]*?)<\/\1>/gi))
1845
+ .map((cell) => tableCellText(cell[2] ?? "")))
1846
+ .filter((row) => row.some((cell) => cell.length > 0));
1847
+ if (!rows.length)
1848
+ return "";
1849
+ const width = Math.max(...rows.map((row) => row.length));
1850
+ const normalized = rows.map((row) => {
1851
+ const copy = row.slice();
1852
+ while (copy.length < width)
1853
+ copy.push("");
1854
+ return copy;
1855
+ });
1856
+ const header = normalized[0] ?? [];
1857
+ if (!header.length)
1858
+ return "";
1859
+ const body = normalized.slice(1);
1860
+ return [
1861
+ `| ${header.join(" | ")} |`,
1862
+ `| ${header.map(() => "---").join(" | ")} |`,
1863
+ ...body.map((row) => `| ${row.join(" | ")} |`),
1864
+ ].join("\n");
1865
+ }
1866
+ function tableCellText(value) {
1867
+ return decodeHtmlEntities(stripHtml(value
1868
+ .replace(/<br\s*\/?>/gi, " ZSK_BR ")
1869
+ .replace(/<\/(p|div|section|li)>/gi, " ZSK_BR ")
1870
+ .replace(/<li\b[^>]*>/gi, "- ")))
1871
+ .replace(/\s*ZSK_BR\s*/g, "; ")
1872
+ .replace(/[ \t\r\n]+/g, " ")
1873
+ .replace(/(?:\s*;\s*){2,}/g, "; ")
1874
+ .replace(/^\s*;\s*|\s*;\s*$/g, "")
1875
+ .replace(/\|/g, "\\|")
1876
+ .trim();
1877
+ }
1878
+ function stripHtml(value) {
1879
+ return value.replace(/<[^>]+>/g, "");
1880
+ }
1881
+ function decodeHtmlEntities(value) {
1882
+ return value
1883
+ .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
1884
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCodePoint(parseInt(code, 16)))
1885
+ .replace(/&nbsp;/g, " ")
1308
1886
  .replace(/&amp;/g, "&")
1309
1887
  .replace(/&lt;/g, "<")
1310
1888
  .replace(/&gt;/g, ">")
1311
1889
  .replace(/&quot;/g, "\"")
1312
- .split("\n")
1313
- .map((line) => line.trim())
1314
- .filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
1315
- .join("\n");
1890
+ .replace(/&apos;/g, "'");
1316
1891
  }
1317
1892
  function fencedBody(body, contentType) {
1318
1893
  const info = contentType.includes("json")
@@ -1330,34 +1905,6 @@ function shouldPreserveMachineReadable(source, originPath, snapshotPath) {
1330
1905
  [".json", ".yaml", ".yml"].includes(ext) ||
1331
1906
  [".json", ".yaml", ".yml"].includes(snapshotExt));
1332
1907
  }
1333
- function chooseStrategy(method) {
1334
- if (method === "local")
1335
- return "local-copy-structured-markdown";
1336
- if (method === "repository")
1337
- return "repository-metadata-only";
1338
- if (method === "url")
1339
- return "playwright-auth-or-direct-fetch";
1340
- return "confirm-acquisition-method";
1341
- }
1342
- function renderDownstreamImpact(results) {
1343
- const changed = results.filter((result) => result.changed);
1344
- const blocked = results.filter((result) => ["blocked-auth", "source-gap", "failed"].includes(result.status));
1345
- return [
1346
- "# Downstream Impact",
1347
- "",
1348
- `Changed sources: ${changed.length}`,
1349
- `Blocked sources: ${blocked.length}`,
1350
- "",
1351
- changed.length === 0
1352
- ? "No downstream refresh is recommended from this sync run."
1353
- : changed.map((result) => `- \`${result.sourcePath}\`: snapshot changed; review dependent proposal/spec/design/tasks/tests before claiming freshness.`).join("\n"),
1354
- "",
1355
- blocked.length === 0
1356
- ? "No blocked sources."
1357
- : blocked.map((result) => `- \`${result.sourcePath}\` [${result.status}]: ${result.reason}`).join("\n"),
1358
- "",
1359
- ].join("\n");
1360
- }
1361
1908
  async function cookieHeaderFromStorageState(storageStatePath, url) {
1362
1909
  try {
1363
1910
  const parsed = JSON.parse(await readFile(storageStatePath, "utf8"));
@@ -1441,74 +1988,46 @@ async function loadProjectPlaywright(target) {
1441
1988
  }
1442
1989
  return await import(resolved);
1443
1990
  }
1444
- async function hashIfExists(path) {
1991
+ async function safeStat(path) {
1445
1992
  try {
1446
- return await sha256(path);
1993
+ const info = await stat(path);
1994
+ return { isDirectory: info.isDirectory(), isFile: info.isFile() };
1447
1995
  }
1448
1996
  catch {
1449
- return undefined;
1997
+ return null;
1450
1998
  }
1451
1999
  }
1452
- async function sha256(path) {
1453
- const content = await readFile(path);
1454
- return `sha256:${createHash("sha256").update(content).digest("hex")}`;
1455
- }
1456
- async function exists(path) {
1457
- try {
1458
- await stat(path);
1459
- return true;
1460
- }
1461
- catch {
1462
- return false;
1463
- }
2000
+ function isGitSubmoduleResource(source) {
2001
+ const values = [
2002
+ source.kind,
2003
+ source.type,
2004
+ source.origin?.kind,
2005
+ source.metadata?.resourceType,
2006
+ ].map((value) => typeof value === "string" ? value.toLowerCase() : "");
2007
+ return values.some((value) => value === "git-submodule" || value === "submodule");
1464
2008
  }
1465
- async function safeReadDir(path) {
2009
+ async function updateGitSubmodule(target, sourcePath) {
1466
2010
  try {
1467
- return await import("node:fs/promises").then((fs) => fs.readdir(path, { withFileTypes: true }));
2011
+ await execFileAsync("git", ["-C", target, "submodule", "update", "--init", "--remote", "--", sourcePath], {
2012
+ timeout: 120_000,
2013
+ maxBuffer: DEFAULT_ADAPTER_COMMAND_MAX_BUFFER,
2014
+ });
2015
+ return { ok: true };
1468
2016
  }
1469
- catch {
1470
- return [];
2017
+ catch (error) {
2018
+ const reason = error instanceof Error ? error.message : String(error);
2019
+ return { ok: false, reason: `git submodule update failed for ${sourcePath}: ${reason}` };
1471
2020
  }
1472
2021
  }
1473
- function safeFileName(value) {
1474
- return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "source";
1475
- }
1476
- function isMigrationLaneCandidate(value, staleNames) {
1477
- return (staleNames.has(value) ||
1478
- /^v?\d+(?:[._-]\d+)*$/i.test(value) ||
1479
- /^(sprint|iteration|release)[-_]?\d*/i.test(value));
1480
- }
1481
- function resolveAuthStatePath(target, config, authState) {
1482
- const statePath = resolve(target, authState);
1483
- const sharedAuthRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
1484
- const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
1485
- const pathParts = statePath.split(/[\\/]/);
1486
- const modulePrivate = isInside(modulesRoot, statePath) && pathParts.includes("_playwright") && pathParts.includes(".auth");
1487
- if (isInside(sharedAuthRoot, statePath) || modulePrivate)
1488
- return statePath;
1489
- throw new Error(`Playwright storageState input must stay under ${sharedAuthRoot} or a module _playwright/.auth directory`);
1490
- }
1491
2022
  function resolveSelectedAuthStatePath(target, config, opts) {
1492
2023
  if (opts.authState)
1493
- return resolveAuthStatePath(target, config, opts.authState);
2024
+ return resolvePlaywrightAuthStatePath(target, config, opts.authState);
1494
2025
  if (opts.authProfile)
1495
- return resolveAuthProfilePath(target, config, opts.authProfile);
2026
+ return resolvePlaywrightAuthProfilePath(target, config, opts.authProfile);
1496
2027
  return undefined;
1497
2028
  }
1498
- function resolveAuthProfilePath(target, config, profile) {
1499
- const authRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
1500
- const fileName = profile ? `${safeFileName(profile)}.json` : "user_data.json";
1501
- return join(authRoot, fileName);
1502
- }
1503
2029
  function resolveAdapterLocalFilePath(target, value) {
1504
2030
  const path = resolve(target, value);
1505
- return isInside(resolve(target), path) ? path : undefined;
1506
- }
1507
- function isInside(root, target) {
1508
- const rel = relative(root, target);
1509
- return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
1510
- }
1511
- export function createSyncRunId(now = new Date()) {
1512
- return now.toISOString().replace(/[:.]/g, "-");
2031
+ return isPathInside(resolve(target), path) ? path : undefined;
1513
2032
  }
1514
2033
  //# sourceMappingURL=prepare-sync.js.map