stylegallery 0.1.0
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/AGENTS.md +156 -0
- package/CATALOG.md +69 -0
- package/DOMAINS.md +65 -0
- package/GOVERNANCE.md +222 -0
- package/GUIDE.md +59 -0
- package/README.md +313 -0
- package/consumer-reference/adapters/style-dictionary.config.mjs +36 -0
- package/consumer-reference/agent-native/README.md +157 -0
- package/consumer-reference/agent-native/registry.json +640 -0
- package/consumer-reference/agent-native/schema/agent-native.schema.json +848 -0
- package/consumer-reference/agent-native/schema/epistemic.schema.json +14 -0
- package/consumer-reference/agent-native/schema/execution.schema.json +12 -0
- package/consumer-reference/agent-native/schema/identity.schema.json +10 -0
- package/consumer-reference/agent-native/schema/learning.schema.json +12 -0
- package/consumer-reference/agent-native/schema/manifest.schema.json +6 -0
- package/consumer-reference/agent-native/schema/operation.schema.json +12 -0
- package/consumer-reference/agent-native/schema/protocol-binding.schema.json +9 -0
- package/consumer-reference/agent-native/schema/retrieval.schema.json +10 -0
- package/consumer-reference/baselines/calibration.json +260 -0
- package/consumer-reference/baselines/manifest.json +21 -0
- package/consumer-reference/contract.md +83 -0
- package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +282 -0
- package/consumer-reference/fixtures/consumer-conformance/manifest.json +102 -0
- package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +32 -0
- package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +18 -0
- package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +142 -0
- package/consumer-reference/fixtures/invalid-stable-ended-support.json +19 -0
- package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +26 -0
- package/consumer-reference/fixtures/promotion/invalid-count-only.json +29 -0
- package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +25 -0
- package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +25 -0
- package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +45 -0
- package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +26 -0
- package/consumer-reference/fixtures/promotion/manifest.json +15 -0
- package/consumer-reference/fixtures/promotion/valid-deferred-example.json +67 -0
- package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +67 -0
- package/consumer-reference/fixtures/token-portability/invalid-cycle.json +7 -0
- package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +6 -0
- package/consumer-reference/fixtures/token-portability/invalid-extends.json +4 -0
- package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +7 -0
- package/consumer-reference/fixtures/token-portability/invalid-resolver.json +9 -0
- package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +10 -0
- package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +6 -0
- package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +6 -0
- package/consumer-reference/fixtures/token-portability/valid-border.json +14 -0
- package/consumer-reference/fixtures/token-portability/valid-reference.json +32 -0
- package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +7 -0
- package/consumer-reference/fixtures/valid-deprecated.json +21 -0
- package/consumer-reference/fixtures/valid-experimental.json +19 -0
- package/consumer-reference/generated/manifest.json +40 -0
- package/consumer-reference/generated/tokens.css +12 -0
- package/consumer-reference/index.md +32 -0
- package/consumer-reference/policies/shared-experimental.json +37 -0
- package/consumer-reference/schema/ax-evidence.schema.json +30 -0
- package/consumer-reference/schema/baseline-manifest.schema.json +55 -0
- package/consumer-reference/schema/calibration-record.schema.json +190 -0
- package/consumer-reference/schema/capture-session.schema.json +161 -0
- package/consumer-reference/schema/component-state.schema.json +150 -0
- package/consumer-reference/schema/consumer-conformance-record.schema.json +365 -0
- package/consumer-reference/schema/dom-evidence.schema.json +38 -0
- package/consumer-reference/schema/evidence-record.schema.json +81 -0
- package/consumer-reference/schema/fixture-manifest.schema.json +32 -0
- package/consumer-reference/schema/item.schema.json +188 -0
- package/consumer-reference/schema/page-evidence-manifest.schema.json +216 -0
- package/consumer-reference/schema/page-evidence-session.schema.json +119 -0
- package/consumer-reference/schema/portable-tokens.schema.json +104 -0
- package/consumer-reference/schema/promotion-rfc.schema.json +219 -0
- package/consumer-reference/schema/runtime-evidence-manifest.schema.json +33 -0
- package/consumer-reference/schema/visual-evidence.schema.json +31 -0
- package/design-engineering/consumer-migration-readiness.md +124 -0
- package/design-engineering/index.md +28 -0
- package/design-engineering/interface-craft.md +107 -0
- package/design-engineering/reference-profiles/external-adaptation/index.md +12 -0
- package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +125 -0
- package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +3504 -0
- package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +65 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +23 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +19 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +21 -0
- package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +12 -0
- package/design-engineering/reference-profiles/governed-local/editorial/profile.json +43 -0
- package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +195 -0
- package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +17 -0
- package/design-engineering/reference-profiles/governed-local/index.md +26 -0
- package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +125 -0
- package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +3504 -0
- package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +65 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +23 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +19 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +21 -0
- package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +12 -0
- package/design-engineering/reference-profiles/governed-local/terminal/profile.json +43 -0
- package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +195 -0
- package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +17 -0
- package/design-engineering/reference-profiles/index.md +19 -0
- package/game-ui/classification.md +82 -0
- package/game-ui/index.md +35 -0
- package/game-ui/reference-record.md +62 -0
- package/game-ui/screen-hierarchy.md +107 -0
- package/game-ui/unity/architecture.md +166 -0
- package/game-ui/unity/cli-loop.md +84 -0
- package/game-ui/unity/data/unity-technologies-public-repositories.json +26654 -0
- package/game-ui/unity/org-wiki.md +146 -0
- package/game-ui/unity/repository-map.md +102 -0
- package/game-ui/unity/ui-systems.md +85 -0
- package/guides/decision-tree.md +149 -0
- package/guides/documentation-mode-taxonomy.md +157 -0
- package/guides/layout-brief.md +220 -0
- package/guides/vocabulary.md +148 -0
- package/guides/webpage-generation-workflow.md +185 -0
- package/index.md +36 -0
- package/layout/index.md +26 -0
- package/motion/index.md +24 -0
- package/motion/practice-reference.md +145 -0
- package/motion/review-workflow.md +86 -0
- package/motion/vocabulary.md +82 -0
- package/package.json +78 -0
- package/patterns/centering/center.md +87 -0
- package/patterns/centering/index.md +6 -0
- package/patterns/centering/super-center.md +89 -0
- package/patterns/containment/box.md +87 -0
- package/patterns/containment/clamped-card.md +86 -0
- package/patterns/containment/content-limiter.md +87 -0
- package/patterns/containment/fluid-styles.md +86 -0
- package/patterns/containment/index.md +8 -0
- package/patterns/grid-repetition/card-grid.md +88 -0
- package/patterns/grid-repetition/columns.md +87 -0
- package/patterns/grid-repetition/dense-grid.md +88 -0
- package/patterns/grid-repetition/grid-wrapper.md +89 -0
- package/patterns/grid-repetition/index.md +12 -0
- package/patterns/grid-repetition/masonry-approx.md +87 -0
- package/patterns/grid-repetition/page-grid.md +90 -0
- package/patterns/grid-repetition/ram-grid.md +88 -0
- package/patterns/grid-repetition/twelve-span-grid.md +92 -0
- package/patterns/in-line-grouping/badge-list.md +94 -0
- package/patterns/in-line-grouping/breadcrumb.md +89 -0
- package/patterns/in-line-grouping/cluster.md +89 -0
- package/patterns/in-line-grouping/deconstructed-pancake.md +92 -0
- package/patterns/in-line-grouping/index.md +13 -0
- package/patterns/in-line-grouping/pagination.md +89 -0
- package/patterns/in-line-grouping/reel.md +90 -0
- package/patterns/in-line-grouping/split-nav.md +97 -0
- package/patterns/in-line-grouping/tab-strip.md +88 -0
- package/patterns/in-line-grouping/wrap-row.md +89 -0
- package/patterns/index.md +13 -0
- package/patterns/media-fit/frame.md +95 -0
- package/patterns/media-fit/icon-frame.md +87 -0
- package/patterns/media-fit/index.md +6 -0
- package/patterns/overlay-exception/imposter.md +95 -0
- package/patterns/overlay-exception/index.md +6 -0
- package/patterns/overlay-exception/overlay-stack.md +89 -0
- package/patterns/split-sidebar/index.md +12 -0
- package/patterns/split-sidebar/list-detail.md +95 -0
- package/patterns/split-sidebar/main-with-rail.md +95 -0
- package/patterns/split-sidebar/media-object.md +102 -0
- package/patterns/split-sidebar/sidebar.md +101 -0
- package/patterns/split-sidebar/split-screen.md +92 -0
- package/patterns/split-sidebar/sticky-aside.md +97 -0
- package/patterns/split-sidebar/supporting-pane.md +95 -0
- package/patterns/split-sidebar/switcher.md +93 -0
- package/patterns/stacking/feed.md +88 -0
- package/patterns/stacking/index.md +8 -0
- package/patterns/stacking/line-up.md +95 -0
- package/patterns/stacking/stack.md +87 -0
- package/patterns/stacking/step-nav.md +88 -0
- package/patterns/viewport-shell/cover.md +90 -0
- package/patterns/viewport-shell/fixed-sidenav-shell.md +97 -0
- package/patterns/viewport-shell/holy-grail.md +112 -0
- package/patterns/viewport-shell/index.md +11 -0
- package/patterns/viewport-shell/panel-layout.md +95 -0
- package/patterns/viewport-shell/scroll-body-shell.md +93 -0
- package/patterns/viewport-shell/sticky-footer.md +92 -0
- package/patterns/viewport-shell/sticky-header.md +86 -0
- package/platform-guides/apple-interaction.md +89 -0
- package/platform-guides/index.md +26 -0
- package/quality/claim-records/index.md +3 -0
- package/quality/claim-records/samples.md +92 -0
- package/quality/claim-records/stylegallery-multidomain-scope.md +42 -0
- package/quality/claims.md +77 -0
- package/quality/evidence/accessibility.md +58 -0
- package/quality/evidence/consumer-migration.md +42 -0
- package/quality/evidence/design-rationale.md +34 -0
- package/quality/evidence/executable-evidence.md +79 -0
- package/quality/evidence/families.md +56 -0
- package/quality/evidence/hci-evaluation.md +36 -0
- package/quality/evidence/index.md +16 -0
- package/quality/evidence/pattern-contract-audit.md +96 -0
- package/quality/evidence/search-metadata-polyhierarchy.md +86 -0
- package/quality/evidence/sources.md +43 -0
- package/quality/evidence/tokens.md +33 -0
- package/quality/evidence/visual-qa.md +41 -0
- package/quality/gates/accessibility-evidence.md +60 -0
- package/quality/gates/consumer-migration-evidence.md +53 -0
- package/quality/gates/design-claim.md +56 -0
- package/quality/gates/harmony-evaluation.md +78 -0
- package/quality/gates/index.md +13 -0
- package/quality/gates/layout.md +52 -0
- package/quality/gates/rationale.md +53 -0
- package/quality/gates/visual-evidence.md +47 -0
- package/quality/index.md +55 -0
- package/quality/principles.md +58 -0
- package/recipes/article-page.md +52 -0
- package/recipes/command-surface.md +55 -0
- package/recipes/dashboard.md +56 -0
- package/recipes/form-flow.md +55 -0
- package/recipes/homepage.md +89 -0
- package/recipes/index.md +13 -0
- package/recipes/list-detail.md +55 -0
- package/recipes/primitive-to-recipe-matrix.md +83 -0
- package/recipes/saas-settings.md +52 -0
- package/scripts/agent-native/a2a-projection.mjs +248 -0
- package/scripts/agent-native/agui-projection.mjs +178 -0
- package/scripts/agent-native/canonical-json.mjs +109 -0
- package/scripts/agent-native/cli-adapter.mjs +68 -0
- package/scripts/agent-native/events.mjs +113 -0
- package/scripts/agent-native/execution.mjs +227 -0
- package/scripts/agent-native/fixture.mjs +159 -0
- package/scripts/agent-native/identity.mjs +184 -0
- package/scripts/agent-native/immutable-store.mjs +102 -0
- package/scripts/agent-native/knowledge.mjs +168 -0
- package/scripts/agent-native/learning-records.mjs +244 -0
- package/scripts/agent-native/learning.mjs +247 -0
- package/scripts/agent-native/mcp-adapter.mjs +173 -0
- package/scripts/agent-native/queries.mjs +173 -0
- package/scripts/agent-native/registry.mjs +154 -0
- package/scripts/agent-native/retrieval.mjs +171 -0
- package/scripts/agent-native/self-description.mjs +160 -0
- package/scripts/artifact-metadata.mjs +20 -0
- package/scripts/audit-evidence-freshness.mjs +308 -0
- package/scripts/baseline-contract.mjs +189 -0
- package/scripts/baseline-schema-parity.mjs +40 -0
- package/scripts/build-reference-artifacts.mjs +127 -0
- package/scripts/calibration-raw-contract.mjs +172 -0
- package/scripts/capture-session-contract.mjs +187 -0
- package/scripts/component-state-artifact-fixture.mjs +44 -0
- package/scripts/component-state-contract.mjs +99 -0
- package/scripts/component-state-semantics.mjs +128 -0
- package/scripts/component-state-workflow-contract.mjs +67 -0
- package/scripts/component-state-workflow-negative-cases.mjs +64 -0
- package/scripts/consumer-conformance-contract.mjs +198 -0
- package/scripts/consumer-evidence-governance-contract.mjs +210 -0
- package/scripts/consumer-reference-case-runner.mjs +93 -0
- package/scripts/consumer-reference-schema.mjs +155 -0
- package/scripts/create-component-state-session.mjs +100 -0
- package/scripts/create-page-evidence-session.mjs +139 -0
- package/scripts/domain-document-boundaries.mjs +40 -0
- package/scripts/domain-validation-checks.mjs +141 -0
- package/scripts/evidence-artifact-contract.mjs +191 -0
- package/scripts/finalize-component-state-evidence.mjs +165 -0
- package/scripts/finalize-page-evidence.mjs +197 -0
- package/scripts/generate-consumer-reference-evidence.mjs +154 -0
- package/scripts/generate-patterns.mjs +218 -0
- package/scripts/governance-matrix-contract.mjs +73 -0
- package/scripts/governance-matrix-negative-cases.mjs +114 -0
- package/scripts/governance-policy-contract.mjs +49 -0
- package/scripts/governance-test-fixture.mjs +321 -0
- package/scripts/governance-workflow-negative-cases.mjs +241 -0
- package/scripts/governed-profile-registry.mjs +125 -0
- package/scripts/json-schema-formats.mjs +31 -0
- package/scripts/markdown-structure.mjs +45 -0
- package/scripts/page-artifact-metadata.mjs +124 -0
- package/scripts/page-evidence-contract.mjs +297 -0
- package/scripts/page-evidence-fixture.mjs +271 -0
- package/scripts/pattern-data.mjs +325 -0
- package/scripts/profile-record-contract.mjs +140 -0
- package/scripts/promotion-attestation-contract.mjs +91 -0
- package/scripts/promotion-boundary-test-contract.mjs +92 -0
- package/scripts/promotion-fixture-inventory.mjs +109 -0
- package/scripts/promotion-governance-contract.mjs +28 -0
- package/scripts/reference-artifact-path-contract.mjs +28 -0
- package/scripts/reference-profile-contract.mjs +114 -0
- package/scripts/reference-token-contract.mjs +156 -0
- package/scripts/run-consumer-page-evidence-ci.mjs +170 -0
- package/scripts/sg-mcp.mjs +16 -0
- package/scripts/sg.mjs +27 -0
- package/scripts/strict-json.mjs +72 -0
- package/scripts/summarize-sentinel-calibration.mjs +98 -0
- package/scripts/test-agent-adapter-conformance.mjs +250 -0
- package/scripts/test-agent-conformance-receipt.mjs +106 -0
- package/scripts/test-agent-events.mjs +81 -0
- package/scripts/test-agent-execution-effects.mjs +114 -0
- package/scripts/test-agent-execution-fixtures.mjs +140 -0
- package/scripts/test-agent-execution-kernel.mjs +39 -0
- package/scripts/test-agent-execution-learning.mjs +130 -0
- package/scripts/test-agent-execution-lifecycle.mjs +143 -0
- package/scripts/test-agent-execution-safety.mjs +158 -0
- package/scripts/test-agent-execution-views.mjs +117 -0
- package/scripts/test-agent-identity-integrity-cases.mjs +160 -0
- package/scripts/test-agent-knowledge-kernel.mjs +183 -0
- package/scripts/test-agent-learning.mjs +246 -0
- package/scripts/test-agent-mcp.mjs +246 -0
- package/scripts/test-agent-native-schemas.mjs +215 -0
- package/scripts/test-agent-projections.mjs +249 -0
- package/scripts/test-agent-registry-mcp-integrity.mjs +215 -0
- package/scripts/test-agent-registry-mutations.mjs +110 -0
- package/scripts/test-audit-evidence-freshness.mjs +177 -0
- package/scripts/test-component-state-sentinel.mjs +35 -0
- package/scripts/test-component-state-source-contract.mjs +83 -0
- package/scripts/test-consumer-conformance-sentinel.mjs +165 -0
- package/scripts/test-consumer-reference-sentinel.mjs +31 -0
- package/scripts/test-generate-consumer-reference-evidence.mjs +124 -0
- package/scripts/test-json-cli-output.mjs +151 -0
- package/scripts/test-json-schema-formats.mjs +40 -0
- package/scripts/test-reference-adapters.mjs +199 -0
- package/scripts/test-renderer-purity.mjs +34 -0
- package/scripts/test-sg-cli.mjs +129 -0
- package/scripts/test-summarize-sentinel-calibration.mjs +226 -0
- package/scripts/test-validate-baseline-manifest.mjs +234 -0
- package/scripts/test-validate-component-state-artifacts.mjs +243 -0
- package/scripts/test-validate-component-state.mjs +106 -0
- package/scripts/test-validate-consumer-conformance.mjs +427 -0
- package/scripts/test-validate-consumer-reference-cli.mjs +69 -0
- package/scripts/test-validate-consumer-reference.mjs +263 -0
- package/scripts/test-validate-domains.mjs +357 -0
- package/scripts/test-validate-governance.mjs +292 -0
- package/scripts/test-validate-ia.mjs +262 -0
- package/scripts/test-validate-okf.mjs +73 -0
- package/scripts/test-validate-page-evidence.mjs +302 -0
- package/scripts/test-validate-patterns.mjs +157 -0
- package/scripts/test-validate-promotion-rfc.mjs +250 -0
- package/scripts/test-validate-reference-profiles.mjs +170 -0
- package/scripts/test-validate-unity-org-wiki.mjs +57 -0
- package/scripts/test-validate-unity-source-contracts.mjs +67 -0
- package/scripts/test-validate-vocabulary.mjs +174 -0
- package/scripts/test-validate-webpage-workflow.mjs +293 -0
- package/scripts/validate-baseline-manifest.mjs +44 -0
- package/scripts/validate-catalog.mjs +65 -0
- package/scripts/validate-component-state.mjs +141 -0
- package/scripts/validate-consumer-conformance.mjs +477 -0
- package/scripts/validate-consumer-reference.mjs +288 -0
- package/scripts/validate-domains.mjs +237 -0
- package/scripts/validate-governance.mjs +248 -0
- package/scripts/validate-ia.mjs +136 -0
- package/scripts/validate-links.mjs +66 -0
- package/scripts/validate-okf.mjs +104 -0
- package/scripts/validate-page-evidence.mjs +191 -0
- package/scripts/validate-patterns.mjs +246 -0
- package/scripts/validate-promotion-rfc.mjs +224 -0
- package/scripts/validate-reference-artifacts.mjs +125 -0
- package/scripts/validate-renderer-purity.mjs +56 -0
- package/scripts/validate-unity-org-wiki.mjs +146 -0
- package/scripts/validate-unity-source-contracts.mjs +107 -0
- package/scripts/validate-vocabulary.mjs +115 -0
- package/scripts/validate-webpage-workflow.mjs +254 -0
- package/scripts/visual-expectation-contract.mjs +20 -0
- package/scripts/workflow-action-contract.mjs +163 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const allowedTypes = new Set(["border", "color", "dimension", "duration"]);
|
|
2
|
+
const aliasPattern = /^\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}$/;
|
|
3
|
+
const tokenNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4
|
+
const unsafePathSegments = new Set(["__proto__", "constructor", "prototype"]);
|
|
5
|
+
|
|
6
|
+
function isObject(value) {
|
|
7
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function finding(code, tokenPath, message) {
|
|
11
|
+
return { code, message, path: tokenPath || "<root>" };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function validatePathSegment(segment, tokenPath) {
|
|
15
|
+
if (unsafePathSegments.has(segment)) return [finding("token_path_segment_unsafe", tokenPath, `path segment ${segment} is unsafe for the pinned adapter`)];
|
|
16
|
+
if (!tokenNamePattern.test(segment)) return [finding("token_name_invalid", tokenPath, "token names must use lowercase kebab-case")];
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function exactKeys(value, allowed, tokenPath) {
|
|
21
|
+
return Object.keys(value)
|
|
22
|
+
.filter((key) => !allowed.has(key))
|
|
23
|
+
.map((key) => finding("token_value_property_unknown", tokenPath, `unsupported value property ${key}`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateDimension(value, tokenPath) {
|
|
27
|
+
if (!isObject(value)) return [finding("token_value_invalid", tokenPath, "dimension must be an object")];
|
|
28
|
+
const failures = exactKeys(value, new Set(["unit", "value"]), tokenPath);
|
|
29
|
+
if (typeof value.value !== "number" || !Number.isFinite(value.value)) failures.push(finding("token_value_invalid", tokenPath, "dimension value must be finite"));
|
|
30
|
+
if (!["px", "rem"].includes(value.unit)) failures.push(finding("token_unit_unsupported", tokenPath, "dimension unit must be px or rem"));
|
|
31
|
+
return failures;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateDuration(value, tokenPath) {
|
|
35
|
+
if (!isObject(value)) return [finding("token_value_invalid", tokenPath, "duration must be an object")];
|
|
36
|
+
const failures = exactKeys(value, new Set(["unit", "value"]), tokenPath);
|
|
37
|
+
if (typeof value.value !== "number" || !Number.isFinite(value.value) || value.value < 0) failures.push(finding("token_value_invalid", tokenPath, "duration value must be non-negative and finite"));
|
|
38
|
+
if (!["ms", "s"].includes(value.unit)) failures.push(finding("token_unit_unsupported", tokenPath, "duration unit must be ms or s"));
|
|
39
|
+
return failures;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateColor(value, tokenPath) {
|
|
43
|
+
if (!isObject(value)) return [finding("token_value_invalid", tokenPath, "color must be an object")];
|
|
44
|
+
const failures = exactKeys(value, new Set(["alpha", "colorSpace", "components"]), tokenPath);
|
|
45
|
+
const validComponents = Array.isArray(value.components) && value.components.length === 3 && value.components.every((component) => typeof component === "number" && component >= 0 && component <= 1);
|
|
46
|
+
if (value.colorSpace !== "srgb" || !validComponents || (value.alpha !== undefined && (typeof value.alpha !== "number" || value.alpha < 0 || value.alpha > 1))) {
|
|
47
|
+
failures.push(finding("token_value_invalid", tokenPath, "color must use bounded sRGB components and optional alpha"));
|
|
48
|
+
}
|
|
49
|
+
return failures;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseReference(value, expectedType, tokenPath) {
|
|
53
|
+
const match = typeof value === "string" ? aliasPattern.exec(value) : null;
|
|
54
|
+
if (!match) return { failures: [finding("alias_whole_required", tokenPath, "references must be whole-token curly aliases")], references: [] };
|
|
55
|
+
return { failures: [], references: [{ expectedType, source: tokenPath, target: match[1] }] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateBorder(value, tokenPath) {
|
|
59
|
+
if (!isObject(value)) return { failures: [finding("token_value_invalid", tokenPath, "border must be an object")], references: [] };
|
|
60
|
+
const failures = exactKeys(value, new Set(["color", "style", "width"]), tokenPath);
|
|
61
|
+
const references = [];
|
|
62
|
+
if (value.style !== "solid") failures.push(finding("token_value_invalid", tokenPath, "only the tested solid border style is supported"));
|
|
63
|
+
if (typeof value.color === "string") {
|
|
64
|
+
const result = parseReference(value.color, "color", tokenPath);
|
|
65
|
+
failures.push(...result.failures);
|
|
66
|
+
references.push(...result.references);
|
|
67
|
+
} else failures.push(...validateColor(value.color, tokenPath));
|
|
68
|
+
if (typeof value.width === "string") {
|
|
69
|
+
const result = parseReference(value.width, "dimension", tokenPath);
|
|
70
|
+
failures.push(...result.failures);
|
|
71
|
+
references.push(...result.references);
|
|
72
|
+
} else failures.push(...validateDimension(value.width, tokenPath));
|
|
73
|
+
return { failures, references };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateDirectValue(token) {
|
|
77
|
+
if (typeof token.value === "string") return parseReference(token.value, token.type, token.path);
|
|
78
|
+
if (token.type === "border") return validateBorder(token.value, token.path);
|
|
79
|
+
if (token.type === "color") return { failures: validateColor(token.value, token.path), references: [] };
|
|
80
|
+
if (token.type === "dimension") return { failures: validateDimension(token.value, token.path), references: [] };
|
|
81
|
+
return { failures: validateDuration(token.value, token.path), references: [] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validatePortableTokens(document) {
|
|
85
|
+
const failures = [];
|
|
86
|
+
const references = [];
|
|
87
|
+
const tokens = [];
|
|
88
|
+
if (!isObject(document)) return { failures: [finding("token_document_object_required", "", "token document must be an object")], tokens };
|
|
89
|
+
|
|
90
|
+
function walk(node, segments, inheritedType) {
|
|
91
|
+
if (!isObject(node)) {
|
|
92
|
+
failures.push(finding("token_node_object_required", segments.join("."), "groups and tokens must be objects"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const tokenPath = segments.join(".");
|
|
96
|
+
if (Object.hasOwn(node, "$description") && typeof node.$description !== "string") failures.push(finding("token_description_invalid", tokenPath, "$description must be a string"));
|
|
97
|
+
const serialized = JSON.stringify(node);
|
|
98
|
+
if (/"(?:resolver|modifiers?|themes?)"\s*:/.test(serialized)) failures.push(finding("token_resolver_forbidden", tokenPath, "resolvers, modifiers, and themes are outside the portable subset"));
|
|
99
|
+
const reserved = Object.keys(node).filter((key) => key.startsWith("$"));
|
|
100
|
+
if (reserved.includes("$extends")) failures.push(finding("token_extends_forbidden", tokenPath, "$extends is outside the portable subset"));
|
|
101
|
+
if (reserved.includes("$ref")) failures.push(finding("token_json_pointer_forbidden", tokenPath, "JSON Pointer references are outside the portable subset"));
|
|
102
|
+
for (const key of reserved.filter((key) => !["$description", "$type", "$value", "$extends", "$ref"].includes(key))) {
|
|
103
|
+
failures.push(finding("token_unknown_reserved", tokenPath, `unsupported reserved field ${key}`));
|
|
104
|
+
}
|
|
105
|
+
const declaredType = node.$type;
|
|
106
|
+
if (declaredType !== undefined && !allowedTypes.has(declaredType)) failures.push(finding("token_type_unsupported", tokenPath, `unsupported token type ${String(declaredType)}`));
|
|
107
|
+
const effectiveType = allowedTypes.has(declaredType) ? declaredType : inheritedType;
|
|
108
|
+
if (Object.hasOwn(node, "$value")) {
|
|
109
|
+
const allowed = new Set(["$description", "$type", "$value"]);
|
|
110
|
+
for (const key of Object.keys(node).filter((key) => !allowed.has(key) && !key.startsWith("$"))) failures.push(finding("token_property_unknown", tokenPath, `token contains child property ${key}`));
|
|
111
|
+
if (!effectiveType) failures.push(finding("token_type_required", tokenPath, "token requires an allowed explicit or inherited type"));
|
|
112
|
+
else tokens.push({ path: tokenPath, type: effectiveType, value: node.$value });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const children = Object.entries(node).filter(([key]) => !key.startsWith("$"));
|
|
116
|
+
if (children.length === 0) failures.push(finding("token_group_empty", tokenPath, "group must contain at least one token"));
|
|
117
|
+
for (const [key, child] of children) {
|
|
118
|
+
failures.push(...validatePathSegment(key, [...segments, key].join(".")));
|
|
119
|
+
walk(child, [...segments, key], effectiveType);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const [key, node] of Object.entries(document)) {
|
|
124
|
+
if (key.startsWith("$")) failures.push(finding("token_unknown_reserved", "", `root field ${key} is unsupported`));
|
|
125
|
+
else {
|
|
126
|
+
failures.push(...validatePathSegment(key, key));
|
|
127
|
+
walk(node, [key], undefined);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const token of tokens) {
|
|
131
|
+
const result = validateDirectValue(token);
|
|
132
|
+
failures.push(...result.failures);
|
|
133
|
+
references.push(...result.references);
|
|
134
|
+
}
|
|
135
|
+
const byPath = new Map(tokens.map((token) => [token.path, token]));
|
|
136
|
+
for (const reference of references) {
|
|
137
|
+
const target = byPath.get(reference.target);
|
|
138
|
+
if (!target) failures.push(finding("alias_dangling", reference.source, `alias target ${reference.target} does not exist`));
|
|
139
|
+
else if (target.type !== reference.expectedType) failures.push(finding("alias_type_mismatch", reference.source, `alias target ${reference.target} has type ${target.type}`));
|
|
140
|
+
}
|
|
141
|
+
const edges = new Map(references.filter((reference) => byPath.has(reference.target)).map((reference) => [reference.source, reference.target]));
|
|
142
|
+
for (const start of edges.keys()) {
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
let cursor = start;
|
|
145
|
+
while (edges.has(cursor)) {
|
|
146
|
+
if (seen.has(cursor)) {
|
|
147
|
+
failures.push(finding("alias_cycle", start, "alias graph contains a cycle"));
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
seen.add(cursor);
|
|
151
|
+
cursor = edges.get(cursor);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const uniqueFailures = [...new Map(failures.map((item) => [`${item.code}:${item.path}:${item.message}`, item])).values()];
|
|
155
|
+
return { failures: uniqueFailures, tokens: tokens.sort((left, right) => left.path.localeCompare(right.path)) };
|
|
156
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { canonicalSourceManifest } from "./page-evidence-contract.mjs";
|
|
9
|
+
|
|
10
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const workspaceRoot = process.cwd();
|
|
12
|
+
const temporaryRoot = path.join(workspaceRoot, ".tmp");
|
|
13
|
+
const syntheticBranch = "main";
|
|
14
|
+
const syntheticRepository = "example/stylegallery-page-evidence-ci";
|
|
15
|
+
const sourcePaths = [
|
|
16
|
+
"playwright.config.mjs",
|
|
17
|
+
"tests/consumer-conformance.spec.mjs",
|
|
18
|
+
"tests/fixtures/consumer-conformance-scenarios.mjs",
|
|
19
|
+
"tests/helpers/render-consumer-conformance.mjs",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function trustedTemporaryRoot() {
|
|
23
|
+
const resolvedWorkspace = path.resolve(workspaceRoot);
|
|
24
|
+
if (fs.realpathSync(resolvedWorkspace) !== resolvedWorkspace) throw new Error("workspace root must not be a filesystem redirect");
|
|
25
|
+
if (!fs.existsSync(temporaryRoot)) fs.mkdirSync(temporaryRoot);
|
|
26
|
+
const stat = fs.lstatSync(temporaryRoot);
|
|
27
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(temporaryRoot) !== temporaryRoot) throw new Error(".tmp must be a real directory inside the workspace");
|
|
28
|
+
return temporaryRoot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function containedPath({ base, candidate, label }) {
|
|
32
|
+
if (base === temporaryRoot) trustedTemporaryRoot();
|
|
33
|
+
const resolved = path.resolve(workspaceRoot, candidate);
|
|
34
|
+
const relative = path.relative(base, resolved);
|
|
35
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(label + " must be inside " + base);
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function run({ args, command, cwd }) {
|
|
40
|
+
const child = spawnSync(command, args, { cwd, encoding: "utf8" });
|
|
41
|
+
if (child.status !== 0) throw new Error(command + " " + args.join(" ") + " failed (" + child.status + ")\n" + child.stdout + child.stderr);
|
|
42
|
+
return child.stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeJson(file, value) {
|
|
46
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", { flag: "wx" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseArguments() {
|
|
51
|
+
const command = process.argv[2];
|
|
52
|
+
const values = {};
|
|
53
|
+
for (let index = 3; index < process.argv.length; index += 2) {
|
|
54
|
+
const name = process.argv[index];
|
|
55
|
+
const value = process.argv[index + 1];
|
|
56
|
+
if (!name?.startsWith("--") || !value || value.startsWith("--")) throw new Error("invalid argument pair at " + (name ?? "<missing>"));
|
|
57
|
+
values[name.slice(2)] = value;
|
|
58
|
+
}
|
|
59
|
+
if (!["prepare", "verify-session", "package", "cleanup"].includes(command)) throw new Error("first argument must be prepare, verify-session, package, or cleanup");
|
|
60
|
+
return { command, values };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function prepare(values) {
|
|
64
|
+
const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" });
|
|
65
|
+
const recordFile = containedPath({ base: consumerRoot, candidate: values.record, label: "--record" });
|
|
66
|
+
const artifactRoot = containedPath({ base: consumerRoot, candidate: values["artifact-root"], label: "--artifact-root" });
|
|
67
|
+
if (fs.existsSync(consumerRoot)) throw new Error("--consumer-root must not already exist");
|
|
68
|
+
if (!fs.statSync(path.join(repositoryRoot, "node_modules")).isDirectory()) throw new Error("node_modules must exist before preparing the consumer");
|
|
69
|
+
|
|
70
|
+
fs.mkdirSync(consumerRoot, { recursive: true });
|
|
71
|
+
run({ command: "git", args: ["init", "-b", syntheticBranch], cwd: consumerRoot });
|
|
72
|
+
run({ command: "git", args: ["config", "user.email", "page-evidence-ci@example.invalid"], cwd: consumerRoot });
|
|
73
|
+
run({ command: "git", args: ["config", "user.name", "StyleGallery page-evidence CI"], cwd: consumerRoot });
|
|
74
|
+
run({ command: "git", args: ["remote", "add", "origin", `https://github.com/${syntheticRepository}.git`], cwd: consumerRoot });
|
|
75
|
+
|
|
76
|
+
for (const reference of sourcePaths) {
|
|
77
|
+
const destination = path.join(consumerRoot, reference);
|
|
78
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
79
|
+
fs.copyFileSync(path.join(repositoryRoot, reference), destination, fs.constants.COPYFILE_EXCL);
|
|
80
|
+
}
|
|
81
|
+
fs.symlinkSync(path.join(repositoryRoot, "node_modules"), path.join(consumerRoot, "node_modules"), "dir");
|
|
82
|
+
run({ command: "git", args: ["add", ...sourcePaths], cwd: consumerRoot });
|
|
83
|
+
run({ command: "git", args: ["commit", "-m", "test: prepare consumer page evidence"], cwd: consumerRoot });
|
|
84
|
+
|
|
85
|
+
const revision = run({ command: "git", args: ["rev-parse", "HEAD"], cwd: consumerRoot });
|
|
86
|
+
const sourceFailures = [];
|
|
87
|
+
const source = canonicalSourceManifest(consumerRoot, sourcePaths, sourceFailures);
|
|
88
|
+
if (!source || sourceFailures.length > 0) throw new Error("source manifest failed: " + JSON.stringify(sourceFailures));
|
|
89
|
+
const runId = process.env.GITHUB_RUN_ID ? "github-" + process.env.GITHUB_RUN_ID : "local-" + revision.slice(0, 12);
|
|
90
|
+
const sessionId = "page-evidence-" + crypto.randomUUID();
|
|
91
|
+
const scenarioId = "responsive-layout";
|
|
92
|
+
const reference = (file) => path.relative(consumerRoot, file).split(path.sep).join("/");
|
|
93
|
+
|
|
94
|
+
writeJson(recordFile, {
|
|
95
|
+
claim_boundary: "Synthetic CI evidence for the copied consumer-conformance probe; not product certification.",
|
|
96
|
+
consumer: {
|
|
97
|
+
relevant_sources: sourcePaths,
|
|
98
|
+
repository: syntheticRepository,
|
|
99
|
+
revision,
|
|
100
|
+
},
|
|
101
|
+
id: "stylegallery-consumer-page-evidence-ci",
|
|
102
|
+
page_evidence: {
|
|
103
|
+
manifest: reference(path.join(artifactRoot, "page-evidence-manifest.json")),
|
|
104
|
+
status: "applicable",
|
|
105
|
+
},
|
|
106
|
+
record_kind: "consumer_migration_conformance",
|
|
107
|
+
scenarios: [{
|
|
108
|
+
assertions: ["The selected responsive layout case passes its browser assertions."],
|
|
109
|
+
argv: ["npx", "playwright", "test", "tests/consumer-conformance.spec.mjs", "--project=chromium", "--grep", "state-w1024-focus"],
|
|
110
|
+
evidence_method: "browser",
|
|
111
|
+
exit_code: 0,
|
|
112
|
+
id: scenarioId,
|
|
113
|
+
observable_actions: ["Render and capture state-w1024-focus in Chromium."],
|
|
114
|
+
result_artifact: reference(path.join(artifactRoot, "runner", scenarioId + ".json")),
|
|
115
|
+
run_id: runId,
|
|
116
|
+
session_id: sessionId,
|
|
117
|
+
source_digest: source.sha256,
|
|
118
|
+
}],
|
|
119
|
+
schema_version: "1.0",
|
|
120
|
+
});
|
|
121
|
+
return { artifactRoot, consumerRoot, recordFile, revision, sourceSha256: source.sha256 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function verifySession(values) {
|
|
125
|
+
const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" });
|
|
126
|
+
const artifactRoot = containedPath({ base: consumerRoot, candidate: values["artifact-root"], label: "--artifact-root" });
|
|
127
|
+
const receiptFile = path.join(artifactRoot, "page-evidence-session.json");
|
|
128
|
+
const receipt = JSON.parse(fs.readFileSync(receiptFile, "utf8"));
|
|
129
|
+
const expected = {
|
|
130
|
+
branch: syntheticBranch,
|
|
131
|
+
repository: syntheticRepository,
|
|
132
|
+
revision: run({ command: "git", args: ["rev-parse", "HEAD"], cwd: consumerRoot }),
|
|
133
|
+
};
|
|
134
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
135
|
+
if (receipt[field] !== value) throw new Error(`page-evidence receipt ${field} must be ${value}, received ${receipt[field]}`);
|
|
136
|
+
}
|
|
137
|
+
return { ...expected, receiptFile };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function packagePacket(values) {
|
|
141
|
+
const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" });
|
|
142
|
+
const artifactRoot = containedPath({ base: consumerRoot, candidate: values["artifact-root"], label: "--artifact-root" });
|
|
143
|
+
const output = containedPath({ base: temporaryRoot, candidate: values.output, label: "--output" });
|
|
144
|
+
const outputRelativeToConsumer = path.relative(consumerRoot, output);
|
|
145
|
+
if (!outputRelativeToConsumer.startsWith("..") && !path.isAbsolute(outputRelativeToConsumer)) throw new Error("--output must be outside --consumer-root");
|
|
146
|
+
if (!fs.statSync(artifactRoot).isDirectory()) throw new Error("--artifact-root must be a directory");
|
|
147
|
+
if (fs.existsSync(output)) throw new Error("--output must not already exist");
|
|
148
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
149
|
+
fs.cpSync(artifactRoot, output, { errorOnExist: true, force: false, recursive: true });
|
|
150
|
+
return { output };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cleanup(values) {
|
|
154
|
+
const consumerRoot = containedPath({ base: temporaryRoot, candidate: values["consumer-root"], label: "--consumer-root" });
|
|
155
|
+
if (fs.existsSync(consumerRoot)) {
|
|
156
|
+
const stat = fs.lstatSync(consumerRoot);
|
|
157
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(consumerRoot) !== consumerRoot) throw new Error("--consumer-root must be a real directory before cleanup");
|
|
158
|
+
}
|
|
159
|
+
fs.rmSync(consumerRoot, { force: true, recursive: true });
|
|
160
|
+
return { consumerRoot };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const { command, values } = parseArguments();
|
|
165
|
+
const result = command === "prepare" ? prepare(values) : command === "verify-session" ? verifySession(values) : command === "package" ? packagePacket(values) : cleanup(values);
|
|
166
|
+
process.stdout.write(JSON.stringify({ command, ok: true, ...result }, null, 2) + "\n");
|
|
167
|
+
} catch (error) {
|
|
168
|
+
process.stderr.write((error instanceof Error ? error.message : String(error)) + "\n");
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
|
|
5
|
+
import { createStyleGalleryMcpServer } from "./agent-native/mcp-adapter.mjs";
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const server = createStyleGalleryMcpServer();
|
|
9
|
+
const transport = new StdioServerTransport();
|
|
10
|
+
await server.connect(transport);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await main().catch((error) => {
|
|
14
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
});
|
package/scripts/sg.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
function emergencyReport(error) {
|
|
4
|
+
return {
|
|
5
|
+
failures: [{
|
|
6
|
+
code: error?.code ?? "cli_failed",
|
|
7
|
+
message: error instanceof Error ? error.message : String(error),
|
|
8
|
+
...(error?.path ? { path: error.path } : {}),
|
|
9
|
+
}],
|
|
10
|
+
ok: false,
|
|
11
|
+
operation: null,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
try {
|
|
17
|
+
const { runCli } = await import("./agent-native/cli-adapter.mjs");
|
|
18
|
+
const result = runCli(process.argv.slice(2));
|
|
19
|
+
process.stdout.write(result.output);
|
|
20
|
+
process.exitCode = result.exitCode;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stdout.write(`${JSON.stringify(emergencyReport(error), null, 2)}\n`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await main();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function parseStrictJson(source) {
|
|
2
|
+
const value = JSON.parse(source);
|
|
3
|
+
let index = 0;
|
|
4
|
+
|
|
5
|
+
function skipWhitespace() {
|
|
6
|
+
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function scanString() {
|
|
10
|
+
const start = index;
|
|
11
|
+
index += 1;
|
|
12
|
+
while (source[index] !== '"') {
|
|
13
|
+
if (source[index] === "\\") index += 1;
|
|
14
|
+
index += 1;
|
|
15
|
+
}
|
|
16
|
+
index += 1;
|
|
17
|
+
return JSON.parse(source.slice(start, index));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scanValue() {
|
|
21
|
+
skipWhitespace();
|
|
22
|
+
if (source[index] === "{") scanObject();
|
|
23
|
+
else if (source[index] === "[") scanArray();
|
|
24
|
+
else if (source[index] === '"') scanString();
|
|
25
|
+
else while (index < source.length && !/[\s,}\]]/.test(source[index])) index += 1;
|
|
26
|
+
skipWhitespace();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scanArray() {
|
|
30
|
+
index += 1;
|
|
31
|
+
skipWhitespace();
|
|
32
|
+
if (source[index] === "]") {
|
|
33
|
+
index += 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
while (index < source.length) {
|
|
37
|
+
scanValue();
|
|
38
|
+
if (source[index] === "]") {
|
|
39
|
+
index += 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scanObject() {
|
|
47
|
+
const keys = new Set();
|
|
48
|
+
index += 1;
|
|
49
|
+
skipWhitespace();
|
|
50
|
+
if (source[index] === "}") {
|
|
51
|
+
index += 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
while (index < source.length) {
|
|
55
|
+
const key = scanString();
|
|
56
|
+
if (keys.has(key)) throw new SyntaxError(`duplicate JSON property ${key}`);
|
|
57
|
+
keys.add(key);
|
|
58
|
+
skipWhitespace();
|
|
59
|
+
index += 1;
|
|
60
|
+
scanValue();
|
|
61
|
+
if (source[index] === "}") {
|
|
62
|
+
index += 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
index += 1;
|
|
66
|
+
skipWhitespace();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
scanValue();
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { BASELINE_ENVIRONMENT, BASELINE_REFERENCE, finding, sha256, validateCalibration } from "./baseline-contract.mjs";
|
|
6
|
+
import { parseCalibrationRun } from "./calibration-raw-contract.mjs";
|
|
7
|
+
|
|
8
|
+
const options = { artifactName: "", checkoutSha: "", executionRepository: "", headSha: "", input: "", json: false, output: "", repository: "", runAttempt: "", runId: "", sha: "", workflow: "" };
|
|
9
|
+
const valueOptions = new Map([
|
|
10
|
+
["--artifact-name", "artifactName"], ["--checkout-sha", "checkoutSha"], ["--execution-repository", "executionRepository"], ["--head-sha", "headSha"],
|
|
11
|
+
["--input", "input"], ["--output", "output"], ["--repository", "repository"],
|
|
12
|
+
["--run-attempt", "runAttempt"], ["--run-id", "runId"], ["--sha", "sha"], ["--workflow", "workflow"],
|
|
13
|
+
]);
|
|
14
|
+
const failures = [];
|
|
15
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
16
|
+
const argument = process.argv[index];
|
|
17
|
+
if (argument === "--json") options.json = true;
|
|
18
|
+
else if (valueOptions.has(argument)) {
|
|
19
|
+
const value = process.argv[index + 1];
|
|
20
|
+
if (!value) failures.push(finding("argument_value_required", "<cli>", `${argument} requires a value`));
|
|
21
|
+
else {
|
|
22
|
+
options[valueOptions.get(argument)] = value;
|
|
23
|
+
index += 1;
|
|
24
|
+
}
|
|
25
|
+
} else failures.push(finding("argument_unknown", "<cli>", `unsupported argument ${argument}`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!options.input || !options.output) failures.push(finding("calibration_path_required", "<cli>", "--input and --output are required"));
|
|
29
|
+
const inputIsDirectory = Boolean(options.input && fs.existsSync(options.input) && !fs.lstatSync(options.input).isSymbolicLink() && fs.lstatSync(options.input).isDirectory());
|
|
30
|
+
if (options.input && fs.existsSync(options.input) && !inputIsDirectory) failures.push(finding("calibration_input_invalid", options.input, "input must be a regular raw calibration directory, not a file or symlink"));
|
|
31
|
+
const expectedOutput = options.input ? path.join(path.resolve(options.input), "calibration.json") : "";
|
|
32
|
+
if (options.output && path.resolve(options.output) !== expectedOutput) failures.push(finding("calibration_output_route", options.output, "output must be calibration.json directly inside the raw input directory"));
|
|
33
|
+
if (inputIsDirectory && options.output && path.resolve(options.output) === expectedOutput && fs.existsSync(options.output)) {
|
|
34
|
+
if (fs.lstatSync(options.output).isDirectory()) failures.push(finding("calibration_output_invalid", options.output, "output path must not be a directory"));
|
|
35
|
+
else fs.rmSync(options.output);
|
|
36
|
+
}
|
|
37
|
+
const runs = [];
|
|
38
|
+
if (options.input && fs.existsSync(options.input)) {
|
|
39
|
+
if (inputIsDirectory) {
|
|
40
|
+
const directories = fs.readdirSync(options.input, { withFileTypes: true });
|
|
41
|
+
const expectedDirectories = Array.from({ length: 20 }, (_, index) => `run-${String(index + 1).padStart(2, "0")}`);
|
|
42
|
+
for (const directory of directories.filter((entry) => !entry.isDirectory() || !expectedDirectories.includes(entry.name))) {
|
|
43
|
+
failures.push(finding("calibration_run_directory_unknown", path.join(options.input, directory.name), "raw input may contain only run-01 through run-20 directories"));
|
|
44
|
+
}
|
|
45
|
+
for (let run = 1; run <= 20; run += 1) {
|
|
46
|
+
const name = expectedDirectories[run - 1];
|
|
47
|
+
const root = path.join(options.input, name);
|
|
48
|
+
if (!fs.existsSync(root)) failures.push(finding("calibration_run_directory_missing", root, `${name} is required`));
|
|
49
|
+
else if (fs.lstatSync(root).isSymbolicLink() || !fs.lstatSync(root).isDirectory() || !fs.realpathSync(root).startsWith(`${fs.realpathSync(options.input)}${path.sep}`)) failures.push(finding("calibration_run_directory_invalid", root, `${name} must be a contained regular directory, not a file or symlink`));
|
|
50
|
+
else {
|
|
51
|
+
const parsed = parseCalibrationRun(root, run, failures);
|
|
52
|
+
if (parsed) runs.push(parsed);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else if (options.input) failures.push(finding("calibration_input_missing", options.input, "input artifact directory does not exist"));
|
|
57
|
+
|
|
58
|
+
const rawEvidence = failures.length === 0 ? sha256(Array.from({ length: 20 }, (_, index) => path.join(options.input, `run-${String(index + 1).padStart(2, "0")}`))
|
|
59
|
+
.flatMap((root) => fs.readdirSync(root).sort().map((name) => {
|
|
60
|
+
const file = path.join(root, name);
|
|
61
|
+
return `${path.relative(options.input, file)}:${sha256(fs.readFileSync(file))}`;
|
|
62
|
+
})).join("\n")) : "";
|
|
63
|
+
|
|
64
|
+
const record = {
|
|
65
|
+
baseline_owner_approval: "pending",
|
|
66
|
+
committed_ci: {
|
|
67
|
+
artifact_name: options.artifactName,
|
|
68
|
+
checkout_sha: options.checkoutSha,
|
|
69
|
+
execution_repository: options.executionRepository,
|
|
70
|
+
external_verification: null,
|
|
71
|
+
head_sha: options.headSha,
|
|
72
|
+
raw_evidence_sha256: rawEvidence,
|
|
73
|
+
repository: options.repository,
|
|
74
|
+
run_attempt: options.runAttempt,
|
|
75
|
+
run_id: options.runId,
|
|
76
|
+
sha: options.sha,
|
|
77
|
+
workflow: options.workflow,
|
|
78
|
+
},
|
|
79
|
+
environment: BASELINE_ENVIRONMENT,
|
|
80
|
+
required_runs: 20,
|
|
81
|
+
reference: BASELINE_REFERENCE,
|
|
82
|
+
runs,
|
|
83
|
+
schema_version: "1.0",
|
|
84
|
+
status: "awaiting_external_verification",
|
|
85
|
+
};
|
|
86
|
+
failures.push(...validateCalibration(record, options.output || "<output>"));
|
|
87
|
+
if (failures.length === 0) {
|
|
88
|
+
const temporaryOutput = `${options.output}.tmp`;
|
|
89
|
+
fs.writeFileSync(temporaryOutput, `${JSON.stringify(record, null, 2)}\n`);
|
|
90
|
+
fs.renameSync(temporaryOutput, options.output);
|
|
91
|
+
}
|
|
92
|
+
const diffs = runs.map((run) => run.screenshot_diff_pixels).sort((left, right) => left - right);
|
|
93
|
+
const stats = diffs.length === 20 ? { max: diffs[19], mean: diffs.reduce((sum, value) => sum + value, 0) / 20, min: diffs[0], p95: diffs[18] } : null;
|
|
94
|
+
const uniqueFailures = [...new Map(failures.map((failure) => [`${failure.code}:${failure.path}:${failure.message}`, failure])).values()];
|
|
95
|
+
const result = { failures: uniqueFailures, ok: uniqueFailures.length === 0, runs: runs.length, stats, status: uniqueFailures.length === 0 ? "awaiting_external_verification" : "incomplete", warnings: [] };
|
|
96
|
+
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
97
|
+
else if (!result.ok) process.stderr.write(`${result.failures.map((failure) => `${failure.code}: ${failure.message}`).join("\n")}\n`);
|
|
98
|
+
if (!result.ok) process.exitCode = 1;
|