stylegallery 0.1.0 → 0.1.1

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.
Files changed (230) hide show
  1. package/AGENTS.md +4 -1
  2. package/DOMAINS.md +2 -6
  3. package/GOVERNANCE.md +10 -9
  4. package/README.md +23 -127
  5. package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
  6. package/consumer-reference/agent-native/v2/material-registry.json +1 -0
  7. package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
  8. package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
  9. package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
  10. package/consumer-reference/contract.md +1 -1
  11. package/consumer-reference/index.md +5 -3
  12. package/layout/index.md +124 -0
  13. package/log.md +51 -0
  14. package/package.json +181 -22
  15. package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
  16. package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
  17. package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
  18. package/scripts/agent-native/v2/material-admission.mjs +295 -0
  19. package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
  20. package/scripts/agent-native/v2/material-context.mjs +251 -0
  21. package/scripts/agent-native/v2/material-identity.mjs +71 -0
  22. package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
  23. package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
  24. package/scripts/agent-native/v2/material-queries.mjs +398 -0
  25. package/scripts/agent-native/v2/material-registry.mjs +319 -0
  26. package/scripts/sg-material-mcp.mjs +98 -0
  27. package/scripts/sg-material.mjs +27 -0
  28. package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
  29. package/consumer-reference/baselines/calibration.json +0 -260
  30. package/consumer-reference/baselines/manifest.json +0 -21
  31. package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
  32. package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
  33. package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
  34. package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
  35. package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
  36. package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
  37. package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
  38. package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
  39. package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
  40. package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
  41. package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
  42. package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
  43. package/consumer-reference/fixtures/promotion/manifest.json +0 -15
  44. package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
  45. package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
  46. package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
  47. package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
  48. package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
  49. package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
  50. package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
  51. package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
  52. package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
  53. package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
  54. package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
  55. package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
  56. package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
  57. package/consumer-reference/fixtures/valid-deprecated.json +0 -21
  58. package/consumer-reference/fixtures/valid-experimental.json +0 -19
  59. package/consumer-reference/generated/manifest.json +0 -40
  60. package/consumer-reference/generated/tokens.css +0 -12
  61. package/consumer-reference/policies/shared-experimental.json +0 -37
  62. package/consumer-reference/schema/ax-evidence.schema.json +0 -30
  63. package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
  64. package/consumer-reference/schema/calibration-record.schema.json +0 -190
  65. package/consumer-reference/schema/capture-session.schema.json +0 -161
  66. package/consumer-reference/schema/component-state.schema.json +0 -150
  67. package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
  68. package/consumer-reference/schema/dom-evidence.schema.json +0 -38
  69. package/consumer-reference/schema/evidence-record.schema.json +0 -81
  70. package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
  71. package/consumer-reference/schema/item.schema.json +0 -188
  72. package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
  73. package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
  74. package/consumer-reference/schema/portable-tokens.schema.json +0 -104
  75. package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
  76. package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
  77. package/consumer-reference/schema/visual-evidence.schema.json +0 -31
  78. package/design-engineering/consumer-migration-readiness.md +0 -124
  79. package/design-engineering/interface-craft.md +0 -107
  80. package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
  81. package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
  82. package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
  83. package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
  84. package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
  85. package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
  86. package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
  87. package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
  88. package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
  89. package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
  90. package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
  91. package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
  92. package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
  93. package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
  94. package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
  95. package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
  96. package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
  97. package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
  98. package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
  99. package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
  100. package/game-ui/classification.md +0 -82
  101. package/game-ui/reference-record.md +0 -62
  102. package/game-ui/screen-hierarchy.md +0 -107
  103. package/game-ui/unity/architecture.md +0 -166
  104. package/game-ui/unity/cli-loop.md +0 -84
  105. package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
  106. package/game-ui/unity/org-wiki.md +0 -146
  107. package/game-ui/unity/repository-map.md +0 -102
  108. package/game-ui/unity/ui-systems.md +0 -85
  109. package/motion/practice-reference.md +0 -145
  110. package/motion/review-workflow.md +0 -86
  111. package/motion/vocabulary.md +0 -82
  112. package/platform-guides/apple-interaction.md +0 -89
  113. package/scripts/agent-native/events.mjs +0 -113
  114. package/scripts/agent-native/immutable-store.mjs +0 -102
  115. package/scripts/artifact-metadata.mjs +0 -20
  116. package/scripts/audit-evidence-freshness.mjs +0 -308
  117. package/scripts/baseline-contract.mjs +0 -189
  118. package/scripts/baseline-schema-parity.mjs +0 -40
  119. package/scripts/build-reference-artifacts.mjs +0 -127
  120. package/scripts/calibration-raw-contract.mjs +0 -172
  121. package/scripts/capture-session-contract.mjs +0 -187
  122. package/scripts/component-state-artifact-fixture.mjs +0 -44
  123. package/scripts/component-state-contract.mjs +0 -99
  124. package/scripts/component-state-semantics.mjs +0 -128
  125. package/scripts/component-state-workflow-contract.mjs +0 -67
  126. package/scripts/component-state-workflow-negative-cases.mjs +0 -64
  127. package/scripts/consumer-conformance-contract.mjs +0 -198
  128. package/scripts/consumer-evidence-governance-contract.mjs +0 -210
  129. package/scripts/consumer-reference-case-runner.mjs +0 -93
  130. package/scripts/consumer-reference-schema.mjs +0 -155
  131. package/scripts/create-component-state-session.mjs +0 -100
  132. package/scripts/create-page-evidence-session.mjs +0 -139
  133. package/scripts/domain-document-boundaries.mjs +0 -40
  134. package/scripts/domain-validation-checks.mjs +0 -141
  135. package/scripts/evidence-artifact-contract.mjs +0 -191
  136. package/scripts/finalize-component-state-evidence.mjs +0 -165
  137. package/scripts/finalize-page-evidence.mjs +0 -197
  138. package/scripts/generate-consumer-reference-evidence.mjs +0 -154
  139. package/scripts/generate-patterns.mjs +0 -218
  140. package/scripts/governance-matrix-contract.mjs +0 -73
  141. package/scripts/governance-matrix-negative-cases.mjs +0 -114
  142. package/scripts/governance-policy-contract.mjs +0 -49
  143. package/scripts/governance-test-fixture.mjs +0 -321
  144. package/scripts/governance-workflow-negative-cases.mjs +0 -241
  145. package/scripts/governed-profile-registry.mjs +0 -125
  146. package/scripts/json-schema-formats.mjs +0 -31
  147. package/scripts/markdown-structure.mjs +0 -45
  148. package/scripts/page-artifact-metadata.mjs +0 -124
  149. package/scripts/page-evidence-contract.mjs +0 -297
  150. package/scripts/page-evidence-fixture.mjs +0 -271
  151. package/scripts/pattern-data.mjs +0 -325
  152. package/scripts/profile-record-contract.mjs +0 -140
  153. package/scripts/promotion-attestation-contract.mjs +0 -91
  154. package/scripts/promotion-boundary-test-contract.mjs +0 -92
  155. package/scripts/promotion-fixture-inventory.mjs +0 -109
  156. package/scripts/promotion-governance-contract.mjs +0 -28
  157. package/scripts/reference-artifact-path-contract.mjs +0 -28
  158. package/scripts/reference-profile-contract.mjs +0 -114
  159. package/scripts/reference-token-contract.mjs +0 -156
  160. package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
  161. package/scripts/summarize-sentinel-calibration.mjs +0 -98
  162. package/scripts/test-agent-adapter-conformance.mjs +0 -250
  163. package/scripts/test-agent-conformance-receipt.mjs +0 -106
  164. package/scripts/test-agent-events.mjs +0 -81
  165. package/scripts/test-agent-execution-effects.mjs +0 -114
  166. package/scripts/test-agent-execution-fixtures.mjs +0 -140
  167. package/scripts/test-agent-execution-kernel.mjs +0 -39
  168. package/scripts/test-agent-execution-learning.mjs +0 -130
  169. package/scripts/test-agent-execution-lifecycle.mjs +0 -143
  170. package/scripts/test-agent-execution-safety.mjs +0 -158
  171. package/scripts/test-agent-execution-views.mjs +0 -117
  172. package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
  173. package/scripts/test-agent-knowledge-kernel.mjs +0 -183
  174. package/scripts/test-agent-learning.mjs +0 -246
  175. package/scripts/test-agent-mcp.mjs +0 -246
  176. package/scripts/test-agent-native-schemas.mjs +0 -215
  177. package/scripts/test-agent-projections.mjs +0 -249
  178. package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
  179. package/scripts/test-agent-registry-mutations.mjs +0 -110
  180. package/scripts/test-audit-evidence-freshness.mjs +0 -177
  181. package/scripts/test-component-state-sentinel.mjs +0 -35
  182. package/scripts/test-component-state-source-contract.mjs +0 -83
  183. package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
  184. package/scripts/test-consumer-reference-sentinel.mjs +0 -31
  185. package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
  186. package/scripts/test-json-cli-output.mjs +0 -151
  187. package/scripts/test-json-schema-formats.mjs +0 -40
  188. package/scripts/test-reference-adapters.mjs +0 -199
  189. package/scripts/test-renderer-purity.mjs +0 -34
  190. package/scripts/test-sg-cli.mjs +0 -129
  191. package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
  192. package/scripts/test-validate-baseline-manifest.mjs +0 -234
  193. package/scripts/test-validate-component-state-artifacts.mjs +0 -243
  194. package/scripts/test-validate-component-state.mjs +0 -106
  195. package/scripts/test-validate-consumer-conformance.mjs +0 -427
  196. package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
  197. package/scripts/test-validate-consumer-reference.mjs +0 -263
  198. package/scripts/test-validate-domains.mjs +0 -357
  199. package/scripts/test-validate-governance.mjs +0 -292
  200. package/scripts/test-validate-ia.mjs +0 -262
  201. package/scripts/test-validate-okf.mjs +0 -73
  202. package/scripts/test-validate-page-evidence.mjs +0 -302
  203. package/scripts/test-validate-patterns.mjs +0 -157
  204. package/scripts/test-validate-promotion-rfc.mjs +0 -250
  205. package/scripts/test-validate-reference-profiles.mjs +0 -170
  206. package/scripts/test-validate-unity-org-wiki.mjs +0 -57
  207. package/scripts/test-validate-unity-source-contracts.mjs +0 -67
  208. package/scripts/test-validate-vocabulary.mjs +0 -174
  209. package/scripts/test-validate-webpage-workflow.mjs +0 -293
  210. package/scripts/validate-baseline-manifest.mjs +0 -44
  211. package/scripts/validate-catalog.mjs +0 -65
  212. package/scripts/validate-component-state.mjs +0 -141
  213. package/scripts/validate-consumer-conformance.mjs +0 -477
  214. package/scripts/validate-consumer-reference.mjs +0 -288
  215. package/scripts/validate-domains.mjs +0 -237
  216. package/scripts/validate-governance.mjs +0 -248
  217. package/scripts/validate-ia.mjs +0 -136
  218. package/scripts/validate-links.mjs +0 -66
  219. package/scripts/validate-okf.mjs +0 -104
  220. package/scripts/validate-page-evidence.mjs +0 -191
  221. package/scripts/validate-patterns.mjs +0 -246
  222. package/scripts/validate-promotion-rfc.mjs +0 -224
  223. package/scripts/validate-reference-artifacts.mjs +0 -125
  224. package/scripts/validate-renderer-purity.mjs +0 -56
  225. package/scripts/validate-unity-org-wiki.mjs +0 -146
  226. package/scripts/validate-unity-source-contracts.mjs +0 -107
  227. package/scripts/validate-vocabulary.mjs +0 -115
  228. package/scripts/validate-webpage-workflow.mjs +0 -254
  229. package/scripts/visual-expectation-contract.mjs +0 -20
  230. package/scripts/workflow-action-contract.mjs +0 -163
