@captain_z/zsk 1.8.4 → 1.8.6

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 (129) hide show
  1. package/dist/bin.js +13 -0
  2. package/dist/bin.js.map +1 -1
  3. package/dist/commands/check.js +14 -575
  4. package/dist/commands/check.js.map +1 -1
  5. package/dist/commands/config.js +4 -3
  6. package/dist/commands/config.js.map +1 -1
  7. package/dist/commands/demo.d.ts +5 -0
  8. package/dist/commands/demo.js +70 -297
  9. package/dist/commands/demo.js.map +1 -1
  10. package/dist/commands/doctor.js +9 -4
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/gate.d.ts +1 -0
  13. package/dist/commands/gate.js +8 -2
  14. package/dist/commands/gate.js.map +1 -1
  15. package/dist/commands/prepare.js +14 -1
  16. package/dist/commands/prepare.js.map +1 -1
  17. package/dist/commands/project-init.js +31 -8
  18. package/dist/commands/project-init.js.map +1 -1
  19. package/dist/core/config.d.ts +68 -0
  20. package/dist/core/config.js +213 -17
  21. package/dist/core/config.js.map +1 -1
  22. package/dist/core/demo-auth.d.ts +30 -0
  23. package/dist/core/demo-auth.js +213 -0
  24. package/dist/core/demo-auth.js.map +1 -0
  25. package/dist/core/demo-scenarios.d.ts +62 -0
  26. package/dist/core/demo-scenarios.js +276 -0
  27. package/dist/core/demo-scenarios.js.map +1 -0
  28. package/dist/core/demo-sources.d.ts +37 -0
  29. package/dist/core/demo-sources.js +198 -0
  30. package/dist/core/demo-sources.js.map +1 -0
  31. package/dist/core/mcp-registry-discovery.d.ts +16 -0
  32. package/dist/core/mcp-registry-discovery.js +187 -0
  33. package/dist/core/mcp-registry-discovery.js.map +1 -0
  34. package/dist/core/origin-detection.js +1 -1
  35. package/dist/core/origin-detection.js.map +1 -1
  36. package/dist/core/prepare-artifacts.d.ts +16 -0
  37. package/dist/core/prepare-artifacts.js +25 -0
  38. package/dist/core/prepare-artifacts.js.map +1 -0
  39. package/dist/core/prepare-auth-helper.d.ts +8 -0
  40. package/dist/core/prepare-auth-helper.js +32 -0
  41. package/dist/core/prepare-auth-helper.js.map +1 -0
  42. package/dist/core/prepare-lifecycle.d.ts +2 -0
  43. package/dist/core/prepare-lifecycle.js +49 -0
  44. package/dist/core/prepare-lifecycle.js.map +1 -1
  45. package/dist/core/prepare-materialization.d.ts +8 -0
  46. package/dist/core/prepare-materialization.js +26 -0
  47. package/dist/core/prepare-materialization.js.map +1 -0
  48. package/dist/core/prepare-migration.d.ts +6 -0
  49. package/dist/core/prepare-migration.js +57 -0
  50. package/dist/core/prepare-migration.js.map +1 -0
  51. package/dist/core/prepare-reporting.d.ts +5 -0
  52. package/dist/core/prepare-reporting.js +106 -0
  53. package/dist/core/prepare-reporting.js.map +1 -0
  54. package/dist/core/prepare-routing.d.ts +12 -0
  55. package/dist/core/prepare-routing.js +182 -0
  56. package/dist/core/prepare-routing.js.map +1 -0
  57. package/dist/core/prepare-sync.d.ts +11 -22
  58. package/dist/core/prepare-sync.js +811 -260
  59. package/dist/core/prepare-sync.js.map +1 -1
  60. package/dist/core/prepare-utils.d.ts +6 -0
  61. package/dist/core/prepare-utils.js +35 -0
  62. package/dist/core/prepare-utils.js.map +1 -0
  63. package/dist/core/provider-policy.d.ts +26 -0
  64. package/dist/core/provider-policy.js +180 -0
  65. package/dist/core/provider-policy.js.map +1 -0
  66. package/dist/core/provider-readiness.d.ts +39 -0
  67. package/dist/core/provider-readiness.js +78 -0
  68. package/dist/core/provider-readiness.js.map +1 -0
  69. package/dist/core/source-adapter-normalization.d.ts +31 -0
  70. package/dist/core/source-adapter-normalization.js +235 -0
  71. package/dist/core/source-adapter-normalization.js.map +1 -0
  72. package/dist/core/source-snapshot-adapters.d.ts +3 -3
  73. package/dist/core/source-snapshot-adapters.js +2 -24
  74. package/dist/core/source-snapshot-adapters.js.map +1 -1
  75. package/dist/core/staffing-plan.d.ts +1 -0
  76. package/dist/core/staffing-plan.js +61 -3
  77. package/dist/core/staffing-plan.js.map +1 -1
  78. package/dist/core/stage-clarity-verification.js +21 -18
  79. package/dist/core/stage-clarity-verification.js.map +1 -1
  80. package/dist/core/stage-output-quality.d.ts +3 -0
  81. package/dist/core/stage-output-quality.js +122 -0
  82. package/dist/core/stage-output-quality.js.map +1 -0
  83. package/dist/core/stage-quality-contracts.d.ts +19 -0
  84. package/dist/core/stage-quality-contracts.js.map +1 -1
  85. package/dist/core/stage-quality-criteria.js +0 -37
  86. package/dist/core/stage-quality-criteria.js.map +1 -1
  87. package/dist/core/stage-quality-rendering.d.ts +4 -2
  88. package/dist/core/stage-quality-rendering.js +130 -12
  89. package/dist/core/stage-quality-rendering.js.map +1 -1
  90. package/dist/core/stage-quality.js +17 -6
  91. package/dist/core/stage-quality.js.map +1 -1
  92. package/dist/core/template-registry.js +12 -15
  93. package/dist/core/template-registry.js.map +1 -1
  94. package/dist/core/workspace-conformance.d.ts +39 -0
  95. package/dist/core/workspace-conformance.js +603 -0
  96. package/dist/core/workspace-conformance.js.map +1 -0
  97. package/package.json +2 -2
  98. package/schemas/providers.schema.json +74 -0
  99. package/schemas/zsk-config.schema.json +417 -1
  100. package/templates/project-init/.zsk/README.md +57 -0
  101. package/templates/project-init/.zsk/config.yaml +41 -33
  102. package/templates/project-init/.zsk/docs/CONFIG-SCHEMA.md +131 -0
  103. package/templates/project-init/.zsk/docs/PROJECT-CONFIG.md +49 -28
  104. package/templates/project-init/.zsk/docs/README.md +10 -0
  105. package/templates/project-init/.zsk/docs/SECURITY.md +34 -0
  106. package/templates/project-init/.zsk/docs/SYSTEM-SPEC.md +26 -9
  107. package/templates/project-init/.zsk/evidence/README.md +21 -0
  108. package/templates/project-init/.zsk/evidence/prepare/README.md +22 -0
  109. package/templates/project-init/.zsk/issues/README.md +10 -0
  110. package/templates/project-init/.zsk/modules/README.md +19 -0
  111. package/templates/project-init/.zsk/modules/index.md +9 -5
  112. package/templates/project-init/.zsk/raws/README.md +39 -0
  113. package/templates/project-init/.zsk/roles.yaml +36 -105
  114. package/templates/project-init/.zsk/templates/config.examples.yaml +104 -0
  115. package/templates/project-init/.zsk/templates/issue-card.md +23 -0
  116. package/templates/project-init/.zsk/templates/module/README.md +13 -0
  117. package/templates/project-init/.zsk/templates/module/design.md +22 -0
  118. package/templates/project-init/.zsk/templates/module/module.yaml +15 -0
  119. package/templates/project-init/.zsk/templates/module/proposal.md +20 -0
  120. package/templates/project-init/.zsk/templates/module/spec.md +22 -0
  121. package/templates/project-init/.zsk/templates/module/tasks.md +16 -0
  122. package/templates/project-init/.zsk/raws/index.md +0 -34
  123. package/templates/project-init/.zsk/raws/manifest.json +0 -4
  124. package/templates/project-init/.zsk/raws/prepare/backend/index.md +0 -4
  125. package/templates/project-init/.zsk/raws/prepare/design/index.md +0 -21
  126. package/templates/project-init/.zsk/raws/prepare/index.md +0 -37
  127. package/templates/project-init/.zsk/raws/prepare/product/index.md +0 -4
  128. package/templates/project-init/.zsk/raws/prepare/qa/index.md +0 -4
  129. package/templates/project-init/.zsk/raws/prepare/ux/index.md +0 -22
