@reqvire-org/ads 0.3.20 → 0.3.21
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/README.md +2 -0
- package/dist/behaviors.mjs +14 -4
- package/dist/bootstrap-ds/README.md +1 -1
- package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +1 -1
- package/dist/bootstrap-ds/tsconfig.build.json +9 -2
- package/dist/boundaries.mjs +2 -0
- package/dist/check.mjs +25 -5
- package/dist/config.mjs +21 -4
- package/dist/contract-migration.mjs +110 -0
- package/dist/contracts.d.mts +22 -0
- package/dist/evidence.mjs +1 -1
- package/dist/index.mjs +1 -0
- package/dist/init.mjs +1 -1
- package/dist/mcp/catalog.mjs +9 -2
- package/dist/mcp/context.mjs +1 -1
- package/dist/mcp/edit-preparation.mjs +6 -3
- package/dist/mcp/preview-widget.bundle.js +41 -39
- package/dist/mcp/server.mjs +3 -1
- package/dist/paint-contracts.mjs +124 -0
- package/dist/preview-resources.mjs +1 -1
- package/dist/public-props.mjs +2 -2
- package/dist/runtime-contracts.mjs +225 -0
- package/dist/scenario-trace.mjs +32 -4
- package/dist/scenarios-vite.mjs +1 -1
- package/dist/scenarios.mjs +82 -26
- package/dist/scoped-tokens.mjs +9 -4
- package/dist/skills/ads-break-ui/SKILL.md +4 -0
- package/dist/skills/ads-break-ui/references/contracts.md +250 -0
- package/dist/skills/ads-designer/SKILL.md +4 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
- package/dist/skills/ads-designer/references/contracts.md +250 -0
- package/dist/skills/ads-import/SKILL.md +4 -0
- package/dist/skills/ads-import/references/contracts.md +250 -0
- package/dist/skills/ads-migrate/SKILL.md +4 -0
- package/dist/skills/ads-migrate/references/contracts.md +250 -0
- package/dist/skills/ads-motion/SKILL.md +4 -0
- package/dist/skills/ads-motion/references/contracts.md +250 -0
- package/dist/token-origins.mjs +24 -0
- package/dist/tokens.mjs +4 -3
- package/dist/unit-contracts.mjs +203 -0
- package/dist/units.mjs +9 -3
- package/dist/verification/browser.mjs +156 -26
- package/dist/verification/playwright-reporter.cjs +13 -3
- package/docs/ADS.md +8 -1
- package/docs/ARCHITECTURE.md +36 -0
- package/docs/BEHAVIORS.md +8 -0
- package/docs/CHANGELOG.md +19 -0
- package/docs/CONTRACTS.md +250 -0
- package/docs/GUIDE.md +26 -1
- package/docs/MCP.md +15 -0
- package/package.json +4 -1
- /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
- /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ uses Preact. See [implementation support](docs/GUIDE.md#implementation-support).
|
|
|
31
31
|
proposed candidates, exposed as MCP image resources.
|
|
32
32
|
- **Verify behavior** through project-owned guarantees and registered tests,
|
|
33
33
|
with progress, timings and evidence tied to current source.
|
|
34
|
+
- **Keep contracts explicit** for typed paint inputs, scoped data bindings and
|
|
35
|
+
declarative APIs, without adding ADS to production components.
|
|
34
36
|
- **Guide agents** with prompts and portable skills for building, importing,
|
|
35
37
|
migrating, motion and UI stress testing.
|
|
36
38
|
|
package/dist/behaviors.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { inspectUnits } from "./units.mjs";
|
|
|
9
9
|
import { scenarioApi } from "./public-api.mjs";
|
|
10
10
|
import { inspectScenarios } from "./scenarios.mjs";
|
|
11
11
|
import { uiOperation } from "./ui-framework.mjs";
|
|
12
|
+
import { inspectUnitContracts } from "./unit-contracts.mjs";
|
|
12
13
|
|
|
13
14
|
const id = z.string().regex(/^[a-zA-Z0-9][\w.-]{0,99}$/);
|
|
14
15
|
const reference = z.string().min(1).max(500);
|
|
@@ -27,20 +28,22 @@ export const behaviorSchema = z.object({
|
|
|
27
28
|
export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
28
29
|
const { root, dsRoot, config } = options;
|
|
29
30
|
const inventory = inspectUnits(options);
|
|
30
|
-
const
|
|
31
|
+
const declarations = inspectUnitContracts(options);
|
|
32
|
+
const typed = declarations.contracts.filter(entry => entry.behavior);
|
|
33
|
+
const files = [...sourceFiles(dsRoot).filter(isBehaviorFile), ...typed.map(entry => path.resolve(root, entry.file))];
|
|
31
34
|
if (!files.length) return { contracts: [], diagnostics: [] };
|
|
32
|
-
const program = suppliedProgram ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
|
|
35
|
+
const program = suppliedProgram ?? declarations.program ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
|
|
33
36
|
const checker = program.getTypeChecker();
|
|
34
37
|
const apis = new Map(scenarioApi(program, options).map(item => [item.name, item]));
|
|
35
38
|
const scenarios = (suppliedRegistry ?? inspectScenarios(options, program)).scenarios;
|
|
36
|
-
const diagnostics = [], contracts = [], owners = new Set();
|
|
39
|
+
const diagnostics = [...declarations.diagnostics], contracts = [], owners = new Set();
|
|
37
40
|
const relative = file => path.relative(root, file).replaceAll("\\", "/");
|
|
38
41
|
for (const file of files) {
|
|
39
42
|
const report = message => diagnostics.push({ file: relative(file), rule: "behavior-contract", line: 1, column: 1, message });
|
|
40
43
|
let value;
|
|
41
44
|
try {
|
|
42
45
|
if (statSync(file).size > 128 * 1024 || realpathSync(file) !== file) throw new Error("Behavior contracts must be regular files of at most 128 KiB.");
|
|
43
|
-
const parsed = behaviorSchema.safeParse(JSON.parse(readFileSync(file, "utf8")));
|
|
46
|
+
const parsed = behaviorSchema.safeParse(typed.find(entry => path.resolve(root, entry.file) === file)?.behavior ?? JSON.parse(readFileSync(file, "utf8")));
|
|
44
47
|
if (!parsed.success) throw new Error(parsed.error.issues.slice(0, 5).map(issue => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
45
48
|
value = parsed.data;
|
|
46
49
|
} catch (error) { report(error.message); continue; }
|
|
@@ -85,6 +88,13 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
|
85
88
|
if (cases.has(identity)) report(`Duplicate execution identity for check ${name}. Use one check ID and reference it from several guarantees.`);
|
|
86
89
|
cases.add(identity);
|
|
87
90
|
}
|
|
91
|
+
const declaration=typed.find(entry=>entry.unit===unit);
|
|
92
|
+
for(const entry of [...Object.values(declaration?.bindings??{}),...(declaration?.declarativeParts??[])])for(const name of entry.checks) {
|
|
93
|
+
const check=value.checks[name],scenario=check&&scenarios.find(scenario=>scenario.id===check.scenario);
|
|
94
|
+
const parent=entry.parentName??entry.scope.apiName;
|
|
95
|
+
if(!check||config.mcp.behaviorProfiles[check.profile]?.mode!=="browser"||!scenario?.covers.includes(parent)||entry.parentName&&!scenario.exercises?.includes(entry.apiName))report(`Required contract check ${name} must use a browser profile and a scenario covering ${parent}${entry.parentName?` and exercising ${entry.apiName}`:""}. Source registration does not establish rendered behavior.`);
|
|
96
|
+
if(!value.guarantees.some(guarantee=>guarantee.checks.includes(name)))report(`Required contract check ${name} must back a named guarantee, not remain an unclaimed execution.`);
|
|
97
|
+
}
|
|
88
98
|
contracts.push({ ...value, unit, file: relative(file), demonstrations: Object.fromEntries(Object.entries(value.subjects).filter(([, item]) => item.api).map(([name, item]) => [name, scenarios.filter(scenario => scenario.exercises?.includes(item.api)).map(scenario => scenario.id)])) });
|
|
89
99
|
}
|
|
90
100
|
return { contracts, diagnostics };
|
|
@@ -56,7 +56,7 @@ Its MCP client discovers entries, invokes preview tools and follows notification
|
|
|
56
56
|
There are no required TokensPage/ComponentsPage/PatternsPage/CompositionPage
|
|
57
57
|
files, and no separate project browser app to implement or rebuild.
|
|
58
58
|
|
|
59
|
-
The existing `tokens/Foundation.scenario.tsx` and colocated fixture visually demonstrate all
|
|
59
|
+
The existing `tokens/Foundation.ads.scenario.tsx` and colocated fixture visually demonstrate all
|
|
60
60
|
starter token values. The fixture owns `{ token, property, probe }` bindings;
|
|
61
61
|
the public `TokenPreview` pattern owns their presentation and applies the real CSS
|
|
62
62
|
variables. Add/remove samples alongside token definitions. `ads check` rejects
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { TokenPreview } from "@ds/review";
|
|
2
|
-
import { fixture } from "./Foundation.fixture";
|
|
2
|
+
import { fixture } from "./Foundation.ads.fixture";
|
|
3
3
|
export const scenario = { title: fixture.title, covers: ["TokenPreview"], samples: fixture.samples };
|
|
4
4
|
export default function Scenario() { return <TokenPreview {...fixture} />; }
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
"rootDir": "design-system",
|
|
8
8
|
"outDir": "dist"
|
|
9
9
|
},
|
|
10
|
-
"include": [
|
|
11
|
-
|
|
10
|
+
"include": [
|
|
11
|
+
"design-system/index.ts"
|
|
12
|
+
],
|
|
13
|
+
"exclude": [
|
|
14
|
+
"design-system/showcase",
|
|
15
|
+
"design-system/review",
|
|
16
|
+
"design-system/composition",
|
|
17
|
+
"**/*.ads.*"
|
|
18
|
+
]
|
|
12
19
|
}
|
package/dist/boundaries.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { importedUiName } from "./ui-framework.mjs";
|
|
|
4
4
|
import { isInside, isStateVariable, isTestFile, isDevelopmentFile } from "./config.mjs";
|
|
5
5
|
import { isSharedToken } from "./token-policy.mjs";
|
|
6
6
|
import { scopedTokenOwner } from "./scoped-tokens.mjs";
|
|
7
|
+
import { runtimeBindingOwner } from "./unit-contracts.mjs";
|
|
7
8
|
|
|
8
9
|
export function projectCompilerOptions({ root, config, dsRoot }) {
|
|
9
10
|
const file = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
|
|
@@ -64,6 +65,7 @@ export function checkComponentVariable(name, filename, options) {
|
|
|
64
65
|
const showcase = development;
|
|
65
66
|
const owned = scopedTokenOwner(options, name);
|
|
66
67
|
if (owned) return path.resolve(options.root, owned.file) === filename ? null : { rule: "token-ownership", message: `Define ${name} only in ${owned.file}, owned by ${owned.unit}. Consume it with var(); runtime geometry remains a separate styleWrites boundary.` };
|
|
68
|
+
if (runtimeBindingOwner(options, name)) return { rule: "binding-ownership", message: `${name} belongs to a declared runtime producer. Do not redefine instance bindings in CSS or arbitrary style objects.` };
|
|
67
69
|
if (isSharedToken(name)) return tokens ? null : { rule: "token-ownership", message: `Define ${name} in ADS tokens, not component, pattern, showcase, or app styles.` };
|
|
68
70
|
if (name.startsWith("--ux-")) return patterns || showcase ? null : { rule: "variable-ownership", message: "--ux-* variables belong to ADS patterns and showcase composition." };
|
|
69
71
|
if (name.startsWith("--showcase-")) return showcase ? null : { rule: "variable-ownership", message: "--showcase-* variables belong to the ADS showcase." };
|
package/dist/check.mjs
CHANGED
|
@@ -9,6 +9,10 @@ import { foundationFiles, foundationImportTarget, isSharedToken } from "./token-
|
|
|
9
9
|
import { inspectAssets } from "./assets.mjs";
|
|
10
10
|
import { appImportViolation, showcaseImportViolation, checkComponentVariable, importResolver, projectCompilerOptions, isUiCssProperties, styleWriteViolation } from "./boundaries.mjs";
|
|
11
11
|
import { inspectPublicProps } from "./public-props.mjs";
|
|
12
|
+
import { adsFileRole } from "./config.mjs";
|
|
13
|
+
import { inspectPaintContracts, inspectTokenValueFlows } from "./paint-contracts.mjs";
|
|
14
|
+
import { runtimeBindingOwner } from "./unit-contracts.mjs";
|
|
15
|
+
import { inspectRuntimeContracts } from "./runtime-contracts.mjs";
|
|
12
16
|
import { inspectHtml, isMountCss } from "./html.mjs";
|
|
13
17
|
import { inspectShowcasePages } from "./showcase.mjs";
|
|
14
18
|
import { inspectShowcaseCoverage } from "./showcase-coverage.mjs";
|
|
@@ -43,7 +47,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
43
47
|
if (!existsSync(file) || (field === "dsDir" ? !statSync(file).isDirectory() : !statSync(file).isFile())) diagnostics.push({ rule: "project-structure", file: path.relative(root, file), line: 1, column: 1, message: `Missing or invalid ${field}. Bootstrap an empty ADS or configure its actual path.` });
|
|
44
48
|
}
|
|
45
49
|
if (diagnostics.length) return architectureEvidence({ ok: false, diagnostics });
|
|
46
|
-
const tokenInventory = inspectTokens(root);
|
|
50
|
+
const tokenInventory = inspectTokens(root, options);
|
|
47
51
|
const declaredTokens = new Set(tokenInventory.tokens.map((token) => token.name));
|
|
48
52
|
diagnostics.push(...tokenInventory.diagnostics);
|
|
49
53
|
diagnostics.push(...inspectAssets(options));
|
|
@@ -57,6 +61,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
57
61
|
const compiler = projectCompilerOptions(options);
|
|
58
62
|
const program = ts.createProgram(scriptFiles, { ...compiler, paths: { ...compiler.paths, [config.publicAlias]: [path.join(dsRoot, config.publicEntry)] }, allowJs: true, noResolve: true, noLib: true, types: [], target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve });
|
|
59
63
|
const checker = program.getTypeChecker();
|
|
64
|
+
const paintContracts = inspectPaintContracts(options);
|
|
65
|
+
diagnostics.push(...paintContracts.diagnostics);
|
|
66
|
+
const runtimeContracts = inspectRuntimeContracts(options, program);
|
|
67
|
+
diagnostics.push(...runtimeContracts.diagnostics);
|
|
68
|
+
for (const binding of runtimeContracts.bindings) if (scoped.definitions.some(entry=>entry.namespace.startsWith(binding.prefix)||binding.prefix.startsWith(entry.namespace))) diagnostics.push({file:binding.file,rule:"binding-collision",line:1,column:1,message:`Runtime family ${binding.variable} overlaps an owner-local design-token namespace. Keep instance bindings separate from token definitions.`});
|
|
60
69
|
const atomicCompositionReview = createAtomicCompositionReview(program, options);
|
|
61
70
|
const scenarioTrace = createScenarioTrace(program, options);
|
|
62
71
|
let scenarioSlot, scenarioSelector;
|
|
@@ -65,6 +74,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
65
74
|
}
|
|
66
75
|
const layers = createLayerBoundaries(program, options, report), layerStyles = [];
|
|
67
76
|
layers.inspectDependencies();
|
|
77
|
+
inspectTokenValueFlows(program, options, tokenInventory, scoped, report);
|
|
68
78
|
for (const file of layers.runtimeHelpers()) if (!files.includes(file)) files.push(file);
|
|
69
79
|
inspectStyleBoundaries(options, program, report);
|
|
70
80
|
inspectShowcasePages(options, program, resolveImport, report);
|
|
@@ -154,6 +164,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
154
164
|
const variable = checkComponentVariable(decl.prop, file, options);
|
|
155
165
|
if (variable) report(file, variable.rule, variable.message, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
156
166
|
for (const ref of references(decl.value)) {
|
|
167
|
+
const binding = runtimeBindingOwner(options, ref.name);
|
|
168
|
+
if (binding) {
|
|
169
|
+
if (scoped.inventory.owner(scoped.inventory.relative(path.relative(root, file))) !== binding.unit) report(file, "binding-ownership", `${ref.name} is an instance binding owned by ${binding.unit}, not a shared design token.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
157
172
|
const owned = scopedTokenOwner(options, ref.name);
|
|
158
173
|
if (owned) {
|
|
159
174
|
if (scoped.inventory.owner(scoped.inventory.relative(path.relative(root, file))) !== owned.unit || !declaredTokens.has(ref.name)) report(file, "token-reference", `${ref.name} belongs to ${owned.unit}. Use shared tokens across owners; owner-local tokens must be defined and consumed by their owner.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
@@ -179,6 +194,9 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
179
194
|
});
|
|
180
195
|
}
|
|
181
196
|
for (const file of files) {
|
|
197
|
+
const role = adsFileRole(file);
|
|
198
|
+
const roleExtension=role==="contract"?/\.[cm]?ts$/.test(file):["fixture","fixtures"].includes(role)?/\.(?:[cm]?[jt]sx?|json)$/.test(file):/\.[cm]?[jt]sx?$/.test(file);
|
|
199
|
+
if (role && (!["contract", "scenario", "fixture", "fixtures", "helpers", "test", "spec", "behavior.spec"].includes(role)||!roleExtension)) report(file, "development-role", `Unknown .ads. role or unsupported extension: ${role}. Use a TypeScript contract, a script scenario/fixture/helpers/test/behavior.spec, or JSON fixture; marking development code does not grant styling or mutation permissions.`);
|
|
182
200
|
if (scopedFiles.has(file)) { try{checkRegistrations(file,postcss.parse(readFileSync(file,"utf8"),{from:file}));}catch{/* Token inspection reports malformed sheets. */} continue; } // Closed definition-only contract checked by inspectTokens.
|
|
183
201
|
if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) { checkCss(file, readFileSync(file, "utf8"), undefined, true); continue; }
|
|
184
202
|
if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
|
|
@@ -212,7 +230,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
212
230
|
const selector = native ? (scenarioSelector ??= createScenarioSelectors(program, options))(node) : { ok: false, reasons: ["A selector cannot establish custom widget behavior or keyboard semantics."] };
|
|
213
231
|
if (!selector.ok) nodeReport(node, "showcase-control", `Scenario controls must compose public ADS runtime UI. Native semantics require source evidence of owned cloning props or an attached runtime Linaria selector addressing that native tag or a verified runtime-owned part in that position. A prop name, broad selector or scenario-authored marker grants no permission. ${selector.reasons.join(" ")} Preserve valid APIs when tracing is inconclusive; report the limitation instead of changing the API to satisfy coverage. ${scenarioLayoutGuidance} See ads://guides/scenarios.`, { failure: failure("architecture", "source-ownership", "ownership-not-proven", { certainty: "inconclusive" }) });
|
|
214
232
|
}
|
|
215
|
-
if (!showcase && (isInside(path.join(dsRoot, "components"), file) || isInside(path.join(dsRoot, "patterns"), file))) inspectPublicProps(source, checker, nodeReport);
|
|
233
|
+
if (!showcase && (isInside(path.join(dsRoot, "components"), file) || isInside(path.join(dsRoot, "patterns"), file))) inspectPublicProps(source, checker, nodeReport, paintContracts.allowedProperties);
|
|
216
234
|
function isLinariaTag(tag) {
|
|
217
235
|
if (ts.isCallExpression(tag)) tag = tag.expression;
|
|
218
236
|
let symbolNode = tag;
|
|
@@ -295,13 +313,14 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
295
313
|
if (specifier && resolveImport(specifier, file).some((target) => (specifier.startsWith(".") || existsSync(target)) && target.endsWith(".css") && isInside(root, target) && !isInside(dsRoot, target) && target !== path.join(sourceRoot, "app-mount.css") && !isInside(path.join(root, "node_modules"), target))) nodeReport(node, "style-ownership", "Imported project CSS belongs to ADS components, patterns, or the foundation. Only structural app-mount.css is allowed outside the ADS.");
|
|
296
314
|
if (specifier && appImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "no-app-import", "ADS implementations and showcase fixtures must not import app code. Use public ADS APIs with local fixture data; keep application integration harnesses in application tests.");
|
|
297
315
|
if (specifier && showcaseImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "showcase-import", "Showcase code and styles are development fixtures; runtime code must not import them.");
|
|
298
|
-
|
|
316
|
+
const boundStyle = runtimeContracts.allowedStyles.has(`${file}:${node.pos}`);
|
|
317
|
+
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && (!inlineAllowed || showcase) && !boundStyle) nodeReport(node, "no-inline-style", showcase ? `Scenarios define no inline styles. ${scenarioLayoutGuidance}` : inlineStyleGuidance);
|
|
299
318
|
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && node.initializer) {
|
|
300
319
|
inspectProperty(node.initializer);
|
|
301
320
|
}
|
|
302
321
|
const mutation = styleWriteViolation(node, file, options);
|
|
303
322
|
if (mutation) nodeReport(node, "style-mutation", mutation);
|
|
304
|
-
if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a development-only review/ helper.");
|
|
323
|
+
if (!inlineAllowed && isUiCssProperties(node, checker) && !runtimeContracts.allowedStyleTypes.has(`${file}:${node.pos}`)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a development-only review/ helper.");
|
|
305
324
|
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
|
|
306
325
|
inspectClass(node.initializer);
|
|
307
326
|
}
|
|
@@ -318,5 +337,6 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
318
337
|
// Bounded clients should see actionable warnings before source-analysis limits.
|
|
319
338
|
for (const { file, css, offset } of layerStyles) layers.inspectStyles(file, css, offset);
|
|
320
339
|
advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
|
|
321
|
-
|
|
340
|
+
const uniqueDiagnostics=[...new Map(diagnostics.map(diagnostic=>[JSON.stringify([diagnostic.file,diagnostic.rule,diagnostic.line,diagnostic.column,diagnostic.message]),diagnostic])).values()];
|
|
341
|
+
return architectureEvidence({ ok: uniqueDiagnostics.length === 0, diagnostics:uniqueDiagnostics, ...(advisories.length ? { advisories } : {}) });
|
|
322
342
|
}
|
package/dist/config.mjs
CHANGED
|
@@ -163,10 +163,27 @@ export function isTokenManifestFile(file) {
|
|
|
163
163
|
return /(?:^|[\\/])tokens\.json$|\.tokens\.json$/.test(file);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/** A visible development boundary, independent of whether its role is supported. */
|
|
167
|
+
export function adsFileRole(file) {
|
|
168
|
+
const match = path.basename(file).match(/\.ads\.(.+)\.[^.]+$/);
|
|
169
|
+
return match ? match[1] : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function isContractFile(file) {
|
|
173
|
+
return /\.ads\.contract\.[cm]?ts$/.test(file);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Filenames may change without changing the identity used by clients/checks. */
|
|
177
|
+
export function scenarioIdentity(file, dsRoot) {
|
|
178
|
+
let id = path.relative(dsRoot, file).replaceAll("\\", "/").replace(/\.ads(?=\.)/, "").replace(/(?:\/Scenario|\.scenario)\.[^.]+$/, "");
|
|
179
|
+
if (adsFileRole(file) === "scenario" && path.posix.basename(id) === path.posix.basename(path.posix.dirname(id))) id = path.posix.dirname(id);
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
|
|
166
183
|
export function isDevelopmentFile(file, { dsRoot }) {
|
|
167
|
-
if (!isInside(dsRoot, file)) return isTestFile(file);
|
|
184
|
+
if (!isInside(dsRoot, file)) return isTestFile(file) || adsFileRole(file) !== null;
|
|
168
185
|
const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
|
|
169
|
-
return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
|
|
186
|
+
return adsFileRole(file) !== null || isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
|
|
170
187
|
|| /^(?:showcase|composition|review)\//.test(relative)
|
|
171
188
|
|| /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
|
|
172
189
|
|| /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
|
|
@@ -174,7 +191,7 @@ export function isDevelopmentFile(file, { dsRoot }) {
|
|
|
174
191
|
|
|
175
192
|
/** Review implementations may own demo presentation; scenarios/fixtures may not. */
|
|
176
193
|
export function isReviewFile(file, { dsRoot }) {
|
|
177
|
-
return isInside(path.join(dsRoot, "review"), file) && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
|
|
194
|
+
return isInside(path.join(dsRoot, "review"), file) && adsFileRole(file) === null && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
const ignored = new Set(["node_modules", "dist", "dist-showcase", "dist-kit", ".git", ".vite", ".ds", "coverage"]);
|
|
@@ -194,7 +211,7 @@ export function sourceFiles(root) {
|
|
|
194
211
|
export function importViolation(specifier, filename, options, resolve) {
|
|
195
212
|
const { root, dsRoot, config } = options;
|
|
196
213
|
// ADS implementation code needs its private imports; the showcase is a consumer.
|
|
197
|
-
if (isInside(dsRoot, filename) && !isDevelopmentFile(filename, options)) return false;
|
|
214
|
+
if (isInside(dsRoot, filename) && (!isDevelopmentFile(filename, options) || isContractFile(filename))) return false; // Contract symbol ownership is checked without executing imports.
|
|
198
215
|
if (specifier === `${config.publicAlias}/review`) return !isDevelopmentFile(filename, options) && !isTestFile(filename);
|
|
199
216
|
if (specifier.startsWith(`${config.publicAlias}/`)) return true;
|
|
200
217
|
if (specifier === config.publicAlias) return false;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { adsFileRole, isBehaviorFile, isScenarioFile, loadConfig, scenarioIdentity } from "./config.mjs";
|
|
5
|
+
import { inspectUnitContracts } from "./unit-contracts.mjs";
|
|
6
|
+
import { publicApi } from "./public-api.mjs";
|
|
7
|
+
import { projectCompilerOptions } from "./boundaries.mjs";
|
|
8
|
+
|
|
9
|
+
const slash=value=>value.replaceAll("\\","/");
|
|
10
|
+
|
|
11
|
+
/** Mechanical edits in an ordinary owner draft. Never writes live source. */
|
|
12
|
+
export function migrateContractDraft(options, prepared) {
|
|
13
|
+
const base=prepared.directory, name=path.posix.basename(prepared.unit), renames=new Map(), notes=[];
|
|
14
|
+
const relative=file=>slash(path.relative(base,file));
|
|
15
|
+
const resolve=file=>path.join(base,file);
|
|
16
|
+
const files=prepared.files.filter(file=>!file.endsWith("ds.config.json")&&file.startsWith(options.config.dsDir+"/"));
|
|
17
|
+
const owned=files.filter(file=>!file.endsWith("/"+options.config.publicEntry)&&!file.endsWith("/"+options.config.reviewEntry));
|
|
18
|
+
for(const file of owned) {
|
|
19
|
+
if(adsFileRole(file)||!/\.[cm]?[jt]sx?$/.test(file))continue;
|
|
20
|
+
let next;
|
|
21
|
+
if(isScenarioFile(file))next=path.basename(file).startsWith("Scenario.")?path.join(path.dirname(file),`${name}.ads.scenario${path.extname(file)}`):file.replace(/\.scenario\./,".ads.scenario.");
|
|
22
|
+
else if(/(?:^|\/)fixture\.[cm]?[jt]sx?$/.test(file))next=path.join(path.dirname(file),`${name}.ads.fixture${path.extname(file)}`);
|
|
23
|
+
else if(/\.(?:fixture|fixtures|test|spec)\.[cm]?[jt]sx?$/.test(file))next=file.replace(/\.(behavior\.spec|fixture|fixtures|test|spec)(\.[^.]+)$/,".ads.$1$2");
|
|
24
|
+
else if(/\.scenario-adapter\.[cm]?[jt]sx?$/.test(file))next=file.replace(/\.scenario-adapter\./,".ads.helpers.");
|
|
25
|
+
if(next&&next!==file) {
|
|
26
|
+
next=slash(next);
|
|
27
|
+
if(isScenarioFile(file)&&scenarioIdentity(path.join(options.root,file),options.dsRoot)===prepared.unit&&scenarioIdentity(path.join(options.root,next),options.dsRoot)!==prepared.unit){notes.push(`Kept ${file}: a default-directory rename would change this flat owner's identity. Review a deliberate owner migration separately.`);continue;}
|
|
28
|
+
if([...renames.values()].includes(next)&&isScenarioFile(file))next=next.replace(".ads.scenario.",".named.ads.scenario.");
|
|
29
|
+
if(existsSync(resolve(next)))throw new Error(`Migration would overwrite ${next}; resolve the naming conflict in this draft.`);
|
|
30
|
+
renames.set(file,next);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for(const file of owned.filter(isScenarioFile)) {
|
|
34
|
+
const fixtureBase=path.basename(file).startsWith("Scenario.")?path.join(path.dirname(file),"fixture"):file.replace(/\.scenario\.[^.]+$/,".fixture");
|
|
35
|
+
const fixture=owned.find(candidate=>candidate.startsWith(fixtureBase+".")&&/\.(?:[cm]?[jt]sx?|json)$/.test(candidate));
|
|
36
|
+
if(!fixture)continue;
|
|
37
|
+
const scenario=renames.get(file);
|
|
38
|
+
if(scenario)renames.set(fixture,scenario.replace(/\.scenario\.[^.]+$/,".fixture")+path.extname(fixture));
|
|
39
|
+
else renames.delete(fixture);
|
|
40
|
+
}
|
|
41
|
+
if(new Set(renames.values()).size!==renames.size)throw new Error("Migration has colliding development filenames; resolve the owner draft deliberately.");
|
|
42
|
+
const jsonFiles=owned.filter(file=>isBehaviorFile(file)||/(?:^|\/)tokens\.json$|\.tokens\.json$/.test(file));
|
|
43
|
+
for(const [from,to]of renames)if(isScenarioFile(from)&&scenarioIdentity(path.join(options.root,from),options.dsRoot)!==scenarioIdentity(path.join(options.root,to),options.dsRoot)) {
|
|
44
|
+
const text=readFileSync(resolve(from),"utf8"),source=ts.createSourceFile(from,text,ts.ScriptTarget.Latest,true,ts.ScriptKind.TSX);
|
|
45
|
+
const declaration=source.statements.filter(ts.isVariableStatement).flatMap(statement=>statement.declarationList.declarations).find(declaration=>ts.isIdentifier(declaration.name)&&declaration.name.text==="scenario");
|
|
46
|
+
let expression=declaration?.initializer;while(expression&&(ts.isAsExpression(expression)||ts.isSatisfiesExpression(expression)||ts.isParenthesizedExpression(expression)))expression=expression.expression;
|
|
47
|
+
if(expression&&ts.isObjectLiteralExpression(expression)) {
|
|
48
|
+
if(!expression.properties.some(property=>property.name?.getText()==="id")){const at=expression.getStart()+1;writeFileSync(resolve(from),text.slice(0,at)+`id:${JSON.stringify(scenarioIdentity(path.join(options.root,from),options.dsRoot))},`+text.slice(at));}
|
|
49
|
+
}else {renames.delete(from);notes.push(`Kept ${from}: its indirect metadata needs an explicit stable id before this rename.`);}
|
|
50
|
+
}
|
|
51
|
+
const runtime=owned.find(file=>!adsFileRole(file)&&!isScenarioFile(file)&&new RegExp(`(?:^|/)${name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\.[cm]?[jt]sx?$`).test(file));
|
|
52
|
+
const directory=runtime?path.dirname(runtime):path.dirname(owned[0]??path.join(options.config.dsDir,prepared.unit,"entry.ts"));
|
|
53
|
+
const contractFile=slash(path.join(directory,`${name}.ads.contract.ts`));
|
|
54
|
+
const contract={schemaVersion:1},imports=[],references=new Map();
|
|
55
|
+
const compiler=ts.createProgram(files.filter(file=>/\.[cm]?[jt]sx?$/.test(file)).map(file=>path.resolve(options.root,file)),{...projectCompilerOptions(options),allowJs:true});
|
|
56
|
+
const checker=compiler.getTypeChecker(),apis=publicApi(compiler,options);
|
|
57
|
+
function importReference(file,symbol) {
|
|
58
|
+
const source=compiler.getSourceFile(path.resolve(options.root,file));
|
|
59
|
+
const module=source&&checker.getSymbolAtLocation(source);
|
|
60
|
+
const exported=module&&checker.getExportsOfModule(module).find(item=>item.name===symbol);
|
|
61
|
+
if(!exported)throw new Error(`${file}#${symbol} is not an importable internal export; keep the legacy section until its owner deliberately exposes a local contract reference.`);
|
|
62
|
+
const key=`${file}#${symbol}`;
|
|
63
|
+
if(!references.has(key)) {
|
|
64
|
+
const alias=`subject${references.size}`;references.set(key,alias);
|
|
65
|
+
let specifier=slash(path.relative(path.dirname(contractFile),file)).replace(/\.[cm]?[jt]sx?$/,"");if(!specifier.startsWith("."))specifier="./"+specifier;
|
|
66
|
+
imports.push(`import { ${symbol} as ${alias} } from ${JSON.stringify(specifier)};`);
|
|
67
|
+
}
|
|
68
|
+
return `__ads_reference_${references.get(key)}__`;
|
|
69
|
+
}
|
|
70
|
+
const remove=[];
|
|
71
|
+
if(!existsSync(resolve(contractFile)))for(const file of jsonFiles) {
|
|
72
|
+
const value=JSON.parse(readFileSync(resolve(file),"utf8"));
|
|
73
|
+
if(isBehaviorFile(file)) {
|
|
74
|
+
try {
|
|
75
|
+
const subjects={};
|
|
76
|
+
for(const [id,subject]of Object.entries(value.subjects??{})) {
|
|
77
|
+
if(subject.api) {const api=apis.find(api=>api.name===subject.api);if(!api)throw new Error(`Unknown API ${subject.api}`);subjects[id]={api:importReference(slash(path.relative(options.root,api.file)),api.declaration.name?.text??subject.api)};}
|
|
78
|
+
else if(subject.symbol)subjects[id]={implementation:importReference(slash(path.normalize(path.join(path.dirname(file),subject.source))),subject.symbol)};
|
|
79
|
+
else throw new Error(`Subject ${id} names a whole file. Select an actual implementation symbol before migrating this behavior section.`);
|
|
80
|
+
}
|
|
81
|
+
const {schemaVersion:_version,...behavior}=value;contract.behavior={...behavior,subjects};remove.push(file);
|
|
82
|
+
}catch(error){notes.push(error.message);}
|
|
83
|
+
}else {contract.tokens={namespace:value.namespace,stylesheet:"./"+slash(path.relative(path.dirname(contractFile),file.replace(/\.json$/,".css")))};remove.push(file);}
|
|
84
|
+
}
|
|
85
|
+
for(const file of files) {
|
|
86
|
+
if(!/\.[cm]?[jt]sx?$|\.json$|\.md$/.test(file)||remove.includes(file))continue;
|
|
87
|
+
const original=readFileSync(resolve(file),"utf8");let text=original;
|
|
88
|
+
for(const [before,after]of renames) {
|
|
89
|
+
let from=slash(path.relative(path.dirname(file),before)),to=slash(path.relative(path.dirname(renames.get(file)??file),after));
|
|
90
|
+
if(!from.startsWith("."))from="./"+from;if(!to.startsWith("."))to="./"+to;
|
|
91
|
+
for(const [left,right]of [[before,after],[from,to],[from.replace(/\.[cm]?[jt]sx?$/,""),to.replace(/\.[cm]?[jt]sx?$/,"")]]) {
|
|
92
|
+
for(const quote of ['"',"'","`"] )text=text.replaceAll(quote+left+quote,quote+right+quote);
|
|
93
|
+
if(file.endsWith(".md"))text=text.replaceAll(`](${left})`,`](${right})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if(text!==original)writeFileSync(resolve(file),text);
|
|
97
|
+
}
|
|
98
|
+
if(remove.length) {
|
|
99
|
+
if(contract.behavior)for(const check of Object.values(contract.behavior.checks)) {
|
|
100
|
+
const old=slash(path.normalize(path.join(directory,check.file))),next=renames.get(old);
|
|
101
|
+
if(next)check.file="./"+slash(path.relative(directory,next));
|
|
102
|
+
}
|
|
103
|
+
let text=JSON.stringify(contract,null,2);
|
|
104
|
+
for(const alias of references.values())text=text.replaceAll(JSON.stringify(`__ads_reference_${alias}__`),alias);
|
|
105
|
+
writeFileSync(resolve(contractFile),`import type { UnitContract } from "@reqvire-org/ads/contracts";\n${imports.filter(line=>[...references.values()].some(alias=>line.includes(` as ${alias} `)&&new RegExp(`\\b${alias}\\b`).test(text))).join("\n")}\nexport default ${text} satisfies UnitContract;\n`);
|
|
106
|
+
for(const file of remove) {const fsPath=resolve(file);renameSync(fsPath,path.join(base,"..",`migrated-${path.basename(file)}`));}
|
|
107
|
+
}else if(inspectUnitContracts(loadConfig(options.root)).contracts.some(contract=>contract.unit===prepared.unit))notes.push("Existing TypeScript contract retained. Only legacy file roles/references were renamed.");
|
|
108
|
+
for(const [from,to]of renames)renameSync(resolve(from),resolve(to));
|
|
109
|
+
return {...prepared,migration:{renamed:[...renames].map(([from,to])=>({from,to})),converted:remove,contractFile:remove.length?relative(resolve(contractFile)):null,notes},next:"Review the migrated draft and its references, then use action: diff and ads_apply_patch dryRun: true. Live source is unchanged; unresolved sections retain their legacy registration. Migrate one passing owner at a time."};
|
|
110
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Development-only declaration types. ADS reads the contract AST without execution. */
|
|
2
|
+
export type Implementation = ((...args: never[]) => unknown) | (abstract new (...args: never[]) => unknown);
|
|
3
|
+
export interface BehaviorCheck { file: string; case: string; scenario?: string; profile: string; }
|
|
4
|
+
export interface UnitContract {
|
|
5
|
+
schemaVersion: 1;
|
|
6
|
+
tokens?: { namespace: string; stylesheet: string };
|
|
7
|
+
paintInputs?: readonly { api: Implementation; property: string; kind: "paint-reference"; bindings?: readonly string[] }[];
|
|
8
|
+
bindings?: Readonly<Record<string, {
|
|
9
|
+
input: { api: Implementation; property: string };
|
|
10
|
+
producer: Implementation;
|
|
11
|
+
valueFrom: string;
|
|
12
|
+
variable: string;
|
|
13
|
+
scope: { api: Implementation; selector: string };
|
|
14
|
+
checks: readonly string[];
|
|
15
|
+
}>>;
|
|
16
|
+
declarativeParts?: readonly { api: Implementation; parent: Implementation; consumer: Implementation; checks: readonly string[] }[];
|
|
17
|
+
behavior?: {
|
|
18
|
+
subjects: Readonly<Record<string, { api: Implementation } | { implementation: Implementation }>>;
|
|
19
|
+
guarantees: readonly { id: string; description: string; subjects: readonly string[]; checks: readonly string[] }[];
|
|
20
|
+
checks: Readonly<Record<string, BehaviorCheck>>;
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/evidence.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export function acceptance(scope, passed, established, unverified) {
|
|
|
12
12
|
|
|
13
13
|
export function architectureEvidence(result) {
|
|
14
14
|
return { ...result,
|
|
15
|
-
diagnostics: result.diagnostics.map(diagnostic => ({ ...diagnostic, failure: diagnostic.failure ?? failure("architecture", "source-analysis", diagnostic.rule) })),
|
|
15
|
+
diagnostics: result.diagnostics.map(diagnostic => ({ ...diagnostic, ...(/^(?:unit-contract|paint-.+|binding-.+|declarative-.+|development-role)$/.test(diagnostic.rule)&&!diagnostic.guidanceUri?{guidanceUri:"ads://guides/contracts"}:{}), failure: diagnostic.failure ?? failure("architecture", "source-analysis", diagnostic.rule) })),
|
|
16
16
|
acceptance: acceptance("core-architecture", result.ok, result.ok ? ["core-source-guards"] : [], ["project-checks", "compiled-preview", "browser-behavior", "visual-parity", "accessibility-audit"]),
|
|
17
17
|
};
|
|
18
18
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { initProject } from "./init.mjs";
|
|
2
2
|
export { checkProject } from "./check.mjs";
|
|
3
3
|
export { inspectTokens, tokenContract } from "./tokens.mjs";
|
|
4
|
+
export { inspectTokenOrigins } from "./token-origins.mjs";
|
|
4
5
|
export { generateLintConfig } from "./lint-config.mjs";
|
|
5
6
|
export { generateBrandAssets } from "./brand-assets.mjs";
|
|
6
7
|
// Node-only helpers for registered project lint extensions. They reuse the
|
package/dist/init.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
|
|
|
152
152
|
return file;
|
|
153
153
|
});
|
|
154
154
|
if (!existingTokens.length) {
|
|
155
|
-
for (const name of ["Foundation.scenario.tsx", "Foundation.fixture.ts"]) {
|
|
155
|
+
for (const name of ["Foundation.ads.scenario.tsx", "Foundation.ads.fixture.ts"]) {
|
|
156
156
|
const content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8").replaceAll('"@ds/review"', JSON.stringify(`${config.publicAlias}/review`)).replaceAll('"@ds"', JSON.stringify(config.publicAlias));
|
|
157
157
|
addMissing(path.join(tokenRoot, name), content);
|
|
158
158
|
}
|
package/dist/mcp/catalog.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { isInside, sourceFiles } from "../config.mjs";
|
|
|
5
5
|
import { projectCompilerOptions } from "../boundaries.mjs";
|
|
6
6
|
import { publicApi } from "../public-api.mjs";
|
|
7
7
|
import { projectDocumentFiles } from "../project-documents.mjs";
|
|
8
|
+
import { inspectUnitContracts } from "../unit-contracts.mjs";
|
|
8
9
|
|
|
9
10
|
export const PROJECT_URI = "ads://project";
|
|
10
11
|
export const sourceUri = (file) => `${PROJECT_URI}/files/${file.split("/").map(encodeURIComponent).join("/")}`;
|
|
@@ -32,7 +33,7 @@ export function inspectCatalog(options, scenarios = []) {
|
|
|
32
33
|
const { root, dsRoot, config } = options;
|
|
33
34
|
const entry = path.join(dsRoot, config.publicEntry);
|
|
34
35
|
if (!existsSync(entry)) return [];
|
|
35
|
-
const program = ts.createProgram([entry], { ...projectCompilerOptions(options), allowJs: true });
|
|
36
|
+
const program = inspectUnitContracts(options).program ?? ts.createProgram([entry], { ...projectCompilerOptions(options), allowJs: true });
|
|
36
37
|
const checker = program.getTypeChecker();
|
|
37
38
|
const source = program.getSourceFile(entry);
|
|
38
39
|
const module = source && checker.getSymbolAtLocation(source);
|
|
@@ -40,6 +41,7 @@ export function inspectCatalog(options, scenarios = []) {
|
|
|
40
41
|
const documents = projectDocuments(options);
|
|
41
42
|
const items = [];
|
|
42
43
|
const api = new Map(publicApi(program, options).map(item => [item.name, item]));
|
|
44
|
+
const contracts = inspectUnitContracts(options).contracts;
|
|
43
45
|
for (const exported of checker.getExportsOfModule(module)) {
|
|
44
46
|
let symbol = exported;
|
|
45
47
|
if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
@@ -54,7 +56,12 @@ export function inspectCatalog(options, scenarios = []) {
|
|
|
54
56
|
const relative = path.relative(root, file).replaceAll("\\", "/");
|
|
55
57
|
const line = declaration.getSourceFile().getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
|
|
56
58
|
const guides = documents.filter((doc) => doc.file.endsWith(".md") && path.dirname(doc.file) === path.dirname(file)).map((doc) => doc.uri);
|
|
57
|
-
|
|
59
|
+
const contract=contracts.find(contract=>[...(contract.paintInputs??[]),...(contract.declarativeParts??[])].some(input=>input.apiName===exported.name)||Object.values(contract.bindings??{}).some(binding=>binding.apiName===exported.name));
|
|
60
|
+
const declarative=contract?.declarativeParts?.find(part=>part.apiName===exported.name);
|
|
61
|
+
const bindings=Object.entries(contract?.bindings??{}).filter(([,binding])=>binding.apiName===exported.name);
|
|
62
|
+
items.push({ name: exported.name, kind, apiKind: declarative ? "declarative" : ui ? "ui" : "behavior", importFrom: config.publicAlias, file: relative, line, sourceUri: sourceUri(relative), guides,
|
|
63
|
+
...(contract?{contractUri:sourceUri(contract.file),paintInputCount:contract.paintInputs?.filter(input=>input.apiName===exported.name).length??0,paintInputs:contract.paintInputs?.filter(input=>input.apiName===exported.name).slice(0,8).map(input=>({property:input.property,kind:input.kind,bindings:input.bindings??[]})),...(declarative?{consumer:{parent:declarative.parentName,sourceUri:sourceUri(declarative.consumer.source),symbol:declarative.consumer.symbol,checks:declarative.checks.slice(0,8),checkCount:declarative.checks.length},evidence:"registered relationship; source and browser verification are separate"}:{})}:{}),
|
|
64
|
+
...(bindings.length?{bindingCount:bindings.length,bindings:bindings.slice(0,4).map(([id,binding])=>({id,variable:binding.variable,scope:binding.scope.selector,producer:{sourceUri:sourceUri(binding.producer.source),symbol:binding.producer.symbol},checks:binding.checks.slice(0,8),checkCount:binding.checks.length})),bindingEvidence:"registered; verify source diagnostics and current browser checks"}:{}),
|
|
58
65
|
previews: scenarios.filter(item => item.covers.includes(exported.name) || item.exercises?.includes(exported.name)).map(item => item.id) });
|
|
59
66
|
}
|
|
60
67
|
return items.sort((a, b) => a.name.localeCompare(b.name));
|
package/dist/mcp/context.mjs
CHANGED
|
@@ -57,7 +57,7 @@ Use this server for UI components/patterns, tokens/themes/branding, layout, inte
|
|
|
57
57
|
3. Select one workflow with ads_skills, then ads_read_skill. ${route.skills.map((skill) => `${skill.task}: ${skill.name}`).join("; ")}. Native skill activation is host-controlled. Relative skill references resolve from its SKILL.md URI. Read support files only when relevant.
|
|
58
58
|
4. ${route.preview} Token samples: ads_scenarios(kind: tokens), then ads_show_scenario or ads_show_tokens(category: id). Token source inspection: ads_tokens with a focused query. Registered previews: ${previews.map((preview) => `${preview.id}: ${preview.tool ?? "ads_show_scenario"}`).join(", ") || "none"}. Showcase URL: ${context.showcase.url ?? "not configured; use the project's showcase command"}.
|
|
59
59
|
Inspection connects a public export to its owning source, available guides, token values and discovered preview IDs. Bounded tools keep discovery focused; explicit resource reads supply detail. Source metadata does not prove layout, interaction or motion. A scenario renders the real public implementation and its fixture context in either review surface.
|
|
60
|
-
5. Check ads_status.mutations. When enabled, discover one owner with ads_units, read relevant source ranges with ads_read_file and submit only a Git unified diff through ads_apply_patch({unit, patch, dryRun}). Include that owner's scenarios, fixtures and required public re-exports, never peer units. tokens is the foundation unit; its manifest lists companionFiles for required canonical foundation imports when token files are created/deleted in the same patch. Preserve unrelated imports/comments and public-entry statements; a real selected-owner re-export can remove its redundant export {};. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit; use ads_apply_transaction for coupled owners instead of bypassing rejection with direct writes. ads_prepare_patch creates a local owner draft/diff; ads_prepare_token_samples reconciles explicit project bindings without guessing visual meaning. Intentional project setup, registered guard/compiler code and exceptions use configuration; shared globals use foundation, guides documentation and assets their discovered assets/* unit. Keep mandatory ADS gates. Binary/large Git diffs may use patchFile under .ds/patches/ to avoid model-context bytes. In read-only mode, source edits use the client editor/filesystem tools. Reuse the local public API (${context.publicEntry}). Edit presentation in its ADS owner; keep application data/services/routing in application code and development fixtures beside their scenario; larger scenarios live in composition/. Demo framing and token renderers live in development-only review/, imported through ${context.review.importFrom}; never export them from the production barrel or import them into application runtime. Read ads://guides/review for
|
|
60
|
+
5. Check ads_status.mutations. When enabled, discover one owner with ads_units, read relevant source ranges with ads_read_file and submit only a Git unified diff through ads_apply_patch({unit, patch, dryRun}). Include that owner's scenarios, fixtures and required public re-exports, never peer units. tokens is the foundation unit; its manifest lists companionFiles for required canonical foundation imports when token files are created/deleted in the same patch. Preserve unrelated imports/comments and public-entry statements; a real selected-owner re-export can remove its redundant export {};. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit; use ads_apply_transaction for coupled owners instead of bypassing rejection with direct writes. ads_prepare_patch creates a local owner draft/diff; ads_prepare_token_samples reconciles explicit project bindings without guessing visual meaning. Intentional project setup, registered guard/compiler code and exceptions use configuration; shared globals use foundation, guides documentation and assets their discovered assets/* unit. Keep mandatory ADS gates. Binary/large Git diffs may use patchFile under .ds/patches/ to avoid model-context bytes. In read-only mode, source edits use the client editor/filesystem tools. Reuse the local public API (${context.publicEntry}). Edit presentation in its ADS owner; keep application data/services/routing in application code and development fixtures beside their scenario; larger scenarios live in composition/. Demo framing and token renderers live in development-only review/, imported through ${context.review.importFrom}; never export them from the production barrel or import them into application runtime. Read ads://guides/review for review owners/aliases and ads://guides/contracts when editing static owner declarations, .ads. development roles, constrained paints/bindings or declarative APIs. Contract registration, source evidence and browser verification remain separate. Use ads_prepare_patch action: migrate for a reviewable owner draft, then normal diff and guarded application. Linaria extracts owned styling. Inspect relevant tokens with ads_tokens and source/owning guides before edits.
|
|
61
61
|
6. ${route.validation} Registered commands: ${context.commands.map((command) => command.args?.join(" ") ?? `choose the project package manager for ${command.script}`).join("; ") || "none discovered; inspect the project's build setup"}. Run them from ${context.projectRoot}.
|
|
62
62
|
Execution replies from ads_verify and guarded edits contain counts, acceptance and resultId, not full reports. Read only needed outcomes/diagnostics/captures with ads_results, following nextCursor with identical filters. A truncated row has recordIndex for bounded text ranges. For one failure, use ads_investigate with its unit/check/runId; read request/error/artifact sections only when needed. Only the latest verification and mutation results, and latest investigation per check, are available. Notifications do not establish freshness.
|
|
63
63
|
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { isInside } from "../config.mjs";
|
|
6
6
|
import { checkUnitFiles, inspectUnits, validateUnitId } from "../units.mjs";
|
|
7
7
|
import { textDiff } from "./text-diff.mjs";
|
|
8
|
+
import { migrateContractDraft } from "../contract-migration.mjs";
|
|
8
9
|
|
|
9
10
|
const hash = value => createHash("sha256").update(value).digest("hex");
|
|
10
11
|
const binary = bytes => bytes?.includes(0) || bytes && !Buffer.from(bytes.toString("utf8")).equals(bytes);
|
|
@@ -26,8 +27,9 @@ export function storePreparedPatch(options, patch) {
|
|
|
26
27
|
export function prepareUnitPatch(options, {unit, action = "create", draft}) {
|
|
27
28
|
validateUnitId(unit);
|
|
28
29
|
const inventory = inspectUnits(options);
|
|
29
|
-
if (action === "create") {
|
|
30
|
+
if (action === "create" || action === "migrate") {
|
|
30
31
|
const owner = inventory.units.find(entry => entry.id === unit);
|
|
32
|
+
if(action==="migrate"&&!owner)throw new Error("Discover an existing owner before preparing its contract migration.");
|
|
31
33
|
const files = [...new Set([...(owner?.files ?? []), ...(owner?.companionFiles ?? []).map(entry => entry.path),
|
|
32
34
|
...(!owner ? ["ds.config.json"] : []),
|
|
33
35
|
...(/^(?:components|patterns|controllers|review)\//.test(unit) ? [path.relative(options.root,path.join(options.dsRoot,unit.startsWith("review/")?options.config.reviewEntry:options.config.publicEntry)).replaceAll("\\","/")] : [])])];
|
|
@@ -58,7 +60,8 @@ export function prepareUnitPatch(options, {unit, action = "create", draft}) {
|
|
|
58
60
|
}
|
|
59
61
|
mkdirSync(folder,{recursive:true});
|
|
60
62
|
writeFileSync(base,JSON.stringify({schemaVersion:1,unit,entries}));
|
|
61
|
-
|
|
63
|
+
const prepared={draft,unit,directory:folder,files, next:"Edit only this draft, then call ads_prepare_patch with action: diff and this draft ID. Live source is unchanged; the resulting Git patch still requires ads_apply_patch and all its gates."};
|
|
64
|
+
return action === "migrate" ? migrateContractDraft(options,prepared) : prepared;
|
|
62
65
|
}
|
|
63
66
|
if (!/^[a-f0-9-]{36}$/.test(draft ?? "")) throw new Error("Use the draft ID returned by ads_prepare_patch.");
|
|
64
67
|
const base = path.join(options.root,".ds/patch-drafts",draft);
|
|
@@ -66,7 +69,7 @@ export function prepareUnitPatch(options, {unit, action = "create", draft}) {
|
|
|
66
69
|
const metadata = JSON.parse(readFileSync(path.join(base,"manifest.json"),"utf8"));
|
|
67
70
|
if (metadata.schemaVersion !== 1 || metadata.unit !== unit) throw new Error("Draft and selected unit do not match.");
|
|
68
71
|
if (action === "discard") {rmSync(base,{recursive:true});return {discarded:true,draft,unit};}
|
|
69
|
-
if (action !== "diff") throw new Error("Choose create, diff or discard.");
|
|
72
|
+
if (action !== "diff") throw new Error("Choose create, migrate, diff or discard.");
|
|
70
73
|
const files = new Set(metadata.entries.map(entry => entry.file));
|
|
71
74
|
function walk(directory) {
|
|
72
75
|
if (realpathSync(directory) !== directory) throw new Error("Draft directories cannot resolve through symlinks.");
|