@reqvire-org/ads 0.3.10 → 0.3.12
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/dist/mcp/build-status.mjs +14 -4
- package/dist/mcp/catalog.mjs +3 -3
- package/dist/mcp/mutations.mjs +1 -1
- package/dist/mcp/preview-watch.mjs +7 -5
- package/dist/mcp/server.mjs +13 -10
- package/dist/preview-inputs.mjs +24 -0
- package/dist/project-documents.mjs +29 -6
- package/dist/scenarios-compiler.mjs +26 -20
- package/dist/showcase/client.bundle.js +17 -17
- package/dist/skills/ads-break-ui/SKILL.md +6 -4
- package/dist/skills/ads-break-ui/references/guarded-editing.md +10 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +32 -0
- package/dist/skills/ads-designer/SKILL.md +6 -4
- package/dist/skills/ads-designer/references/guarded-editing.md +10 -0
- package/dist/skills/ads-designer/references/scenarios.md +32 -0
- package/dist/skills/ads-import/SKILL.md +6 -4
- package/dist/skills/ads-import/references/guarded-editing.md +10 -0
- package/dist/skills/ads-import/references/implementation.md +3 -1
- package/dist/skills/ads-import/references/scenarios.md +32 -0
- package/dist/skills/ads-migrate/SKILL.md +6 -4
- package/dist/skills/ads-migrate/references/guarded-editing.md +10 -0
- package/dist/skills/ads-migrate/references/scenarios.md +32 -0
- package/dist/skills/ads-migrate/references/testing.md +5 -0
- package/dist/skills/ads-motion/SKILL.md +6 -4
- package/dist/skills/ads-motion/references/guarded-editing.md +10 -0
- package/dist/skills/ads-motion/references/scenarios.md +32 -0
- package/dist/units.mjs +7 -6
- package/dist/verification.mjs +2 -2
- package/docs/ADS.md +4 -0
- package/docs/ARCHITECTURE.md +15 -4
- package/docs/CHANGELOG.md +24 -0
- package/docs/MCP.md +34 -2
- package/package.json +1 -1
|
@@ -2,7 +2,8 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { isInside, projectPath } from "../config.mjs";
|
|
5
|
+
import { isInside, projectPath, loadConfig } from "../config.mjs";
|
|
6
|
+
import { previewSourceRevision } from "../preview-inputs.mjs";
|
|
6
7
|
import { readPreviewManifest } from "../preview-resources.mjs";
|
|
7
8
|
import { previewArtifactPath, withPreviewSession } from "../preview-scope.mjs";
|
|
8
9
|
|
|
@@ -10,7 +11,7 @@ export const BUILD_URI = "ads://project/build";
|
|
|
10
11
|
export const buildStatusFile = (root) => path.join(root, ".ds/build-status.json");
|
|
11
12
|
const maxBytes = 256 * 1024;
|
|
12
13
|
const diagnostic = z.object({ message: z.string().max(4000), file: z.string().max(1000).optional(), line: z.number().int().positive().optional(), column: z.number().int().positive().optional(), plugin: z.string().max(200).optional(), code: z.string().max(100).optional() }).strict();
|
|
13
|
-
const preview = z.object({ id: z.string().max(256), state: z.enum(["idle", "building", "ready", "error"]), revision: z.number().int().nonnegative(), lastSuccessfulRevision: z.number().int().positive().nullable(), artifactDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(), diagnostics: z.array(diagnostic).max(20), diagnosticSet: z.number().int().nonnegative().optional() }).strict();
|
|
14
|
+
const preview = z.object({ id: z.string().max(256), state: z.enum(["idle", "building", "ready", "error"]), revision: z.number().int().nonnegative(), lastSuccessfulRevision: z.number().int().positive().nullable(), artifactDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(), sourceRevision:z.string().regex(/^[a-f0-9]{64}$/).optional(), diagnostics: z.array(diagnostic).max(20), diagnosticSet: z.number().int().nonnegative().optional() }).strict();
|
|
14
15
|
const progressSchema = z.object({ phase: z.enum(["queued", "validation", "compilation", "validation-and-compilation", "publication", "idle"]), startedAt: z.string().datetime(), updatedAt: z.string().datetime() }).strict();
|
|
15
16
|
const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), progress: progressSchema.optional(), previews: z.array(preview), diagnostics: z.array(diagnostic).max(20).optional(), diagnosticSets: z.array(z.array(diagnostic).max(20)).optional() }).strict().refine(value => value.previews.every(entry => entry.diagnosticSet === undefined || value.diagnosticSets?.[entry.diagnosticSet]), "Invalid shared diagnostic reference.");
|
|
16
17
|
export const artifactDigest = (file) => {
|
|
@@ -93,8 +94,9 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
93
94
|
apps = next;
|
|
94
95
|
save();
|
|
95
96
|
},
|
|
96
|
-
begin(id) {
|
|
97
|
+
begin(id,{sourceRevision = previewSourceRevision(loadConfig(root),apps)} = {}) {
|
|
97
98
|
const value = entry(id);
|
|
99
|
+
value.sourceRevision = sourceRevision;
|
|
98
100
|
value.state = "building";
|
|
99
101
|
value.revision++;
|
|
100
102
|
value.diagnostics = [];
|
|
@@ -137,6 +139,11 @@ export function inspectBuildStatus(root, config, { session, previewSession, proc
|
|
|
137
139
|
}
|
|
138
140
|
} catch (error) { reportError = `Invalid preview build status: ${error.message.slice(0, 500)}`; }
|
|
139
141
|
let graphError;
|
|
142
|
+
let sourceRevision, sourceError;
|
|
143
|
+
if (report?.previews.some(entry=>entry.state==="ready")) {
|
|
144
|
+
try { sourceRevision = previewSourceRevision(loadConfig(root),config.apps); }
|
|
145
|
+
catch (error) { sourceError = `Cannot verify preview inputs: ${error.message}`; }
|
|
146
|
+
}
|
|
140
147
|
if (config.previewManifest && report?.previews.some(entry => entry.state === "ready")) {
|
|
141
148
|
try {
|
|
142
149
|
if (!readPreviewManifest(options, { verify: true })) throw new Error("Preview resource manifest is missing.");
|
|
@@ -154,13 +161,16 @@ export function inspectBuildStatus(root, config, { session, previewSession, proc
|
|
|
154
161
|
if (state === "ready") {
|
|
155
162
|
try {
|
|
156
163
|
if (value.lastSuccessfulRevision !== value.revision || artifactDigest(artifact) !== value.artifactDigest) throw new Error("Preview output does not match its successful build. Rebuild before reviewing it.");
|
|
164
|
+
if (sourceError) throw new Error(sourceError);
|
|
165
|
+
if (!value.sourceRevision) throw new Error("Preview builder did not record its source revision. Rebuild using the current ADS build reporter.");
|
|
166
|
+
if (value.sourceRevision !== sourceRevision) { state = "idle"; diagnostics = [{code:"ADS_PREVIEW_STALE_SOURCE",message:"Project inputs changed after this preview was built. Waiting for a build of the current source."}]; }
|
|
157
167
|
} catch (error) { state = "error"; diagnostics = [{ message: error.message }]; }
|
|
158
168
|
}
|
|
159
169
|
if (processState?.state === "failed") { state = "error"; diagnostics = [{ message: processState.error ?? "Preview watcher exited." }, ...diagnostics].slice(0, 20); }
|
|
160
170
|
if (["stopped", "stopping", "idle"].includes(processState?.state)) state = "unreported";
|
|
161
171
|
const current = state === "ready";
|
|
162
172
|
return { id: app.id, state, session: report?.session ?? null, revision: value?.revision ?? null, lastSuccessfulRevision: value?.lastSuccessfulRevision ?? null,
|
|
163
|
-
current, stale: state === "unreported" ? null : !current, available, diagnostics };
|
|
173
|
+
current, stale: state === "unreported" ? null : !current, available, ...(value?.sourceRevision ? {sourceRevision:value.sourceRevision} : {}), diagnostics };
|
|
164
174
|
});
|
|
165
175
|
const state = reportError || report?.diagnostics?.length || previews.some((entry) => entry.state === "error") ? "error" : previews.some((entry) => entry.state === "building") ? "building" : previews.some((entry) => entry.state === "unreported") ? "unreported" : previews.some(entry => entry.current) ? "ready" : previews.length ? "idle" : "disabled";
|
|
166
176
|
return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, ...(report?.progress ? { progress: report.progress } : {}), previews, ...(report?.diagnostics?.length ? { diagnostics: report.diagnostics } : {}), resourceUri: BUILD_URI };
|
package/dist/mcp/catalog.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import ts from "typescript";
|
|
|
4
4
|
import { isInside, sourceFiles } from "../config.mjs";
|
|
5
5
|
import { projectCompilerOptions } from "../boundaries.mjs";
|
|
6
6
|
import { publicApi } from "../public-api.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { projectDocumentFiles } from "../project-documents.mjs";
|
|
8
8
|
|
|
9
9
|
export const PROJECT_URI = "ads://project";
|
|
10
10
|
export const sourceUri = (file) => `${PROJECT_URI}/files/${file.split("/").map(encodeURIComponent).join("/")}`;
|
|
@@ -21,10 +21,10 @@ export function projectDocuments(options) {
|
|
|
21
21
|
}
|
|
22
22
|
for (const directory of new Set(files.map((file) => path.dirname(file)))) docs(directory);
|
|
23
23
|
docs(dsRoot);
|
|
24
|
-
for (const
|
|
24
|
+
for (const directory of new Set([root,dsRoot])) for (const name of projectDocumentFiles(directory)) files.push(path.join(directory, name));
|
|
25
25
|
return [...new Set(files)].filter((file) => isInside(root, realpathSync(file))).sort().map((file) => {
|
|
26
26
|
const relative = path.relative(root, file).replaceAll("\\", "/");
|
|
27
|
-
return { uri: sourceUri(relative), name: relative, mimeType:
|
|
27
|
+
return { uri: sourceUri(relative), name: relative, mimeType: /\.md$/i.test(file) ? "text/markdown" : file.endsWith(".json") ? "application/json" : "text/plain", file };
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
package/dist/mcp/mutations.mjs
CHANGED
|
@@ -65,7 +65,7 @@ function validateContent(change, unit) {
|
|
|
65
65
|
if (change.content === null) return;
|
|
66
66
|
const bytes = Buffer.from(change.content);
|
|
67
67
|
if (bytes.length > mutationLimits.fileBytes) throw fail("mutation-content", "The patched file exceeds 16 MiB.", change.path);
|
|
68
|
-
if (textFile(change.path) && (bytes.includes(0) || !Buffer.from(bytes.toString("utf8")).equals(bytes))) throw fail("mutation-content", "Text source must contain valid UTF-8 without NUL bytes.", change.path);
|
|
68
|
+
if ((unit === "documentation" || textFile(change.path)) && (bytes.includes(0) || !Buffer.from(bytes.toString("utf8")).equals(bytes))) throw fail("mutation-content", "Text source and documentation must contain valid UTF-8 without NUL bytes.", change.path);
|
|
69
69
|
if (/\.jsonc?$/.test(change.path)) {
|
|
70
70
|
const content=bytes.toString("utf8");
|
|
71
71
|
if (unit === "configuration" && !["package.json","ds.config.json"].includes(path.basename(change.path))) {
|
|
@@ -31,16 +31,17 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
31
31
|
const ready = new Set();
|
|
32
32
|
const requests = new Set();
|
|
33
33
|
let active;
|
|
34
|
+
let invalidation;
|
|
34
35
|
let requestFile = path.join(root, ".ds", `preview-requests-${session}.json`);
|
|
35
36
|
let reportFile = path.join(root, ".ds", `build-status-${session}.json`);
|
|
36
|
-
function request(file) {
|
|
37
|
-
if (!command || requests.has(file) && active === file) return;
|
|
37
|
+
function request(file,force = false) {
|
|
38
|
+
if (!command || !force && requests.has(file) && active === file) return;
|
|
38
39
|
requests.add(file);
|
|
39
40
|
active = file;
|
|
40
41
|
mkdirSync(path.dirname(requestFile), { recursive: true });
|
|
41
42
|
if (realpathSync(path.dirname(requestFile)) !== path.dirname(requestFile)) throw new Error("Preview requests cannot resolve through a symlink.");
|
|
42
43
|
const temporary = `${requestFile}.tmp`;
|
|
43
|
-
writeFileSync(temporary, JSON.stringify({ session, active: path.relative(root, active).replaceAll("\\", "/"), files: [...requests].map((file) => path.relative(root, file).replaceAll("\\", "/")) }));
|
|
44
|
+
writeFileSync(temporary, JSON.stringify({ session, active: path.relative(root, active).replaceAll("\\", "/"), files: [...requests].map((file) => path.relative(root, file).replaceAll("\\", "/")), ...(invalidation ? {invalidation} : {}) }));
|
|
44
45
|
renameSync(temporary, requestFile);
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -135,7 +136,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
135
136
|
if (state === "failed" || restarting || stalled) await restart();
|
|
136
137
|
if (disposed) throw new Error("Preview watcher is stopped.");
|
|
137
138
|
start();
|
|
138
|
-
request(file);
|
|
139
|
+
request(file,buildStatus().previews.some(entry=>entry.diagnostics.some(item=>item.code==="ADS_PREVIEW_STALE_SOURCE")));
|
|
139
140
|
if (signal?.aborted) throw new Error("Preview request cancelled.");
|
|
140
141
|
const app = config.apps.find((entry) => path.resolve(root, entry.file) === file);
|
|
141
142
|
let baseline = baselines.get(file) ?? null;
|
|
@@ -181,7 +182,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
181
182
|
})();
|
|
182
183
|
return stopping;
|
|
183
184
|
}
|
|
184
|
-
return { start, ensure, async stop() { disposed = true; await restarting; return stopChild(); }, kill: () => kill("SIGKILL"), status, resourceSession, buildStatus, setApps(apps) {
|
|
185
|
+
return { start, ensure, invalidate() { invalidation = randomUUID(); if (active) request(active,true); }, async stop() { disposed = true; await restarting; return stopChild(); }, kill: () => kill("SIGKILL"), status, resourceSession, buildStatus, setApps(apps) {
|
|
185
186
|
for (const app of apps) {
|
|
186
187
|
const file = path.resolve(root, app.file);
|
|
187
188
|
if (!baselines.has(file)) baselines.set(file, stamp(file));
|
|
@@ -202,6 +203,7 @@ export function createReconfigurablePreviewWatch(root, config, options) {
|
|
|
202
203
|
status() { return replacing ? { state: "reconfiguring" } : current.status(); },
|
|
203
204
|
resourceSession() { return current.resourceSession(); },
|
|
204
205
|
buildStatus() { return current.buildStatus(); },
|
|
206
|
+
async invalidate() { await pending; if (!stopped) current.invalidate(); },
|
|
205
207
|
setApps(next) { apps = next; current.setApps(next); },
|
|
206
208
|
kill() { current.kill(); },
|
|
207
209
|
reconfigure(next, force = false) {
|
package/dist/mcp/server.mjs
CHANGED
|
@@ -46,6 +46,14 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
46
46
|
cache.registry = cache.catalog = cache.documents = undefined;
|
|
47
47
|
}
|
|
48
48
|
const getShowcaseUrl = () => showcaseUrl?.() ?? options.config.mcp.showcaseUrl;
|
|
49
|
+
function scenarioUrl(id) {
|
|
50
|
+
const base = getShowcaseUrl();
|
|
51
|
+
if (!base) return null;
|
|
52
|
+
const url = new URL(base);
|
|
53
|
+
url.searchParams.delete("category");
|
|
54
|
+
url.searchParams.set("scenario", id);
|
|
55
|
+
return url.href;
|
|
56
|
+
}
|
|
49
57
|
const project = () => { const context = projectContext(options); context.showcase.url = getShowcaseUrl(); return context; };
|
|
50
58
|
const subscribed = new Set();
|
|
51
59
|
const server = new Server({ name: "reqvire-ads", version: pkg.version }, {
|
|
@@ -171,6 +179,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
171
179
|
if (result.applied) {
|
|
172
180
|
try {
|
|
173
181
|
if (result.changes.some(change => change.path === "ds.config.json") || result.unit === "configuration" || result.units?.includes("configuration")) await previewWatch?.reconfigure?.(loadConfig(root).config.mcp,true);
|
|
182
|
+
await previewWatch?.invalidate?.();
|
|
174
183
|
await refresh(true);
|
|
175
184
|
} catch (error) {result.warnings=[...(result.warnings ?? []),{rule:"mutation-notification",message:`Changes were applied, but preview refresh failed: ${error.message.slice(0,2000)}. Recheck ads_status and reopen the scenario.`}];}
|
|
176
185
|
}
|
|
@@ -197,10 +206,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
197
206
|
const build = await previewBuild(app, signal);
|
|
198
207
|
const graph = graphSelection(id);
|
|
199
208
|
if (!options.config.mcp.previewManifest) projectApp(options, app, build);
|
|
200
|
-
|
|
201
|
-
url?.searchParams.delete("category");
|
|
202
|
-
url?.searchParams.set("scenario", id);
|
|
203
|
-
return json({ widget: { resourceUri: scenarioWidget() }, scenario: { ...entry, resourceUri: appUri(id) }, ...graph, build, showcaseUrl: url?.href ?? null });
|
|
209
|
+
return json({ widget: { resourceUri: scenarioWidget() }, scenario: { ...entry, resourceUri: appUri(id) }, ...graph, build, showcaseUrl: scenarioUrl(id) });
|
|
204
210
|
} },
|
|
205
211
|
{ name: "ads_tokens", description: "Use before styling, theme, brand or motion-token changes to inspect existing semantic definitions/values. Query specific token names with a small limit. ads_show_tokens renders project token scenarios; definition inspection does not validate component layout.", schema: query, call: ({ query: search = "", limit }) => {
|
|
206
212
|
const inventory = inspectTokens(root);
|
|
@@ -233,10 +239,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
233
239
|
const build = await previewBuild(app, signal);
|
|
234
240
|
const graph = graphSelection(category);
|
|
235
241
|
if (!options.config.mcp.previewManifest) projectApp(options, app, build);
|
|
236
|
-
|
|
237
|
-
url?.searchParams.delete("scenario");
|
|
238
|
-
url?.searchParams.set("category", category);
|
|
239
|
-
return json({ widget: { resourceUri: options.config.mcp.previewManifest ? PREVIEW_APP_URI : TOKENS_APP_URI }, category, scenario: { ...entry, resourceUri: appUri(category) }, ...graph, build, showcaseUrl: url?.href ?? null });
|
|
242
|
+
return json({ widget: { resourceUri: options.config.mcp.previewManifest ? PREVIEW_APP_URI : TOKENS_APP_URI }, category, scenario: { ...entry, resourceUri: appUri(category) }, ...graph, build, showcaseUrl: scenarioUrl(category) });
|
|
240
243
|
}
|
|
241
244
|
return json({ widget: { resourceUri: TOKENS_APP_URI }, category: null, tokenCount: inspectTokens(root).tokens.length, dataUri: `${PROJECT_URI}/tokens`, showcaseUrl: getShowcaseUrl() });
|
|
242
245
|
} },
|
|
@@ -257,9 +260,9 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
257
260
|
await refresh();
|
|
258
261
|
return { ...json(result), ...(!result.ok ? { isError: true } : {}) };
|
|
259
262
|
} },
|
|
260
|
-
{ name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; shared controllers; composition; tokens; foundation shared files; documentation; assets/brand, assets/fonts and asset groups; configuration with ds.config.json, package/build/lint configuration and registered project guard/compiler sources. List summaries with query, or pass an exact unit ID for its manifest. Manifests include companionFiles with permitted shared-entry or owner-only style-boundary bookkeeping; preserve peer permissions and unrelated configuration. Imported peers stay separate. New owners follow the existing colocation convention.", schema: query.extend({ unit: z.string().min(1).max(256).optional() }), call: ({ unit, query: search = "", limit }) => {
|
|
263
|
+
{ name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; shared controllers; composition; tokens; foundation shared files; documentation including shared credits/license notices; assets/brand, assets/fonts and asset groups; configuration with ds.config.json, package/build/lint configuration and registered project guard/compiler sources. List summaries with query, or pass an exact unit ID for its manifest. Manifests include filePolicy for supported new documentation paths and companionFiles with permitted shared-entry or owner-only style-boundary bookkeeping; preserve peer permissions and unrelated configuration. Imported peers stay separate. New owners follow the existing colocation convention.", schema: query.extend({ unit: z.string().min(1).max(256).optional() }), call: ({ unit, query: search = "", limit }) => {
|
|
261
264
|
const matches = edits.units().filter(item => unit ? item.id === unit : item.id.toLowerCase().includes(search.toLowerCase()));
|
|
262
|
-
return json({ total: matches.length, units: matches.slice(0, limit).map(item => ({ id: item.id, kind: item.kind, fileCount: item.files.length, ...(unit ? { files: item.files.slice(0, 200), filesTruncated: item.files.length > 200, ...(item.companionFiles ? { companionFiles: item.companionFiles } : {}) } : {}) })) });
|
|
265
|
+
return json({ total: matches.length, units: matches.slice(0, limit).map(item => ({ id: item.id, kind: item.kind, fileCount: item.files.length, ...(unit ? { files: item.files.slice(0, 200), filesTruncated: item.files.length > 200, ...(item.companionFiles ? { companionFiles: item.companionFiles } : {}), ...(item.filePolicy ? { filePolicy: item.filePolicy } : {}) } : {}) })) });
|
|
263
266
|
} },
|
|
264
267
|
{ name: "ads_read_file", description: "Read a bounded range of ADS or registered configuration/extension source. Returns unit, full-file digest, startLine and nextOffset; default/max is 12000 UTF-16 characters. Binary assets return byte count/digest and no encoded content. Paths come from ads_units or discovery. Missing new paths return digest null. Read only relevant context before preparing a Git patch.", schema: z.object({ path: z.string().min(1).max(500), offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(mutationLimits.readChars).default(mutationLimits.readChars) }).strict(), call: ({ path: file, ...range }) => json(edits.read(file, range)) },
|
|
265
268
|
...(mutations ? [{ name: "ads_prepare_patch", description: "Prepare a Git patch without a separate project mirror. Create copies only one owner plus its declared companions into a local draft; edit that directory with local file tools. Diff checks baseline/source freshness and owner paths, then writes a patchFile under .ds/patches. Discard removes the draft. Live source is never changed; ads_apply_patch still validates all gates. Remote clients can continue sending inline Git diffs instead.", schema:z.object({unit:z.string().min(1).max(256),action:z.enum(["create","diff","discard"]).default("create"),draft:z.string().max(100).optional()}).strict(), call:args=>json(prepareUnitPatch(options,args)) },
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isInside } from "./config.mjs";
|
|
5
|
+
|
|
6
|
+
/** Shared source evidence for compilation, watch recovery and status reads. */
|
|
7
|
+
export function previewSourceRevision({root,config}, apps = config.mcp.apps ?? []) {
|
|
8
|
+
const rows = [], generated = config.assets.generatedPaths.map(file => path.resolve(root,file));
|
|
9
|
+
const outputs = new Set(apps.map(app => path.resolve(root,app.file)));
|
|
10
|
+
function digest(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); }
|
|
11
|
+
function visit(directory) {
|
|
12
|
+
for (const entry of readdirSync(directory,{withFileTypes:true})) {
|
|
13
|
+
if (entry.isSymbolicLink() || ["node_modules",".git",".ds",".vite","coverage"].includes(entry.name)) continue;
|
|
14
|
+
const file = path.join(directory,entry.name);
|
|
15
|
+
if (outputs.has(file) || generated.some(base => isInside(base,file))) continue;
|
|
16
|
+
if (entry.isDirectory()) visit(file);
|
|
17
|
+
else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|css|html|json|md|svg|png|jpe?g|webp|gif|woff2?|ttf|otf|lock|lockb)$/.test(file)) rows.push([path.relative(root,file),digest(file)]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
visit(root);
|
|
21
|
+
const installed = path.join(root,"node_modules/.package-lock.json");
|
|
22
|
+
rows.push(["installed-packages",existsSync(installed) ? digest(installed) : null]);
|
|
23
|
+
return createHash("sha256").update(JSON.stringify(rows.sort((a,b)=>a[0].localeCompare(b[0])))).digest("hex");
|
|
24
|
+
}
|
|
@@ -2,15 +2,31 @@ import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
const rootNames = ["README.md", "ADS.md", "DS.md", "ARCHITECTURE.md", "Architecture.md", "GUIDE.md", "MCP.md", "CHANGELOG.md"];
|
|
5
|
+
const noticeStem = /^(?:CREDITS|LICEN[CS]ES?|NOTICES?|COPYING|COPYRIGHT)(?:[-_][A-Za-z0-9][A-Za-z0-9._-]*)?$/i;
|
|
6
|
+
const textExtension = /\.(?:md|txt)$/i;
|
|
7
|
+
|
|
8
|
+
export const documentationFilePolicy = "Project guides; root CREDITS, LICENSE/LICENCE, NOTICE, COPYING and COPYRIGHT notices (optional -/_ qualifiers); licenses/ collections, also under docs/ or doc/. Notices use .md, .txt, or conventional extensionless names and valid UTF-8. The same document paths may live at the ADS root. Component-local notices remain with their owner; code, configuration, hidden and generated paths are excluded.";
|
|
9
|
+
|
|
10
|
+
function noticeName(name) {
|
|
11
|
+
const extension = path.posix.extname(name);
|
|
12
|
+
return (!extension || textExtension.test(name)) && noticeStem.test(extension ? name.slice(0,-extension.length) : name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isProjectNotice(file) {
|
|
16
|
+
if (!file.includes("/")) return noticeName(file);
|
|
17
|
+
return /^(?:(?:docs|doc)\/)?licenses\/.+$/i.test(file) && (textExtension.test(file) || noticeName(path.posix.basename(file)));
|
|
18
|
+
}
|
|
5
19
|
|
|
6
20
|
/** Recognize documentation ownership even before a new guide exists. */
|
|
7
21
|
export function isProjectDocument(file) {
|
|
8
|
-
|
|
22
|
+
if (file.includes("\\") || file.includes("\0") || file.split("/").some(part => !part || part.startsWith("."))) return false;
|
|
23
|
+
return rootNames.includes(file) || isProjectNotice(file) || /^(?:docs|doc)\/.+\.md$/i.test(file);
|
|
9
24
|
}
|
|
10
25
|
|
|
11
|
-
/** Shared
|
|
12
|
-
export function
|
|
13
|
-
|
|
26
|
+
/** Shared document discovery for MCP resources and editable units. */
|
|
27
|
+
export function projectDocumentFiles(root) {
|
|
28
|
+
if (!existsSync(root) || !lstatSync(root).isDirectory()) return [];
|
|
29
|
+
const files = [];
|
|
14
30
|
function walk(relative) {
|
|
15
31
|
const directory = path.join(root, relative);
|
|
16
32
|
if (!existsSync(directory) || !lstatSync(directory).isDirectory()) return;
|
|
@@ -21,7 +37,14 @@ export function projectGuideFiles(root) {
|
|
|
21
37
|
else if (entry.isFile() && isProjectDocument(file)) files.push(file);
|
|
22
38
|
}
|
|
23
39
|
}
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
for (const entry of readdirSync(root,{withFileTypes:true})) {
|
|
41
|
+
if (entry.isFile() && isProjectDocument(entry.name)) files.push(entry.name);
|
|
42
|
+
else if (entry.isDirectory() && /^(?:docs|doc|licenses)$/i.test(entry.name)) walk(entry.name);
|
|
43
|
+
}
|
|
26
44
|
return files.sort();
|
|
27
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Provenance stays discoverable without loading notice inventories as workflow guides. */
|
|
48
|
+
export function projectGuideFiles(root) {
|
|
49
|
+
return projectDocumentFiles(root).filter(file => !isProjectNotice(file));
|
|
50
|
+
}
|
|
@@ -6,6 +6,7 @@ import { inspectScenarios } from "./scenarios.mjs";
|
|
|
6
6
|
import { artifactDigest, createBuildReporter } from "./mcp/build-status.mjs";
|
|
7
7
|
import { cachedPreviewPublication, previewArtifactHtml, readPreviewManifest } from "./preview-resources.mjs";
|
|
8
8
|
import { previewArtifactPath, withPreviewSession } from "./preview-scope.mjs";
|
|
9
|
+
import { previewSourceRevision } from "./preview-inputs.mjs";
|
|
9
10
|
|
|
10
11
|
const digest = value => createHash("sha256").update(value).digest("hex");
|
|
11
12
|
const fileDigest = file => { try { return digest(readFileSync(file)); } catch { return null; } };
|
|
@@ -13,8 +14,8 @@ const fileDigest = file => { try { return digest(readFileSync(file)); } catch {
|
|
|
13
14
|
/** Cache only successful validation + matching source/dependency/output content.
|
|
14
15
|
* Cache state is development output, never proof that a failed build is current. */
|
|
15
16
|
export function createPreviewCache(options, compilerFiles = []) {
|
|
16
|
-
const { root
|
|
17
|
-
const adapterFiles = [import.meta.filename, ...["preview-resources.mjs", "preview-scope.mjs", "scenarios-vite.mjs", "linaria.mjs", "linaria/atomic-css.cjs", "linaria/atomic-styled.cjs", "linaria/preserve-values.cjs"].map(file => path.join(import.meta.dirname, file)), ...compilerFiles,
|
|
17
|
+
const { root } = options;
|
|
18
|
+
const adapterFiles = [import.meta.filename, ...["preview-inputs.mjs", "preview-resources.mjs", "preview-scope.mjs", "scenarios-vite.mjs", "linaria.mjs", "linaria/atomic-css.cjs", "linaria/atomic-styled.cjs", "linaria/preserve-values.cjs"].map(file => path.join(import.meta.dirname, file)), ...compilerFiles,
|
|
18
19
|
...["package.json", "package-lock.json", "bun.lock", "bun.lockb", "node_modules/.package-lock.json"].map(file => path.join(root, file))];
|
|
19
20
|
const adapterDigest = () => digest(JSON.stringify(adapterFiles.map(file => [file, fileDigest(file)])));
|
|
20
21
|
const loadedAdapter = adapterDigest();
|
|
@@ -27,20 +28,7 @@ export function createPreviewCache(options, compilerFiles = []) {
|
|
|
27
28
|
}
|
|
28
29
|
} catch { /* Missing or corrupt cache means validate and rebuild. */ }
|
|
29
30
|
function inputs() {
|
|
30
|
-
const rows = [];
|
|
31
|
-
const generated = config.assets.generatedPaths.map(file => path.resolve(root, file));
|
|
32
|
-
function visit(directory) {
|
|
33
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
34
|
-
if (entry.isSymbolicLink() || ["node_modules", ".git", ".ds", ".vite", "coverage"].includes(entry.name)) continue;
|
|
35
|
-
const file = path.join(directory, entry.name);
|
|
36
|
-
if (generated.some(base => isInside(base, file))) continue;
|
|
37
|
-
if (entry.isDirectory()) visit(file);
|
|
38
|
-
else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|css|html|json|md|svg|png|jpe?g|webp|gif|woff2?|ttf|otf|lock|lockb)$/.test(file)) rows.push([path.relative(root, file), fileDigest(file)]);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
visit(root);
|
|
42
|
-
rows.push(["installed-packages", fileDigest(path.join(root, "node_modules/.package-lock.json"))]);
|
|
43
|
-
rows.push(["node-runtime", `${process.version}/${process.platform}/${process.arch}`]);
|
|
31
|
+
const rows = [["project-inputs",previewSourceRevision(options)],["node-runtime",`${process.version}/${process.platform}/${process.arch}`]];
|
|
44
32
|
for (const file of compilerFiles) rows.push([file, fileDigest(file)]);
|
|
45
33
|
// Tool upgrades invalidate validation even when the project's lockfile is unchanged.
|
|
46
34
|
for (const name of readdirSync(import.meta.dirname).filter(name => name.endsWith(".mjs"))) {
|
|
@@ -119,6 +107,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
119
107
|
let generation = 0;
|
|
120
108
|
let publishedStamp;
|
|
121
109
|
let validation;
|
|
110
|
+
let observedSourceRevision = previewSourceRevision(options);
|
|
111
|
+
let invalidation;
|
|
122
112
|
function failure(error) {
|
|
123
113
|
try { reporter.failure(error); } catch (reportError) { console.error(reportError); }
|
|
124
114
|
console.error(error);
|
|
@@ -144,7 +134,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
144
134
|
}
|
|
145
135
|
async function compile(entry) {
|
|
146
136
|
if (buildGraph) return compileGraph();
|
|
147
|
-
const revision = reporter.begin(entry.id);
|
|
137
|
+
const revision = reporter.begin(entry.id,{sourceRevision:previewSourceRevision(options)});
|
|
148
138
|
const started = generation;
|
|
149
139
|
try {
|
|
150
140
|
const stamp = cache.inputs();
|
|
@@ -187,7 +177,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
187
177
|
}
|
|
188
178
|
}
|
|
189
179
|
async function compileGraph() {
|
|
190
|
-
const
|
|
180
|
+
const sourceRevision = previewSourceRevision(options);
|
|
181
|
+
const revisions = new Map(entries.map(entry => [entry.id, reporter.begin(entry.id,{sourceRevision})]));
|
|
191
182
|
const started = generation;
|
|
192
183
|
try {
|
|
193
184
|
const stamp = cache.inputs();
|
|
@@ -257,6 +248,10 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
257
248
|
try {
|
|
258
249
|
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
259
250
|
if (value.session !== process.env.ADS_MCP_BUILD_SESSION) return;
|
|
251
|
+
if (value.invalidation !== invalidation) {
|
|
252
|
+
invalidation = value.invalidation;
|
|
253
|
+
if (requested.size) sourceChanged("invalidate",options.config.dsDir,true);
|
|
254
|
+
}
|
|
260
255
|
active = entries.find(entry => entry.artifact === value.active)?.id;
|
|
261
256
|
for (const file of value.files) {
|
|
262
257
|
const entry = entries.find(entry => entry.artifact === file);
|
|
@@ -288,6 +283,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
288
283
|
const changes = [...changed]; changed.clear();
|
|
289
284
|
try {
|
|
290
285
|
entries = registry();
|
|
286
|
+
observedSourceRevision = previewSourceRevision(options);
|
|
291
287
|
registryFailure = undefined;
|
|
292
288
|
reporter.setApps(buildApps());
|
|
293
289
|
reporter.clearFailure();
|
|
@@ -297,7 +293,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
297
293
|
// compilation. Check active first, then rebuild only changed dependencies.
|
|
298
294
|
for (const id of requested) {
|
|
299
295
|
const entry = entries.find(entry => entry.id === id);
|
|
300
|
-
if (entry) { reporter.begin(id); pending.add(id); }
|
|
296
|
+
if (entry) { reporter.begin(id,{sourceRevision:observedSourceRevision}); pending.add(id); }
|
|
301
297
|
}
|
|
302
298
|
if (changes.length) void drain();
|
|
303
299
|
} catch (error) {
|
|
@@ -344,7 +340,17 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
344
340
|
} catch (error) { failure(error); }
|
|
345
341
|
}
|
|
346
342
|
reconcileWatches(true);
|
|
347
|
-
const reconcileTimer = setInterval(
|
|
343
|
+
const reconcileTimer = setInterval(() => {
|
|
344
|
+
reconcileWatches();
|
|
345
|
+
try {
|
|
346
|
+
const current = previewSourceRevision(options);
|
|
347
|
+
if (current !== observedSourceRevision) {
|
|
348
|
+
observedSourceRevision = current;
|
|
349
|
+
sourceChanged("reconcile",options.config.dsDir,true);
|
|
350
|
+
}
|
|
351
|
+
request();
|
|
352
|
+
} catch (error) { failure(error); }
|
|
353
|
+
}, 1000);
|
|
348
354
|
request();
|
|
349
355
|
console.log(`Scenario compiler ready: ${entries.length} entries; builds are requested on demand.`);
|
|
350
356
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|