@@ -1,40 +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";
8
19
  import { acquireSourceSnapshot, chooseSourceSnapshotStrategy, createSourceSnapshotContext, } from "./source-snapshot-adapters.js";
20
+ import { isPathInside, resolvePlaywrightAuthProfilePath, resolvePlaywrightAuthStatePath, } from "./workspace-conformance.js";
9
21
  import { getWorkspacePath } from "./workspace-layout.js";
10
- export function resolvePrepareSyncArtifacts(target, config, runId) {
11
- const dir = resolve(target, getWorkspacePath(config, "evidenceRoot"), "prepare", runId);
12
- const authDir = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
13
- return {
14
- runId,
15
- dir,
16
- adapterResultsDir: join(dir, "adapter-results"),
17
- authCheckPath: join(dir, "auth-check.json"),
18
- downstreamImpactPath: join(dir, "downstream-impact.md"),
19
- authScriptPath: join(authDir, "login.mjs"),
20
- authStatePath: join(authDir, "user_data.json"),
21
- migrationPlanPath: join(dir, "raws-migration-plan.md"),
22
- };
23
- }
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;
24
28
  export async function syncPrepareSources(target, config, opts = {}) {
25
29
  const runId = opts.runId ?? createSyncRunId();
26
30
  const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
27
- const entries = flattenProjectSources(config.sources);
31
+ const entries = flattenPrepareEntries(config);
28
32
  const selected = new Set(selectSourceEntries(entries, opts));
29
33
  const results = [];
30
34
  await mkdir(artifacts.adapterResultsDir, { recursive: true });
35
+ await mkdir(artifacts.providerReadinessDir, { recursive: true });
36
+ const providerReadiness = await resolveProviderReadiness(target, artifacts, [...selected]);
31
37
  for (const entry of entries) {
32
38
  if (!selected.has(entry))
33
39
  continue;
34
- const result = await syncEntry(target, config, entry, opts);
40
+ const result = annotateProviderReadiness(target, entry, await syncEntry(target, config, entry, opts, entries), providerReadiness.byProvider);
35
41
  results.push(result);
36
42
  await writeFile(join(artifacts.adapterResultsDir, `${safeFileName(entry.id)}.json`), `${JSON.stringify(result, null, 2)}\n`, "utf8");
37
43
  }
44
+ const routes = opts.dryRun ? [] : await routePreparedResources(target, config, results);
38
45
  if (!opts.dryRun) {
39
46
  const records = [];
40
47
  for (const entry of entries) {
@@ -46,43 +53,22 @@ export async function syncPrepareSources(target, config, opts = {}) {
46
53
  const downstreamImpact = renderDownstreamImpact(results);
47
54
  await mkdir(dirname(artifacts.downstreamImpactPath), { recursive: true });
48
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");
49
59
  return {
50
60
  artifacts,
51
61
  results,
62
+ routes,
63
+ providerReadiness: [...providerReadiness.byProvider.values()].map((item) => item.evidence),
52
64
  downstreamImpact,
65
+ syncReport,
53
66
  };
54
67
  }
55
- export async function writeAuthLoginHelper(target, config, opts = {}) {
56
- const artifacts = resolvePrepareSyncArtifacts(target, config, opts.runId ?? createSyncRunId());
57
- const statePath = opts.out ? resolve(target, opts.out) : resolveAuthProfilePath(target, config, opts.profile);
58
- const authRoot = dirname(artifacts.authScriptPath);
59
- if (!isInside(authRoot, statePath)) {
60
- throw new Error(`Playwright storageState output must stay under ${authRoot}`);
61
- }
62
- const script = [
63
- `import { chromium } from "playwright";`,
64
- ``,
65
- `const targetUrl = process.env.ZSK_PREPARE_AUTH_URL ?? ${JSON.stringify(opts.url ?? "about:blank")};`,
66
- `const storageStatePath = process.env.ZSK_PLAYWRIGHT_STORAGE_STATE ?? ${JSON.stringify(statePath)};`,
67
- `const browser = await chromium.launch({ headless: false });`,
68
- `const context = await browser.newContext();`,
69
- `const page = await context.newPage();`,
70
- `await page.goto(targetUrl, { waitUntil: "domcontentloaded" });`,
71
- `console.log("Complete login in the opened browser, then press Enter here to save storageState.");`,
72
- `await new Promise((resolve) => process.stdin.once("data", resolve));`,
73
- `await context.storageState({ path: storageStatePath });`,
74
- `console.log(\`Saved Playwright storageState to ${"${storageStatePath}"}\`);`,
75
- `await browser.close();`,
76
- ``,
77
- ].join("\n");
78
- await mkdir(dirname(artifacts.authScriptPath), { recursive: true });
79
- await writeFile(artifacts.authScriptPath, script, "utf8");
80
- return { ...artifacts, authStatePath: statePath };
81
- }
82
68
  export async function checkPrepareAuth(target, config, opts = {}) {
83
69
  const runId = opts.runId ?? createSyncRunId();
84
70
  const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
85
- const entries = selectSourceEntries(flattenProjectSources(config.sources), opts);
71
+ const entries = selectSourceEntries(flattenPrepareEntries(config), opts);
86
72
  const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
87
73
  const results = [];
88
74
  for (const entry of entries) {
@@ -92,45 +78,6 @@ export async function checkPrepareAuth(target, config, opts = {}) {
92
78
  await writeFile(artifacts.authCheckPath, `${JSON.stringify({ runId, results }, null, 2)}\n`, "utf8");
93
79
  return { artifacts, results };
94
80
  }
95
- export async function buildRawMigrationPlan(target, config, runId = createSyncRunId()) {
96
- const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
97
- const prepareRoot = resolve(target, getWorkspacePath(config, "resourcesRoot"), "prepare");
98
- const staleNames = new Set([
99
- "qa-engineer",
100
- "test-engineer",
101
- "backend-engineer",
102
- "frontend-engineer",
103
- "product-manager",
104
- "designer",
105
- "jira",
106
- "confluence",
107
- "figma",
108
- "modao",
109
- "manual",
110
- "provider",
111
- "version",
112
- ]);
113
- const entries = await safeReadDir(prepareRoot);
114
- const findings = entries
115
- .filter((entry) => entry.isDirectory() && isMigrationLaneCandidate(entry.name, staleNames))
116
- .map((entry) => ({
117
- path: join(".zsk/raws/prepare", entry.name),
118
- action: "dry-run only; keep readable, require explicit migration before rewriting references",
119
- }));
120
- const content = [
121
- "# Raw Migration Plan",
122
- "",
123
- "Dry-run report only. No files were moved, deleted, or rewritten.",
124
- "",
125
- findings.length === 0
126
- ? "No stale provider/method/version prepare lanes were found."
127
- : findings.map((item) => `- \`${item.path}\`: ${item.action}`).join("\n"),
128
- "",
129
- ].join("\n");
130
- await mkdir(dirname(artifacts.migrationPlanPath), { recursive: true });
131
- await writeFile(artifacts.migrationPlanPath, content, "utf8");
132
- return { artifacts, content };
133
- }
134
81
  function selectSourceEntries(entries, opts) {
135
82
  if (opts.all || !opts.source)
136
83
  return entries;
@@ -145,6 +92,52 @@ function selectSourceEntries(entries, opts) {
145
92
  }
146
93
  return alias;
147
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
+ }
148
141
  async function checkAuthEntry(entry, authStatePath, opts) {
149
142
  const origin = inferSourceOrigin(entry.source);
150
143
  const base = {
@@ -155,20 +148,20 @@ async function checkAuthEntry(entry, authStatePath, opts) {
155
148
  origin: origin.ref,
156
149
  method: origin.method,
157
150
  };
158
- const url = origin.ref;
159
- if (!url || origin.method !== "url") {
151
+ const url = authCheckUrlForSource(entry.source, origin);
152
+ if (!url) {
160
153
  return {
161
154
  ...base,
162
155
  status: "source-gap",
163
156
  authSources: [],
164
157
  directAuthAvailable: false,
165
158
  networkAttempted: false,
166
- 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",
167
160
  validation: { urlOrigin: "fail", secretValuesHidden: "pass" },
168
161
  };
169
162
  }
170
163
  const { headers, authSources } = await headersForSource(entry.source, url, authStatePath);
171
- const directAuthAvailable = Boolean(headers.authorization || headers.cookie);
164
+ const directAuthAvailable = hasRuntimeAuthHeaders(headers);
172
165
  if (!opts.allowNetwork) {
173
166
  return {
174
167
  ...base,
@@ -254,17 +247,37 @@ async function checkAuthEntry(entry, authStatePath, opts) {
254
247
  };
255
248
  }
256
249
  }
257
- async function syncEntry(target, config, entry, opts) {
258
- const context = await createSourceSnapshotContext(target, config, entry, opts, hashIfExists);
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);
259
265
  return acquireSourceSnapshot(context, {
260
266
  dryRun: skippedDryRunSnapshot,
261
267
  local: syncLocalSnapshot,
262
- provider: syncProviderSnapshot,
268
+ provider: (snapshotContext, providerAdapter) => syncProviderSnapshot(snapshotContext, providerAdapter, sharedSnapshot),
263
269
  repository: syncRepositorySnapshot,
264
270
  url: syncUrlSnapshot,
265
271
  providerManaged: blockedProviderManagedSnapshot,
266
272
  });
267
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
+ }
268
281
  function skippedDryRunSnapshot(context) {
269
282
  return {
270
283
  ...context.base,
@@ -276,61 +289,91 @@ function skippedDryRunSnapshot(context) {
276
289
  };
277
290
  }
278
291
  async function syncLocalSnapshot(context) {
279
- const { base, entry, source, snapshotPath, previousSnapshotHash, target } = context;
292
+ const { base, entry, source, snapshotPath, previousSnapshotHash, target, opts } = context;
280
293
  const sourcePath = source.origin?.path ?? source.path;
281
294
  if (!sourcePath) {
282
295
  return sourceGap(base, "configured local origin has no path");
283
296
  }
284
297
  const originPath = resolve(target, sourcePath);
285
- if (!(await exists(originPath))) {
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) {
286
305
  return sourceGap(base, "configured local origin path does not exist");
287
306
  }
288
- await mkdir(dirname(snapshotPath), { recursive: true });
289
- if (shouldPreserveMachineReadable(source, originPath, snapshotPath)) {
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)) {
312
+ await mkdir(dirname(snapshotPath), { recursive: true });
290
313
  await copyFile(originPath, snapshotPath);
314
+ const snapshotHash = await sha256(snapshotPath);
315
+ written = {
316
+ snapshotHash,
317
+ changed: previousSnapshotHash !== snapshotHash,
318
+ };
291
319
  }
292
320
  else {
293
- await writeFile(snapshotPath, await renderStructuredLocalMarkdown(target, entry, originPath), "utf8");
321
+ written = await writePreparedSnapshot(target, snapshotPath, await renderStructuredLocalMarkdown(target, entry, originPath), previousSnapshotHash);
294
322
  }
295
- const snapshotHash = await sha256(snapshotPath);
296
323
  return {
297
324
  ...base,
298
325
  strategy: "local-copy-structured-markdown",
299
326
  status: "materialized",
300
- snapshotHash,
301
- changed: previousSnapshotHash !== snapshotHash,
302
- reason: "local origin copied or materialized into configured snapshot",
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",
303
332
  validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
304
333
  };
305
334
  }
306
- async function syncProviderSnapshot(context, providerAdapter) {
335
+ async function syncProviderSnapshot(context, providerAdapter, sharedSnapshot) {
307
336
  const { target, config, entry, snapshotPath, previousSnapshotHash, opts, origin } = context;
308
337
  const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
309
- const adapterResult = await runProviderAdapter(providerAdapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
338
+ const adapterResult = await runProviderAdapter(providerAdapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
310
339
  if (!adapterResult)
311
340
  return null;
341
+ if (adapterResult.status === "source-gap" && origin.method === "url") {
342
+ if (adapterResult.metadata?.providerGap === "adapter-ambiguous") {
343
+ return adapterResult;
344
+ }
345
+ const fallback = await syncUrlSnapshot(context);
346
+ return annotateFallbackResult(fallback, providerAdapter, adapterResult);
347
+ }
312
348
  if (adapterResult.status === "blocked-auth" && opts.browser && origin.method === "url") {
313
349
  const browserResult = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
314
- return {
315
- ...browserResult,
316
- adapter: `${providerAdapter}-browser-fallback`,
317
- metadata: cleanMetadata({ providerAdapter, fallbackFrom: adapterResult.strategy }),
318
- };
350
+ return annotateFallbackResult(browserResult, providerAdapter, adapterResult, `${providerAdapter}-browser-fallback`);
319
351
  }
320
352
  return adapterResult;
321
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
+ }
322
367
  async function syncRepositorySnapshot(context) {
323
368
  const { base, entry, origin, snapshotPath, previousSnapshotHash } = context;
324
- await mkdir(dirname(snapshotPath), { recursive: true });
325
369
  const content = renderRepositoryMetadata(entry, origin.ref ?? "");
326
- await writeFile(snapshotPath, content, "utf8");
327
- const snapshotHash = await sha256(snapshotPath);
370
+ const written = await writePreparedSnapshot(context.target, snapshotPath, content, previousSnapshotHash);
328
371
  return {
329
372
  ...base,
330
373
  strategy: "repository-metadata-only",
331
374
  status: "metadata-only",
332
- snapshotHash,
333
- changed: previousSnapshotHash !== snapshotHash,
375
+ snapshotHash: written.snapshotHash,
376
+ changed: written.changed,
334
377
  reason: "repository origin recorded as metadata; configure contract paths or checkout policy before broad acquisition",
335
378
  validation: { repositoryNotCrawled: "pass", snapshotWritten: "pass" },
336
379
  };
@@ -368,26 +411,29 @@ function blockedProviderManagedSnapshot(context) {
368
411
  validation: { acquisitionMethodConfirmed: "fail", previousSnapshotPreserved: context.previousSnapshotHash ? "pass" : "skipped" },
369
412
  };
370
413
  }
371
- async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
414
+ async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
372
415
  if (adapter === "confluence") {
373
- return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
416
+ return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
374
417
  }
375
418
  if (adapter === "jira") {
376
- return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
419
+ return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
377
420
  }
378
421
  if (adapter === "gitlab") {
379
- return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
422
+ return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
380
423
  }
381
- return runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
424
+ return runDesignAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
382
425
  }
383
- async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
426
+ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
384
427
  const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "confluence");
385
- const url = firstOriginString(entry.source, "apiUrl", "restUrl", "url") ?? base.origin;
386
- 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";
387
433
  if (!url)
388
- 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");
389
435
  if (!opts.allowNetwork) {
390
- 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 }));
391
437
  }
392
438
  const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
393
439
  if (!fetched.ok)
@@ -396,15 +442,18 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
396
442
  return blockedAdapter(base, strategy, "confluence adapter resolved to login/chrome content; previous snapshot was preserved");
397
443
  }
398
444
  const parsed = parseJsonObject(fetched.body);
445
+ const wordExport = parseConfluenceWordExport(fetched.body, fetched.contentType);
399
446
  const title = parsed ? stringValue(parsed, "title") : undefined;
400
447
  const pageId = parsed ? firstStringValue(parsed, "id", "pageId", "contentId") : undefined;
401
448
  const version = parsed ? versionValue(parsed) : undefined;
402
449
  const storageHtml = parsed ? confluenceBodyValue(parsed) : undefined;
403
- const content = storageHtml
404
- ? htmlToStructuredMarkdown(storageHtml)
405
- : fetched.contentType.includes("html")
406
- ? htmlToStructuredMarkdown(fetched.body)
407
- : 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);
408
457
  if (!hasMaterialContent(content)) {
409
458
  return blockedAdapter(base, strategy, "confluence adapter produced no material body content; previous snapshot was preserved");
410
459
  }
@@ -415,11 +464,11 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
415
464
  contentType: fetched.contentType,
416
465
  content,
417
466
  reason: "confluence source fetched and normalized into structured Markdown",
418
- metadata: cleanMetadata({ title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
467
+ metadata: cleanMetadata({ title: wordExport?.title ?? title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
419
468
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
420
- });
469
+ }, sharedSnapshot);
421
470
  }
422
- async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
471
+ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
423
472
  const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "jira");
424
473
  const csvPath = firstOriginString(entry.source, ...JIRA_LOCAL_FILE_ORIGIN_KEYS);
425
474
  if (csvPath) {
@@ -443,14 +492,15 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
443
492
  reason: "jira CSV export imported and normalized into structured Markdown",
444
493
  metadata: cleanMetadata({ issueCount: rows.length, fallback: "csv" }),
445
494
  validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
446
- });
495
+ }, sharedSnapshot);
447
496
  }
