@sequenceholdings/artifact-studio 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION-PIN.md CHANGED
@@ -4,6 +4,12 @@ A (re)built Artifact Studio app gets whatever `@sequenceholdings/atlas-ui` the
4
4
  **building CLI** carries — not a semver the artifact's own `package.json`
5
5
  declares.
6
6
 
7
+ Artifact Studio 0.2.4 (Studio CLI 0.1.30) pins Atlas UI 3.1.1 exactly.
8
+ Ordinary builds now use its canonical, mode-aware `tokens.css`. Rebuilding an
9
+ existing artifact adopts this toolchain; previously uploaded immutable bundles
10
+ keep their original CSS. Migrate retired numbered black/white utilities to
11
+ semantic roles and verify a preview before activating a rebuilt artifact.
12
+
7
13
  ## What this means
8
14
 
9
15
  `shared/services/artifact-studio/src/build.ts` force-aliases platform peers via
@@ -43,12 +49,16 @@ source (and older rows with null peer fields are rebuilt once to populate them).
43
49
  `seq-studio artifact plan` reports the same decision, so its `action:` line
44
50
  never claims `no-op` for a deploy that would actually rebuild.
45
51
 
46
- ## V2 CSS sheets (sandbox previews)
47
-
48
- `seq-studio artifact build` idempotently injects `tokens.v2.css` + `theme.v2.css`
49
- when the artifact CSS entry lacks them (iframes do not inherit Atlas CSSOM; the
50
- host only stamps `data-tokens` / brand vars). Keep listing `tokens.css` for the
51
- ungated standalone/`vite dev` fallback. **Do not** import `tokens.v2.tenants.css`.
52
+ ## CSS entry points (sandbox previews)
52
53
 
53
- **DES-227 cutover:** remove the build inject; make `tokens.css` the v2 default;
54
- optionally sweep hand-written `@import …/tokens.v2.css` / `theme.v2.css` lines.
54
+ `tokens.css` is the canonical stylesheet and includes semantic roles plus the
55
+ Tailwind utility bridge. The legacy `tokens.v2.css` and `theme.v2.css` paths are
56
+ temporary forwarding aliases; the compiler normalizes their source imports to one
57
+ canonical layer and reports a migration warning. An explicit
58
+ `seq-studio artifact dev --link <atlas-ui-package-dir>` preview additionally
59
+ reports retired numbered black/white opacity utilities and uncertain dynamic
60
+ class construction. Legacy stylesheet aliases normalize to canonical tokens;
61
+ they do not restore retired utility scales. The runtime forwards brand variables
62
+ independently of `data-tokens`.
63
+ Import `tokens.css` for standalone/`vite dev` compatibility. **Do not** import
64
+ `tokens.v2.tenants.css`.
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Official (active-channel) Artifact Studio deploys must come from a **clean
3
- * `main` checkout with a resolved commit** — whether the source is a platform
4
- * git-service repo (`--repo`) or a monorepo / local checkout. Preview deploys
5
- * may use feature branches, dirty trees, or other refs.
2
+ * Official (active-channel) Artifact Studio deploys must come from `main` with
3
+ * a resolved commit and a clean artifact source directory — whether the source
4
+ * is a platform git-service repo (`--repo`) or a monorepo / local checkout.
5
+ * Preview deploys may use feature branches, dirty trees, or other refs.
6
6
  *
7
7
  * This checks reported provenance (branch name + explicit clean flag + commit).
8
8
  * It does **not** independently verify that `gitCommit` equals the remote
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Official (active-channel) Artifact Studio deploys must come from a **clean
3
- * `main` checkout with a resolved commit** — whether the source is a platform
4
- * git-service repo (`--repo`) or a monorepo / local checkout. Preview deploys
5
- * may use feature branches, dirty trees, or other refs.
2
+ * Official (active-channel) Artifact Studio deploys must come from `main` with
3
+ * a resolved commit and a clean artifact source directory — whether the source
4
+ * is a platform git-service repo (`--repo`) or a monorepo / local checkout.
5
+ * Preview deploys may use feature branches, dirty trees, or other refs.
6
6
  *
7
7
  * This checks reported provenance (branch name + explicit clean flag + commit).
8
8
  * It does **not** independently verify that `gitCommit` equals the remote
