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
|
@@ -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
|
+
}
|