448
- 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);
449
499
  if (!url)
450
500
  return null;
451
501
  const strategy = "jira-rest-search";
452
502
  if (!opts.allowNetwork) {
453
- 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 }));
454
504
  }
455
505
  const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
456
506
  if (!fetched.ok)
@@ -472,9 +522,9 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
472
522
  reason: "jira REST source fetched and normalized into structured Markdown",
473
523
  metadata: cleanMetadata({ issueCount: jiraIssueCount(parsed), runtimeAuth: fetched.authSources.join(", ") || undefined }),
474
524
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
475
- });
525
+ }, sharedSnapshot);
476
526
  }
477
- async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
527
+ async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
478
528
  const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "gitlab");
479
529
  const url = firstOriginString(entry.source, "rawUrl", "apiUrl", "url");
480
530
  if (!url)
@@ -512,10 +562,11 @@ async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHas
512
562
  runtimeAuth: fetched.authSources.join(", ") || undefined,
513
563
  }),
514
564
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
515
- });
565
+ }, sharedSnapshot);
516
566
  }
517
- async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
518
- 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);
519
570
  const exportPath = firstOriginString(entry.source, ...DESIGN_LOCAL_FILE_ORIGIN_KEYS);
520
571
  if (exportPath) {
521
572
  const path = resolveAdapterLocalFilePath(target, exportPath);
@@ -529,7 +580,7 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
529
580
  return blockedAdapter(base, "design-export-import", "design export produced no material content; previous snapshot was preserved");
530
581
  }