@@ -0,0 +1,123 @@
1
+ import { deepFreeze } from "../canonical-json.mjs";
2
+ import { materialContext } from "./material-context.mjs";
3
+ import { materialDiscover, materialGet, materialSearch } from "./material-queries.mjs";
4
+
5
+ export class MaterialOperationRegistryError extends TypeError {
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.name = "MaterialOperationRegistryError";
9
+ this.code = code;
10
+ }
11
+ }
12
+
13
+ const operations = deepFreeze([
14
+ {
15
+ schema_version: "2.0", name: "material-context", stable_ref: "sg:operation/material-context",
16
+ read_only: true, effect_class: "NONE",
17
+ input_schema: { type: "object", additionalProperties: false, required: ["query"], properties: { query: { type: "string" }, budget_tokens: { type: "integer", minimum: 256, maximum: 32768, default: 8192 } } },
18
+ },
19
+ {
20
+ schema_version: "2.0", name: "material-discover", stable_ref: "sg:operation/material-discover",
21
+ read_only: true, effect_class: "NONE", input_schema: { type: "object", additionalProperties: false, properties: {} },
22
+ },
23
+ {
24
+ schema_version: "2.0", name: "material-get", stable_ref: "sg:operation/material-get",
25
+ read_only: true, effect_class: "NONE",
26
+ input_schema: { type: "object", additionalProperties: false, required: ["reference"], properties: { reference: { type: "string" }, offset: { type: "integer", minimum: 0 }, length: { type: "integer", minimum: 1, maximum: 65536 } } },
27
+ },
28
+ {
29
+ schema_version: "2.0", name: "material-search", stable_ref: "sg:operation/material-search",
30
+ read_only: true, effect_class: "NONE",
31
+ input_schema: { type: "object", additionalProperties: false, required: ["query"], properties: { query: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } } },
32
+ },
33
+ ].sort((left, right) => left.name < right.name ? -1 : 1));
34
+
35
+ const PUBLIC_MESSAGES = Object.freeze({
36
+ material_byte_length_mismatch: "material source identity does not match the manifest",
37
+ material_context_accounting_failed: "material context accounting failed",
38
+ material_context_budget_invalid: "budget_tokens must be an integer from 256 to 32768",
39
+ material_context_budget_too_small: "budget cannot contain the material context envelope",
40
+ material_context_input_unsafe: "material context input must contain only own plain data",
41
+ material_context_manifest_override_forbidden: "material manifest override is forbidden",
42
+ material_context_repository_path_forbidden: "repository path input is forbidden",
43
+ material_context_transaction_drift: "material context transaction changed during retrieval",
44
+ material_context_trust_merge_forbidden: "trust-record merge is forbidden",
45
+ material_domain_set_invalid: "governed domain metadata is invalid",
46
+ material_git_mode_invalid: "material source tracking is invalid",
47
+ material_identity_version_invalid: "reference identity version is invalid",
48
+ material_input_invalid: "operation input must be an object",
49
+ material_input_unknown: "operation input contains an unsupported field",
50
+ material_inventory_failed: "material source tracking is unavailable",
51
+ material_length_invalid: "length must be an integer from 1 to 65536",
52
+ material_manifest_cardinality_invalid: "material manifest metadata is invalid",
53
+ material_manifest_duplicate: "material manifest metadata is invalid",
54
+ material_manifest_invalid: "material manifest metadata is invalid",
55
+ material_manifest_order_invalid: "material manifest metadata is invalid",
56
+ material_manifest_path_set_invalid: "material manifest metadata is invalid",
57
+ material_manifest_version_invalid: "material manifest metadata is invalid",
58
+ material_offset_invalid: "offset must be a non-negative safe integer",
59
+ material_offset_past_end: "offset is past the end of the material",
60
+ material_operation_failed: "v2 material operation failed",
61
+ material_operation_required: "operation name is required",
62
+ material_operation_unknown: "unknown v2 material operation",
63
+ material_path_escape: "material source containment is invalid",
64
+ material_path_not_regular: "material source type is invalid",
65
+ material_path_symlink: "material source type is invalid",
66
+ material_path_unavailable: "material source is unavailable",
67
+ material_path_untracked: "material source tracking is invalid",
68
+ material_query_empty: "query must contain a Unicode word token",
69
+ material_query_invalid: "query must be a valid Unicode string",
70
+ material_query_oversized: "query exceeds 4096 UTF-8 bytes",
71
+ material_range_overflow: "offset plus length exceeds the safe integer range",
72
+ material_read_failed: "material source could not be read",
73
+ material_read_invalid: "material source read is invalid",
74
+ material_record_binding_invalid: "material manifest metadata is invalid",
75
+ material_record_invalid: "material manifest metadata is invalid",
76
+ material_reference_invalid: "reference must be a v2 material StableRef",
77
+ material_reference_not_found: "v2 material StableRef is absent from the manifest",
78
+ material_registry_canonical_invalid: "material registry metadata is invalid",
79
+ material_registry_file_invalid: "material registry is unavailable",
80
+ material_registry_json_invalid: "material registry metadata is invalid",
81
+ material_registry_race: "material registry identity changed during read",
82
+ material_repository_invalid: "material repository is unavailable",
83
+ material_search_limit_invalid: "limit must be an integer from 1 to 100",
84
+ material_source_hash_mismatch: "material source identity does not match the manifest",
85
+ material_source_race: "material source identity changed during read",
86
+ material_source_utf8_invalid: "material source is not valid UTF-8",
87
+ material_utf8_split_end: "page end splits a UTF-8 code point",
88
+ material_utf8_split_start: "offset splits a UTF-8 code point",
89
+ });
90
+ function failure(code) { return { code, message: PUBLIC_MESSAGES[code] ?? PUBLIC_MESSAGES.material_operation_failed }; }
91
+ function failed(operation, error) {
92
+ const code = typeof error?.code === "string" && Object.hasOwn(PUBLIC_MESSAGES, error.code) ? error.code : "material_operation_failed";
93
+ return deepFreeze({ ok: false, operation, failures: [failure(code)] });
94
+ }
95
+ function succeeded(operation, result) { return deepFreeze({ ok: true, operation, result }); }
96
+
97
+ export function createMaterialOperationRegistry({ repositoryRoot, fileSystem, sourceReader, gitRunner } = {}) {
98
+ if (typeof repositoryRoot !== "string" || repositoryRoot.length === 0) throw new MaterialOperationRegistryError("material_repository_invalid", "repositoryRoot is required");
99
+ const handlers = new Map([
100
+ ["material-context", (input) => materialContext({ repositoryRoot, input, fileSystem, sourceReader, gitRunner })],
101
+ ["material-discover", (input) => {
102
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new MaterialOperationRegistryError("material_input_invalid", "input must be an object");
103
+ if (Object.keys(input).length > 0) throw new MaterialOperationRegistryError("material_input_unknown", "material-discover accepts no input fields");
104
+ return materialDiscover({ repositoryRoot, fileSystem });
105
+ }],
106
+ ["material-get", (input) => materialGet({ repositoryRoot, input, fileSystem, sourceReader, gitRunner })],
107
+ ["material-search", (input) => materialSearch({ repositoryRoot, input, fileSystem, sourceReader, gitRunner })],
108
+ ]);
109
+ const invoke = (operation, input = {}) => {
110
+ if (typeof operation !== "string" || operation.length === 0) return failed(null, new MaterialOperationRegistryError("material_operation_required", "operation name is required"));
111
+ const handler = handlers.get(operation);
112
+ if (!handler) return failed(null, new MaterialOperationRegistryError("material_operation_unknown", "unknown v2 material operation"));
113
+ try { return succeeded(operation, handler(input)); } catch (error) { return failed(operation, error); }
114
+ };
115
+ return Object.freeze({ invoke, operations });
116
+ }
117
+
118
+ export function invokeMaterialOperation(operation, input, registry) {
119
+ if (!registry || typeof registry.invoke !== "function") throw new MaterialOperationRegistryError("material_registry_invalid", "v2 material registry is required");
120
+ return registry.invoke(operation, input);
121
+ }
122
+
123
+ export const materialOperationSpecs = operations;
@@ -0,0 +1,398 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { TextDecoder } from "node:util";
6
+
7
+ import { canonicalize, deepFreeze } from "../canonical-json.mjs";
8
+ import {
9
+ materialAdmissionPolicy,
10
+ materialStableRefForPath,
11
+ materialVersionIdForSource,
12
+ } from "./material-admission.mjs";
13
+ import { parseMaterialStableRef, materialIdentityForRecord } from "./material-identity.mjs";
14
+ import { MATERIAL_REGISTRY_PATH } from "./material-registry.mjs";
15
+
16
+ export const MATERIAL_QUERY_MAX_BYTES = 4096;
17
+ export const MATERIAL_GET_MAX_BYTES = 64 * 1024;
18
+ export const MATERIAL_SEARCH_WEIGHTS = deepFreeze({ title: 16, path: 8, body: 1 });
19
+ const utf8Fatal = new TextDecoder("utf-8", { fatal: true });
20
+ const RECORD_KEYS = ["byte_length", "domain", "lifecycle", "media_type", "record_kind", "repository_path", "schema_version", "source_sha256", "stable_ref", "version_id"];
21
+ const MANIFEST_KEYS = ["admission_policy_ref", "admission_policy_version_id", "materials", "record_kind", "schema_version", "stable_ref", "version_id"];
22
+ const MANIFEST_REF = "sg:manifest/material-admission-v2";
23
+ const HASH = /^[a-f0-9]{64}$/;
24
+ const QUERY_SNAPSHOTS = new WeakMap();
25
+ const SNAPSHOT_DRIFT_CODES = new Set([
26
+ "material_byte_length_mismatch", "material_git_mode_invalid", "material_inventory_failed",
27
+ "material_path_escape", "material_path_not_regular", "material_path_symlink", "material_path_unavailable",
28
+ "material_path_untracked", "material_read_failed", "material_read_invalid", "material_source_hash_mismatch",
29
+ "material_source_race", "material_source_utf8_invalid",
30
+ ]);
31
+
32
+ export class MaterialQueryError extends TypeError {
33
+ constructor(code, message) {
34
+ super(message);
35
+ this.name = "MaterialQueryError";
36
+ this.code = code;
37
+ }
38
+ }
39
+
40
+ function fail(code, message) { throw new MaterialQueryError(code, message); }
41
+ function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
42
+ function compare(left, right) { return left < right ? -1 : left > right ? 1 : 0; }
43
+ function realpath(fileSystem, target) {
44
+ const implementation = fileSystem.realpathSync.native ?? fileSystem.realpathSync;
45
+ return implementation.call(fileSystem.realpathSync, target);
46
+ }
47
+ function assertObject(input) {
48
+ if (!input || typeof input !== "object" || Array.isArray(input)) fail("material_input_invalid", "input must be an object");
49
+ }
50
+ function assertOnly(input, allowed) {
51
+ if (Object.keys(input).some((key) => !allowed.has(key))) fail("material_input_unknown", "input contains an unsupported field");
52
+ }
53
+ function hasUnpairedSurrogate(value) {
54
+ return /(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF])/.test(value);
55
+ }
56
+
57
+ /**
58
+ * Validate scalar Unicode, NFKC-normalize, apply ECMAScript's
59
+ * locale-independent Unicode lowercase mapping, and return maximal words that
60
+ * begin with Letter/Number and retain subsequent Letter/Number/Mark code
61
+ * points. Standalone marks delimit and are ignored.
62
+ */
63
+ export function tokenizeMaterialText(value, { query = false } = {}) {
64
+ if (typeof value !== "string" || hasUnpairedSurrogate(value)) {
65
+ if (query) fail("material_query_invalid", "query must be a valid Unicode string");
66
+ return [];
67
+ }
68
+ return value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}\p{M}]*/gu) ?? [];
69
+ }
70
+
71
+ function validateQueryInput(input) {
72
+ assertObject(input);
73
+ assertOnly(input, new Set(["query", "limit"]));
74
+ if (typeof input.query !== "string" || hasUnpairedSurrogate(input.query)) fail("material_query_invalid", "query must be a valid Unicode string");
75
+ const queryBytes = Buffer.byteLength(input.query, "utf8");
76
+ if (queryBytes > MATERIAL_QUERY_MAX_BYTES) fail("material_query_oversized", "query exceeds 4096 UTF-8 bytes");
77
+ const tokens = tokenizeMaterialText(input.query, { query: true });
78
+ if (queryBytes === 0 || tokens.length === 0) fail("material_query_empty", "query must contain an alphanumeric token");
79
+ const limit = input.limit ?? 20;
80
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) fail("material_search_limit_invalid", "limit must be an integer from 1 to 100");
81
+ return { query: input.query, limit, tokens, scoringTokens: [...new Set(tokens)] };
82
+ }
83
+
84
+ function inspectRoot(repositoryRoot, fileSystem) {
85
+ if (typeof repositoryRoot !== "string" || repositoryRoot.length === 0) fail("material_repository_invalid", "repository root is required");
86
+ const resolvedRoot = path.resolve(repositoryRoot);
87
+ let stats;
88
+ try { stats = fileSystem.lstatSync(resolvedRoot); } catch { fail("material_repository_invalid", "repository root is unavailable"); }
89
+ if (!stats.isDirectory() || stats.isSymbolicLink()) fail("material_repository_invalid", "repository root must be a non-symlink directory");
90
+ return { resolvedRoot, canonicalRoot: realpath(fileSystem, resolvedRoot) };
91
+ }
92
+
93
+ function inspectBoundPath(root, repositoryPath, fileSystem, unavailableCode = "material_path_unavailable") {
94
+ let target = root.resolvedRoot;
95
+ let stats;
96
+ try {
97
+ for (const component of repositoryPath.split("/")) {
98
+ target = path.join(target, component);
99
+ stats = fileSystem.lstatSync(target);
100
+ if (stats.isSymbolicLink()) fail("material_path_symlink", "manifest-bound path components must not be symlinks");
101
+ }
102
+ } catch (error) {
103
+ if (error instanceof MaterialQueryError) throw error;
104
+ fail(unavailableCode, "manifest-bound file is unavailable");
105
+ }
106
+ if (!stats.isFile()) fail("material_path_not_regular", "manifest-bound path must be a regular file");
107
+ const canonicalTarget = realpath(fileSystem, target);
108
+ const expected = path.resolve(root.canonicalRoot, ...repositoryPath.split("/"));
109
+ if (canonicalTarget !== expected || !canonicalTarget.startsWith(`${root.canonicalRoot}${path.sep}`)) fail("material_path_escape", "manifest-bound path escapes repository containment");
110
+ return { target, canonicalTarget, dev: stats.dev, ino: stats.ino };
111
+ }
112
+
113
+ function identityUnchanged(inspection, fileSystem) {
114
+ try {
115
+ const stats = fileSystem.lstatSync(inspection.target);
116
+ return stats.isFile() && !stats.isSymbolicLink() && stats.dev === inspection.dev && stats.ino === inspection.ino
117
+ && realpath(fileSystem, inspection.target) === inspection.canonicalTarget;
118
+ } catch { return false; }
119
+ }
120
+
121
+ function descriptorUnchanged(descriptor, inspection, fileSystem) {
122
+ try {
123
+ const stats = fileSystem.fstatSync(descriptor);
124
+ return stats.isFile() && stats.dev === inspection.dev && stats.ino === inspection.ino && identityUnchanged(inspection, fileSystem);
125
+ } catch { return false; }
126
+ }
127
+
128
+ function readInspected(inspection, fileSystem, sourceReader, raceCode) {
129
+ let descriptor;
130
+ try {
131
+ descriptor = fileSystem.openSync(inspection.target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
132
+ if (!descriptorUnchanged(descriptor, inspection, fileSystem)) fail(raceCode, "manifest-bound file identity changed during read");
133
+ const bytes = sourceReader ? sourceReader(descriptor) : fileSystem.readFileSync(descriptor);
134
+ if (!(bytes instanceof Uint8Array)) fail("material_read_invalid", "source reader did not return bytes");
135
+ if (!descriptorUnchanged(descriptor, inspection, fileSystem)) fail(raceCode, "manifest-bound file identity changed during read");
136
+ return Buffer.from(bytes);
137
+ } catch (error) {
138
+ if (error instanceof MaterialQueryError) throw error;
139
+ if (error?.code === "ELOOP" || error?.code === "ENOENT") fail(raceCode, "manifest-bound file identity changed during read");
140
+ fail("material_read_failed", "manifest-bound file could not be read");
141
+ } finally { if (descriptor !== undefined) fileSystem.closeSync(descriptor); }
142
+ }
143
+
144
+ function canonicalVersionId(stableRef, value) {
145
+ const payload = { ...value };
146
+ delete payload.version_id;
147
+ return `${stableRef}@sha256:${sha256(Buffer.from(canonicalize(payload), "utf8"))}`;
148
+ }
149
+
150
+ function validateMetadata(manifest) {
151
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)
152
+ || canonicalize(Object.keys(manifest).sort()) !== canonicalize(MANIFEST_KEYS)
153
+ || manifest.schema_version !== "2.0" || manifest.record_kind !== "material_manifest" || manifest.stable_ref !== MANIFEST_REF
154
+ || manifest.admission_policy_ref !== materialAdmissionPolicy.stable_ref
155
+ || manifest.admission_policy_version_id !== materialAdmissionPolicy.version_id
156
+ || !Array.isArray(manifest.materials)) fail("material_manifest_invalid", "material manifest metadata is invalid");
157
+ if (manifest.version_id !== canonicalVersionId(MANIFEST_REF, manifest)) fail("material_manifest_version_invalid", "material manifest identity is invalid");
158
+ const policyRoutes = new Map(materialAdmissionPolicy.allowed_materials.map((route) => [route.repository_path, route]));
159
+ if (manifest.materials.length !== policyRoutes.size) fail("material_manifest_cardinality_invalid", "material manifest member set is invalid");
160
+ const refs = new Set();
161
+ const paths = new Set();
162
+ let previous = "";
163
+ for (const record of manifest.materials) {
164
+ if (!record || typeof record !== "object" || Array.isArray(record)
165
+ || canonicalize(Object.keys(record).sort()) !== canonicalize(RECORD_KEYS)
166
+ || record.schema_version !== "2.0" || record.record_kind !== "material" || record.media_type !== "text/markdown"
167
+ || !HASH.test(record.source_sha256 ?? "") || !Number.isSafeInteger(record.byte_length) || record.byte_length < 0) {
168
+ fail("material_record_invalid", "material manifest member metadata is invalid");
169
+ }
170
+ const route = policyRoutes.get(record.repository_path);
171
+ if (!route || record.domain !== route.domain || record.lifecycle !== route.lifecycle
172
+ || record.stable_ref !== materialStableRefForPath(record.repository_path)
173
+ || record.version_id !== materialVersionIdForSource(record.stable_ref, record.source_sha256)) {
174
+ fail("material_record_binding_invalid", "material manifest member binding is invalid");
175
+ }
176
+ if (previous && compare(previous, record.stable_ref) >= 0) fail("material_manifest_order_invalid", "material manifest member order is invalid");
177
+ if (refs.has(record.stable_ref) || paths.has(record.repository_path)) fail("material_manifest_duplicate", "material manifest members must be unique");
178
+ previous = record.stable_ref;
179
+ refs.add(record.stable_ref);
180
+ paths.add(record.repository_path);
181
+ }
182
+ if ([...policyRoutes.keys()].some((entry) => !paths.has(entry))) fail("material_manifest_path_set_invalid", "material manifest member set is invalid");
183
+ return manifest;
184
+ }
185
+
186
+ function loadMetadata(repositoryRoot, fileSystem = fs) {
187
+ const root = inspectRoot(repositoryRoot, fileSystem);
188
+ const inspection = inspectBoundPath(root, MATERIAL_REGISTRY_PATH, fileSystem, "material_registry_file_invalid");
189
+ const bytes = readInspected(inspection, fileSystem, undefined, "material_registry_race");
190
+ let manifest;
191
+ try { manifest = JSON.parse(bytes.toString("utf8")); } catch { fail("material_registry_json_invalid", "material registry JSON is invalid"); }
192
+ if (canonicalize(manifest) !== bytes.toString("utf8")) fail("material_registry_canonical_invalid", "material registry is not canonical JSON");
193
+ return { root, manifest: deepFreeze(validateMetadata(manifest)), manifestBytes: bytes };
194
+ }
195
+
196
+ function trackedInventory(repositoryRoot, gitRunner = spawnSync, repositoryPath) {
197
+ const args = ["-C", repositoryRoot, "ls-files", "-s", "-z", "--", ...(repositoryPath ? [repositoryPath] : [])];
198
+ const result = gitRunner("git", args, { encoding: "buffer", timeout: 30_000, maxBuffer: 16 * 1024 * 1024 });
199
+ if (!result || result.status !== 0) fail("material_inventory_failed", "tracked source identity is unavailable");
200
+ const inventory = new Map();
201
+ for (const text of Buffer.from(result.stdout ?? []).toString("utf8").replace(/\0$/, "").split("\0").filter(Boolean)) {
202
+ const tab = text.indexOf("\t");
203
+ const metadata = tab < 0 ? [] : text.slice(0, tab).split(" ");
204
+ if (metadata.length !== 3) fail("material_inventory_failed", "tracked source identity is unavailable");
205
+ inventory.set(text.slice(tab + 1), { mode: metadata[0], object_id: metadata[1], stage: metadata[2] });
206
+ }
207
+ return inventory;
208
+ }
209
+ function assertTracked(record, inventory) {
210
+ const metadata = inventory.get(record.repository_path);
211
+ if (!metadata) fail("material_path_untracked", "material source is not tracked");
212
+ if (!new Set(["100644", "100755"]).has(metadata.mode) || metadata.stage !== "0") fail("material_git_mode_invalid", "material source Git mode is invalid");
213
+ }
214
+
215
+ function requireSnapshot(snapshot) {
216
+ const state = snapshot && QUERY_SNAPSHOTS.get(snapshot);
217
+ if (!state) fail("material_context_transaction_drift", "material context transaction changed");
218
+ return state;
219
+ }
220
+
221
+ /** Capture one authenticated manifest byte image and Git inventory for a context transaction. */
222
+ export function captureMaterialQuerySnapshot({ repositoryRoot, fileSystem = fs, gitRunner }) {
223
+ const metadata = loadMetadata(repositoryRoot, fileSystem);
224
+ const inventory = trackedInventory(repositoryRoot, gitRunner);
225
+ for (const record of metadata.manifest.materials) assertTracked(record, inventory);
226
+ const snapshot = Object.freeze(Object.create(null));
227
+ QUERY_SNAPSHOTS.set(snapshot, Object.freeze({
228
+ root: metadata.root,
229
+ manifest: metadata.manifest,
230
+ manifestBytes: metadata.manifestBytes.toString("utf8"),
231
+ manifestSha256: sha256(metadata.manifestBytes),
232
+ inventory,
233
+ }));
234
+ return snapshot;
235
+ }
236
+
237
+ function snapshotManifestIsLive(state, fileSystem) {
238
+ const inspection = inspectBoundPath(state.root, MATERIAL_REGISTRY_PATH, fileSystem, "material_registry_file_invalid");
239
+ const bytes = readInspected(inspection, fileSystem, undefined, "material_registry_race");
240
+ return sha256(bytes) === state.manifestSha256 && bytes.toString("utf8") === state.manifestBytes;
241
+ }
242
+
243
+ /** Normalize only post-authentication drift when the pinned manifest itself transitioned. */
244
+ export function rethrowMaterialQuerySnapshotError({ snapshot, error, fileSystem = fs }) {
245
+ if (error?.code === "material_context_transaction_drift") throw error;
246
+ if (!SNAPSHOT_DRIFT_CODES.has(error?.code)) throw error;
247
+ const state = requireSnapshot(snapshot);
248
+ try {
249
+ if (!snapshotManifestIsLive(state, fileSystem)) fail("material_context_transaction_drift", "material context manifest changed");
250
+ } catch (manifestError) {
251
+ if (manifestError?.code === "material_context_transaction_drift") throw manifestError;
252
+ fail("material_context_transaction_drift", "material context manifest changed");
253
+ }
254
+ throw error;
255
+ }
256
+
257
+ function assertSnapshotManifestLive(state, fileSystem) {
258
+ try {
259
+ if (!snapshotManifestIsLive(state, fileSystem)) fail("material_context_transaction_drift", "material context manifest changed");
260
+ } catch (error) {
261
+ if (error?.code === "material_context_transaction_drift") throw error;
262
+ fail("material_context_transaction_drift", "material context manifest changed");
263
+ }
264
+ }
265
+
266
+ /** Recheck the live manifest and selected source descriptors immediately before context emission. */
267
+ export function verifyMaterialQuerySnapshot({ repositoryRoot, snapshot, selectedSourceRefs, fileSystem = fs, sourceReader, gitRunner }) {
268
+ const state = requireSnapshot(snapshot);
269
+ assertSnapshotManifestLive(state, fileSystem);
270
+ let liveInventory;
271
+ try { liveInventory = trackedInventory(repositoryRoot, gitRunner); }
272
+ catch (error) { rethrowMaterialQuerySnapshotError({ snapshot, error, fileSystem }); }
273
+ const selected = new Set(selectedSourceRefs);
274
+ for (const record of state.manifest.materials) {
275
+ if (!selected.has(record.stable_ref)) continue;
276
+ try {
277
+ assertTracked(record, liveInventory);
278
+ if (canonicalize(liveInventory.get(record.repository_path)) !== canonicalize(state.inventory.get(record.repository_path))) {
279
+ fail("material_source_race", "material source tracking changed during context verification");
280
+ }
281
+ readRecord({ repositoryRoot, root: state.root, record, fileSystem, sourceReader, gitRunner, inventory: liveInventory });
282
+ } catch (error) { rethrowMaterialQuerySnapshotError({ snapshot, error, fileSystem }); }
283
+ }
284
+ assertSnapshotManifestLive(state, fileSystem);
285
+ }
286
+
287
+ function readRecord({ repositoryRoot, root, record, fileSystem = fs, sourceReader, gitRunner, inventory }) {
288
+ const suppliedInventory = inventory !== undefined;
289
+ const before = inventory ?? trackedInventory(repositoryRoot, gitRunner, record.repository_path);
290
+ assertTracked(record, before);
291
+ const inspection = inspectBoundPath(root, record.repository_path, fileSystem);
292
+ const bytes = readInspected(inspection, fileSystem, sourceReader, "material_source_race");
293
+ if (!suppliedInventory) {
294
+ const after = trackedInventory(repositoryRoot, gitRunner, record.repository_path);
295
+ assertTracked(record, after);
296
+ if (canonicalize(before.get(record.repository_path)) !== canonicalize(after.get(record.repository_path))) fail("material_source_race", "material source tracking changed during read");
297
+ }
298
+ if (sha256(bytes) !== record.source_sha256) fail("material_source_hash_mismatch", "material source hash does not match the manifest");
299
+ if (bytes.byteLength !== record.byte_length) fail("material_byte_length_mismatch", "material source length does not match the manifest");
300
+ return bytes;
301
+ }
302
+
303
+ function titleOf(text) {
304
+ const frontmatter = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(text);
305
+ if (frontmatter) {
306
+ const title = /^title:\s*(.+?)\s*$/m.exec(frontmatter[1]);
307
+ if (title) return title[1].replace(/^(["'])(.*)\1$/, "$2");
308
+ }
309
+ return /^#{1,6}\s+(.+?)\s*$/m.exec(text)?.[1] ?? "";
310
+ }
311
+ function fieldMembership(fieldTokens, queryTokens) {
312
+ const members = new Set(fieldTokens);
313
+ let count = 0;
314
+ for (const token of queryTokens) if (members.has(token)) count += 1;
315
+ return count;
316
+ }
317
+ function sourceIdentity(record) {
318
+ return { stable_ref: record.stable_ref, version_id: record.version_id, source_sha256: record.source_sha256, byte_length: record.byte_length };
319
+ }
320
+
321
+ export function materialDiscover({ repositoryRoot, fileSystem = fs }) {
322
+ const { manifest } = loadMetadata(repositoryRoot, fileSystem);
323
+ const domains = manifest.materials.map((record) => ({ record, identity: materialIdentityForRecord(record) }))
324
+ .filter(({ identity }) => identity.kind === "domain")
325
+ .map(({ record, identity }) => ({ identity, source: sourceIdentity(record), lifecycle: record.lifecycle }))
326
+ .sort((left, right) => compare(left.identity.stable_ref, right.identity.stable_ref));
327
+ if (domains.length !== 5) fail("material_domain_set_invalid", "material manifest does not contain the five governed domains");
328
+ return deepFreeze({ schema_version: "2.0", manifest_version_id: manifest.version_id, domains });
329
+ }
330
+
331
+ export function materialSearch({ repositoryRoot, input, fileSystem = fs, sourceReader, gitRunner, snapshot }) {
332
+ const normalized = validateQueryInput(input);
333
+ const state = snapshot ? requireSnapshot(snapshot) : loadMetadata(repositoryRoot, fileSystem);
334
+ const { root, manifest } = state;
335
+ const beforeInventory = snapshot ? state.inventory : trackedInventory(repositoryRoot, gitRunner);
336
+ const results = [];
337
+ for (const record of manifest.materials) {
338
+ const bytes = readRecord({ repositoryRoot, root, record, fileSystem, sourceReader, gitRunner, inventory: beforeInventory });
339
+ let body;
340
+ try { body = utf8Fatal.decode(bytes); } catch { fail("material_source_utf8_invalid", "material source is not valid UTF-8"); }
341
+ const title = titleOf(body);
342
+ const titleCount = fieldMembership(tokenizeMaterialText(title), normalized.scoringTokens);
343
+ const pathCount = fieldMembership(tokenizeMaterialText(record.repository_path), normalized.scoringTokens);
344
+ const bodyCount = fieldMembership(tokenizeMaterialText(body), normalized.scoringTokens);
345
+ const score = titleCount * MATERIAL_SEARCH_WEIGHTS.title + pathCount * MATERIAL_SEARCH_WEIGHTS.path + bodyCount * MATERIAL_SEARCH_WEIGHTS.body;
346
+ if (score === 0) continue;
347
+ results.push({ identity: materialIdentityForRecord(record), source: sourceIdentity(record), title, score, match_counts: { title: titleCount, path: pathCount, body: bodyCount } });
348
+ }
349
+ const afterInventory = trackedInventory(repositoryRoot, gitRunner);
350
+ for (const record of manifest.materials) {
351
+ assertTracked(record, afterInventory);
352
+ if (canonicalize(beforeInventory.get(record.repository_path)) !== canonicalize(afterInventory.get(record.repository_path))) fail("material_source_race", "material source tracking changed during read");
353
+ }
354
+ results.sort((left, right) => right.score - left.score || compare(left.identity.stable_ref, right.identity.stable_ref));
355
+ return deepFreeze({
356
+ schema_version: "2.0", manifest_version_id: manifest.version_id,
357
+ normalization: "Unicode NFKC; ECMAScript locale-independent lowercase; Letter/Number-led Unicode words retaining attached Marks; unique query-token field membership",
358
+ query: normalized.query.normalize("NFKC").toLowerCase(), tokens: normalized.tokens,
359
+ scoring_tokens: normalized.scoringTokens, weights: MATERIAL_SEARCH_WEIGHTS,
360
+ results: results.slice(0, normalized.limit), total_matches: results.length,
361
+ });
362
+ }
363
+
364
+ function validateGetInput(input) {
365
+ assertObject(input);
366
+ assertOnly(input, new Set(["reference", "offset", "length"]));
367
+ let parsed;
368
+ try { parsed = parseMaterialStableRef(input.reference); } catch { fail("material_reference_invalid", "reference must be a v2 material StableRef"); }
369
+ const offset = input.offset ?? 0;
370
+ const length = input.length ?? MATERIAL_GET_MAX_BYTES;
371
+ if (!Number.isSafeInteger(offset) || offset < 0) fail("material_offset_invalid", "offset must be a non-negative safe integer");
372
+ if (!Number.isSafeInteger(length) || length < 1 || length > MATERIAL_GET_MAX_BYTES) fail("material_length_invalid", "length must be an integer from 1 to 65536");
373
+ if (!Number.isSafeInteger(offset + length)) fail("material_range_overflow", "offset plus length exceeds the safe integer range");
374
+ return { parsed, offset, length };
375
+ }
376
+
377
+ export function materialGet({ repositoryRoot, input, fileSystem = fs, sourceReader, gitRunner, snapshot, contextUtf8SafeEnd = false }) {
378
+ const normalized = validateGetInput(input);
379
+ const state = snapshot ? requireSnapshot(snapshot) : loadMetadata(repositoryRoot, fileSystem);
380
+ const { root, manifest } = state;
381
+ const match = manifest.materials.map((record) => ({ record, identity: materialIdentityForRecord(record) }))
382
+ .find(({ record, identity }) => normalized.parsed.stable_ref === record.stable_ref || normalized.parsed.stable_ref === identity.stable_ref);
383
+ if (!match) fail("material_reference_not_found", "v2 material StableRef is absent from the manifest");
384
+ const bytes = readRecord({ repositoryRoot, root, record: match.record, fileSystem, sourceReader, gitRunner, inventory: snapshot ? state.inventory : undefined });
385
+ if (normalized.offset > bytes.byteLength) fail("material_offset_past_end", "offset is past the end of the material");
386
+ let end = Math.min(bytes.byteLength, normalized.offset + normalized.length);
387
+ if (normalized.offset < bytes.byteLength && (bytes[normalized.offset] & 0xc0) === 0x80) fail("material_utf8_split_start", "offset splits a UTF-8 code point");
388
+ if (contextUtf8SafeEnd) while (end > normalized.offset && end < bytes.byteLength && (bytes[end] & 0xc0) === 0x80) end -= 1;
389
+ else if (end < bytes.byteLength && (bytes[end] & 0xc0) === 0x80) fail("material_utf8_split_end", "page end splits a UTF-8 code point");
390
+ const page = bytes.subarray(normalized.offset, end);
391
+ let content;
392
+ try { content = utf8Fatal.decode(page); } catch { fail("material_source_utf8_invalid", "material source page is not valid UTF-8"); }
393
+ return deepFreeze({
394
+ schema_version: "2.0", identity: match.identity, source: sourceIdentity(match.record),
395
+ offset: normalized.offset, length: page.byteLength, next_offset: end < bytes.byteLength ? end : null,
396
+ complete: normalized.offset === 0 && end === bytes.byteLength, content, bytes_base64: page.toString("base64"),
397
+ });
398
+ }