stylegallery 0.1.0 → 0.1.2
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 +4 -1
- package/DOMAINS.md +2 -6
- package/GOVERNANCE.md +10 -9
- package/README.md +29 -127
- package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
- package/consumer-reference/agent-native/v2/material-registry.json +1 -0
- package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
- package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
- package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
- package/consumer-reference/contract.md +1 -1
- package/consumer-reference/index.md +5 -3
- package/layout/index.md +124 -0
- package/log.md +57 -0
- package/package.json +183 -22
- package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
- package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
- package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
- package/scripts/agent-native/v2/material-admission.mjs +295 -0
- package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
- package/scripts/agent-native/v2/material-context.mjs +251 -0
- package/scripts/agent-native/v2/material-identity.mjs +71 -0
- package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
- package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
- package/scripts/agent-native/v2/material-queries.mjs +398 -0
- package/scripts/agent-native/v2/material-registry.mjs +319 -0
- package/scripts/sg-material-mcp.mjs +98 -0
- package/scripts/sg-material.mjs +27 -0
- package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
- package/consumer-reference/baselines/calibration.json +0 -260
- package/consumer-reference/baselines/manifest.json +0 -21
- package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
- package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
- package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
- package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
- package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
- package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
- package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
- package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
- package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
- package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
- package/consumer-reference/fixtures/promotion/manifest.json +0 -15
- package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
- package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
- package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
- package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
- package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
- package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
- package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
- package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
- package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
- package/consumer-reference/fixtures/valid-deprecated.json +0 -21
- package/consumer-reference/fixtures/valid-experimental.json +0 -19
- package/consumer-reference/generated/manifest.json +0 -40
- package/consumer-reference/generated/tokens.css +0 -12
- package/consumer-reference/policies/shared-experimental.json +0 -37
- package/consumer-reference/schema/ax-evidence.schema.json +0 -30
- package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
- package/consumer-reference/schema/calibration-record.schema.json +0 -190
- package/consumer-reference/schema/capture-session.schema.json +0 -161
- package/consumer-reference/schema/component-state.schema.json +0 -150
- package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
- package/consumer-reference/schema/dom-evidence.schema.json +0 -38
- package/consumer-reference/schema/evidence-record.schema.json +0 -81
- package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
- package/consumer-reference/schema/item.schema.json +0 -188
- package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
- package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
- package/consumer-reference/schema/portable-tokens.schema.json +0 -104
- package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
- package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
- package/consumer-reference/schema/visual-evidence.schema.json +0 -31
- package/design-engineering/consumer-migration-readiness.md +0 -124
- package/design-engineering/interface-craft.md +0 -107
- package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
- package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
- package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
- package/game-ui/classification.md +0 -82
- package/game-ui/reference-record.md +0 -62
- package/game-ui/screen-hierarchy.md +0 -107
- package/game-ui/unity/architecture.md +0 -166
- package/game-ui/unity/cli-loop.md +0 -84
- package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
- package/game-ui/unity/org-wiki.md +0 -146
- package/game-ui/unity/repository-map.md +0 -102
- package/game-ui/unity/ui-systems.md +0 -85
- package/motion/practice-reference.md +0 -145
- package/motion/review-workflow.md +0 -86
- package/motion/vocabulary.md +0 -82
- package/platform-guides/apple-interaction.md +0 -89
- package/scripts/agent-native/events.mjs +0 -113
- package/scripts/agent-native/immutable-store.mjs +0 -102
- package/scripts/artifact-metadata.mjs +0 -20
- package/scripts/audit-evidence-freshness.mjs +0 -308
- package/scripts/baseline-contract.mjs +0 -189
- package/scripts/baseline-schema-parity.mjs +0 -40
- package/scripts/build-reference-artifacts.mjs +0 -127
- package/scripts/calibration-raw-contract.mjs +0 -172
- package/scripts/capture-session-contract.mjs +0 -187
- package/scripts/component-state-artifact-fixture.mjs +0 -44
- package/scripts/component-state-contract.mjs +0 -99
- package/scripts/component-state-semantics.mjs +0 -128
- package/scripts/component-state-workflow-contract.mjs +0 -67
- package/scripts/component-state-workflow-negative-cases.mjs +0 -64
- package/scripts/consumer-conformance-contract.mjs +0 -198
- package/scripts/consumer-evidence-governance-contract.mjs +0 -210
- package/scripts/consumer-reference-case-runner.mjs +0 -93
- package/scripts/consumer-reference-schema.mjs +0 -155
- package/scripts/create-component-state-session.mjs +0 -100
- package/scripts/create-page-evidence-session.mjs +0 -139
- package/scripts/domain-document-boundaries.mjs +0 -40
- package/scripts/domain-validation-checks.mjs +0 -141
- package/scripts/evidence-artifact-contract.mjs +0 -191
- package/scripts/finalize-component-state-evidence.mjs +0 -165
- package/scripts/finalize-page-evidence.mjs +0 -197
- package/scripts/generate-consumer-reference-evidence.mjs +0 -154
- package/scripts/generate-patterns.mjs +0 -218
- package/scripts/governance-matrix-contract.mjs +0 -73
- package/scripts/governance-matrix-negative-cases.mjs +0 -114
- package/scripts/governance-policy-contract.mjs +0 -49
- package/scripts/governance-test-fixture.mjs +0 -321
- package/scripts/governance-workflow-negative-cases.mjs +0 -241
- package/scripts/governed-profile-registry.mjs +0 -125
- package/scripts/json-schema-formats.mjs +0 -31
- package/scripts/markdown-structure.mjs +0 -45
- package/scripts/page-artifact-metadata.mjs +0 -124
- package/scripts/page-evidence-contract.mjs +0 -297
- package/scripts/page-evidence-fixture.mjs +0 -271
- package/scripts/pattern-data.mjs +0 -325
- package/scripts/profile-record-contract.mjs +0 -140
- package/scripts/promotion-attestation-contract.mjs +0 -91
- package/scripts/promotion-boundary-test-contract.mjs +0 -92
- package/scripts/promotion-fixture-inventory.mjs +0 -109
- package/scripts/promotion-governance-contract.mjs +0 -28
- package/scripts/reference-artifact-path-contract.mjs +0 -28
- package/scripts/reference-profile-contract.mjs +0 -114
- package/scripts/reference-token-contract.mjs +0 -156
- package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
- package/scripts/summarize-sentinel-calibration.mjs +0 -98
- package/scripts/test-agent-adapter-conformance.mjs +0 -250
- package/scripts/test-agent-conformance-receipt.mjs +0 -106
- package/scripts/test-agent-events.mjs +0 -81
- package/scripts/test-agent-execution-effects.mjs +0 -114
- package/scripts/test-agent-execution-fixtures.mjs +0 -140
- package/scripts/test-agent-execution-kernel.mjs +0 -39
- package/scripts/test-agent-execution-learning.mjs +0 -130
- package/scripts/test-agent-execution-lifecycle.mjs +0 -143
- package/scripts/test-agent-execution-safety.mjs +0 -158
- package/scripts/test-agent-execution-views.mjs +0 -117
- package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
- package/scripts/test-agent-knowledge-kernel.mjs +0 -183
- package/scripts/test-agent-learning.mjs +0 -246
- package/scripts/test-agent-mcp.mjs +0 -246
- package/scripts/test-agent-native-schemas.mjs +0 -215
- package/scripts/test-agent-projections.mjs +0 -249
- package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
- package/scripts/test-agent-registry-mutations.mjs +0 -110
- package/scripts/test-audit-evidence-freshness.mjs +0 -177
- package/scripts/test-component-state-sentinel.mjs +0 -35
- package/scripts/test-component-state-source-contract.mjs +0 -83
- package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
- package/scripts/test-consumer-reference-sentinel.mjs +0 -31
- package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
- package/scripts/test-json-cli-output.mjs +0 -151
- package/scripts/test-json-schema-formats.mjs +0 -40
- package/scripts/test-reference-adapters.mjs +0 -199
- package/scripts/test-renderer-purity.mjs +0 -34
- package/scripts/test-sg-cli.mjs +0 -129
- package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
- package/scripts/test-validate-baseline-manifest.mjs +0 -234
- package/scripts/test-validate-component-state-artifacts.mjs +0 -243
- package/scripts/test-validate-component-state.mjs +0 -106
- package/scripts/test-validate-consumer-conformance.mjs +0 -427
- package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
- package/scripts/test-validate-consumer-reference.mjs +0 -263
- package/scripts/test-validate-domains.mjs +0 -357
- package/scripts/test-validate-governance.mjs +0 -292
- package/scripts/test-validate-ia.mjs +0 -262
- package/scripts/test-validate-okf.mjs +0 -73
- package/scripts/test-validate-page-evidence.mjs +0 -302
- package/scripts/test-validate-patterns.mjs +0 -157
- package/scripts/test-validate-promotion-rfc.mjs +0 -250
- package/scripts/test-validate-reference-profiles.mjs +0 -170
- package/scripts/test-validate-unity-org-wiki.mjs +0 -57
- package/scripts/test-validate-unity-source-contracts.mjs +0 -67
- package/scripts/test-validate-vocabulary.mjs +0 -174
- package/scripts/test-validate-webpage-workflow.mjs +0 -293
- package/scripts/validate-baseline-manifest.mjs +0 -44
- package/scripts/validate-catalog.mjs +0 -65
- package/scripts/validate-component-state.mjs +0 -141
- package/scripts/validate-consumer-conformance.mjs +0 -477
- package/scripts/validate-consumer-reference.mjs +0 -288
- package/scripts/validate-domains.mjs +0 -237
- package/scripts/validate-governance.mjs +0 -248
- package/scripts/validate-ia.mjs +0 -136
- package/scripts/validate-links.mjs +0 -66
- package/scripts/validate-okf.mjs +0 -104
- package/scripts/validate-page-evidence.mjs +0 -191
- package/scripts/validate-patterns.mjs +0 -246
- package/scripts/validate-promotion-rfc.mjs +0 -224
- package/scripts/validate-reference-artifacts.mjs +0 -125
- package/scripts/validate-renderer-purity.mjs +0 -56
- package/scripts/validate-unity-org-wiki.mjs +0 -146
- package/scripts/validate-unity-source-contracts.mjs +0 -107
- package/scripts/validate-vocabulary.mjs +0 -115
- package/scripts/validate-webpage-workflow.mjs +0 -254
- package/scripts/visual-expectation-contract.mjs +0 -20
- package/scripts/workflow-action-contract.mjs +0 -163
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import Ajv2020 from "ajv/dist/2020.js";
|
|
5
|
-
import { isPlainObject, validateItemSchema } from "./consumer-reference-schema.mjs";
|
|
6
|
-
import { resolveProfileRecords } from "./profile-record-contract.mjs";
|
|
7
|
-
import { validateReferenceProfile, validateReferenceProfileSet } from "./reference-profile-contract.mjs";
|
|
8
|
-
import { parseStrictJson } from "./strict-json.mjs";
|
|
9
|
-
|
|
10
|
-
const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const itemSchema = parseStrictJson(fs.readFileSync(path.join(moduleRoot, "consumer-reference/schema/item.schema.json"), "utf8"));
|
|
12
|
-
const validateFullItemSchema = new Ajv2020({ allErrors: true, strict: false }).compile(itemSchema);
|
|
13
|
-
|
|
14
|
-
export const canonicalGovernedProfilePaths = Object.freeze([
|
|
15
|
-
"design-engineering/reference-profiles/governed-local/editorial/profile.json",
|
|
16
|
-
"design-engineering/reference-profiles/governed-local/terminal/profile.json",
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
function finding(code, recordPath, message) {
|
|
20
|
-
return { code, message, path: recordPath };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function isInside(base, target) {
|
|
24
|
-
const relative = path.relative(base, target);
|
|
25
|
-
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function resolveRegisteredFile(root, relative, failures) {
|
|
29
|
-
if (!canonicalGovernedProfilePaths.includes(relative)) {
|
|
30
|
-
failures.push(finding("profile_registry_unregistered", relative, "profile path is not in the canonical governed profile inventory"));
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
const resolvedRoot = path.resolve(root);
|
|
34
|
-
const target = path.resolve(resolvedRoot, relative);
|
|
35
|
-
if (!isInside(resolvedRoot, target) || target !== path.join(resolvedRoot, relative)) {
|
|
36
|
-
failures.push(finding("profile_registry_path_invalid", relative, "registered profile path must remain normalized and repository-local"));
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
let current = resolvedRoot;
|
|
40
|
-
for (const segment of relative.split("/")) {
|
|
41
|
-
current = path.join(current, segment);
|
|
42
|
-
if (!fs.existsSync(current)) {
|
|
43
|
-
failures.push(finding("profile_registry_missing", relative, "registered profile path does not exist"));
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
47
|
-
failures.push(finding("profile_registry_symlink", relative, "registered profile path must not traverse a symlink"));
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (!fs.lstatSync(target).isFile()) {
|
|
52
|
-
failures.push(finding("profile_registry_type_invalid", relative, "registered profile must be a regular JSON file"));
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
const realRoot = fs.realpathSync(resolvedRoot);
|
|
56
|
-
const realTarget = fs.realpathSync(target);
|
|
57
|
-
if (!isInside(realRoot, realTarget) || realTarget !== path.join(realRoot, relative)) {
|
|
58
|
-
failures.push(finding("profile_registry_redirect", relative, "registered profile must not resolve through a filesystem redirect"));
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
return realTarget;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function validateGovernedProfile({ relative, root }) {
|
|
65
|
-
const failures = [];
|
|
66
|
-
const file = resolveRegisteredFile(root, relative, failures);
|
|
67
|
-
if (!file) return { failures, profile: undefined, summary: undefined };
|
|
68
|
-
let profile;
|
|
69
|
-
try {
|
|
70
|
-
profile = parseStrictJson(fs.readFileSync(file, "utf8"));
|
|
71
|
-
} catch (error) {
|
|
72
|
-
failures.push(finding("profile_json_invalid", relative, error instanceof Error ? error.message : String(error)));
|
|
73
|
-
return { failures, profile: undefined, summary: undefined };
|
|
74
|
-
}
|
|
75
|
-
if (!validateFullItemSchema(profile)) {
|
|
76
|
-
for (const error of validateFullItemSchema.errors ?? []) failures.push(finding("item_schema_invalid", relative, `${error.instancePath || "/"} ${error.message}`));
|
|
77
|
-
}
|
|
78
|
-
for (const issue of validateItemSchema(profile, itemSchema)) failures.push(finding(issue.code, relative, issue.message));
|
|
79
|
-
const profileValidation = validateReferenceProfile({ item: profile, relative, root });
|
|
80
|
-
failures.push(...profileValidation.failures);
|
|
81
|
-
const records = resolveProfileRecords(path.dirname(file), failures);
|
|
82
|
-
if (profile.handoff?.status !== "declared" || profile.handoff?.record !== relative) {
|
|
83
|
-
failures.push(finding("profile_registry_handoff_mismatch", relative, "registered profile handoff must name its exact inventory path"));
|
|
84
|
-
}
|
|
85
|
-
if (records?.profile?.id !== profile.id) failures.push(finding("profile_identity_mismatch", relative, "resolved profile identity must match the validated registry item"));
|
|
86
|
-
return { failures, profile, summary: profileValidation.summary };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function validateGovernedProfileInventory({ root }) {
|
|
90
|
-
const records = new Map();
|
|
91
|
-
const failures = [];
|
|
92
|
-
const summaries = [];
|
|
93
|
-
const resolvedRoot = path.resolve(root);
|
|
94
|
-
if (!fs.existsSync(resolvedRoot) || fs.lstatSync(resolvedRoot).isSymbolicLink() || !fs.lstatSync(resolvedRoot).isDirectory() || fs.realpathSync(resolvedRoot) !== resolvedRoot) {
|
|
95
|
-
failures.push(finding("profile_registry_root_invalid", resolvedRoot, "governed profile repository root must be a regular non-symlink directory"));
|
|
96
|
-
return { failures, records };
|
|
97
|
-
}
|
|
98
|
-
for (const relative of canonicalGovernedProfilePaths) {
|
|
99
|
-
const result = validateGovernedProfile({ relative, root });
|
|
100
|
-
records.set(relative, result);
|
|
101
|
-
failures.push(...result.failures);
|
|
102
|
-
if (result.summary) summaries.push(result.summary);
|
|
103
|
-
}
|
|
104
|
-
const identities = new Map();
|
|
105
|
-
for (const [relative, result] of records) {
|
|
106
|
-
if (!isPlainObject(result.profile) || typeof result.profile.id !== "string") continue;
|
|
107
|
-
const previous = identities.get(result.profile.id);
|
|
108
|
-
if (previous) failures.push(finding("profile_registry_identity_duplicate", relative, `profile identity duplicates ${previous}`));
|
|
109
|
-
else identities.set(result.profile.id, relative);
|
|
110
|
-
}
|
|
111
|
-
failures.push(...validateReferenceProfileSet(summaries));
|
|
112
|
-
const uniqueFailures = [...new Map(failures.map((issue) => [`${issue.code}:${issue.path}:${issue.message}`, issue])).values()];
|
|
113
|
-
return { failures: uniqueFailures, records };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function isIndependentPromotionProfile(profile) {
|
|
117
|
-
return isPlainObject(profile)
|
|
118
|
-
&& profile.profile_kind === "governed_local"
|
|
119
|
-
&& profile.artifact_mode === "governed_local"
|
|
120
|
-
&& profile.fixture_independence === "independent"
|
|
121
|
-
&& !Object.hasOwn(profile, "related_fixture_set_id")
|
|
122
|
-
&& profile.example_only === false
|
|
123
|
-
&& profile.maturity !== "deprecated"
|
|
124
|
-
&& profile.support?.status === "active";
|
|
125
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import addFormats from "ajv-formats";
|
|
2
|
-
|
|
3
|
-
const rfc3339DateTime = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
4
|
-
|
|
5
|
-
export function isRfc3339DateTime(value) {
|
|
6
|
-
if (typeof value !== "string") return false;
|
|
7
|
-
const match = rfc3339DateTime.exec(value);
|
|
8
|
-
if (!match || !Number.isFinite(Date.parse(value))) return false;
|
|
9
|
-
|
|
10
|
-
const year = Number(match.groups.year);
|
|
11
|
-
const month = Number(match.groups.month);
|
|
12
|
-
const day = Number(match.groups.day);
|
|
13
|
-
const hour = Number(match.groups.hour);
|
|
14
|
-
const minute = Number(match.groups.minute);
|
|
15
|
-
const second = Number(match.groups.second);
|
|
16
|
-
const canonical = new Date(0);
|
|
17
|
-
canonical.setUTCFullYear(year, month - 1, day);
|
|
18
|
-
canonical.setUTCHours(hour, minute, second, 0);
|
|
19
|
-
|
|
20
|
-
return canonical.getUTCFullYear() === year
|
|
21
|
-
&& canonical.getUTCMonth() === month - 1
|
|
22
|
-
&& canonical.getUTCDate() === day
|
|
23
|
-
&& canonical.getUTCHours() === hour
|
|
24
|
-
&& canonical.getUTCMinutes() === minute
|
|
25
|
-
&& canonical.getUTCSeconds() === second;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function addDateTimeFormat(ajv) {
|
|
29
|
-
addFormats(ajv, ["date-time"]);
|
|
30
|
-
return ajv;
|
|
31
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export function stripFencedCodeBlocks(content) {
|
|
2
|
-
const kept = [];
|
|
3
|
-
let fence = null;
|
|
4
|
-
|
|
5
|
-
for (const line of content.split("\n")) {
|
|
6
|
-
const marker = line.match(/^ {0,3}(`{3,})[^`]*$/)?.[1] ?? line.match(/^ {0,3}(~{3,}).*$/)?.[1];
|
|
7
|
-
if (!fence && marker) {
|
|
8
|
-
fence = { character: marker[0], length: marker.length };
|
|
9
|
-
continue;
|
|
10
|
-
}
|
|
11
|
-
if (fence) {
|
|
12
|
-
const closing = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/)?.[1];
|
|
13
|
-
if (closing && closing[0] === fence.character && closing.length >= fence.length) fence = null;
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
kept.push(line);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return kept.join("\n");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function stripHtmlComments(content) {
|
|
23
|
-
return content.replace(/<!--[\s\S]*?(?:-->|$)/g, "");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function structuralMarkdown(content) {
|
|
27
|
-
return stripFencedCodeBlocks(stripHtmlComments(content));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function markdownLinkDestinations(content) {
|
|
31
|
-
const body = structuralMarkdown(content);
|
|
32
|
-
const destinations = [];
|
|
33
|
-
const inline = /!?\[[^\]]*\]\(\s*<?([^\s)>]+)>?(?:\s+[^)]*)?\)/g;
|
|
34
|
-
const reference = /^ {0,3}\[[^\]]+\]:\s*<?([^\s>]+)>?(?:\s+.*)?$/gm;
|
|
35
|
-
for (const pattern of [inline, reference]) {
|
|
36
|
-
for (const match of body.matchAll(pattern)) destinations.push(match[1]);
|
|
37
|
-
}
|
|
38
|
-
return destinations;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function isOmoDependency(destination) {
|
|
42
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(destination) || destination.startsWith("#")) return false;
|
|
43
|
-
const pathOnly = destination.split(/[?#]/, 1)[0];
|
|
44
|
-
return pathOnly.split(/[\\/]/).includes(".omo");
|
|
45
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import { inflateSync } from "node:zlib";
|
|
3
|
-
|
|
4
|
-
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5
|
-
const pngBitDepths = new Map([
|
|
6
|
-
[0, new Set([1, 2, 4, 8, 16])],
|
|
7
|
-
[2, new Set([8, 16])],
|
|
8
|
-
[3, new Set([1, 2, 4, 8])],
|
|
9
|
-
[4, new Set([8, 16])],
|
|
10
|
-
[6, new Set([8, 16])],
|
|
11
|
-
]);
|
|
12
|
-
const pngChannels = new Map([[0, 1], [2, 3], [3, 1], [4, 2], [6, 4]]);
|
|
13
|
-
const adam7Passes = Object.freeze([
|
|
14
|
-
[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4],
|
|
15
|
-
[0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2],
|
|
16
|
-
]);
|
|
17
|
-
const maxDecodedPngBytes = 256 * 1024 * 1024;
|
|
18
|
-
export const MAX_ARTIFACT_FILE_BYTES = 64 * 1024 * 1024;
|
|
19
|
-
const maxPngChunkBytes = 32 * 1024 * 1024;
|
|
20
|
-
const maxPngCompressedBytes = 64 * 1024 * 1024;
|
|
21
|
-
const crcTable = Uint32Array.from({ length: 256 }, (_, index) => {
|
|
22
|
-
let value = index;
|
|
23
|
-
for (let bit = 0; bit < 8; bit += 1) value = (value >>> 1) ^ (0xedb88320 & -(value & 1));
|
|
24
|
-
return value >>> 0;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
function crc32(bytes) {
|
|
28
|
-
let value = 0xffffffff;
|
|
29
|
-
for (const byte of bytes) value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
|
|
30
|
-
return (value ^ 0xffffffff) >>> 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function passExtent(size, start, step) {
|
|
34
|
-
return size <= start ? 0 : Math.ceil((size - start) / step);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function validateDecodedPng(idat, { bitDepth, colorType, height, interlace, width }) {
|
|
38
|
-
const bitsPerPixel = pngChannels.get(colorType) * bitDepth;
|
|
39
|
-
const passes = interlace === 0 ? [[0, 0, 1, 1]] : adam7Passes;
|
|
40
|
-
const rows = passes.flatMap(([startX, startY, stepX, stepY]) => {
|
|
41
|
-
const passWidth = passExtent(width, startX, stepX);
|
|
42
|
-
const passHeight = passExtent(height, startY, stepY);
|
|
43
|
-
return passWidth === 0 || passHeight === 0 ? [] : [{ bytes: Math.ceil((passWidth * bitsPerPixel) / 8), count: passHeight }];
|
|
44
|
-
});
|
|
45
|
-
const expected = rows.reduce((total, row) => total + (row.bytes + 1) * row.count, 0);
|
|
46
|
-
if (!Number.isSafeInteger(expected) || expected <= 0 || expected > maxDecodedPngBytes) return false;
|
|
47
|
-
const compressed = Buffer.concat(idat);
|
|
48
|
-
let decoded;
|
|
49
|
-
let consumed;
|
|
50
|
-
try {
|
|
51
|
-
const inflated = inflateSync(compressed, { info: true, maxOutputLength: expected + 1 });
|
|
52
|
-
decoded = inflated.buffer;
|
|
53
|
-
consumed = inflated.engine.bytesWritten;
|
|
54
|
-
}
|
|
55
|
-
catch { return false; }
|
|
56
|
-
if (consumed !== compressed.length || decoded.length !== expected) return false;
|
|
57
|
-
let offset = 0;
|
|
58
|
-
for (const row of rows) {
|
|
59
|
-
for (let index = 0; index < row.count; index += 1) {
|
|
60
|
-
if (decoded[offset] > 4) return false;
|
|
61
|
-
offset += row.bytes + 1;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return offset === decoded.length;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function pngDimensions(bytes) {
|
|
68
|
-
if (bytes.length > MAX_ARTIFACT_FILE_BYTES) return undefined;
|
|
69
|
-
if (bytes.length < 8 || !bytes.subarray(0, 8).equals(pngSignature)) return undefined;
|
|
70
|
-
let dimensions;
|
|
71
|
-
let idatClosed = false;
|
|
72
|
-
let sawIdat = false;
|
|
73
|
-
const idat = [];
|
|
74
|
-
let compressedBytes = 0;
|
|
75
|
-
let header;
|
|
76
|
-
let offset = 8;
|
|
77
|
-
while (offset < bytes.length) {
|
|
78
|
-
if (bytes.length - offset < 12) return undefined;
|
|
79
|
-
const length = bytes.readUInt32BE(offset);
|
|
80
|
-
if (length > maxPngChunkBytes) return undefined;
|
|
81
|
-
const typeOffset = offset + 4;
|
|
82
|
-
const dataOffset = typeOffset + 4;
|
|
83
|
-
const crcOffset = dataOffset + length;
|
|
84
|
-
const nextOffset = crcOffset + 4;
|
|
85
|
-
if (nextOffset > bytes.length) return undefined;
|
|
86
|
-
const typeBytes = bytes.subarray(typeOffset, dataOffset);
|
|
87
|
-
if ([...typeBytes].some((byte) => !((byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122)))) return undefined;
|
|
88
|
-
if (crc32(bytes.subarray(typeOffset, crcOffset)) !== bytes.readUInt32BE(crcOffset)) return undefined;
|
|
89
|
-
const type = typeBytes.toString("ascii");
|
|
90
|
-
if (offset === 8 && type !== "IHDR") return undefined;
|
|
91
|
-
if (type === "IHDR") {
|
|
92
|
-
if (offset !== 8 || length !== 13 || dimensions) return undefined;
|
|
93
|
-
const width = bytes.readUInt32BE(dataOffset);
|
|
94
|
-
const height = bytes.readUInt32BE(dataOffset + 4);
|
|
95
|
-
const bitDepth = bytes[dataOffset + 8];
|
|
96
|
-
const colorType = bytes[dataOffset + 9];
|
|
97
|
-
const bitDepths = pngBitDepths.get(colorType);
|
|
98
|
-
if (width === 0 || width > 0x7fffffff || height === 0 || height > 0x7fffffff || !bitDepths?.has(bitDepth)) return undefined;
|
|
99
|
-
if (bytes[dataOffset + 10] !== 0 || bytes[dataOffset + 11] !== 0 || bytes[dataOffset + 12] > 1) return undefined;
|
|
100
|
-
header = { bitDepth, colorType, height, interlace: bytes[dataOffset + 12], width };
|
|
101
|
-
dimensions = { height, width };
|
|
102
|
-
} else if (type === "IDAT") {
|
|
103
|
-
if (!dimensions || idatClosed) return undefined;
|
|
104
|
-
sawIdat = true;
|
|
105
|
-
idat.push(bytes.subarray(dataOffset, crcOffset));
|
|
106
|
-
compressedBytes += length;
|
|
107
|
-
if (compressedBytes > maxPngCompressedBytes) return undefined;
|
|
108
|
-
} else if (type === "IEND") {
|
|
109
|
-
return length === 0 && nextOffset === bytes.length && dimensions && sawIdat && validateDecodedPng(idat, header) ? dimensions : undefined;
|
|
110
|
-
} else if (sawIdat) idatClosed = true;
|
|
111
|
-
offset = nextOffset;
|
|
112
|
-
}
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function artifactMetadata(bytes, mediaType) {
|
|
117
|
-
const metadata = {
|
|
118
|
-
byte_length: bytes.length,
|
|
119
|
-
media_type: mediaType,
|
|
120
|
-
sha256: `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`,
|
|
121
|
-
};
|
|
122
|
-
if (mediaType === "image/png") Object.assign(metadata, pngDimensions(bytes));
|
|
123
|
-
return metadata;
|
|
124
|
-
}
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
import { isDeepStrictEqual } from "node:util";
|
|
6
|
-
import Ajv2020 from "ajv/dist/2020.js";
|
|
7
|
-
import { artifactMetadata, MAX_ARTIFACT_FILE_BYTES } from "./page-artifact-metadata.mjs";
|
|
8
|
-
import { addDateTimeFormat } from "./json-schema-formats.mjs";
|
|
9
|
-
import { parseStrictJson } from "./strict-json.mjs";
|
|
10
|
-
|
|
11
|
-
export const PAGE_EVIDENCE_CLAIM = "Page evidence is claim-scoped runtime evidence, not visual-regression approval or accessibility certification.";
|
|
12
|
-
export const PAGE_EVIDENCE_MANIFEST = "page-evidence-manifest.json";
|
|
13
|
-
export const PAGE_EVIDENCE_RECEIPT = "page-evidence-session.json";
|
|
14
|
-
export const PAGE_EVIDENCE_MAX_PACKET_BYTES = 256 * 1024 * 1024;
|
|
15
|
-
|
|
16
|
-
export function finding(code, message, recordPath) {
|
|
17
|
-
return { code, message, path: recordPath };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function digest(bytes) {
|
|
21
|
-
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function metadata(bytes, mediaType, reference) {
|
|
25
|
-
const result = artifactMetadata(bytes, mediaType);
|
|
26
|
-
return { ...result, path: reference, sha256: result.sha256.replace(/^sha256:/, "") };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function sameJson(left, right) {
|
|
30
|
-
return isDeepStrictEqual(left, right);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function sameStringSet(left, right) {
|
|
34
|
-
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && new Set(left).size === left.length && left.every((entry) => right.includes(entry));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function safeRelativePath(reference) {
|
|
38
|
-
return typeof reference === "string"
|
|
39
|
-
&& reference.length > 0
|
|
40
|
-
&& !path.posix.isAbsolute(reference)
|
|
41
|
-
&& !path.win32.isAbsolute(reference)
|
|
42
|
-
&& !reference.includes("\\")
|
|
43
|
-
&& !reference.includes("?")
|
|
44
|
-
&& !reference.includes("#")
|
|
45
|
-
&& !/^[A-Za-z][A-Za-z\d+.-]*:/.test(reference)
|
|
46
|
-
&& path.posix.normalize(reference) === reference
|
|
47
|
-
&& !reference.split("/").some((segment) => segment === "." || segment === "..");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isInside(root, target) {
|
|
51
|
-
const relative = path.relative(root, target);
|
|
52
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function normalizeReference(root, candidate) {
|
|
56
|
-
const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(root, candidate);
|
|
57
|
-
const reference = path.relative(path.resolve(root), absolute).split(path.sep).join("/");
|
|
58
|
-
return safeRelativePath(reference) ? reference : undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function resolveContained({ allowMissing = false, expectedType = "file", prefix, reference, root }, failures) {
|
|
62
|
-
const absoluteRoot = path.resolve(root);
|
|
63
|
-
if (!fs.existsSync(absoluteRoot) || !fs.lstatSync(absoluteRoot).isDirectory()) {
|
|
64
|
-
failures.push(finding(`${prefix}_root_invalid`, "trust root must be an existing directory", absoluteRoot));
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
if (fs.lstatSync(absoluteRoot).isSymbolicLink() || fs.realpathSync(absoluteRoot) !== absoluteRoot) {
|
|
68
|
-
failures.push(finding(`${prefix}_redirect`, "trust root must not be a filesystem redirect", absoluteRoot));
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
if (!safeRelativePath(reference)) {
|
|
72
|
-
failures.push(finding(`${prefix}_escape`, "path must be normalized and repository-relative", String(reference)));
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
const target = path.resolve(absoluteRoot, reference);
|
|
76
|
-
if (!isInside(absoluteRoot, target)) {
|
|
77
|
-
failures.push(finding(`${prefix}_escape`, "path escapes its trust root", reference));
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
let current = absoluteRoot;
|
|
81
|
-
const segments = reference.split("/");
|
|
82
|
-
for (const [index, segment] of segments.entries()) {
|
|
83
|
-
current = path.join(current, segment);
|
|
84
|
-
if (!fs.existsSync(current)) {
|
|
85
|
-
if (allowMissing) return { file: target, reference };
|
|
86
|
-
failures.push(finding(`${prefix}_missing`, "required file is missing", reference));
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
const stat = fs.lstatSync(current);
|
|
90
|
-
if (stat.isSymbolicLink()) {
|
|
91
|
-
failures.push(finding(`${prefix}_symlink`, "path must not traverse a symbolic link", reference));
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
const final = index === segments.length - 1;
|
|
95
|
-
if ((!final && !stat.isDirectory()) || (final && expectedType === "file" && !stat.isFile()) || (final && expectedType === "directory" && !stat.isDirectory())) {
|
|
96
|
-
failures.push(finding(`${prefix}_type_invalid`, `path must resolve to a regular ${expectedType}`, reference));
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const canonical = path.join(fs.realpathSync(absoluteRoot), ...segments);
|
|
101
|
-
if (fs.realpathSync(target) !== canonical) {
|
|
102
|
-
failures.push(finding(`${prefix}_redirect`, "path resolves through a filesystem redirect", reference));
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
return { file: target, reference };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function readJsonFile({ prefix, reference, root }, failures) {
|
|
109
|
-
const resolved = readContainedBytes({ prefix, reference, root }, failures);
|
|
110
|
-
if (!resolved) return undefined;
|
|
111
|
-
try {
|
|
112
|
-
return { ...resolved, value: parseStrictJson(resolved.bytes.toString("utf8")) };
|
|
113
|
-
} catch (error) {
|
|
114
|
-
failures.push(finding(`${prefix}_json_invalid`, error instanceof Error ? error.message : String(error), reference));
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function readContainedBytes({ prefix, reference, root }, failures) {
|
|
120
|
-
const resolved = resolveContained({ expectedType: "file", prefix, reference, root }, failures);
|
|
121
|
-
if (!resolved) return undefined;
|
|
122
|
-
let descriptor;
|
|
123
|
-
try {
|
|
124
|
-
descriptor = fs.openSync(resolved.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
125
|
-
const before = fs.fstatSync(descriptor);
|
|
126
|
-
const namedBefore = fs.lstatSync(resolved.file);
|
|
127
|
-
if (!before.isFile() || before.dev !== namedBefore.dev || before.ino !== namedBefore.ino) throw new Error("file identity changed before read");
|
|
128
|
-
if (before.size > MAX_ARTIFACT_FILE_BYTES) throw new Error(`file exceeds ${MAX_ARTIFACT_FILE_BYTES} bytes`);
|
|
129
|
-
const bytes = fs.readFileSync(descriptor);
|
|
130
|
-
const after = fs.fstatSync(descriptor);
|
|
131
|
-
const namedAfter = fs.lstatSync(resolved.file);
|
|
132
|
-
if (before.dev !== after.dev || before.ino !== after.ino || after.dev !== namedAfter.dev || after.ino !== namedAfter.ino || after.size !== bytes.length) throw new Error("file identity changed during read");
|
|
133
|
-
const expected = path.join(fs.realpathSync(path.resolve(root)), ...reference.split("/"));
|
|
134
|
-
if (fs.realpathSync(resolved.file) !== expected) throw new Error("file path redirected during read");
|
|
135
|
-
return { ...resolved, bytes };
|
|
136
|
-
} catch (error) {
|
|
137
|
-
failures.push(finding(`${prefix}_read_untrusted`, error instanceof Error ? error.message : String(error), reference));
|
|
138
|
-
return undefined;
|
|
139
|
-
} finally {
|
|
140
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function readSchema(schemaRoot, name) {
|
|
145
|
-
const file = path.join(schemaRoot, name);
|
|
146
|
-
try {
|
|
147
|
-
return parseStrictJson(fs.readFileSync(file, "utf8"));
|
|
148
|
-
} catch (cause) {
|
|
149
|
-
throw new Error(`${name} is not strict JSON: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function compilePageEvidenceSchemas(schemaRoot) {
|
|
154
|
-
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
155
|
-
addDateTimeFormat(ajv);
|
|
156
|
-
const sessionSchema = readSchema(schemaRoot, "page-evidence-session.schema.json");
|
|
157
|
-
const manifestSchema = readSchema(schemaRoot, "page-evidence-manifest.schema.json");
|
|
158
|
-
ajv.addSchema(sessionSchema);
|
|
159
|
-
ajv.addSchema(manifestSchema);
|
|
160
|
-
return {
|
|
161
|
-
manifest: ajv.getSchema(manifestSchema.$id),
|
|
162
|
-
runner: ajv.compile({ $ref: `${manifestSchema.$id}#/$defs/runner_result` }),
|
|
163
|
-
session: ajv.getSchema(sessionSchema.$id),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function addSchemaFindings(validate, value, recordPath, code, failures) {
|
|
168
|
-
if (validate(value)) return true;
|
|
169
|
-
for (const error of validate.errors ?? []) failures.push(finding(code, `${error.instancePath || "/"} ${error.message}`, recordPath));
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function canonicalSourceManifest(root, references, failures) {
|
|
174
|
-
if (!Array.isArray(references) || references.length === 0 || new Set(references).size !== references.length) {
|
|
175
|
-
failures.push(finding("page_evidence_source_list_invalid", "relevant sources must be a nonempty unique path list", "<conformance-record>"));
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
const files = [];
|
|
179
|
-
let totalBytes = 0;
|
|
180
|
-
for (const reference of [...references].sort()) {
|
|
181
|
-
const resolved = readContainedBytes({ prefix: "page_evidence_source", reference, root }, failures);
|
|
182
|
-
if (!resolved) continue;
|
|
183
|
-
const size = resolved.bytes.length;
|
|
184
|
-
totalBytes += size;
|
|
185
|
-
if (size > MAX_ARTIFACT_FILE_BYTES || totalBytes > PAGE_EVIDENCE_MAX_PACKET_BYTES) {
|
|
186
|
-
failures.push(finding("page_evidence_source_limit", "relevant sources exceed the bounded file or aggregate byte budget", reference));
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const bytes = resolved.bytes;
|
|
190
|
-
files.push({ byte_length: bytes.length, path: reference, sha256: digest(bytes) });
|
|
191
|
-
}
|
|
192
|
-
return files.length === references.length ? { files, sha256: digest(Buffer.from(JSON.stringify(files))) } : undefined;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function git(root, ...args) {
|
|
196
|
-
return execFileSync("git", ["-c", `safe.directory=${path.resolve(root)}`, ...args], { cwd: root, encoding: "utf8" }).trim();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function remoteIdentity(remote) {
|
|
200
|
-
if (!remote) return undefined;
|
|
201
|
-
const cleaned = remote.replace(/\.git$/, "");
|
|
202
|
-
const scp = cleaned.match(/^[^@]+@[^:]+:(.+)$/);
|
|
203
|
-
if (scp) return scp[1];
|
|
204
|
-
try {
|
|
205
|
-
const parsed = new URL(cleaned);
|
|
206
|
-
return parsed.pathname.replace(/^\//, "");
|
|
207
|
-
} catch {
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function gitIdentity(root, failures) {
|
|
213
|
-
try {
|
|
214
|
-
const canonical = git(root, "rev-parse", "--show-toplevel");
|
|
215
|
-
if (path.resolve(canonical) !== path.resolve(root) || fs.realpathSync(canonical) !== path.resolve(root)) throw new Error("--root must be the canonical Git worktree root");
|
|
216
|
-
let remote;
|
|
217
|
-
try { remote = git(root, "config", "--get", "remote.origin.url"); } catch { remote = ""; }
|
|
218
|
-
return {
|
|
219
|
-
branch: git(root, "branch", "--show-current") || "detached",
|
|
220
|
-
repository: remoteIdentity(remote),
|
|
221
|
-
revision: git(root, "rev-parse", "HEAD"),
|
|
222
|
-
};
|
|
223
|
-
} catch (error) {
|
|
224
|
-
failures.push(finding("page_evidence_git_invalid", error instanceof Error ? error.message : String(error), path.resolve(root)));
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function dirtyRelevantSources(root, references) {
|
|
230
|
-
const dirty = [];
|
|
231
|
-
for (const reference of references) {
|
|
232
|
-
try { git(root, "ls-files", "--error-unmatch", "--", reference); }
|
|
233
|
-
catch { dirty.push(`untracked:${reference}`); }
|
|
234
|
-
}
|
|
235
|
-
const status = git(root, "status", "--porcelain=v1", "--untracked-files=all", "--", ...references);
|
|
236
|
-
return [...new Set([...dirty, ...status.split("\n").filter(Boolean)])].sort();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function readReceipt(artifactRoot, schemas, failures) {
|
|
240
|
-
const record = readJsonFile({ prefix: "page_evidence_session", reference: PAGE_EVIDENCE_RECEIPT, root: artifactRoot }, failures);
|
|
241
|
-
if (!record) return undefined;
|
|
242
|
-
const valid = addSchemaFindings(schemas.session, record.value, record.reference, "page_evidence_session_schema_invalid", failures);
|
|
243
|
-
const artifact = metadata(record.bytes, "application/json", PAGE_EVIDENCE_RECEIPT);
|
|
244
|
-
return { ...record, artifact, digest: artifact.sha256, valid };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export function withinSession(recordedAt, startedAt, completedAt) {
|
|
248
|
-
const recorded = Date.parse(recordedAt);
|
|
249
|
-
const started = Date.parse(startedAt);
|
|
250
|
-
const completed = Date.parse(completedAt);
|
|
251
|
-
return Number.isFinite(recorded) && Number.isFinite(started) && Number.isFinite(completed) && recorded >= started && recorded <= completed;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function listArtifactFiles(root, failures) {
|
|
255
|
-
const files = [];
|
|
256
|
-
let visited = 0;
|
|
257
|
-
let limitReported = false;
|
|
258
|
-
let totalBytes = 0;
|
|
259
|
-
function visit(directory, prefix = "", depth = 0) {
|
|
260
|
-
if (depth > 16) {
|
|
261
|
-
failures.push(finding("page_evidence_artifact_limit", "artifact tree exceeds the maximum directory depth", prefix));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const handle = fs.opendirSync(directory);
|
|
265
|
-
try {
|
|
266
|
-
let entry;
|
|
267
|
-
while ((entry = handle.readSync()) !== null) {
|
|
268
|
-
if (limitReported) return;
|
|
269
|
-
const reference = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
270
|
-
visited += 1;
|
|
271
|
-
if (visited > 4096) {
|
|
272
|
-
if (!limitReported) failures.push(finding("page_evidence_artifact_limit", "artifact tree exceeds 4096 entries", reference));
|
|
273
|
-
limitReported = true;
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const absolute = path.join(directory, entry.name);
|
|
277
|
-
const stat = fs.lstatSync(absolute);
|
|
278
|
-
if (stat.isSymbolicLink()) failures.push(finding("page_evidence_artifact_symlink", "artifact root must not contain symbolic links", reference));
|
|
279
|
-
else if (stat.isDirectory()) visit(absolute, reference, depth + 1);
|
|
280
|
-
else if (stat.isFile()) {
|
|
281
|
-
totalBytes += stat.size;
|
|
282
|
-
if (stat.size > MAX_ARTIFACT_FILE_BYTES || totalBytes > PAGE_EVIDENCE_MAX_PACKET_BYTES) {
|
|
283
|
-
if (!limitReported) failures.push(finding("page_evidence_artifact_limit", "artifact packet exceeds the bounded file or aggregate byte budget", reference));
|
|
284
|
-
limitReported = true;
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
files.push(reference);
|
|
288
|
-
}
|
|
289
|
-
else failures.push(finding("page_evidence_artifact_type_invalid", "artifact root contains a non-regular entry", reference));
|
|
290
|
-
}
|
|
291
|
-
} finally {
|
|
292
|
-
handle.closeSync();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
visit(root);
|
|
296
|
-
return files.sort();
|
|
297
|
-
}
|