531
582
  return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
532
- adapter: "design-source",
583
+ adapter,
533
584
  strategy: "design-export-import",
534
585
  resolvedOrigin: exportPath,
535
586
  contentType: contentTypeForPath(exportPath),
@@ -537,21 +588,56 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
537
588
  reason: "design export imported and normalized into structured Markdown",
538
589
  metadata: cleanMetadata({ provider: base.provider, fallback: "export" }),
539
590
  validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
540
- });
591
+ }, sharedSnapshot);
541
592
  }
542
- const url = firstOriginString(entry.source, "apiUrl", "exportUrl", "url") ?? base.origin;
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));
595
+ }
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;
543
629
  if (!url) {
544
- 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));
545
631
  }
546
632
  if (!opts.allowNetwork) {
547
- 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));
548
634
  }
549
635
  const directRuntimeAuth = await hasDirectRuntimeAuthForSource(entry.source, url, authStatePath);
550
636
  if (opts.browser && !directRuntimeAuth) {
551
637
  const rendered = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
552
638
  return {
553
639
  ...rendered,
554
- adapter: "design-source",
640
+ adapter,
555
641
  metadata: cleanMetadata({ provider: base.provider, acquisition: "browser" }),
556
642
  };
557
643
  }
@@ -559,29 +645,35 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
559
645
  if (!fetched.ok) {
560
646
  if (opts.browser)
561
647
  return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
562
- 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));
563
649
  }