@@ -41,9 +41,12 @@ export function isActiveDeployDefaultBranch(branch) {
41
41
  * Preview callers must not invoke this.
42
42
  */
43
43
  export function assertActiveDeployProvenance(provenance) {
44
- // Require an explicit clean recording — omitting gitDirty must not pass.
44
+ // Require an explicit clean source-directory recording — omitting gitDirty
45
+ // must not pass. The local source resolver scopes this value to the
46
+ // artifact directory, so unrelated changes elsewhere in a containing
47
+ // checkout do not block an official deploy.
45
48
  if (provenance.gitDirty !== false) {
46
- throw new ActiveDeployPolicyError('Active (official) artifact deploys require a clean git tree ' +
49
+ throw new ActiveDeployPolicyError('Active (official) artifact deploys require a clean artifact source tree ' +
47
50
  '(`gitDirty: false`). Commit or stash your changes, or use ' +
48
51
  '`seq-studio artifact dev` / a preview deploy for dirty feature-branch work.');
49
52
  }
@@ -1,15 +1,36 @@
1
1
  import type { Plugin } from 'vite';
2
+ export interface AtlasUiResolvedTokenSelection {
3
+ specifier: '@sequenceholdings/atlas-ui/tokens.css';
4
+ injectedBy: string | null;
5
+ }
2
6
  /**
3
- * Idempotent v2 token sheets for Artifact Studio builds. Sandbox iframes do not
4
- * inherit Atlas CSSOM; the host stamps `data-tokens` (floors to v2) but roles
5
- * only resolve if the bundle includes these sheets. Skip when the entry already
6
- * imports them. Remove this inject at DES-227 cutover when `tokens.css` *is* v2.
7
+ * Every supported source shape becomes one canonical import before Tailwind
8
+ * expands it. The aliases remain package-compatible, but are no longer allowed
9
+ * to create duplicate token layers in an Artifact Studio bundle (DES-437).
7
10
  */
8
- export declare function cssAlreadyImportsSheet(code: string, sheetFileName: string): boolean;
9
- /** Absolute or package `@import` lines to prepend when the CSS entry lacks v2 sheets. */
10
- export declare function planAtlasUiV2CssImports(code: string, _atlasUiDir: string): string[];
11
- export declare function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRendererDist, }: {
11
+ export declare function normalizeAtlasUiTokenImports(code: string): {
12
+ code: string;
13
+ deprecatedImports: string[];
14
+ };
15
+ /**
16
+ * Evidence mirrors the CSS transform: aliases are normalized away, so the only
17
+ * selected sheet is the canonical token entry point.
18
+ */
19
+ export declare function collectAtlasUiResolvedTokenSelections({ cssModules, }: {
20
+ cssModules: readonly {
21
+ id: string;
22
+ code: string;
23
+ }[];
24
+ atlasUiDir: string;
25
+ atlasUiDist: string;
26
+ formRendererDist: string | null;
27
+ }): AtlasUiResolvedTokenSelection[];
28
+ export declare function atlasUiCssResolverPlugin({ atlasUiDir: _atlasUiDir, atlasUiDist, formRendererDist, onTransform, }: {
12
29
  atlasUiDir: string;
13
30
  atlasUiDist: string;
14
31
  formRendererDist: string | null;
32
+ onTransform?: (cssModule: {
33
+ id: string;
34
+ code: string;
35
+ }) => void;
15
36
  }): Plugin;
@@ -17,36 +17,67 @@ function resolveTwAnimateCssPath() {
17
17
  }
18
18
  return resolved;
19
19
  }
20
+ const CANONICAL_TOKEN_IMPORT = '@import "@sequenceholdings/atlas-ui/tokens.css";';
21
+ const ATLAS_UI_TOKEN_IMPORT = /@import\s+(?:url\(\s*)?(?:["'][^"']*?@sequenceholdings\/atlas-ui\/|[^"'()\s;]*@sequenceholdings\/atlas-ui\/)(tokens\.css|tokens\.v2\.css|theme\.v2\.css)(?:["'])?\s*\)?[^;\n]*;/gi;
22
+ function atlasUiTokenImportSheets(code) {
23
+ return [...code.matchAll(ATLAS_UI_TOKEN_IMPORT)]
24
+ .map((match) => match[1]?.toLowerCase())
25
+ .filter((sheet) => sheet !== undefined);
26
+ }
20
27
  /**
21
- * Idempotent v2 token sheets for Artifact Studio builds. Sandbox iframes do not
22
- * inherit Atlas CSSOM; the host stamps `data-tokens` (floors to v2) but roles
23
- * only resolve if the bundle includes these sheets. Skip when the entry already
24
- * imports them. Remove this inject at DES-227 cutover when `tokens.css` *is* v2.
28
+ * Every supported source shape becomes one canonical import before Tailwind
29
+ * expands it. The aliases remain package-compatible, but are no longer allowed
30
+ * to create duplicate token layers in an Artifact Studio bundle (DES-437).
25
31
  */
26
- export function cssAlreadyImportsSheet(code, sheetFileName) {
27
- return code.includes(sheetFileName);
32
+ export function normalizeAtlasUiTokenImports(code) {
33
+ const deprecatedImports = new Set();
34
+ const withoutTokenImports = code.replace(ATLAS_UI_TOKEN_IMPORT, (importLine, sheet) => {
35
+ const normalizedSheet = String(sheet).toLowerCase();
36
+ if (normalizedSheet !== 'tokens.css')
37
+ deprecatedImports.add(normalizedSheet);
38
+ return '';
39
+ });
40
+ return {
41
+ code: insertAfterImports(withoutTokenImports, [CANONICAL_TOKEN_IMPORT]),
42
+ deprecatedImports: [...deprecatedImports],
43
+ };
28
44
  }
29
- /** Absolute or package `@import` lines to prepend when the CSS entry lacks v2 sheets. */
30
- export function planAtlasUiV2CssImports(code, _atlasUiDir) {
31
- const imports = [];
32
- // Package subpaths — resolved via Vite alias (PEER_RESOLVE_ALIAS). Prefer
33
- // package form over absolute paths so Tailwind/Vite follow the same path as
34
- // hand-written author imports.
35
- if (!cssAlreadyImportsSheet(code, 'tokens.v2.css')) {
36
- imports.push('@import "@sequenceholdings/atlas-ui/tokens.v2.css";');
45
+ /**
46
+ * Evidence mirrors the CSS transform: aliases are normalized away, so the only
47
+ * selected sheet is the canonical token entry point.
48
+ */
49
+ export function collectAtlasUiResolvedTokenSelections({ cssModules, }) {
50
+ for (const { code } of cssModules) {
51
+ if (!code.includes('@import "tailwindcss"'))
52
+ continue;
53
+ return [{
54
+ specifier: '@sequenceholdings/atlas-ui/tokens.css',
55
+ injectedBy: atlasUiTokenImportSheets(code).includes('tokens.css') ? null : 'atlas-ui-css-resolver',
56
+ }];
37
57
  }
38
- if (!cssAlreadyImportsSheet(code, 'theme.v2.css')) {
39
- imports.push('@import "@sequenceholdings/atlas-ui/theme.v2.css";');
58
+ return [];
59
+ }
60
+ /** Put Tailwind source directives after the import block but before style rules. */
61
+ function insertAfterImports(code, lines) {
62
+ const importPattern = /@import\s+[^;]+;/gi;
63
+ let lastImport = null;
64
+ for (let match = importPattern.exec(code); match; match = importPattern.exec(code)) {
65
+ lastImport = match;
40
66
  }
41
- return imports;
67
+ const block = `\n${lines.join('\n')}`;
68
+ if (lastImport?.index === undefined)
69
+ return `${lines.join('\n')}\n${code}`;
70
+ const end = lastImport.index + lastImport[0].length;
71
+ return `${code.slice(0, end)}${block}${code.slice(end)}`;
42
72
  }
43
- export function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRendererDist, }) {
73
+ export function atlasUiCssResolverPlugin({ atlasUiDir: _atlasUiDir, atlasUiDist, formRendererDist, onTransform, }) {
44
74
  return {
45
75
  name: 'atlas-ui-css-resolver',
46
76
  enforce: 'pre',
47
77
  transform(code, id) {
48
78
  if (!id.endsWith('.css'))
49
79
  return undefined;
80
+ onTransform?.({ id, code });
50
81
  let result = code;
51
82
  if (result.includes('@import "tailwindcss"')) {
52
83
  result = result.replace('@import "tailwindcss"', `@import ${JSON.stringify(tailwindCssPath)}`);
@@ -57,8 +88,9 @@ export function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRenderer
57
88
  if (formRendererDist) {
58
89
  sources.push(`@source ${JSON.stringify(formRendererDist)};`);
59
90
  }
60
- const v2Imports = planAtlasUiV2CssImports(result, atlasUiDir);
61
- result = `${sources.join('\n')}\n${v2Imports.join('\n')}${v2Imports.length > 0 ? '\n' : ''}${result}`;
91
+ const tokenImports = normalizeAtlasUiTokenImports(result);
92
+ result = tokenImports.code;
93
+ result = insertAfterImports(result, sources);
62
94
  }
63
95
  if (result.includes('@import "tw-animate-css"')) {
64
96
  result = result.replace('@import "tw-animate-css"', `@import ${JSON.stringify(twAnimateCssPath)}`);
@@ -0,0 +1,7 @@
1
+ export declare class AtlasUiPackageContractError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare function validateAtlasUiTokenStylesheetContract({ packageDir, resolvedTokensCssPath, }: {
5
+ packageDir: string;
6
+ resolvedTokensCssPath: string;
7
+ }): string;
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ const ATLAS_UI_PACKAGE = '@sequenceholdings/atlas-ui';
4
+ const TOKENS_CSS_EXPORT = './tokens.css';
5
+ export class AtlasUiPackageContractError extends Error {
6
+ constructor(message) {
7
+ super(`Atlas UI package contract error: ${message}`);
8
+ this.name = 'AtlasUiPackageContractError';
9
+ }
10
+ }
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function readExportTarget(value) {
15
+ if (typeof value === 'string')
16
+ return value;
17
+ if (isRecord(value)) {
18
+ for (const key of ['style', 'default', 'import']) {
19
+ const target = value[key];
20
+ if (typeof target === 'string')
21
+ return target;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ export function validateAtlasUiTokenStylesheetContract({ packageDir, resolvedTokensCssPath, }) {
27
+ const packageJsonPath = resolve(packageDir, 'package.json');
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
31
+ }
32
+ catch (error) {
33
+ throw new AtlasUiPackageContractError(`unable to read ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
34
+ }
35
+ if (!isRecord(parsed)) {
36
+ throw new AtlasUiPackageContractError(`${packageJsonPath} is not a JSON object`);
37
+ }
38
+ if (parsed.name !== ATLAS_UI_PACKAGE) {
39
+ throw new AtlasUiPackageContractError(`${packageJsonPath} must be ${ATLAS_UI_PACKAGE}, got ${String(parsed.name)}`);
40
+ }
41
+ const exportsMap = parsed.exports;
42
+ if (!isRecord(exportsMap)) {
43
+ throw new AtlasUiPackageContractError(`${ATLAS_UI_PACKAGE} package.json must declare an exports map`);
44
+ }
45
+ const exportTarget = readExportTarget(exportsMap[TOKENS_CSS_EXPORT]);
46
+ if (!exportTarget) {
47
+ throw new AtlasUiPackageContractError(`${ATLAS_UI_PACKAGE} must export ${TOKENS_CSS_EXPORT} to a CSS file included in the package`);
48
+ }
49
+ const declaredPath = resolve(packageDir, exportTarget);
50
+ if (!existsSync(declaredPath)) {
51
+ throw new AtlasUiPackageContractError(`${ATLAS_UI_PACKAGE}${TOKENS_CSS_EXPORT} points to ${exportTarget}, but ${declaredPath} does not exist`);
52
+ }
53
+ if (!existsSync(resolvedTokensCssPath)) {
54
+ throw new AtlasUiPackageContractError(`${ATLAS_UI_PACKAGE}${TOKENS_CSS_EXPORT} resolved to ${resolvedTokensCssPath}, but that file does not exist`);
55
+ }
56
+ if (resolve(resolvedTokensCssPath) !== declaredPath) {
57
+ throw new AtlasUiPackageContractError(`${ATLAS_UI_PACKAGE}${TOKENS_CSS_EXPORT} export target (${declaredPath}) and build resolver (${resolvedTokensCssPath}) disagree`);
58
+ }
59
+ return declaredPath;
60
+ }
@@ -0,0 +1,55 @@
1
+ import type { ResolvedStylesheet } from './resolved-stylesheets.js';
2
+ import type { ArtifactStudioPeerVersions } from './peer-versions.js';
3
+ import type { AtlasUiResolvedTokenSelection } from './atlas-ui-css-plugin.js';
4
+ export declare const ARTIFACT_BUILD_PROVENANCE_VERSION = 1;
5
+ export interface ArtifactSourceRevision {
6
+ commit: string | null;
7
+ branch: string | null;
8
+ dirty: boolean | null;
9
+ }
10
+ export interface ArtifactTokenResolution {
11
+ specifier: string;
12
+ resolvedPath: string;
13
+ contentHash: string;
14
+ cssLayers: string[];
15
+ injectedBy: string | null;
16
+ }
17
+ /** Observed local build evidence; values are not independently verified by Atlas. */
18
+ export interface ObservedBuildEvidence {
19
+ schemaVersion: typeof ARTIFACT_BUILD_PROVENANCE_VERSION;
20
+ channel: 'active' | 'preview';
21
+ /** `cliVersion` is retained for historical records and identifies Artifact Studio. */
22
+ toolchain: {
23
+ cliVersion: string;
24
+ selection: 'published' | 'linked';
25
+ };
26
+ atlasUi: {
27
+ version: string;
28
+ contentHash: string;
29
+ };
30
+ tokenResolution: {
31
+ modelVersion: 1;
32
+ entries: ArtifactTokenResolution[];
33
+ };
34
+ /** CSS modules that actually contribute to this artifact's visible cascade. */
35
+ stylesheets: ResolvedStylesheet[];
36
+ sourceRevision: ArtifactSourceRevision;
37
+ }
38
+ /**
39
+ * Evidence from the exact Atlas UI alias targets selected by this build. The
40
+ * format deliberately records paths and resolved layers, rather than naming a
41
+ * migration generation; future canonical token entry points need no schema
42
+ * change.
43
+ */
44
+ export declare function createObservedBuildEvidence({ atlasUiDir, atlasUiDist, atlasUiTokens, channel, linked, peerVersions, sourceRevision, stylesheets, tokenSelections }: {
45
+ atlasUiDir: string;
46
+ atlasUiDist: string;
47
+ /** Exact tokens.css alias target; releases and linked candidates use different layouts. */
48
+ atlasUiTokens: string;
49
+ channel: ObservedBuildEvidence['channel'];
50
+ linked: boolean;
51
+ peerVersions: ArtifactStudioPeerVersions;
52
+ sourceRevision: ArtifactSourceRevision;
53
+ stylesheets?: ResolvedStylesheet[];
54
+ tokenSelections: readonly AtlasUiResolvedTokenSelection[];
55
+ }): ObservedBuildEvidence;
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { relative } from 'node:path';
3
+ import { sha256Hex } from './hash.js';
4
+ export const ARTIFACT_BUILD_PROVENANCE_VERSION = 1;
5
+ function cssLayers(content) {
6
+ return [...content.matchAll(/@layer\s+([\w-]+)/g)].map((match) => match[1]).filter((x) => x !== undefined);
7
+ }
8
+ function contentIdentity(entries) {
9
+ return sha256Hex(entries.slice().sort((a, b) => a.path.localeCompare(b.path)).map((entry) => `${entry.path}\0${entry.contentHash}`).join('\0'));
10
+ }
11
+ /**
12
+ * Evidence from the exact Atlas UI alias targets selected by this build. The
13
+ * format deliberately records paths and resolved layers, rather than naming a
14
+ * migration generation; future canonical token entry points need no schema
15
+ * change.
16
+ */
17
+ export function createObservedBuildEvidence({ atlasUiDir, atlasUiDist, atlasUiTokens, channel, linked, peerVersions, sourceRevision, stylesheets = [], tokenSelections }) {
18
+ const knownTargets = new Map([
19
+ ['@sequenceholdings/atlas-ui/tokens.css', { filePath: atlasUiTokens }],
20
+ ['@sequenceholdings/atlas-ui/tokens.v2.css', { filePath: `${atlasUiDir}/tokens.v2.css` }],
21
+ ['@sequenceholdings/atlas-ui/theme.v2.css', { filePath: `${atlasUiDir}/theme.v2.css` }],
22
+ ]);
23
+ const entries = tokenSelections.map((target) => {
24
+ const resolved = knownTargets.get(target.specifier);
25
+ if (!resolved)
26
+ throw new Error(`Unknown Atlas UI token target: ${target.specifier}`);
27
+ const filePath = resolved.filePath;
28
+ const content = readFileSync(filePath, 'utf8');
29
+ return { specifier: target.specifier, resolvedPath: relative(atlasUiDir, filePath), contentHash: sha256Hex(content), cssLayers: cssLayers(content), injectedBy: target.injectedBy };
30
+ });
31
+ const identityEntries = [
32
+ ...entries.map((entry) => ({ path: entry.resolvedPath, contentHash: entry.contentHash })),
33
+ { path: 'dist/index.mjs', contentHash: sha256Hex(readFileSync(`${atlasUiDist}/index.mjs`)) },
34
+ { path: 'dist/charts.mjs', contentHash: sha256Hex(readFileSync(`${atlasUiDist}/charts.mjs`)) },
35
+ ];
36
+ return {
37
+ schemaVersion: ARTIFACT_BUILD_PROVENANCE_VERSION,
38
+ channel,
39
+ toolchain: { cliVersion: peerVersions.cliVersion, selection: linked ? 'linked' : 'published' },
40
+ atlasUi: { version: peerVersions.atlasUiVersion, contentHash: contentIdentity(identityEntries) },
41
+ tokenResolution: { modelVersion: 1, entries },
42
+ stylesheets,
43
+ sourceRevision,
44
+ };
45
+ }
@@ -6,6 +6,8 @@ export interface IsolatedBuildOptions {
6
6
  signal?: AbortSignal;
7
7
  /** Parent environment override for deterministic security tests. */
8
8
  sourceEnv?: NodeJS.ProcessEnv;
9
+ /** Explicitly report canonical-token compatibility for an Atlas UI --link preview. */
10
+ candidateTokenCompatibility?: boolean;
9
11
  }
10
12
  /**
11
13
  * Run a single artifact build in a short-lived child process and return its
@@ -14,4 +16,4 @@ export interface IsolatedBuildOptions {
14
16
  * long-running watch loops; one-shot commands can call
15
17
  * `buildArtifactStudioProject` directly.
16
18
  */
17
- export declare function buildArtifactStudioProjectIsolated(dir: string, { workerPath, signal, sourceEnv }?: IsolatedBuildOptions): Promise<ArtifactStudioBuildResult>;
19
+ export declare function buildArtifactStudioProjectIsolated(dir: string, { workerPath, signal, sourceEnv, candidateTokenCompatibility }?: IsolatedBuildOptions): Promise<ArtifactStudioBuildResult>;
@@ -17,7 +17,7 @@ const DEFAULT_WORKER_PATH = existsSync(BUILT_WORKER_PATH)
17
17
  * long-running watch loops; one-shot commands can call
18
18
  * `buildArtifactStudioProject` directly.
19
19
  */
20
- export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEFAULT_WORKER_PATH, signal, sourceEnv } = {}) {
20
+ export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEFAULT_WORKER_PATH, signal, sourceEnv, candidateTokenCompatibility = false } = {}) {
21
21
  const scratch = await mkdtemp(join(tmpdir(), 'artifact-build-'));
22
22
  const outFile = join(scratch, 'result.json');
23
23
  const homeDir = join(scratch, 'home');
@@ -29,6 +29,7 @@ export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEF
29
29
  outFile,
30
30
  homeDir,
31
31
  signal,
32
+ candidateTokenCompatibility,
32
33
  ...(sourceEnv ? { sourceEnv } : {}),
33
34
  });
34
35
  return JSON.parse(await readFile(outFile, 'utf8'));
@@ -37,7 +38,7 @@ export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEF
37
38
  await rm(scratch, { recursive: true, force: true });
38
39
  }
39
40
  }
40
- function runWorker({ workerPath, dir, outFile, homeDir, signal, sourceEnv, }) {
41
+ function runWorker({ workerPath, dir, outFile, homeDir, signal, sourceEnv, candidateTokenCompatibility, }) {
41
42
  return new Promise((resolve, reject) => {
42
43
  if (signal?.aborted) {
43
44
  reject(new Error('build aborted'));
@@ -46,6 +47,8 @@ function runWorker({ workerPath, dir, outFile, homeDir, signal, sourceEnv, }) {
46
47
  const workerArgs = workerPath.endsWith('.ts')
47
48
  ? ['--import', 'tsx', workerPath, dir, outFile]
48
49
  : [workerPath, dir, outFile];
50
+ if (candidateTokenCompatibility)
51
+ workerArgs.push('--candidate-token-compatibility');
49
52
  const child = spawn(process.execPath, workerArgs, {
50
53
  stdio: ['ignore', 'inherit', 'pipe'],
51
54
  env: createScrubbedChildEnv({
@@ -13,13 +13,15 @@
13
13
  import { writeFile } from 'node:fs/promises';
14
14
  import { buildArtifactStudioProject } from './build.js';
15
15
  async function main() {
16
- const [dir, outFile] = process.argv.slice(2);
16
+ const [dir, outFile, candidateFlag] = process.argv.slice(2);
17
17
  if (!dir || !outFile) {
18
18
  process.stderr.write('usage: build-worker <projectDir> <outFile>\n');
19
19
  process.exitCode = 2;
20
20
  return;
21
21
  }
22
- const result = await buildArtifactStudioProject(dir);
22
+ const result = await buildArtifactStudioProject(dir, {
23
+ candidateTokenCompatibility: candidateFlag === '--candidate-token-compatibility',
24
+ });
23
25
  await writeFile(outFile, JSON.stringify(result), 'utf8');
24
26
  }
25
27
  main().catch((error) => {
package/dist/build.d.ts CHANGED
@@ -1,20 +1,14 @@
1
+ import { type ObservedBuildEvidence } from './build-provenance.js';
1
2
  import { type ArtifactStudioSourceFile } from './project.js';
2
3
  import type { ArtifactStudioManifest } from './manifest.js';
3
4
  import { type ArtifactStudioPeerVersions } from './peer-versions.js';
4
5
  /**
5
- * Import aliases for the Vite build. Artifacts are built against their own
6
- * source tree, which usually has no installed node_modules, so every bare
7
- * import the scaffold relies on is aliased to this package's pinned copy.
8
- * This also pins a single instance of each library — one React, one
9
- * react-query QueryClient context — so a duplicate copy can't silently break
10
- * hooks or the provider/consumer link. Exported so it can be regression-tested
11
- * (a missing/unresolvable peer must fail loudly).
12
- *
13
- * **CLI atlas-ui stamp (DES-254):** the atlas-ui a bundle gets is whatever
14
- * *this* CLI package resolves — not a semver the artifact declares.
15
- * See `VERSION-PIN.md`.
6
+ * Released Atlas UI 2.0.1 exports its canonical sheet from dist, while the
7
+ * candidate layout puts the generated entry at package root so it can be read
8
+ * before a workspace build. Accept both: ordinary CLI builds retain their
9
+ * released pin, and explicit linked previews prefer the synced root entry.
16
10
  */
17
- /** Platform + common artifact runtime peers — always aliased over node_modules. */
11
+ export declare function resolveAtlasUiTokensPath(packageDir: string): string;
18
12
  export declare const PEER_RESOLVE_ALIAS: Record<string, string>;
19
13
  export interface ArtifactStudioRuntimeChunk {
20
14
  /** Vite/Rollup output path, always under `chunks/`. */
@@ -39,8 +33,16 @@ export interface ArtifactStudioBuildResult {
39
33
  bundleHash: string;
40
34
  /** CLI + force-aliased atlas-ui versions that built this bundle (DES-254). */
41
35
  peerVersions: ArtifactStudioPeerVersions;
36
+ /** Exact CLI-resolved UI/token evidence; deployment adds source revision + channel. */
37
+ observedBuildEvidence: Omit<ObservedBuildEvidence, 'channel' | 'sourceRevision'>;
38
+ /** Source diagnostics that do not prevent a build. */
39
+ diagnostics?: string[];
42
40
  }
43
- export declare function buildArtifactStudioProject(rootDir: string): Promise<ArtifactStudioBuildResult>;
41
+ export declare function buildArtifactStudioProject(rootDir: string, { candidateTokenCompatibility, atlasUiPackageDir, }?: {
42
+ candidateTokenCompatibility?: boolean;
43
+ /** Explicit linked Atlas UI package, used by candidate toolchain hosts. */
44
+ atlasUiPackageDir?: string;
45
+ }): Promise<ArtifactStudioBuildResult>;
44
46
  export declare function writeBuildArtifact(result: ArtifactStudioBuildResult, outPath: string): Promise<void>;
45
47
  export declare function hashBundleAndChunks({ bundle, chunks, }: {
46
48
  bundle: string;