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,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseStrictJson } from "./strict-json.mjs";
|
|
4
|
+
|
|
5
|
+
const referenceKinds = Object.freeze({
|
|
6
|
+
component: { field: "component_records", pattern: /^components\/[a-z0-9-]+\.component\.json$/, required: 1 },
|
|
7
|
+
evidence: { field: "evidence_records", pattern: /^evidence\/[a-z0-9-]+\.evidence\.json$/, required: 1 },
|
|
8
|
+
fixture: { field: "fixture_records", pattern: /^fixtures\/[a-z0-9-]+\.fixture\.json$/, required: 1 },
|
|
9
|
+
generated: { field: "generated_records", pattern: /^generated\/[a-z0-9-]+\.md$/, required: 3 },
|
|
10
|
+
states: { field: "state_records", pattern: /^states\/[a-z0-9-]+\.states\.json$/, required: 1 },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function finding(code, file, message) {
|
|
14
|
+
return { code, message, path: file };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isInside(base, target) {
|
|
18
|
+
const relative = path.relative(base, target);
|
|
19
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isNormalizedRelative(reference) {
|
|
23
|
+
return typeof reference === "string"
|
|
24
|
+
&& reference.length > 0
|
|
25
|
+
&& !path.posix.isAbsolute(reference)
|
|
26
|
+
&& !path.win32.isAbsolute(reference)
|
|
27
|
+
&& !reference.includes("\\")
|
|
28
|
+
&& !reference.includes("?")
|
|
29
|
+
&& !reference.includes("#")
|
|
30
|
+
&& !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference)
|
|
31
|
+
&& path.posix.normalize(reference) === reference
|
|
32
|
+
&& !reference.split("/").some((segment) => segment === "." || segment === "..");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveReference(profileRoot, reference, kind, profileFile, failures) {
|
|
36
|
+
const definition = referenceKinds[kind];
|
|
37
|
+
if (!isNormalizedRelative(reference)) {
|
|
38
|
+
failures.push(finding("profile_reference_outside", profileFile, `${definition.field} contains an unsafe reference`));
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!definition.pattern.test(reference)) {
|
|
42
|
+
failures.push(finding("profile_reference_unknown", profileFile, `${definition.field} contains unknown reference ${reference}`));
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const target = path.resolve(profileRoot, reference);
|
|
46
|
+
if (!isInside(profileRoot, target)) {
|
|
47
|
+
failures.push(finding("profile_reference_outside", profileFile, `${reference} escapes its profile`));
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (!fs.existsSync(target)) {
|
|
51
|
+
failures.push(finding("profile_reference_missing", profileFile, `${reference} does not exist`));
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const metadata = fs.lstatSync(target);
|
|
55
|
+
if (metadata.isSymbolicLink()) {
|
|
56
|
+
failures.push(finding("profile_reference_symlink", profileFile, `${reference} must not be a symlink`));
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!metadata.isFile()) {
|
|
60
|
+
failures.push(finding("profile_reference_type_invalid", profileFile, `${reference} must be a regular file`));
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const realRoot = fs.realpathSync(profileRoot);
|
|
64
|
+
const realTarget = fs.realpathSync(target);
|
|
65
|
+
if (!isInside(realRoot, realTarget)) {
|
|
66
|
+
failures.push(finding("profile_reference_outside", profileFile, `${reference} resolves outside its profile`));
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
if (realTarget !== path.join(realRoot, reference)) {
|
|
70
|
+
failures.push(finding("profile_reference_redirect", profileFile, `${reference} resolves through a redirect`));
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (kind === "generated") return { path: realTarget, reference };
|
|
74
|
+
try {
|
|
75
|
+
return { path: realTarget, reference, value: parseStrictJson(fs.readFileSync(realTarget, "utf8")) };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
failures.push(finding("profile_reference_json_invalid", reference, error instanceof Error ? error.message : String(error)));
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveFixtureLinks(resolved, profileFile, failures) {
|
|
83
|
+
const fixture = resolved.fixture[0];
|
|
84
|
+
const component = resolved.component[0];
|
|
85
|
+
const states = resolved.states[0];
|
|
86
|
+
if (!fixture || !component || !states) return;
|
|
87
|
+
for (const [field, expected] of [["component_record", component.path], ["state_record", states.path]]) {
|
|
88
|
+
const reference = fixture.value[field];
|
|
89
|
+
const safe = typeof reference === "string"
|
|
90
|
+
&& reference.length > 0
|
|
91
|
+
&& !path.posix.isAbsolute(reference)
|
|
92
|
+
&& !path.win32.isAbsolute(reference)
|
|
93
|
+
&& !reference.includes("\\")
|
|
94
|
+
&& !reference.includes("?")
|
|
95
|
+
&& !reference.includes("#")
|
|
96
|
+
&& !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference)
|
|
97
|
+
&& path.posix.normalize(reference) === reference;
|
|
98
|
+
if (!safe) {
|
|
99
|
+
failures.push(finding("fixture_reference_invalid", fixture.reference, `${field} must be normalized and profile-local`));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const actual = path.resolve(path.dirname(fixture.path), reference);
|
|
103
|
+
if (actual !== expected) failures.push(finding("fixture_reference_mismatch", fixture.reference, `${field} does not resolve to the profile-declared record`));
|
|
104
|
+
}
|
|
105
|
+
if (fixture.value.profile_id !== component.value.profile_id || fixture.value.profile_id !== states.value.profile_id) {
|
|
106
|
+
failures.push(finding("profile_identity_mismatch", profileFile, "fixture links cross profile identities"));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveProfileRecords(profileRoot, failures) {
|
|
111
|
+
const profileFile = path.join(profileRoot, "profile.json");
|
|
112
|
+
if (!fs.existsSync(profileFile) || fs.lstatSync(profileFile).isSymbolicLink() || !fs.lstatSync(profileFile).isFile()) {
|
|
113
|
+
failures.push(finding("profile_file_invalid", profileFile, "profile.json must be a regular non-symlink file"));
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
let profile;
|
|
117
|
+
try {
|
|
118
|
+
profile = parseStrictJson(fs.readFileSync(profileFile, "utf8"));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
failures.push(finding("profile_json_invalid", profileFile, error instanceof Error ? error.message : String(error)));
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const resolved = {};
|
|
124
|
+
for (const [kind, definition] of Object.entries(referenceKinds)) {
|
|
125
|
+
const references = profile[definition.field];
|
|
126
|
+
if (!Array.isArray(references)) {
|
|
127
|
+
failures.push(finding("profile_reference_required", profileFile, `${definition.field} must contain exactly ${definition.required} references`));
|
|
128
|
+
resolved[kind] = [];
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (new Set(references).size !== references.length) failures.push(finding("profile_reference_duplicate", profileFile, `${definition.field} must not repeat references`));
|
|
132
|
+
if (references.length !== definition.required) failures.push(finding("profile_reference_required", profileFile, `${definition.field} must contain exactly ${definition.required} references`));
|
|
133
|
+
resolved[kind] = references.map((reference) => resolveReference(profileRoot, reference, kind, profileFile, failures)).filter(Boolean);
|
|
134
|
+
}
|
|
135
|
+
for (const record of [resolved.component[0], resolved.evidence[0], resolved.fixture[0], resolved.states[0]].filter(Boolean)) {
|
|
136
|
+
if (record.value.profile_id !== profile.id) failures.push(finding("profile_identity_mismatch", record.reference, `record profile_id must equal ${profile.id}`));
|
|
137
|
+
}
|
|
138
|
+
resolveFixtureLinks(resolved, profileFile, failures);
|
|
139
|
+
return { profile, profileFile, records: resolved };
|
|
140
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isPlainObject } from "./consumer-reference-schema.mjs";
|
|
4
|
+
import { canonicalGovernedProfilePaths, isIndependentPromotionProfile } from "./governed-profile-registry.mjs";
|
|
5
|
+
|
|
6
|
+
function finding(code, message, recordPath) {
|
|
7
|
+
return { code, message, path: recordPath };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function safeProfilePath(relative, profileRoot, recordPath, failures) {
|
|
11
|
+
if (typeof relative !== "string" || !/^(?!\/)(?!.*\/\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?![A-Za-z][A-Za-z0-9+.-]*:)[^\\?#]+\.json$/.test(relative)) {
|
|
12
|
+
failures.push(finding("promotion_profile_path_invalid", "profile_record must be a normalized repository-relative JSON path", recordPath));
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const target = path.resolve(profileRoot, relative);
|
|
16
|
+
if (target === profileRoot || !target.startsWith(`${profileRoot}${path.sep}`)) {
|
|
17
|
+
failures.push(finding("promotion_profile_path_invalid", "profile_record escapes the repository", recordPath));
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
let current = profileRoot;
|
|
21
|
+
for (const segment of path.relative(profileRoot, target).split(path.sep)) {
|
|
22
|
+
current = path.join(current, segment);
|
|
23
|
+
if (!fs.existsSync(current)) {
|
|
24
|
+
failures.push(finding("promotion_profile_missing", `${relative} does not exist`, recordPath));
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
28
|
+
failures.push(finding("promotion_profile_path_symlink", `${relative} traverses a symlink`, recordPath));
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!fs.lstatSync(target).isFile()) {
|
|
33
|
+
failures.push(finding("promotion_profile_invalid", `${relative} must be a regular JSON file`, recordPath));
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const realRoot = fs.realpathSync(profileRoot);
|
|
37
|
+
const real = fs.realpathSync(target);
|
|
38
|
+
if (!real.startsWith(`${realRoot}${path.sep}`) || real !== path.join(realRoot, relative)) {
|
|
39
|
+
failures.push(finding("promotion_profile_path_invalid", `${relative} resolves outside the repository`, recordPath));
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return relative;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validatePromotionAttestations({ inventory, profileRoot, recordPath, value }) {
|
|
46
|
+
const attestations = Array.isArray(value.attestations) ? value.attestations : [];
|
|
47
|
+
const consumers = new Set();
|
|
48
|
+
const countedContexts = new Set();
|
|
49
|
+
const countedOrganizations = new Set();
|
|
50
|
+
const failures = [];
|
|
51
|
+
const identities = new Set();
|
|
52
|
+
let eligibleCount = 0;
|
|
53
|
+
for (const attestation of attestations) {
|
|
54
|
+
if (!isPlainObject(attestation)) continue;
|
|
55
|
+
const identity = `${attestation.consumer_id ?? ""}:${attestation.context_id ?? ""}:${attestation.organization_id ?? ""}`;
|
|
56
|
+
if (identities.has(identity) || consumers.has(attestation.consumer_id)) failures.push(finding("promotion_attestation_duplicate", `duplicate consumer or attestation identity ${identity}`, recordPath));
|
|
57
|
+
identities.add(identity);
|
|
58
|
+
consumers.add(attestation.consumer_id);
|
|
59
|
+
const counted = attestation.count_toward_gate === true;
|
|
60
|
+
if (attestation.profile_record === undefined) {
|
|
61
|
+
failures.push(finding("promotion_attestation_profile_required", `${identity} has no canonical repository-local profile record`, recordPath));
|
|
62
|
+
if (counted) failures.push(finding("promotion_independence_unproven", `${identity} cannot establish counted independence without a profile record`, recordPath));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const registeredPath = safeProfilePath(attestation.profile_record, profileRoot, recordPath, failures);
|
|
66
|
+
if (!registeredPath) continue;
|
|
67
|
+
const registered = inventory.records.get(registeredPath);
|
|
68
|
+
if (!canonicalGovernedProfilePaths.includes(registeredPath) || !registered || registered.failures.length > 0 || inventory.failures.length > 0) {
|
|
69
|
+
failures.push(finding("promotion_independence_unproven", `${attestation.consumer_id} is not a fully validated registered governed profile`, recordPath));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const profile = registered.profile;
|
|
73
|
+
if (!isPlainObject(profile)) continue;
|
|
74
|
+
const identityMatches = profile.id === attestation.consumer_id;
|
|
75
|
+
if (!identityMatches) failures.push(finding("promotion_profile_identity_mismatch", `${attestation.profile_record} does not match ${attestation.consumer_id}`, recordPath));
|
|
76
|
+
if (!counted) continue;
|
|
77
|
+
if (attestation.relationship !== "independent") failures.push(finding("promotion_related_consumer_counted", `${identity} is related and cannot count toward the independent gateway`, recordPath));
|
|
78
|
+
if (countedOrganizations.has(attestation.organization_id) || countedContexts.has(attestation.context_id)) {
|
|
79
|
+
failures.push(finding("promotion_independence_unproven", `${identity} does not establish a distinct organization and context`, recordPath));
|
|
80
|
+
}
|
|
81
|
+
countedOrganizations.add(attestation.organization_id);
|
|
82
|
+
countedContexts.add(attestation.context_id);
|
|
83
|
+
const canonicalIndependent = profile.fixture_independence === "independent" && typeof profile.related_fixture_set_id !== "string";
|
|
84
|
+
if (!canonicalIndependent) failures.push(finding("promotion_related_consumer_counted", `${attestation.consumer_id} is a canonical related fixture and cannot count`, recordPath));
|
|
85
|
+
if (identityMatches && attestation.relationship === "independent" && canonicalIndependent && isIndependentPromotionProfile(profile)) eligibleCount += 1;
|
|
86
|
+
else if (identityMatches && canonicalIndependent && !isIndependentPromotionProfile(profile)) {
|
|
87
|
+
failures.push(finding("promotion_independence_unproven", `${attestation.consumer_id} is not lifecycle- and scope-eligible independent adoption evidence`, recordPath));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { eligibleCount, failures };
|
|
91
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function copyTreeNoSymlinks(source, target) {
|
|
5
|
+
fs.cpSync(source, target, { recursive: true, filter: (candidate) => {
|
|
6
|
+
if (fs.lstatSync(candidate).isSymbolicLink()) throw new Error(`refusing to copy symlink ${candidate}`);
|
|
7
|
+
return true;
|
|
8
|
+
} });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function copyFixtureRoot(fixtureRoot, tempRoot, name) {
|
|
12
|
+
const root = path.join(tempRoot, name);
|
|
13
|
+
copyTreeNoSymlinks(fixtureRoot, root);
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function editJson(file, mutate) {
|
|
18
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
19
|
+
mutate(value);
|
|
20
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function result(runArguments, name, args, expected) {
|
|
24
|
+
const child = runArguments(args);
|
|
25
|
+
const codes = child.report.failures.map((failure) => failure.code);
|
|
26
|
+
return { actual: { codes, status: child.status }, expected, name, ok: child.status > 0 && codes.includes(expected) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function testPromotionBoundaries({ fixtureRoot, repositoryRoot, runArguments, tempRoot }) {
|
|
30
|
+
const results = [];
|
|
31
|
+
for (const [name, args] of [["file_option_value", ["--file", "--json"]], ["policy_option_value", ["--policy", "--json"]], ["profile_root_option_value", ["--profile-repository-root", "--json"]]]) {
|
|
32
|
+
results.push(result(runArguments, name, args, "argument_value_required"));
|
|
33
|
+
}
|
|
34
|
+
const yamlRfc = path.join(tempRoot, "promotion.yaml");
|
|
35
|
+
const yamlPolicy = path.join(tempRoot, "policy.yaml");
|
|
36
|
+
fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), yamlRfc);
|
|
37
|
+
fs.copyFileSync(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json"), yamlPolicy);
|
|
38
|
+
results.push(result(runArguments, "rfc_yaml_extension", ["--file", yamlRfc, "--json"], "promotion_json_path_required"));
|
|
39
|
+
results.push(result(runArguments, "policy_yaml_extension", ["--policy", yamlPolicy, "--json"], "promotion_json_path_required"));
|
|
40
|
+
|
|
41
|
+
const linkedRfc = path.join(tempRoot, "linked-rfc.json");
|
|
42
|
+
const linkedPolicy = path.join(tempRoot, "linked-policy.json");
|
|
43
|
+
fs.symlinkSync(path.join(fixtureRoot, "valid-deferred-example.json"), linkedRfc);
|
|
44
|
+
fs.symlinkSync(path.join(repositoryRoot, "consumer-reference/policies/shared-experimental.json"), linkedPolicy);
|
|
45
|
+
results.push(result(runArguments, "rfc_symlink", ["--file", linkedRfc, "--json"], "promotion_input_path_invalid"));
|
|
46
|
+
results.push(result(runArguments, "policy_symlink", ["--policy", linkedPolicy, "--json"], "promotion_input_path_invalid"));
|
|
47
|
+
|
|
48
|
+
for (const [name, mutate] of [
|
|
49
|
+
["inventory_extra_json", (root) => fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), path.join(root, "extra.json"))],
|
|
50
|
+
["inventory_extra_yaml", (root) => fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), path.join(root, "extra.yaml"))],
|
|
51
|
+
["inventory_missing", (root) => fs.rmSync(path.join(root, "invalid-count-only.json"))],
|
|
52
|
+
["inventory_duplicate", (root) => editJson(path.join(root, "manifest.json"), (value) => value.valid_records.push(value.valid_records[0]))],
|
|
53
|
+
]) {
|
|
54
|
+
const root = copyFixtureRoot(fixtureRoot, tempRoot, name);
|
|
55
|
+
mutate(root);
|
|
56
|
+
results.push(result(runArguments, name, ["--promotion-fixture-root", root, "--json"], "promotion_fixture_inventory_invalid"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rejectedRoot = copyFixtureRoot(fixtureRoot, tempRoot, "canonical-rejected");
|
|
60
|
+
editJson(path.join(rejectedRoot, "valid-deferred-example.json"), (value) => { value.decision = "rejected"; });
|
|
61
|
+
results.push(result(runArguments, "canonical_rejected", ["--promotion-fixture-root", rejectedRoot, "--json"], "promotion_canonical_example_invalid"));
|
|
62
|
+
const attestedRoot = copyFixtureRoot(fixtureRoot, tempRoot, "canonical-attested");
|
|
63
|
+
editJson(path.join(attestedRoot, "valid-deferred-example.json"), (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "test", consumer_id: "fake", count_toward_gate: false, organization_id: "test", relationship: "related" }]; });
|
|
64
|
+
results.push(result(runArguments, "canonical_attested", ["--promotion-fixture-root", attestedRoot, "--json"], "promotion_canonical_example_invalid"));
|
|
65
|
+
|
|
66
|
+
const unbound = path.join(tempRoot, "unbound.json");
|
|
67
|
+
const duplicateEvidence = path.join(tempRoot, "duplicate-evidence.json");
|
|
68
|
+
fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), unbound);
|
|
69
|
+
editJson(unbound, (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "test", consumer_id: "fake", count_toward_gate: false, organization_id: "test", relationship: "related" }]; });
|
|
70
|
+
fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), duplicateEvidence);
|
|
71
|
+
editJson(duplicateEvidence, (value) => { value.evidence.push(structuredClone(value.evidence[0])); });
|
|
72
|
+
results.push(result(runArguments, "unbound_uncounted", ["--file", unbound, "--json"], "promotion_attestation_profile_required"));
|
|
73
|
+
results.push(result(runArguments, "duplicate_evidence_id", ["--file", duplicateEvidence, "--json"], "promotion_evidence_duplicate"));
|
|
74
|
+
results.push(result(runArguments, "missing_profile_root", ["--file", path.join(fixtureRoot, "valid-deferred-example.json"), "--profile-repository-root", path.join(tempRoot, "missing-root"), "--json"], "promotion_profile_registry_invalid"));
|
|
75
|
+
const linkedProfileRoot = path.join(tempRoot, "linked-profile-root");
|
|
76
|
+
fs.symlinkSync(repositoryRoot, linkedProfileRoot);
|
|
77
|
+
results.push(result(runArguments, "symlink_profile_root", ["--file", path.join(fixtureRoot, "valid-deferred-example.json"), "--profile-repository-root", linkedProfileRoot, "--json"], "promotion_profile_registry_invalid"));
|
|
78
|
+
const linkedManifestRoot = copyFixtureRoot(fixtureRoot, tempRoot, "linked-manifest-root");
|
|
79
|
+
fs.rmSync(path.join(linkedManifestRoot, "manifest.json"));
|
|
80
|
+
fs.symlinkSync(path.join(fixtureRoot, "manifest.json"), path.join(linkedManifestRoot, "manifest.json"));
|
|
81
|
+
results.push(result(runArguments, "symlink_manifest", ["--promotion-fixture-root", linkedManifestRoot, "--json"], "promotion_fixture_inventory_invalid"));
|
|
82
|
+
|
|
83
|
+
const strictRoot = path.join(tempRoot, "strict-profile-root");
|
|
84
|
+
copyTreeNoSymlinks(path.join(repositoryRoot, "design-engineering/reference-profiles/governed-local"), path.join(strictRoot, "design-engineering/reference-profiles/governed-local"));
|
|
85
|
+
const foundations = path.join(strictRoot, "design-engineering/reference-profiles/governed-local/editorial/local-foundations.json");
|
|
86
|
+
fs.writeFileSync(foundations, fs.readFileSync(foundations, "utf8").replace('"identity":', '"identity":"Duplicate",\n "identity":'));
|
|
87
|
+
const counted = path.join(tempRoot, "strict-counted.json");
|
|
88
|
+
fs.copyFileSync(path.join(fixtureRoot, "valid-deferred-example.json"), counted);
|
|
89
|
+
editJson(counted, (value) => { value.attestations = [{ attested_by: "Synthetic", context_id: "editorial", consumer_id: "editorial-reference-profile", count_toward_gate: true, organization_id: "test", profile_record: "design-engineering/reference-profiles/governed-local/editorial/profile.json", relationship: "independent" }]; });
|
|
90
|
+
results.push(result(runArguments, "duplicate_foundation_identity", ["--file", counted, "--profile-repository-root", strictRoot, "--json"], "promotion_independence_unproven"));
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseStrictJson } from "./strict-json.mjs";
|
|
4
|
+
|
|
5
|
+
const validRecords = Object.freeze(["valid-deferred-example.json", "valid-normative-bypass.json"]);
|
|
6
|
+
const invalidRecords = Object.freeze([
|
|
7
|
+
["invalid-blocking-evidence.json", "promotion_blocking_evidence_failed"],
|
|
8
|
+
["invalid-count-only.json", "promotion_count_only_forbidden"],
|
|
9
|
+
["invalid-missing-owner-migration.json", "promotion_owner_migration_required"],
|
|
10
|
+
["invalid-normative-bypass-no-regression.json", "promotion_normative_regression_required"],
|
|
11
|
+
["invalid-related-counted-independent.json", "promotion_related_consumer_counted"],
|
|
12
|
+
["invalid-stable-ended-support.json", "stable_support_ended"],
|
|
13
|
+
]);
|
|
14
|
+
const policyRecord = "consumer-reference/policies/shared-experimental.json";
|
|
15
|
+
const schemaRecord = "consumer-reference/schema/promotion-rfc.schema.json";
|
|
16
|
+
|
|
17
|
+
function finding(code, message, recordPath) {
|
|
18
|
+
return { code, message, path: recordPath };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isInside(base, target) {
|
|
22
|
+
const relative = path.relative(base, target);
|
|
23
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveContained({ code, expectedType, repositoryRoot, target }, failures) {
|
|
27
|
+
const root = path.resolve(repositoryRoot);
|
|
28
|
+
const resolved = path.resolve(target);
|
|
29
|
+
const recordPath = path.relative(root, resolved) || resolved;
|
|
30
|
+
if (!isInside(root, resolved) || !fs.existsSync(resolved)) {
|
|
31
|
+
failures.push(finding(code, "promotion input must exist inside the repository", recordPath));
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (fs.realpathSync(root) !== root) {
|
|
35
|
+
failures.push(finding(code, "repository root must not be a filesystem redirect", recordPath));
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
let current = root;
|
|
39
|
+
for (const segment of path.relative(root, resolved).split(path.sep).filter(Boolean)) {
|
|
40
|
+
current = path.join(current, segment);
|
|
41
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
42
|
+
failures.push(finding(code, "promotion input must not traverse a symlink", recordPath));
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const metadata = fs.lstatSync(resolved);
|
|
47
|
+
if ((expectedType === "file" && !metadata.isFile()) || (expectedType === "directory" && !metadata.isDirectory()) || fs.realpathSync(resolved) !== resolved) {
|
|
48
|
+
failures.push(finding(code, `promotion input must be a contained regular ${expectedType}`, recordPath));
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolvePromotionJsonFile({ file, repositoryRoot }, failures) {
|
|
55
|
+
const recordPath = path.relative(repositoryRoot, path.resolve(file)) || file;
|
|
56
|
+
if (path.extname(file) !== ".json") {
|
|
57
|
+
failures.push(finding("promotion_json_path_required", "promotion records must use the .json extension", recordPath));
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return resolveContained({ code: "promotion_input_path_invalid", expectedType: "file", repositoryRoot, target: file }, failures);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sameSet(actual, expected) {
|
|
64
|
+
return Array.isArray(actual) && actual.length === expected.length && new Set(actual).size === expected.length && expected.every((entry) => actual.includes(entry));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validManifest(value) {
|
|
68
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
69
|
+
const keys = ["id", "invalid_records", "policy_record", "schema_record", "schema_version", "valid_records"];
|
|
70
|
+
if (!sameSet(Object.keys(value), keys) || value.id !== "promotion-fixture-inventory" || value.schema_version !== "1.0" || value.policy_record !== policyRecord || value.schema_record !== schemaRecord || !sameSet(value.valid_records, validRecords)) return false;
|
|
71
|
+
if (!Array.isArray(value.invalid_records) || value.invalid_records.length !== invalidRecords.length) return false;
|
|
72
|
+
const encoded = value.invalid_records.map((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && sameSet(Object.keys(entry), ["expected_code", "file"]) ? `${entry.file}:${entry.expected_code}` : "");
|
|
73
|
+
return sameSet(encoded, invalidRecords.map(([file, code]) => `${file}:${code}`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function loadPromotionFixtureInventory({ fixtureRoot, repositoryRoot }) {
|
|
77
|
+
const failures = [];
|
|
78
|
+
const root = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "directory", repositoryRoot, target: fixtureRoot }, failures);
|
|
79
|
+
if (!root) return { failures, invalidRecords: [], policyFile: undefined, schema: undefined, validFiles: [] };
|
|
80
|
+
const manifestFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(root, "manifest.json") }, failures);
|
|
81
|
+
let manifest;
|
|
82
|
+
try {
|
|
83
|
+
if (manifestFile) manifest = parseStrictJson(fs.readFileSync(manifestFile, "utf8"));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
failures.push(finding("promotion_fixture_inventory_invalid", error instanceof Error ? error.message : String(error), path.relative(repositoryRoot, manifestFile)));
|
|
86
|
+
}
|
|
87
|
+
if (!validManifest(manifest)) failures.push(finding("promotion_fixture_inventory_invalid", "promotion manifest must match the canonical closed inventory", path.relative(repositoryRoot, manifestFile ?? root)));
|
|
88
|
+
const expectedFiles = ["manifest.json", ...validRecords, ...invalidRecords.map(([file]) => file)];
|
|
89
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
90
|
+
if (!sameSet(entries.map((entry) => entry.name), expectedFiles) || entries.some((entry) => !entry.isFile() || path.extname(entry.name) !== ".json")) {
|
|
91
|
+
failures.push(finding("promotion_fixture_inventory_invalid", "promotion fixture directory must contain only the declared JSON closed set", path.relative(repositoryRoot, root)));
|
|
92
|
+
}
|
|
93
|
+
for (const file of expectedFiles) resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(root, file) }, failures);
|
|
94
|
+
const policyFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(repositoryRoot, policyRecord) }, failures);
|
|
95
|
+
const schemaFile = resolveContained({ code: "promotion_fixture_inventory_invalid", expectedType: "file", repositoryRoot, target: path.join(repositoryRoot, schemaRecord) }, failures);
|
|
96
|
+
let schema;
|
|
97
|
+
try {
|
|
98
|
+
if (schemaFile) schema = parseStrictJson(fs.readFileSync(schemaFile, "utf8"));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
failures.push(finding("promotion_fixture_inventory_invalid", error instanceof Error ? error.message : String(error), schemaRecord));
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
failures: [...new Map(failures.map((issue) => [`${issue.code}:${issue.path}:${issue.message}`, issue])).values()],
|
|
104
|
+
invalidRecords: invalidRecords.map(([file, expectedCode]) => ({ expectedCode, file: path.join(root, file) })),
|
|
105
|
+
policyFile,
|
|
106
|
+
schema,
|
|
107
|
+
validFiles: validRecords.map((file) => path.join(root, file)),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const requirements = [
|
|
2
|
+
["GOVERNANCE.md", "The StyleGallery-local `>=2` independent-consumer gateway applies only to consumer-local → shared-experimental invariant eligibility."],
|
|
3
|
+
["GOVERNANCE.md", "Shared stable has no numeric adoption threshold"],
|
|
4
|
+
["GOVERNANCE.md", "may waive adoption count only"],
|
|
5
|
+
["GOVERNANCE.md", "zero durable adopter attestations and record no promotion"],
|
|
6
|
+
["GOVERNANCE.md", "never be silently relabeled shared-experimental"],
|
|
7
|
+
["DOMAINS.md", "### Consumer Reference Promotion"],
|
|
8
|
+
["DOMAINS.md", "Promotion records are JSON-only"],
|
|
9
|
+
["consumer-reference/contract.md", "They are not accepted or promoted RFCs."],
|
|
10
|
+
["consumer-reference/contract.md", "boolean aliases are invalid"],
|
|
11
|
+
["consumer-reference/index.md", "[Promotion RFC schema](schema/promotion-rfc.schema.json)"],
|
|
12
|
+
["consumer-reference/index.md", "[Shared-experimental promotion policy](policies/shared-experimental.json)"],
|
|
13
|
+
["consumer-reference/fixtures/promotion/manifest.json", '"id": "promotion-fixture-inventory"'],
|
|
14
|
+
["quality/evidence/executable-evidence.md", "Promotion governance remains a closed JSON-only, invariant-scoped, deferred example contract."],
|
|
15
|
+
["quality/evidence/executable-evidence.md", "Synthetic validation does not authenticate adoption, organizations, owners, support capacity, provenance, or a promotion decision"],
|
|
16
|
+
[".github/workflows/validate.yml", "node -c scripts/promotion-fixture-inventory.mjs"],
|
|
17
|
+
[".github/workflows/validate.yml", "node -c scripts/promotion-boundary-test-contract.mjs"],
|
|
18
|
+
["consumer-reference/policies/shared-experimental.json", '"review_independence": "single_account"'],
|
|
19
|
+
["consumer-reference/policies/shared-experimental.json", '"promotion_occurred": false'],
|
|
20
|
+
["consumer-reference/policies/shared-experimental.json", '"adopter_attestations": 0'],
|
|
21
|
+
["consumer-reference/policies/shared-experimental.json", '"stable_numeric_threshold": null'],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function promotionGovernanceFailures(read) {
|
|
25
|
+
return requirements
|
|
26
|
+
.filter(([relative, text]) => !read(relative).includes(text))
|
|
27
|
+
.map(([relative, text]) => `${relative}: missing ${text}`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function isContained(root, target) {
|
|
5
|
+
const relative = path.relative(root, target);
|
|
6
|
+
return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function inspectArtifactPath(root, candidate, allowMissing) {
|
|
10
|
+
const trustedRoot = fs.realpathSync(root);
|
|
11
|
+
const target = path.resolve(root, candidate);
|
|
12
|
+
if (!isContained(trustedRoot, target)) return { ok: false, path: target, reason: "outside_trust_root" };
|
|
13
|
+
|
|
14
|
+
const segments = path.relative(trustedRoot, target).split(path.sep);
|
|
15
|
+
let current = trustedRoot;
|
|
16
|
+
for (const [index, segment] of segments.entries()) {
|
|
17
|
+
current = path.join(current, segment);
|
|
18
|
+
if (!fs.existsSync(current)) {
|
|
19
|
+
return allowMissing ? { ok: true, path: target } : { ok: false, path: target, reason: "missing" };
|
|
20
|
+
}
|
|
21
|
+
const metadata = fs.lstatSync(current);
|
|
22
|
+
if (metadata.isSymbolicLink()) return { ok: false, path: target, reason: "symbolic_link" };
|
|
23
|
+
const finalSegment = index === segments.length - 1;
|
|
24
|
+
if (finalSegment && !metadata.isFile()) return { ok: false, path: target, reason: "not_regular_file" };
|
|
25
|
+
if (!finalSegment && !metadata.isDirectory()) return { ok: false, path: target, reason: "parent_not_directory" };
|
|
26
|
+
}
|
|
27
|
+
return { ok: true, path: target };
|
|
28
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isPlainObject } from "./consumer-reference-schema.mjs";
|
|
5
|
+
import { validatePortableTokens } from "./reference-token-contract.mjs";
|
|
6
|
+
import { parseStrictJson } from "./strict-json.mjs";
|
|
7
|
+
|
|
8
|
+
const layoutSourceSha = "775430bbaf4ee208a642220f440f6926d79c90a3";
|
|
9
|
+
const requiredResetFields = ["body_margin", "box_sizing", "figure_margin"];
|
|
10
|
+
|
|
11
|
+
function hash(content) {
|
|
12
|
+
return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function finding(code, relative, message) {
|
|
16
|
+
return { code, message, path: relative };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isInside(base, target) {
|
|
20
|
+
const relative = path.relative(base, target);
|
|
21
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson({ failures, kind, relative, root }) {
|
|
25
|
+
const absolute = path.resolve(root, relative);
|
|
26
|
+
if (!fs.existsSync(absolute)) {
|
|
27
|
+
failures.push(finding(`${kind}_unresolved`, relative, "profile artifact must exist beside profile.json"));
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const realRoot = fs.realpathSync(root);
|
|
31
|
+
const realTarget = fs.realpathSync(absolute);
|
|
32
|
+
if (!isInside(realRoot, realTarget)) {
|
|
33
|
+
failures.push(finding(`${kind}_symlink_escape`, relative, "profile artifact resolves outside the repository root"));
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (realTarget !== path.join(realRoot, relative)) {
|
|
37
|
+
failures.push(finding(`${kind}_redirect`, relative, "profile artifact must remain at its exact sibling path"));
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!fs.statSync(realTarget).isFile()) {
|
|
41
|
+
failures.push(finding(`${kind}_type_invalid`, relative, "profile artifact must be a regular JSON file"));
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const bytes = fs.readFileSync(realTarget);
|
|
45
|
+
try {
|
|
46
|
+
return { bytes, value: parseStrictJson(bytes.toString("utf8")) };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
failures.push(finding(`${kind}_json_invalid`, relative, error instanceof Error ? error.message : String(error)));
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateReferenceProfile({ item, relative, root }) {
|
|
54
|
+
const failures = [];
|
|
55
|
+
const add = (code, message) => failures.push(finding(code, relative, message));
|
|
56
|
+
if (item.profile_kind !== "governed_local") add("profile_kind_governed_local", "profile_kind must be governed_local");
|
|
57
|
+
if (item.artifact_mode !== "governed_local") add("profile_artifact_mode_governed_local", "governed-local profiles cannot be external consumers");
|
|
58
|
+
if (item.maturity !== "experimental") add("profile_maturity_experimental", "governed-local profiles must remain experimental");
|
|
59
|
+
if (item.example_only !== true) add("profile_example_only_required", "governed-local profiles must be example_only");
|
|
60
|
+
if (item.default !== false) add("profile_default_forbidden", "governed-local profiles cannot be implicit or default");
|
|
61
|
+
if (item.layout_source_sha !== layoutSourceSha) add("profile_layout_source_sha_required", `layout_source_sha must pin ${layoutSourceSha}`);
|
|
62
|
+
if (typeof item.related_fixture_set_id !== "string" || item.related_fixture_set_id.length === 0) add("profile_related_fixture_set_required", "related_fixture_set_id is required");
|
|
63
|
+
if (!isPlainObject(item.selection) || item.selection.required !== true || item.selection.method !== "profile_path") add("profile_explicit_selection_required", "selection must require an explicit profile_path");
|
|
64
|
+
if (!isPlainObject(item.environment_assumptions) || typeof item.environment_assumptions.user_agent_styles !== "string" || item.environment_assumptions.user_agent_styles.length === 0) {
|
|
65
|
+
add("profile_ua_assumption_required", "user-agent style assumptions must be explicit");
|
|
66
|
+
}
|
|
67
|
+
const reset = isPlainObject(item.environment_assumptions) ? item.environment_assumptions.reset : null;
|
|
68
|
+
if (!isPlainObject(reset) || requiredResetFields.some((field) => typeof reset[field] !== "string" || reset[field].length === 0)) {
|
|
69
|
+
add("profile_reset_assumptions_required", "body, box-sizing, and figure reset assumptions must be explicit");
|
|
70
|
+
}
|
|
71
|
+
if (item.tokens !== "tokens.dtcg.json") add("profile_tokens_path_required", "tokens must resolve to the sibling tokens.dtcg.json");
|
|
72
|
+
if (item.local_foundations !== "local-foundations.json") add("profile_foundations_path_required", "foundations must resolve to the sibling local-foundations.json");
|
|
73
|
+
|
|
74
|
+
const directory = path.posix.dirname(relative);
|
|
75
|
+
const tokenRelative = path.posix.join(directory, "tokens.dtcg.json");
|
|
76
|
+
const foundationRelative = path.posix.join(directory, "local-foundations.json");
|
|
77
|
+
const tokenDocument = readJson({ failures, kind: "profile_tokens", relative: tokenRelative, root });
|
|
78
|
+
const foundations = readJson({ failures, kind: "profile_foundations", relative: foundationRelative, root });
|
|
79
|
+
if (tokenDocument) {
|
|
80
|
+
for (const tokenFailure of validatePortableTokens(tokenDocument.value).failures) {
|
|
81
|
+
failures.push(finding(tokenFailure.code, tokenRelative, tokenFailure.message));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (foundations) {
|
|
85
|
+
if (!isPlainObject(foundations.value)
|
|
86
|
+
|| foundations.value.profile_id !== item.id
|
|
87
|
+
|| typeof foundations.value.identity !== "string"
|
|
88
|
+
|| !isPlainObject(foundations.value.bindings)) {
|
|
89
|
+
failures.push(finding("profile_foundations_invalid", foundationRelative, "foundations require matching profile_id, identity, and bindings"));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const summary = tokenDocument && foundations ? {
|
|
94
|
+
foundationHash: hash(foundations.bytes),
|
|
95
|
+
id: item.id,
|
|
96
|
+
identity: foundations.value.identity,
|
|
97
|
+
layoutSourceSha: item.layout_source_sha,
|
|
98
|
+
relatedFixtureSetId: item.related_fixture_set_id,
|
|
99
|
+
tokenHash: hash(tokenDocument.bytes),
|
|
100
|
+
} : null;
|
|
101
|
+
return { failures, summary };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function validateReferenceProfileSet(summaries) {
|
|
105
|
+
if (summaries.length < 2) return [];
|
|
106
|
+
const failures = [];
|
|
107
|
+
const unique = (field) => new Set(summaries.map((summary) => summary[field])).size;
|
|
108
|
+
if (unique("layoutSourceSha") !== 1) failures.push(finding("profile_layout_source_mismatch", "<profiles>", "profiles must share one Layout source SHA"));
|
|
109
|
+
if (unique("relatedFixtureSetId") !== 1) failures.push(finding("profile_related_set_mismatch", "<profiles>", "profiles must share one related fixture set"));
|
|
110
|
+
if (unique("identity") !== summaries.length) failures.push(finding("profile_identity_not_distinct", "<profiles>", "adversarial profiles must declare distinct identities"));
|
|
111
|
+
if (unique("tokenHash") !== summaries.length) failures.push(finding("profile_tokens_not_distinct", "<profiles>", "adversarial profiles must have distinct token bytes"));
|
|
112
|
+
if (unique("foundationHash") !== summaries.length) failures.push(finding("profile_foundations_not_distinct", "<profiles>", "adversarial profiles must have distinct foundation bytes"));
|
|
113
|
+
return failures;
|
|
114
|
+
}
|