564
650
  if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
565
651
  if (opts.browser)
566
652
  return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
567
- 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));
568
654
  }
569
655
  const content = renderDesignMarkdown(fetched.body, fetched.resolvedOrigin, fetched.contentType);
570
656
  if (!hasMaterialContent(content)) {
571
657
  if (opts.browser)
572
658
  return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
573
- 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));
574
660
  }
575
661
  return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
576
- adapter: "design-source",
662
+ adapter,
577
663
  strategy: "design-api-or-export",
578
664
  resolvedOrigin: fetched.resolvedOrigin,
579
665
  contentType: fetched.contentType,
580
666
  content,
581
667
  reason: "design source fetched and normalized into structured Markdown",
582
- 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
+ }),
583
675
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
584
- });
676
+ }, sharedSnapshot);
585
677
  }
586
678
  function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter) {
587
679
  const origin = inferSourceOrigin(entry.source);
@@ -597,12 +689,10 @@ function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter)
597
689
  previousSnapshotHash,
598
690
  };
599
691
  }
600
- async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result) {
692
+ async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result, sharedSnapshot) {
601
693
  const origin = inferSourceOrigin(entry.source);
602
- const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata);
603
- await mkdir(dirname(snapshotPath), { recursive: true });
604
- await writeFile(snapshotPath, markdown, "utf8");
605
- 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);
606
696
  return {
607
697
  envelopeVersion: 1,
608
698
  sourceKey: entry.id,
@@ -615,11 +705,11 @@ async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapsho
615
705
  origin: origin.ref,
616
706
  resolvedOrigin: result.resolvedOrigin,
617
707
  contentType: result.contentType,
618
- snapshot: relative(target, snapshotPath),
619
- snapshotHash,
708
+ snapshot: written.snapshot,
709
+ snapshotHash: written.snapshotHash,
620
710
  previousSnapshotHash,
621
711
  metadata: result.metadata,
622
- changed: previousSnapshotHash !== snapshotHash,
712
+ changed: written.changed,
623
713
  reason: result.reason,
624
714
  validation: result.validation,
625
715
  };
@@ -643,13 +733,18 @@ async function headersForSource(source, url, authStatePath) {
643
733
  const authSources = [];
644
734
  const token = envValue(authStringList(source, "tokenEnv", "accessTokenEnv", "apiTokenEnv"));
645
735
  if (token.value) {
646
- const headerName = firstAuthString(source, "tokenHeader", "headerName") ?? "authorization";
647
- 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);
648
738
  headers[headerName] = headerName.toLowerCase() === "authorization" && tokenScheme
649
739
  ? `${tokenScheme} ${token.value}`
650
740
  : token.value;
651
741
  authSources.push(`env:${token.name}`);
652
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
+ }
653
748
  const envCookie = cookieHeaderFromEnvironment(source);
654
749
  if (envCookie.source)
655
750
  authSources.push(`env:${envCookie.source}`);
@@ -663,7 +758,10 @@ async function headersForSource(source, url, authStatePath) {
663
758
  }
664
759
  async function hasDirectRuntimeAuthForSource(source, url, authStatePath) {
665
760
  const { headers } = await headersForSource(source, url, authStatePath);
666
- return Boolean(headers.authorization || headers.cookie);
761
+ return hasRuntimeAuthHeaders(headers);
762
+ }
763
+ function hasRuntimeAuthHeaders(headers) {
764
+ return Object.keys(headers).length > 0;
667
765
  }
668
766
  async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath) {
669
767
  const origin = inferSourceOrigin(entry.source);
@@ -698,17 +796,15 @@ async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHas
698
796
  return blocked(base, "remote fetch produced no material body content; previous snapshot was preserved");
699
797
  }
700
798
  const markdown = renderRemoteMarkdown(entry, response.url, contentType, content);
701
- await mkdir(dirname(snapshotPath), { recursive: true });
702
- await writeFile(snapshotPath, markdown, "utf8");
703
- const snapshotHash = await sha256(snapshotPath);
799
+ const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
704
800
  return {
705
801
  ...base,
706
802
  strategy: authSources.length > 0 ? "runtime-auth-direct-fetch" : "direct-fetch-structured-markdown",
707
803
  status: "materialized",
708
804
  resolvedOrigin: response.url,
709
805
  contentType,
710
- snapshotHash,
711
- changed: previousSnapshotHash !== snapshotHash,
806
+ snapshotHash: written.snapshotHash,
807
+ changed: written.changed,
712
808
  reason: "remote URL fetched and normalized into structured Markdown",
713
809
  metadata: cleanMetadata({ runtimeAuth: authSources.join(", ") || undefined }),
714
810
  validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
@@ -753,17 +849,15 @@ async function fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previous
753
849
  return blocked(base, "browser acquisition produced no material body content; previous snapshot was preserved");
754
850
  }
755
851
  const markdown = renderRemoteMarkdown(entry, resolvedUrl, "text/html; rendered=playwright", content, "playwright-headless-browser");
756
- await mkdir(dirname(snapshotPath), { recursive: true });
757
- await writeFile(snapshotPath, markdown, "utf8");
758
- const snapshotHash = await sha256(snapshotPath);
852
+ const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
759
853
  return {
760
854
  ...base,
761
855
  strategy: "playwright-headless-browser",
762
856
  status: "materialized",
763
857
  resolvedOrigin: resolvedUrl,
764
858
  contentType: "text/html; rendered=playwright",
765
- snapshotHash,
766
- changed: previousSnapshotHash !== snapshotHash,
859
+ snapshotHash: written.snapshotHash,
860
+ changed: written.changed,
767
861
  reason: "remote URL rendered through Playwright and normalized into structured Markdown",
768
862
  validation: { browserRendered: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
769
863
  };
@@ -807,6 +901,63 @@ async function renderStructuredLocalMarkdown(target, entry, originPath) {
807
901
  "",
808
902
  ].filter((line) => typeof line === "string").join("\n");
809
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
+ }
810
961
  function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extractionMethod = "direct-fetch") {
811
962
  return [
812
963
  "---",
@@ -835,7 +986,33 @@ function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extracti
835
986
  "",
836
987
  ].filter((line) => typeof line === "string").join("\n");
837
988
  }
838
- 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) {
839
1016
  const metadataLines = metadata && Object.keys(metadata).length > 0
840
1017
  ? [
841
1018
  "## Adapter Metadata",
@@ -844,11 +1021,28 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
844
1021
  "",
845
1022
  ]
846
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
+ : [];
847
1040
  return [
848
1041
  "---",
849
1042
  `sourceKey: ${JSON.stringify(entry.id)}`,
850
1043
  `sourcePath: ${JSON.stringify(entry.path)}`,
851
1044
  entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
1045
+ ...sharedFrontmatter,
852
1046
  `origin: ${JSON.stringify(resolvedOrigin)}`,
853
1047
  `contentType: ${JSON.stringify(contentType)}`,
854
1048
  `adapter: ${JSON.stringify(adapter)}`,
@@ -863,6 +1057,7 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
863
1057
  "",
864
1058
  `- Source key: \`${entry.id}\``,
865
1059
  entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
1060
+ ...sharedProvenance,
866
1061
  `- Resolved origin: \`${resolvedOrigin}\``,
867
1062
  `- Adapter: ${adapter}`,
868
1063
  `- Method: ${extractionMethod}`,
@@ -883,6 +1078,75 @@ function confluenceBodyValue(value) {
883
1078
  const view = recordValue(body, "view");
884
1079
  return stringValue(storage, "value") ?? stringValue(view, "value") ?? stringValue(value, "content") ?? stringValue(value, "value");
885
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
+ }
886
1150
  function versionValue(value) {
887
1151
  const version = recordValue(value, "version");
888
1152
  const number = version?.number;
@@ -1021,13 +1285,230 @@ function designNodeNames(value) {
1021
1285
  const nested = Array.isArray(children) ? children.flatMap(designNodeNames) : [];
1022
1286
  return current ? [current, ...nested] : nested;
1023
1287
  }
1024
- function designProviderGapMetadata(reason) {
1288
+ function designProviderGapMetadata(reason, source, figma) {
1289
+ const mcpProvider = source ? interpretSourceAdapter(source).mcpProviderName : undefined;
1025
1290
  return {
1026
1291
  providerGap: reason,
1027
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("; "),
1028
1303
  reconstructionRisk: "semantic UE reconstruction needs provider raw payload, assets/raw files, reconstruction.json, and known gaps before downstream design use",
1029
1304
  };
1030
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
+ }
1031
1512
  function parseCsvRows(value) {
1032
1513
  const rows = parseCsv(value).filter((row) => row.some((cell) => cell.trim().length > 0));
1033
1514
  const headers = rows.shift() ?? [];
@@ -1118,10 +1599,11 @@ function firstCsvValue(row, ...keys) {
1118
1599
  }
1119
1600
  function firstOriginString(source, ...keys) {
1120
1601
  const origin = source.origin;
1121
- if (!origin)
1122
- return undefined;
1123
1602
  for (const key of keys) {
1124
- 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];
1125
1607
  if (typeof value === "string" && value.trim().length > 0)
1126
1608
  return value.trim();
1127
1609
  }
@@ -1142,9 +1624,13 @@ function authStringList(source, ...keys) {
1142
1624
  for (const key of keys) {
1143
1625
  values.push(...stringListValue(auth?.[key]));
1144
1626
  }
1627
+ if (keys.some((key) => ["tokenEnv", "accessTokenEnv", "apiTokenEnv"].includes(key))) {
1628
+ values.push(...stringListValue(auth?.env));
1629
+ }
1145
1630
  for (const key of keys) {
1146
1631
  values.push(...stringListValue(source.origin?.[key]));
1147
1632
  }
1633
+ values.push(...defaultAuthEnvNames(source, keys));
1148
1634
  return Array.from(new Set(values));
1149
1635
  }
1150
1636
  function authCookieMap(source) {
@@ -1179,6 +1665,77 @@ function envValue(names) {
1179
1665
  }
1180
1666
  return {};
1181
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
+ }
1182
1739
  function cookieHeaderFromEnvironment(source) {
1183
1740
  const parts = [];
1184
1741
  const sources = [];
@@ -1233,12 +1790,6 @@ function contentTypeForPath(path) {
1233
1790
  return "text/html";
1234
1791
  return "text/plain";
1235
1792
  }
1236
- function escapeTable(value) {
1237
- return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
1238
- }
1239
- function isRecord(value) {
1240
- return typeof value === "object" && value !== null && !Array.isArray(value);
1241
- }
1242
1793
  function renderRepositoryMetadata(entry, repository) {
1243
1794
  return [
1244
1795
  "---",
@@ -1272,6 +1823,7 @@ function htmlToStructuredMarkdown(value) {
1272
1823
  .replace(/<style[\s\S]*?<\/style>/gi, "")
1273
1824
  .replace(/<(nav|header|footer|aside)[^>]*>[\s\S]*?<\/\1>/gi, "");
1274
1825
  return body
1826
+ .replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, (table) => `\n${htmlTableToMarkdown(table)}\n`)
1275
1827
  .replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
1276
1828
  .replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
1277
1829
  .replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
@@ -1282,14 +1834,60 @@ function htmlToStructuredMarkdown(value) {
1282
1834
  .replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
1283
1835
  .replace(/<[^>]+>/g, "")
1284
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, " ")
1285
1886
  .replace(/&amp;/g, "&")
1286
1887
  .replace(/&lt;/g, "<")
1287
1888
  .replace(/&gt;/g, ">")
1288
1889
  .replace(/&quot;/g, "\"")
1289
- .split("\n")
1290
- .map((line) => line.trim())
1291
- .filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
1292
- .join("\n");
1890
+ .replace(/&apos;/g, "'");
1293
1891
  }
1294
1892
  function fencedBody(body, contentType) {
1295
1893
  const info = contentType.includes("json")
@@ -1307,25 +1905,6 @@ function shouldPreserveMachineReadable(source, originPath, snapshotPath) {
1307
1905
  [".json", ".yaml", ".yml"].includes(ext) ||
1308
1906
  [".json", ".yaml", ".yml"].includes(snapshotExt));
1309
1907
  }
1310
- function renderDownstreamImpact(results) {
1311
- const changed = results.filter((result) => result.changed);
1312
- const blocked = results.filter((result) => ["blocked-auth", "source-gap", "failed"].includes(result.status));
1313
- return [
1314
- "# Downstream Impact",
1315
- "",
1316
- `Changed sources: ${changed.length}`,
1317
- `Blocked sources: ${blocked.length}`,
1318
- "",
1319
- changed.length === 0
1320
- ? "No downstream refresh is recommended from this sync run."
1321
- : changed.map((result) => `- \`${result.sourcePath}\`: snapshot changed; review dependent proposal/spec/design/tasks/tests before claiming freshness.`).join("\n"),
1322
- "",
1323
- blocked.length === 0
1324
- ? "No blocked sources."
1325
- : blocked.map((result) => `- \`${result.sourcePath}\` [${result.status}]: ${result.reason}`).join("\n"),
1326
- "",
1327
- ].join("\n");
1328
- }
1329
1908
  async function cookieHeaderFromStorageState(storageStatePath, url) {
1330
1909
  try {
1331
1910
  const parsed = JSON.parse(await readFile(storageStatePath, "utf8"));
@@ -1409,74 +1988,46 @@ async function loadProjectPlaywright(target) {
1409
1988
  }
1410
1989
  return await import(resolved);
1411
1990
  }
1412
- async function hashIfExists(path) {
1991
+ async function safeStat(path) {
1413
1992
  try {
1414
- return await sha256(path);
1993
+ const info = await stat(path);
1994
+ return { isDirectory: info.isDirectory(), isFile: info.isFile() };
1415
1995
  }
1416
1996
  catch {
1417
- return undefined;
1997
+ return null;
1418
1998
  }
1419
1999
  }
1420
- async function sha256(path) {
1421
- const content = await readFile(path);
1422
- return `sha256:${createHash("sha256").update(content).digest("hex")}`;
1423
- }
1424
- async function exists(path) {
1425
- try {
1426
- await stat(path);
1427
- return true;
1428
- }
1429
- catch {
1430
- return false;
1431
- }
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");
1432
2008
  }
1433
- async function safeReadDir(path) {
2009
+ async function updateGitSubmodule(target, sourcePath) {
1434
2010
  try {
1435
- 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 };
1436
2016
  }
1437
- catch {
1438
- 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}` };
1439
2020
  }
1440
2021
  }
1441
- function safeFileName(value) {
1442
- return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "source";
1443
- }
1444
- function isMigrationLaneCandidate(value, staleNames) {
1445
- return (staleNames.has(value) ||
1446
- /^v?\d+(?:[._-]\d+)*$/i.test(value) ||
1447
- /^(sprint|iteration|release)[-_]?\d*/i.test(value));
1448
- }
1449
- function resolveAuthStatePath(target, config, authState) {
1450
- const statePath = resolve(target, authState);
1451
- const sharedAuthRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
1452
- const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
1453
- const pathParts = statePath.split(/[\\/]/);
1454
- const modulePrivate = isInside(modulesRoot, statePath) && pathParts.includes("_playwright") && pathParts.includes(".auth");
1455
- if (isInside(sharedAuthRoot, statePath) || modulePrivate)
1456
- return statePath;
1457
- throw new Error(`Playwright storageState input must stay under ${sharedAuthRoot} or a module _playwright/.auth directory`);
1458
- }
1459
2022
  function resolveSelectedAuthStatePath(target, config, opts) {
1460
2023
  if (opts.authState)
1461
- return resolveAuthStatePath(target, config, opts.authState);
2024
+ return resolvePlaywrightAuthStatePath(target, config, opts.authState);
1462
2025
  if (opts.authProfile)
1463
- return resolveAuthProfilePath(target, config, opts.authProfile);
2026
+ return resolvePlaywrightAuthProfilePath(target, config, opts.authProfile);
1464
2027
  return undefined;
1465
2028
  }
1466
- function resolveAuthProfilePath(target, config, profile) {
1467
- const authRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
1468
- const fileName = profile ? `${safeFileName(profile)}.json` : "user_data.json";
1469
- return join(authRoot, fileName);
1470
- }
1471
2029
  function resolveAdapterLocalFilePath(target, value) {
1472
2030
  const path = resolve(target, value);
1473
- return isInside(resolve(target), path) ? path : undefined;
1474
- }
1475
- function isInside(root, target) {
1476
- const rel = relative(root, target);
1477
- return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
1478
- }
1479
- export function createSyncRunId(now = new Date()) {
1480
- return now.toISOString().replace(/[:.]/g, "-");
2031
+ return isPathInside(resolve(target), path) ? path : undefined;
1481
2032
  }
1482
2033
  //# sourceMappingURL=prepare-sync.js.map