stylegallery 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +156 -0
- package/CATALOG.md +69 -0
- package/DOMAINS.md +65 -0
- package/GOVERNANCE.md +222 -0
- package/GUIDE.md +59 -0
- package/README.md +313 -0
- package/consumer-reference/adapters/style-dictionary.config.mjs +36 -0
- package/consumer-reference/agent-native/README.md +157 -0
- package/consumer-reference/agent-native/registry.json +640 -0
- package/consumer-reference/agent-native/schema/agent-native.schema.json +848 -0
- package/consumer-reference/agent-native/schema/epistemic.schema.json +14 -0
- package/consumer-reference/agent-native/schema/execution.schema.json +12 -0
- package/consumer-reference/agent-native/schema/identity.schema.json +10 -0
- package/consumer-reference/agent-native/schema/learning.schema.json +12 -0
- package/consumer-reference/agent-native/schema/manifest.schema.json +6 -0
- package/consumer-reference/agent-native/schema/operation.schema.json +12 -0
- package/consumer-reference/agent-native/schema/protocol-binding.schema.json +9 -0
- package/consumer-reference/agent-native/schema/retrieval.schema.json +10 -0
- package/consumer-reference/baselines/calibration.json +260 -0
- package/consumer-reference/baselines/manifest.json +21 -0
- package/consumer-reference/contract.md +83 -0
- package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +282 -0
- package/consumer-reference/fixtures/consumer-conformance/manifest.json +102 -0
- package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +32 -0
- package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +18 -0
- package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +142 -0
- package/consumer-reference/fixtures/invalid-stable-ended-support.json +19 -0
- package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +26 -0
- package/consumer-reference/fixtures/promotion/invalid-count-only.json +29 -0
- package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +25 -0
- package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +25 -0
- package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +45 -0
- package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +26 -0
- package/consumer-reference/fixtures/promotion/manifest.json +15 -0
- package/consumer-reference/fixtures/promotion/valid-deferred-example.json +67 -0
- package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +67 -0
- package/consumer-reference/fixtures/token-portability/invalid-cycle.json +7 -0
- package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +6 -0
- package/consumer-reference/fixtures/token-portability/invalid-extends.json +4 -0
- package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +7 -0
- package/consumer-reference/fixtures/token-portability/invalid-resolver.json +9 -0
- package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +10 -0
- package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +6 -0
- package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +6 -0
- package/consumer-reference/fixtures/token-portability/valid-border.json +14 -0
- package/consumer-reference/fixtures/token-portability/valid-reference.json +32 -0
- package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +7 -0
- package/consumer-reference/fixtures/valid-deprecated.json +21 -0
- package/consumer-reference/fixtures/valid-experimental.json +19 -0
- package/consumer-reference/generated/manifest.json +40 -0
- package/consumer-reference/generated/tokens.css +12 -0
- package/consumer-reference/index.md +32 -0
- package/consumer-reference/policies/shared-experimental.json +37 -0
- package/consumer-reference/schema/ax-evidence.schema.json +30 -0
- package/consumer-reference/schema/baseline-manifest.schema.json +55 -0
- package/consumer-reference/schema/calibration-record.schema.json +190 -0
- package/consumer-reference/schema/capture-session.schema.json +161 -0
- package/consumer-reference/schema/component-state.schema.json +150 -0
- package/consumer-reference/schema/consumer-conformance-record.schema.json +365 -0
- package/consumer-reference/schema/dom-evidence.schema.json +38 -0
- package/consumer-reference/schema/evidence-record.schema.json +81 -0
- package/consumer-reference/schema/fixture-manifest.schema.json +32 -0
- package/consumer-reference/schema/item.schema.json +188 -0
- package/consumer-reference/schema/page-evidence-manifest.schema.json +216 -0
- package/consumer-reference/schema/page-evidence-session.schema.json +119 -0
- package/consumer-reference/schema/portable-tokens.schema.json +104 -0
- package/consumer-reference/schema/promotion-rfc.schema.json +219 -0
- package/consumer-reference/schema/runtime-evidence-manifest.schema.json +33 -0
- package/consumer-reference/schema/visual-evidence.schema.json +31 -0
- package/design-engineering/consumer-migration-readiness.md +124 -0
- package/design-engineering/index.md +28 -0
- package/design-engineering/interface-craft.md +107 -0
- package/design-engineering/reference-profiles/external-adaptation/index.md +12 -0
- package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +125 -0
- package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +3504 -0
- package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +65 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +23 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +19 -0
- package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +21 -0
- package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +12 -0
- package/design-engineering/reference-profiles/governed-local/editorial/profile.json +43 -0
- package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +195 -0
- package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +17 -0
- package/design-engineering/reference-profiles/governed-local/index.md +26 -0
- package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +125 -0
- package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +3504 -0
- package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +65 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +23 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +19 -0
- package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +21 -0
- package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +12 -0
- package/design-engineering/reference-profiles/governed-local/terminal/profile.json +43 -0
- package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +195 -0
- package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +17 -0
- package/design-engineering/reference-profiles/index.md +19 -0
- package/game-ui/classification.md +82 -0
- package/game-ui/index.md +35 -0
- package/game-ui/reference-record.md +62 -0
- package/game-ui/screen-hierarchy.md +107 -0
- package/game-ui/unity/architecture.md +166 -0
- package/game-ui/unity/cli-loop.md +84 -0
- package/game-ui/unity/data/unity-technologies-public-repositories.json +26654 -0
- package/game-ui/unity/org-wiki.md +146 -0
- package/game-ui/unity/repository-map.md +102 -0
- package/game-ui/unity/ui-systems.md +85 -0
- package/guides/decision-tree.md +149 -0
- package/guides/documentation-mode-taxonomy.md +157 -0
- package/guides/layout-brief.md +220 -0
- package/guides/vocabulary.md +148 -0
- package/guides/webpage-generation-workflow.md +185 -0
- package/index.md +36 -0
- package/layout/index.md +26 -0
- package/motion/index.md +24 -0
- package/motion/practice-reference.md +145 -0
- package/motion/review-workflow.md +86 -0
- package/motion/vocabulary.md +82 -0
- package/package.json +78 -0
- package/patterns/centering/center.md +87 -0
- package/patterns/centering/index.md +6 -0
- package/patterns/centering/super-center.md +89 -0
- package/patterns/containment/box.md +87 -0
- package/patterns/containment/clamped-card.md +86 -0
- package/patterns/containment/content-limiter.md +87 -0
- package/patterns/containment/fluid-styles.md +86 -0
- package/patterns/containment/index.md +8 -0
- package/patterns/grid-repetition/card-grid.md +88 -0
- package/patterns/grid-repetition/columns.md +87 -0
- package/patterns/grid-repetition/dense-grid.md +88 -0
- package/patterns/grid-repetition/grid-wrapper.md +89 -0
- package/patterns/grid-repetition/index.md +12 -0
- package/patterns/grid-repetition/masonry-approx.md +87 -0
- package/patterns/grid-repetition/page-grid.md +90 -0
- package/patterns/grid-repetition/ram-grid.md +88 -0
- package/patterns/grid-repetition/twelve-span-grid.md +92 -0
- package/patterns/in-line-grouping/badge-list.md +94 -0
- package/patterns/in-line-grouping/breadcrumb.md +89 -0
- package/patterns/in-line-grouping/cluster.md +89 -0
- package/patterns/in-line-grouping/deconstructed-pancake.md +92 -0
- package/patterns/in-line-grouping/index.md +13 -0
- package/patterns/in-line-grouping/pagination.md +89 -0
- package/patterns/in-line-grouping/reel.md +90 -0
- package/patterns/in-line-grouping/split-nav.md +97 -0
- package/patterns/in-line-grouping/tab-strip.md +88 -0
- package/patterns/in-line-grouping/wrap-row.md +89 -0
- package/patterns/index.md +13 -0
- package/patterns/media-fit/frame.md +95 -0
- package/patterns/media-fit/icon-frame.md +87 -0
- package/patterns/media-fit/index.md +6 -0
- package/patterns/overlay-exception/imposter.md +95 -0
- package/patterns/overlay-exception/index.md +6 -0
- package/patterns/overlay-exception/overlay-stack.md +89 -0
- package/patterns/split-sidebar/index.md +12 -0
- package/patterns/split-sidebar/list-detail.md +95 -0
- package/patterns/split-sidebar/main-with-rail.md +95 -0
- package/patterns/split-sidebar/media-object.md +102 -0
- package/patterns/split-sidebar/sidebar.md +101 -0
- package/patterns/split-sidebar/split-screen.md +92 -0
- package/patterns/split-sidebar/sticky-aside.md +97 -0
- package/patterns/split-sidebar/supporting-pane.md +95 -0
- package/patterns/split-sidebar/switcher.md +93 -0
- package/patterns/stacking/feed.md +88 -0
- package/patterns/stacking/index.md +8 -0
- package/patterns/stacking/line-up.md +95 -0
- package/patterns/stacking/stack.md +87 -0
- package/patterns/stacking/step-nav.md +88 -0
- package/patterns/viewport-shell/cover.md +90 -0
- package/patterns/viewport-shell/fixed-sidenav-shell.md +97 -0
- package/patterns/viewport-shell/holy-grail.md +112 -0
- package/patterns/viewport-shell/index.md +11 -0
- package/patterns/viewport-shell/panel-layout.md +95 -0
- package/patterns/viewport-shell/scroll-body-shell.md +93 -0
- package/patterns/viewport-shell/sticky-footer.md +92 -0
- package/patterns/viewport-shell/sticky-header.md +86 -0
- package/platform-guides/apple-interaction.md +89 -0
- package/platform-guides/index.md +26 -0
- package/quality/claim-records/index.md +3 -0
- package/quality/claim-records/samples.md +92 -0
- package/quality/claim-records/stylegallery-multidomain-scope.md +42 -0
- package/quality/claims.md +77 -0
- package/quality/evidence/accessibility.md +58 -0
- package/quality/evidence/consumer-migration.md +42 -0
- package/quality/evidence/design-rationale.md +34 -0
- package/quality/evidence/executable-evidence.md +79 -0
- package/quality/evidence/families.md +56 -0
- package/quality/evidence/hci-evaluation.md +36 -0
- package/quality/evidence/index.md +16 -0
- package/quality/evidence/pattern-contract-audit.md +96 -0
- package/quality/evidence/search-metadata-polyhierarchy.md +86 -0
- package/quality/evidence/sources.md +43 -0
- package/quality/evidence/tokens.md +33 -0
- package/quality/evidence/visual-qa.md +41 -0
- package/quality/gates/accessibility-evidence.md +60 -0
- package/quality/gates/consumer-migration-evidence.md +53 -0
- package/quality/gates/design-claim.md +56 -0
- package/quality/gates/harmony-evaluation.md +78 -0
- package/quality/gates/index.md +13 -0
- package/quality/gates/layout.md +52 -0
- package/quality/gates/rationale.md +53 -0
- package/quality/gates/visual-evidence.md +47 -0
- package/quality/index.md +55 -0
- package/quality/principles.md +58 -0
- package/recipes/article-page.md +52 -0
- package/recipes/command-surface.md +55 -0
- package/recipes/dashboard.md +56 -0
- package/recipes/form-flow.md +55 -0
- package/recipes/homepage.md +89 -0
- package/recipes/index.md +13 -0
- package/recipes/list-detail.md +55 -0
- package/recipes/primitive-to-recipe-matrix.md +83 -0
- package/recipes/saas-settings.md +52 -0
- package/scripts/agent-native/a2a-projection.mjs +248 -0
- package/scripts/agent-native/agui-projection.mjs +178 -0
- package/scripts/agent-native/canonical-json.mjs +109 -0
- package/scripts/agent-native/cli-adapter.mjs +68 -0
- package/scripts/agent-native/events.mjs +113 -0
- package/scripts/agent-native/execution.mjs +227 -0
- package/scripts/agent-native/fixture.mjs +159 -0
- package/scripts/agent-native/identity.mjs +184 -0
- package/scripts/agent-native/immutable-store.mjs +102 -0
- package/scripts/agent-native/knowledge.mjs +168 -0
- package/scripts/agent-native/learning-records.mjs +244 -0
- package/scripts/agent-native/learning.mjs +247 -0
- package/scripts/agent-native/mcp-adapter.mjs +173 -0
- package/scripts/agent-native/queries.mjs +173 -0
- package/scripts/agent-native/registry.mjs +154 -0
- package/scripts/agent-native/retrieval.mjs +171 -0
- package/scripts/agent-native/self-description.mjs +160 -0
- package/scripts/artifact-metadata.mjs +20 -0
- package/scripts/audit-evidence-freshness.mjs +308 -0
- package/scripts/baseline-contract.mjs +189 -0
- package/scripts/baseline-schema-parity.mjs +40 -0
- package/scripts/build-reference-artifacts.mjs +127 -0
- package/scripts/calibration-raw-contract.mjs +172 -0
- package/scripts/capture-session-contract.mjs +187 -0
- package/scripts/component-state-artifact-fixture.mjs +44 -0
- package/scripts/component-state-contract.mjs +99 -0
- package/scripts/component-state-semantics.mjs +128 -0
- package/scripts/component-state-workflow-contract.mjs +67 -0
- package/scripts/component-state-workflow-negative-cases.mjs +64 -0
- package/scripts/consumer-conformance-contract.mjs +198 -0
- package/scripts/consumer-evidence-governance-contract.mjs +210 -0
- package/scripts/consumer-reference-case-runner.mjs +93 -0
- package/scripts/consumer-reference-schema.mjs +155 -0
- package/scripts/create-component-state-session.mjs +100 -0
- package/scripts/create-page-evidence-session.mjs +139 -0
- package/scripts/domain-document-boundaries.mjs +40 -0
- package/scripts/domain-validation-checks.mjs +141 -0
- package/scripts/evidence-artifact-contract.mjs +191 -0
- package/scripts/finalize-component-state-evidence.mjs +165 -0
- package/scripts/finalize-page-evidence.mjs +197 -0
- package/scripts/generate-consumer-reference-evidence.mjs +154 -0
- package/scripts/generate-patterns.mjs +218 -0
- package/scripts/governance-matrix-contract.mjs +73 -0
- package/scripts/governance-matrix-negative-cases.mjs +114 -0
- package/scripts/governance-policy-contract.mjs +49 -0
- package/scripts/governance-test-fixture.mjs +321 -0
- package/scripts/governance-workflow-negative-cases.mjs +241 -0
- package/scripts/governed-profile-registry.mjs +125 -0
- package/scripts/json-schema-formats.mjs +31 -0
- package/scripts/markdown-structure.mjs +45 -0
- package/scripts/page-artifact-metadata.mjs +124 -0
- package/scripts/page-evidence-contract.mjs +297 -0
- package/scripts/page-evidence-fixture.mjs +271 -0
- package/scripts/pattern-data.mjs +325 -0
- package/scripts/profile-record-contract.mjs +140 -0
- package/scripts/promotion-attestation-contract.mjs +91 -0
- package/scripts/promotion-boundary-test-contract.mjs +92 -0
- package/scripts/promotion-fixture-inventory.mjs +109 -0
- package/scripts/promotion-governance-contract.mjs +28 -0
- package/scripts/reference-artifact-path-contract.mjs +28 -0
- package/scripts/reference-profile-contract.mjs +114 -0
- package/scripts/reference-token-contract.mjs +156 -0
- package/scripts/run-consumer-page-evidence-ci.mjs +170 -0
- package/scripts/sg-mcp.mjs +16 -0
- package/scripts/sg.mjs +27 -0
- package/scripts/strict-json.mjs +72 -0
- package/scripts/summarize-sentinel-calibration.mjs +98 -0
- package/scripts/test-agent-adapter-conformance.mjs +250 -0
- package/scripts/test-agent-conformance-receipt.mjs +106 -0
- package/scripts/test-agent-events.mjs +81 -0
- package/scripts/test-agent-execution-effects.mjs +114 -0
- package/scripts/test-agent-execution-fixtures.mjs +140 -0
- package/scripts/test-agent-execution-kernel.mjs +39 -0
- package/scripts/test-agent-execution-learning.mjs +130 -0
- package/scripts/test-agent-execution-lifecycle.mjs +143 -0
- package/scripts/test-agent-execution-safety.mjs +158 -0
- package/scripts/test-agent-execution-views.mjs +117 -0
- package/scripts/test-agent-identity-integrity-cases.mjs +160 -0
- package/scripts/test-agent-knowledge-kernel.mjs +183 -0
- package/scripts/test-agent-learning.mjs +246 -0
- package/scripts/test-agent-mcp.mjs +246 -0
- package/scripts/test-agent-native-schemas.mjs +215 -0
- package/scripts/test-agent-projections.mjs +249 -0
- package/scripts/test-agent-registry-mcp-integrity.mjs +215 -0
- package/scripts/test-agent-registry-mutations.mjs +110 -0
- package/scripts/test-audit-evidence-freshness.mjs +177 -0
- package/scripts/test-component-state-sentinel.mjs +35 -0
- package/scripts/test-component-state-source-contract.mjs +83 -0
- package/scripts/test-consumer-conformance-sentinel.mjs +165 -0
- package/scripts/test-consumer-reference-sentinel.mjs +31 -0
- package/scripts/test-generate-consumer-reference-evidence.mjs +124 -0
- package/scripts/test-json-cli-output.mjs +151 -0
- package/scripts/test-json-schema-formats.mjs +40 -0
- package/scripts/test-reference-adapters.mjs +199 -0
- package/scripts/test-renderer-purity.mjs +34 -0
- package/scripts/test-sg-cli.mjs +129 -0
- package/scripts/test-summarize-sentinel-calibration.mjs +226 -0
- package/scripts/test-validate-baseline-manifest.mjs +234 -0
- package/scripts/test-validate-component-state-artifacts.mjs +243 -0
- package/scripts/test-validate-component-state.mjs +106 -0
- package/scripts/test-validate-consumer-conformance.mjs +427 -0
- package/scripts/test-validate-consumer-reference-cli.mjs +69 -0
- package/scripts/test-validate-consumer-reference.mjs +263 -0
- package/scripts/test-validate-domains.mjs +357 -0
- package/scripts/test-validate-governance.mjs +292 -0
- package/scripts/test-validate-ia.mjs +262 -0
- package/scripts/test-validate-okf.mjs +73 -0
- package/scripts/test-validate-page-evidence.mjs +302 -0
- package/scripts/test-validate-patterns.mjs +157 -0
- package/scripts/test-validate-promotion-rfc.mjs +250 -0
- package/scripts/test-validate-reference-profiles.mjs +170 -0
- package/scripts/test-validate-unity-org-wiki.mjs +57 -0
- package/scripts/test-validate-unity-source-contracts.mjs +67 -0
- package/scripts/test-validate-vocabulary.mjs +174 -0
- package/scripts/test-validate-webpage-workflow.mjs +293 -0
- package/scripts/validate-baseline-manifest.mjs +44 -0
- package/scripts/validate-catalog.mjs +65 -0
- package/scripts/validate-component-state.mjs +141 -0
- package/scripts/validate-consumer-conformance.mjs +477 -0
- package/scripts/validate-consumer-reference.mjs +288 -0
- package/scripts/validate-domains.mjs +237 -0
- package/scripts/validate-governance.mjs +248 -0
- package/scripts/validate-ia.mjs +136 -0
- package/scripts/validate-links.mjs +66 -0
- package/scripts/validate-okf.mjs +104 -0
- package/scripts/validate-page-evidence.mjs +191 -0
- package/scripts/validate-patterns.mjs +246 -0
- package/scripts/validate-promotion-rfc.mjs +224 -0
- package/scripts/validate-reference-artifacts.mjs +125 -0
- package/scripts/validate-renderer-purity.mjs +56 -0
- package/scripts/validate-unity-org-wiki.mjs +146 -0
- package/scripts/validate-unity-source-contracts.mjs +107 -0
- package/scripts/validate-vocabulary.mjs +115 -0
- package/scripts/validate-webpage-workflow.mjs +254 -0
- package/scripts/visual-expectation-contract.mjs +20 -0
- package/scripts/workflow-action-contract.mjs +163 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { deepFreeze } from "./canonical-json.mjs";
|
|
2
|
+
import { createStableRef, createVersionId, parseStableRef, parseVersionId } from "./identity.mjs";
|
|
3
|
+
|
|
4
|
+
const CONTROL_KEYS = new Set([
|
|
5
|
+
"record_kind", "schema_version", "stable_ref", "stableRef", "version_id", "versionId",
|
|
6
|
+
]);
|
|
7
|
+
const PASSING = new Set(["PASS", "PASSED", "VALID", "VALIDATED", "OK"]);
|
|
8
|
+
|
|
9
|
+
export class LearningError extends TypeError {
|
|
10
|
+
constructor(code, message, path = "") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "LearningError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
if (path) this.path = path;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function failLearning(code, message, path = "") {
|
|
19
|
+
throw new LearningError(code, message, path);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function own(value, ...keys) {
|
|
23
|
+
if (!value || typeof value !== "object") return undefined;
|
|
24
|
+
for (const key of keys) if (Object.hasOwn(value, key)) return value[key];
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function learningObject(value, path) {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
30
|
+
failLearning("learning_object_required", `${path} expects an object`, path);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function copyLearning(value) {
|
|
36
|
+
if (value === undefined || value === null || typeof value !== "object") return value;
|
|
37
|
+
if (Array.isArray(value)) return value.map((item) => (item === undefined ? null : copyLearning(item)));
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
Object.entries(value)
|
|
40
|
+
.filter(([, item]) => item !== undefined)
|
|
41
|
+
.map(([key, item]) => [key, copyLearning(item)]),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function compactLearning(fields) {
|
|
46
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function textLearning(value, path) {
|
|
50
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
51
|
+
failLearning("learning_text_required", `${path} must be a non-empty string`, path);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function stableLearningRef(value, path, fallback) {
|
|
57
|
+
try {
|
|
58
|
+
return createStableRef(value ?? fallback);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
failLearning("learning_stable_ref_invalid", error instanceof Error ? error.message : String(error), path);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function canonicalFields(source) {
|
|
65
|
+
return Object.fromEntries(
|
|
66
|
+
Object.entries(source)
|
|
67
|
+
.filter(([key, value]) => !CONTROL_KEYS.has(key) && value !== undefined)
|
|
68
|
+
.map(([key, value]) => [key, copyLearning(value)]),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function makeLearningRecord(input, stableRef, recordKind, fields) {
|
|
73
|
+
const source = learningObject(input, recordKind);
|
|
74
|
+
if (source.record_kind !== undefined && source.record_kind !== recordKind) {
|
|
75
|
+
failLearning("learning_record_kind_invalid", `${recordKind} cannot be created from ${source.record_kind}`, "record_kind");
|
|
76
|
+
}
|
|
77
|
+
if (source.schema_version !== undefined && source.schema_version !== "1.0") {
|
|
78
|
+
failLearning("learning_schema_version_invalid", "learning records require schema_version 1.0", "schema_version");
|
|
79
|
+
}
|
|
80
|
+
const stable_ref = stableLearningRef(stableRef, "stable_ref");
|
|
81
|
+
const payload = {
|
|
82
|
+
schema_version: "1.0",
|
|
83
|
+
record_kind: recordKind,
|
|
84
|
+
stable_ref,
|
|
85
|
+
...Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)),
|
|
86
|
+
};
|
|
87
|
+
const version_id = createVersionId({ stable_ref, payload });
|
|
88
|
+
const supplied = own(source, "version_id", "versionId");
|
|
89
|
+
if (supplied !== undefined && supplied !== version_id) {
|
|
90
|
+
failLearning("learning_version_mismatch", "version_id does not match immutable content", "version_id");
|
|
91
|
+
}
|
|
92
|
+
return deepFreeze({ ...copyLearning(payload), version_id });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function assertLearningRecord(value, recordKind, path = recordKind) {
|
|
96
|
+
const source = learningObject(value, path);
|
|
97
|
+
if (Object.hasOwn(source, "stableRef") || Object.hasOwn(source, "versionId")) {
|
|
98
|
+
failLearning("learning_record_alias_forbidden", `${path} must use canonical identity keys`, path);
|
|
99
|
+
}
|
|
100
|
+
if (source.record_kind !== recordKind) {
|
|
101
|
+
failLearning("learning_record_kind_invalid", `${path} must be ${recordKind}`, `${path}.record_kind`);
|
|
102
|
+
}
|
|
103
|
+
if (source.schema_version !== "1.0") {
|
|
104
|
+
failLearning("learning_schema_version_invalid", `${path} must use schema_version 1.0`, `${path}.schema_version`);
|
|
105
|
+
}
|
|
106
|
+
const stable_ref = stableLearningRef(own(source, "stable_ref", "stableRef"), `${path}.stable_ref`);
|
|
107
|
+
const version_id = own(source, "version_id", "versionId");
|
|
108
|
+
const payload = copyLearning(source);
|
|
109
|
+
delete payload.version_id;
|
|
110
|
+
delete payload.versionId;
|
|
111
|
+
try {
|
|
112
|
+
parseVersionId({ payload, stable_ref, version_id });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
failLearning(
|
|
115
|
+
"learning_record_version_invalid",
|
|
116
|
+
`${path} VersionID does not bind its canonical bytes: ${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
`${path}.version_id`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return deepFreeze(copyLearning(source));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function canonicalAliases(source) {
|
|
124
|
+
const fields = canonicalFields(source);
|
|
125
|
+
const aliases = [
|
|
126
|
+
["independence_group", "independentGroup"],
|
|
127
|
+
["proposal_ref", "proposalRef"],
|
|
128
|
+
["proposal_version_id", "proposalVersionId"],
|
|
129
|
+
["receipt_id", "receiptId"],
|
|
130
|
+
["source_ref", "sourceRef"],
|
|
131
|
+
["source_version_id", "sourceVersionId"],
|
|
132
|
+
];
|
|
133
|
+
for (const [canonical, alias] of aliases) {
|
|
134
|
+
if (fields[canonical] === undefined && fields[alias] !== undefined) fields[canonical] = fields[alias];
|
|
135
|
+
delete fields[alias];
|
|
136
|
+
}
|
|
137
|
+
return fields;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyBindings(fields, bindings, path) {
|
|
141
|
+
for (const [key, expected] of Object.entries(bindings)) {
|
|
142
|
+
if (fields[key] !== undefined && fields[key] !== expected) {
|
|
143
|
+
failLearning("learning_link_mismatch", `${path}.${key} does not match its governed parent`, `${path}.${key}`);
|
|
144
|
+
}
|
|
145
|
+
fields[key] = expected;
|
|
146
|
+
}
|
|
147
|
+
return fields;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function makeEvidenceRecords(values, options) {
|
|
151
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
152
|
+
failLearning("learning_evidence_required", "at least one evidence record is required", options.path);
|
|
153
|
+
}
|
|
154
|
+
return values.map((item, index) => {
|
|
155
|
+
const path = `${options.path}/${index}`;
|
|
156
|
+
const source = learningObject(item, path);
|
|
157
|
+
const stableRef = stableLearningRef(
|
|
158
|
+
own(source, "stable_ref", "stableRef"),
|
|
159
|
+
`${path}.stable_ref`,
|
|
160
|
+
`sg:evidence/${options.slug}-${index + 1}`,
|
|
161
|
+
);
|
|
162
|
+
if (parseStableRef(stableRef).kind !== "evidence") {
|
|
163
|
+
failLearning("learning_record_kind_invalid", `${path}.stable_ref must use sg:evidence/...`, `${path}.stable_ref`);
|
|
164
|
+
}
|
|
165
|
+
const fields = applyBindings(canonicalAliases(source), options.bindings ?? {}, path);
|
|
166
|
+
return makeLearningRecord(source, stableRef, "evidence_link", fields);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function makeValidationRecord(value, options) {
|
|
171
|
+
const source = learningObject(value, options.path);
|
|
172
|
+
const fields = canonicalAliases(source);
|
|
173
|
+
if (fields.validator === undefined && fields.validator_actor !== undefined) fields.validator = fields.validator_actor;
|
|
174
|
+
if (fields.validator_version === undefined && fields.version !== undefined) fields.validator_version = fields.version;
|
|
175
|
+
delete fields.validator_actor;
|
|
176
|
+
delete fields.version;
|
|
177
|
+
fields.validator = textLearning(fields.validator, `${options.path}.validator`).trim();
|
|
178
|
+
const status = textLearning(fields.status, `${options.path}.status`).toUpperCase();
|
|
179
|
+
if (!PASSING.has(status)) {
|
|
180
|
+
failLearning("learning_validation_not_passed", `${options.path} must have a passing status`, `${options.path}.status`);
|
|
181
|
+
}
|
|
182
|
+
fields.status = status;
|
|
183
|
+
applyBindings(fields, options.bindings ?? {}, options.path);
|
|
184
|
+
if (parseStableRef(options.stableRef).kind !== "validation") {
|
|
185
|
+
failLearning("learning_record_kind_invalid", `${options.path}.stable_ref must use sg:validation/...`, `${options.path}.stable_ref`);
|
|
186
|
+
}
|
|
187
|
+
return makeLearningRecord(source, options.stableRef, "validation_report", fields);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function assertLink(record, key, expected, path) {
|
|
191
|
+
if (record[key] !== expected) {
|
|
192
|
+
failLearning("learning_link_mismatch", `${path}.${key} must bind ${expected}`, `${path}.${key}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function assertEvidenceRecords(values, bindings, path = "evidence") {
|
|
197
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
198
|
+
failLearning("learning_evidence_required", `${path} requires at least one evidence record`, path);
|
|
199
|
+
}
|
|
200
|
+
return values.map((value, index) => {
|
|
201
|
+
const record = assertLearningRecord(value, "evidence_link", `${path}/${index}`);
|
|
202
|
+
if (parseStableRef(record.stable_ref).kind !== "evidence") {
|
|
203
|
+
failLearning("learning_record_kind_invalid", `${path}/${index} must use sg:evidence/...`, `${path}/${index}/stable_ref`);
|
|
204
|
+
}
|
|
205
|
+
for (const [key, expected] of Object.entries(bindings ?? {})) assertLink(record, key, expected, `${path}/${index}`);
|
|
206
|
+
return record;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function assertValidationRecord(value, bindings, path = "validation") {
|
|
211
|
+
const record = assertLearningRecord(value, "validation_report", path);
|
|
212
|
+
if (parseStableRef(record.stable_ref).kind !== "validation") {
|
|
213
|
+
failLearning("learning_record_kind_invalid", `${path} must use sg:validation/...`, `${path}.stable_ref`);
|
|
214
|
+
}
|
|
215
|
+
if (!PASSING.has(String(record.status).toUpperCase())) {
|
|
216
|
+
failLearning("learning_validation_not_passed", `${path} must have a passing status`, `${path}.status`);
|
|
217
|
+
}
|
|
218
|
+
const validator = textLearning(record.validator, `${path}.validator`);
|
|
219
|
+
if (validator !== validator.trim()) {
|
|
220
|
+
failLearning("learning_actor_invalid", `${path}.validator must be canonical`, `${path}.validator`);
|
|
221
|
+
}
|
|
222
|
+
for (const [key, expected] of Object.entries(bindings ?? {})) assertLink(record, key, expected, path);
|
|
223
|
+
return record;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function nextLearningRegistry(registry, targetRef, versionId) {
|
|
227
|
+
if (registry === undefined) return undefined;
|
|
228
|
+
const source = learningObject(registry, "registry");
|
|
229
|
+
const entries = own(source, "entries");
|
|
230
|
+
if (!Array.isArray(entries)) {
|
|
231
|
+
failLearning("learning_registry_invalid", "registry.entries must be an array", "registry.entries");
|
|
232
|
+
}
|
|
233
|
+
const nextEntries = entries.map(copyLearning);
|
|
234
|
+
const index = nextEntries.findIndex((entry) => own(entry, "stable_ref", "stableRef") === targetRef);
|
|
235
|
+
const replacement = { stable_ref: targetRef, version_id: versionId };
|
|
236
|
+
if (index === -1) nextEntries.push(replacement);
|
|
237
|
+
else nextEntries[index] = { ...nextEntries[index], ...replacement };
|
|
238
|
+
nextEntries.sort((a, b) => {
|
|
239
|
+
const left = String(own(a, "stable_ref", "stableRef"));
|
|
240
|
+
const right = String(own(b, "stable_ref", "stableRef"));
|
|
241
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
242
|
+
});
|
|
243
|
+
return deepFreeze({ ...copyLearning(source), entries: nextEntries });
|
|
244
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { deepFreeze } from "./canonical-json.mjs";
|
|
2
|
+
import { parseStableRef } from "./identity.mjs";
|
|
3
|
+
import {
|
|
4
|
+
LearningError, assertEvidenceRecords, assertLearningRecord, assertLink, assertValidationRecord,
|
|
5
|
+
compactLearning, copyLearning, failLearning, learningObject, makeEvidenceRecords,
|
|
6
|
+
makeLearningRecord, makeValidationRecord, nextLearningRegistry, own, stableLearningRef,
|
|
7
|
+
textLearning,
|
|
8
|
+
} from "./learning-records.mjs";
|
|
9
|
+
|
|
10
|
+
const ACCEPTED = new Set(["ACCEPT", "ACCEPTED", "APPROVE", "APPROVED"]);
|
|
11
|
+
const DECISIONS = new Set([...ACCEPTED, "REJECT", "REJECTED", "DENY", "DENIED", "DEFER", "DEFERRED"]);
|
|
12
|
+
|
|
13
|
+
export { LearningError };
|
|
14
|
+
|
|
15
|
+
function slug(ref) {
|
|
16
|
+
return ref.slice(ref.indexOf("/") + 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireRefKind(ref, kind, path) {
|
|
20
|
+
if (parseStableRef(ref).kind !== kind) {
|
|
21
|
+
failLearning("learning_record_kind_invalid", `${path} must use sg:${kind}/...`, path);
|
|
22
|
+
}
|
|
23
|
+
return ref;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function actorOf(value, path) {
|
|
27
|
+
return textLearning(
|
|
28
|
+
own(value, "verifier", "verifier_actor", "verifierActor", "verified_by", "verifiedBy", "actor", "subject"),
|
|
29
|
+
path,
|
|
30
|
+
).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function blocked(code, message, fields = {}) {
|
|
34
|
+
return deepFreeze({ ...fields, code, message, ok: false, status: "BLOCKED" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function decisionOf(value) {
|
|
38
|
+
const decision = typeof value === "string" ? value : own(value, "decision", "status", "disposition");
|
|
39
|
+
return typeof decision === "string" ? decision.toUpperCase() : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertProposal(value) {
|
|
43
|
+
const proposal = assertLearningRecord(value, "learning_proposal", "proposal");
|
|
44
|
+
requireRefKind(proposal.stable_ref, "proposal", "proposal.stable_ref");
|
|
45
|
+
const proposer = textLearning(proposal.proposer, "proposal.proposer");
|
|
46
|
+
if (proposer !== proposer.trim()) {
|
|
47
|
+
failLearning("learning_actor_invalid", "proposal.proposer must be canonical", "proposal.proposer");
|
|
48
|
+
}
|
|
49
|
+
learningObject(proposal.claim, "proposal.claim");
|
|
50
|
+
if (!Array.isArray(proposal.changes) || proposal.changes.length === 0) {
|
|
51
|
+
failLearning("learning_changes_required", "proposal requires at least one proposed change", "proposal.changes");
|
|
52
|
+
}
|
|
53
|
+
assertEvidenceRecords(proposal.evidence, undefined, "proposal.evidence");
|
|
54
|
+
assertValidationRecord(proposal.validation, undefined, "proposal.validation");
|
|
55
|
+
return proposal;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function proposalOf(input) {
|
|
59
|
+
const proposal = assertProposal(own(input, "proposal"));
|
|
60
|
+
return { proposal, proposalRef: proposal.stable_ref };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertVerification(value, proposal) {
|
|
64
|
+
const verification = assertLearningRecord(value, "learning_verification", "verification");
|
|
65
|
+
requireRefKind(verification.stable_ref, "validation", "verification.stable_ref");
|
|
66
|
+
assertLink(verification, "proposal_ref", proposal.stable_ref, "verification");
|
|
67
|
+
assertLink(verification, "proposal_version_id", proposal.version_id, "verification");
|
|
68
|
+
const verifier = actorOf(verification, "verification.verifier");
|
|
69
|
+
if (verifier === proposal.proposer || verification.independent !== true || verification.verified !== true) {
|
|
70
|
+
failLearning("learning_verification_requires_independence", "verification must be independent from the proposer", "verification.verifier");
|
|
71
|
+
}
|
|
72
|
+
assertLink(verification, "independent_of", proposal.proposer, "verification");
|
|
73
|
+
const bindings = { proposal_ref: proposal.stable_ref, proposal_version_id: proposal.version_id };
|
|
74
|
+
assertEvidenceRecords(verification.evidence, bindings, "verification.evidence");
|
|
75
|
+
const validation = assertValidationRecord(verification.validation, bindings, "verification.validation");
|
|
76
|
+
if (validation.validator === proposal.proposer) {
|
|
77
|
+
failLearning("learning_verification_requires_independence", "proposal author cannot validate its verification", "verification.validation.validator");
|
|
78
|
+
}
|
|
79
|
+
return verification;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertDecision(value, proposal, verification) {
|
|
83
|
+
const governance = assertLearningRecord(value, "governance_decision", "governance_decision");
|
|
84
|
+
requireRefKind(governance.stable_ref, "governance", "governance_decision.stable_ref");
|
|
85
|
+
assertLink(governance, "proposal_ref", proposal.stable_ref, "governance_decision");
|
|
86
|
+
assertLink(governance, "proposal_version_id", proposal.version_id, "governance_decision");
|
|
87
|
+
assertLink(governance, "verification_ref", verification.stable_ref, "governance_decision");
|
|
88
|
+
assertLink(governance, "verification_version_id", verification.version_id, "governance_decision");
|
|
89
|
+
const actor = actorOf(governance, "governance_decision.actor");
|
|
90
|
+
if (actor === proposal.proposer || actor === verification.verifier) {
|
|
91
|
+
failLearning("learning_governance_requires_independence", "proposal author or verifier cannot govern the same proposal", "governance_decision.actor");
|
|
92
|
+
}
|
|
93
|
+
if (!DECISIONS.has(decisionOf(governance))) {
|
|
94
|
+
failLearning("learning_decision_invalid", "governance decision has an unsupported disposition", "governance_decision.decision");
|
|
95
|
+
}
|
|
96
|
+
return governance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createProposal(input) {
|
|
100
|
+
const source = learningObject(input, "createProposal");
|
|
101
|
+
const ref = requireRefKind(stableLearningRef(
|
|
102
|
+
own(source, "stable_ref", "stableRef", "proposal_id", "proposalId"),
|
|
103
|
+
"createProposal.stable_ref",
|
|
104
|
+
), "proposal", "createProposal.stable_ref");
|
|
105
|
+
const claim = learningObject(own(source, "claim"), "createProposal.claim");
|
|
106
|
+
const changes = own(source, "changes", "patch", "delta");
|
|
107
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
108
|
+
failLearning("learning_changes_required", "proposal requires at least one proposed change", "changes");
|
|
109
|
+
}
|
|
110
|
+
const proposer = textLearning(own(source, "proposer", "proposed_by", "proposedBy"), "proposer").trim();
|
|
111
|
+
const evidence = makeEvidenceRecords(own(source, "evidence", "evidence_links", "evidenceLinks"), {
|
|
112
|
+
path: "evidence",
|
|
113
|
+
slug: `${slug(ref)}-proposal-evidence`,
|
|
114
|
+
});
|
|
115
|
+
const validation = makeValidationRecord(own(source, "validation"), {
|
|
116
|
+
path: "validation",
|
|
117
|
+
stableRef: `sg:validation/${slug(ref)}-proposal`,
|
|
118
|
+
});
|
|
119
|
+
return makeLearningRecord(source, ref, "learning_proposal", {
|
|
120
|
+
changes: copyLearning(changes),
|
|
121
|
+
claim: copyLearning(claim),
|
|
122
|
+
evidence,
|
|
123
|
+
proposal_id: ref,
|
|
124
|
+
proposer,
|
|
125
|
+
source_task: copyLearning(own(source, "source_task", "sourceTask", "task_id", "taskId")),
|
|
126
|
+
status: "PROPOSED",
|
|
127
|
+
validation,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function verifyProposal(input) {
|
|
132
|
+
const source = learningObject(input, "verifyProposal");
|
|
133
|
+
const { proposal, proposalRef } = proposalOf(source);
|
|
134
|
+
const verifier = actorOf(source, "verifier");
|
|
135
|
+
const independentOf = own(source, "independent_of", "independentOf");
|
|
136
|
+
if (verifier === proposal.proposer || (independentOf !== undefined && independentOf !== proposal.proposer)) {
|
|
137
|
+
return blocked("learning_verification_requires_independence", "verifier must be independent from proposer", {
|
|
138
|
+
independent: false, verified: false, verifier,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const bindings = { proposal_ref: proposalRef, proposal_version_id: proposal.version_id };
|
|
142
|
+
const evidence = makeEvidenceRecords(own(source, "evidence", "evidence_links", "evidenceLinks"), {
|
|
143
|
+
bindings,
|
|
144
|
+
path: "evidence",
|
|
145
|
+
slug: `${slug(proposalRef)}-verification-evidence`,
|
|
146
|
+
});
|
|
147
|
+
const validation = makeValidationRecord(own(source, "validation"), {
|
|
148
|
+
bindings,
|
|
149
|
+
path: "validation",
|
|
150
|
+
stableRef: `sg:validation/${slug(proposalRef)}-verification-report`,
|
|
151
|
+
});
|
|
152
|
+
if (validation.validator === proposal.proposer) {
|
|
153
|
+
return blocked("learning_verification_requires_independence", "proposal author cannot validate its verification", {
|
|
154
|
+
independent: false, verified: false, verifier,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const ref = `sg:validation/${slug(proposalRef)}-verification`;
|
|
158
|
+
return makeLearningRecord(source, ref, "learning_verification", {
|
|
159
|
+
evidence,
|
|
160
|
+
independent: true,
|
|
161
|
+
independent_of: proposal.proposer,
|
|
162
|
+
proposal_ref: proposalRef,
|
|
163
|
+
proposal_version_id: proposal.version_id,
|
|
164
|
+
status: "VERIFIED",
|
|
165
|
+
validation,
|
|
166
|
+
verified: true,
|
|
167
|
+
verifier,
|
|
168
|
+
verifier_actor: verifier,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function decideProposal(input) {
|
|
173
|
+
const source = learningObject(input, "decideProposal");
|
|
174
|
+
const { proposal, proposalRef } = proposalOf(source);
|
|
175
|
+
const verification = assertVerification(own(source, "verification"), proposal);
|
|
176
|
+
const provided = learningObject(
|
|
177
|
+
own(source, "governance_decision", "governanceDecision") ?? source,
|
|
178
|
+
"governanceDecision",
|
|
179
|
+
);
|
|
180
|
+
const decision = textLearning(own(source, "decision") ?? decisionOf(provided), "decision").toUpperCase();
|
|
181
|
+
if (!DECISIONS.has(decision)) failLearning("learning_decision_invalid", "decision is unsupported", "decision");
|
|
182
|
+
const actor = actorOf(provided, "governanceDecision.actor");
|
|
183
|
+
if (actor === proposal.proposer || actor === verification.verifier) {
|
|
184
|
+
return blocked("learning_governance_requires_independence", "proposal author or verifier cannot govern the same proposal", { actor });
|
|
185
|
+
}
|
|
186
|
+
const ref = `sg:governance/${slug(proposalRef)}-decision`;
|
|
187
|
+
return makeLearningRecord(source, ref, "governance_decision", {
|
|
188
|
+
actor,
|
|
189
|
+
decided_at: copyLearning(own(provided, "decided_at", "decidedAt")),
|
|
190
|
+
decision,
|
|
191
|
+
proposal_ref: proposalRef,
|
|
192
|
+
proposal_version_id: proposal.version_id,
|
|
193
|
+
status: decision,
|
|
194
|
+
verification_ref: verification.stable_ref,
|
|
195
|
+
verification_version_id: verification.version_id,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function promoteProposal(input) {
|
|
200
|
+
const source = learningObject(input, "promoteProposal");
|
|
201
|
+
const { proposal, proposalRef } = proposalOf(source);
|
|
202
|
+
const rawVerification = own(source, "verification") ?? own(proposal, "verification");
|
|
203
|
+
if (!rawVerification) return blocked("learning_promotion_requires_verification", "promotion requires verification", { promoted: false, proposal_ref: proposalRef });
|
|
204
|
+
const verification = assertVerification(rawVerification, proposal);
|
|
205
|
+
const rawGovernance = own(source, "governance_decision", "governanceDecision")
|
|
206
|
+
?? (typeof own(source, "decision") === "object" ? own(source, "decision") : undefined);
|
|
207
|
+
if (!rawGovernance) return blocked("learning_promotion_requires_governance", "promotion requires governance", { promoted: false, proposal_ref: proposalRef });
|
|
208
|
+
const governance = assertDecision(rawGovernance, proposal, verification);
|
|
209
|
+
if (!ACCEPTED.has(decisionOf(governance))) {
|
|
210
|
+
return blocked("learning_promotion_requires_governance", "promotion requires an accepted decision", { promoted: false, proposal_ref: proposalRef });
|
|
211
|
+
}
|
|
212
|
+
const targetRef = stableLearningRef(
|
|
213
|
+
own(source, "target_stable_ref", "targetStableRef", "target_ref", "targetRef")
|
|
214
|
+
?? own(proposal, "target_stable_ref", "targetStableRef")
|
|
215
|
+
?? own(proposal.claim, "stable_ref", "stableRef"),
|
|
216
|
+
"target_stable_ref",
|
|
217
|
+
);
|
|
218
|
+
const version = makeLearningRecord({}, targetRef, "governed_version", {
|
|
219
|
+
changes: copyLearning(proposal.changes),
|
|
220
|
+
claim: copyLearning(proposal.claim),
|
|
221
|
+
evidence: copyLearning(verification.evidence),
|
|
222
|
+
governance_decision_version_id: governance.version_id,
|
|
223
|
+
proposal_ref: proposalRef,
|
|
224
|
+
proposal_version_id: proposal.version_id,
|
|
225
|
+
verification_version_id: verification.version_id,
|
|
226
|
+
});
|
|
227
|
+
const receipt = makeLearningRecord({}, `sg:governance/${slug(proposalRef)}-promotion`, "promotion_receipt", {
|
|
228
|
+
governed_version_id: version.version_id,
|
|
229
|
+
governance_decision_version_id: governance.version_id,
|
|
230
|
+
outcome: "PROMOTED",
|
|
231
|
+
proposal_ref: proposalRef,
|
|
232
|
+
proposal_version_id: proposal.version_id,
|
|
233
|
+
verification_version_id: verification.version_id,
|
|
234
|
+
});
|
|
235
|
+
return deepFreeze(compactLearning({
|
|
236
|
+
promoted: true,
|
|
237
|
+
proposal_ref: proposalRef,
|
|
238
|
+
receipt,
|
|
239
|
+
registry: nextLearningRegistry(own(source, "registry"), targetRef, version.version_id),
|
|
240
|
+
status: "PROMOTED",
|
|
241
|
+
version,
|
|
242
|
+
version_id: version.version_id,
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const createLearningProposal = createProposal, verifyLearningProposal = verifyProposal;
|
|
247
|
+
export const decideLearningProposal = decideProposal, promoteLearningProposal = promoteProposal;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ErrorCode,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
McpError,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
|
|
9
|
+
import { canonicalize } from "./canonical-json.mjs";
|
|
10
|
+
import { parseStableRef } from "./identity.mjs";
|
|
11
|
+
import { agentNativeRegistry } from "./registry.mjs";
|
|
12
|
+
|
|
13
|
+
function operationInputSchema(operation) {
|
|
14
|
+
const schema = structuredClone(operation.input_schema ?? { type: "object" });
|
|
15
|
+
if (Object.hasOwn(schema.properties ?? {}, "reference")) {
|
|
16
|
+
schema.properties.stable_ref = structuredClone(schema.properties.reference);
|
|
17
|
+
schema.required = (schema.required ?? []).filter((name) => name !== "reference");
|
|
18
|
+
schema.anyOf = [{ required: ["reference"] }, { required: ["stable_ref"] }];
|
|
19
|
+
}
|
|
20
|
+
return schema;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeInput(input) {
|
|
24
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
25
|
+
if (!Object.hasOwn(input, "stable_ref")) return input;
|
|
26
|
+
const normalized = { ...input };
|
|
27
|
+
if (!Object.hasOwn(normalized, "reference")) normalized.reference = normalized.stable_ref;
|
|
28
|
+
delete normalized.stable_ref;
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toolResponse(envelope) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: canonicalize(envelope) }],
|
|
35
|
+
isError: envelope.ok !== true,
|
|
36
|
+
structuredContent: envelope,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deniedTool(operation) {
|
|
41
|
+
return toolResponse({
|
|
42
|
+
failures: [{
|
|
43
|
+
code: "operation_not_exposed",
|
|
44
|
+
message: `operation ${operation} is not exposed by the read-only MCP surface`,
|
|
45
|
+
path: "operation",
|
|
46
|
+
}],
|
|
47
|
+
ok: false,
|
|
48
|
+
operation,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toolDefinition(operation) {
|
|
53
|
+
return {
|
|
54
|
+
annotations: {
|
|
55
|
+
destructiveHint: false,
|
|
56
|
+
idempotentHint: true,
|
|
57
|
+
openWorldHint: false,
|
|
58
|
+
readOnlyHint: true,
|
|
59
|
+
},
|
|
60
|
+
description: operation.description,
|
|
61
|
+
inputSchema: operationInputSchema(operation),
|
|
62
|
+
name: operation.name,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resourceContents(uri, value) {
|
|
67
|
+
return {
|
|
68
|
+
contents: [{
|
|
69
|
+
mimeType: "application/json",
|
|
70
|
+
text: canonicalize(value),
|
|
71
|
+
uri,
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertRegistry(registry) {
|
|
77
|
+
if (!registry || typeof registry.invoke !== "function" || !Array.isArray(registry.operations)) {
|
|
78
|
+
throw new TypeError("MCP registry must expose invoke and operations");
|
|
79
|
+
}
|
|
80
|
+
if (!registry.fixture?.manifest || !registry.selfDescription) {
|
|
81
|
+
throw new TypeError("MCP registry must expose an immutable manifest and self-description");
|
|
82
|
+
}
|
|
83
|
+
for (const operation of registry.operations) {
|
|
84
|
+
if (typeof operation.read_only !== "boolean") throw new TypeError(`${operation.name} must declare read_only`);
|
|
85
|
+
if (operation.read_only && operation.effect_class !== "NONE") {
|
|
86
|
+
throw new TypeError(`${operation.name} cannot expose ${operation.effect_class} effects as read-only`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function objectUri(stableRef) {
|
|
92
|
+
return `sg://object/${encodeURIComponent(stableRef)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function objectReference(encoded) {
|
|
96
|
+
if (typeof encoded !== "string" || encoded.length === 0) {
|
|
97
|
+
throw new McpError(ErrorCode.InvalidParams, "object reference must be an encoded StableRef");
|
|
98
|
+
}
|
|
99
|
+
let decoded;
|
|
100
|
+
try { decoded = decodeURIComponent(encoded); } catch {
|
|
101
|
+
throw new McpError(ErrorCode.InvalidParams, "object reference contains invalid percent encoding");
|
|
102
|
+
}
|
|
103
|
+
try { return parseStableRef(decoded).stable_ref; } catch {
|
|
104
|
+
throw new McpError(ErrorCode.InvalidParams, "object reference must decode to one governed StableRef");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveObject(registry, encoded) {
|
|
109
|
+
const resolved = registry.invoke("resolve", { reference: objectReference(encoded) });
|
|
110
|
+
if (resolved.ok) return resolved.result;
|
|
111
|
+
const failure = resolved.failures?.[0] ?? { code: "object_not_found", message: "object could not be resolved" };
|
|
112
|
+
throw new McpError(ErrorCode.InvalidParams, `${failure.code}: ${failure.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createStyleGalleryMcpServer({ registry = agentNativeRegistry } = {}) {
|
|
116
|
+
assertRegistry(registry);
|
|
117
|
+
const server = new McpServer({ name: "StyleGallery", version: "1.0.0" });
|
|
118
|
+
const operations = registry.operations
|
|
119
|
+
.filter((operation) => operation.read_only)
|
|
120
|
+
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
121
|
+
const operationByName = new Map(operations.map((operation) => [operation.name, operation]));
|
|
122
|
+
|
|
123
|
+
server.registerResource(
|
|
124
|
+
"stylegallery-self-description",
|
|
125
|
+
"sg://self",
|
|
126
|
+
{
|
|
127
|
+
description: "Immutable StyleGallery self-description and conformance profile.",
|
|
128
|
+
mimeType: "application/json",
|
|
129
|
+
},
|
|
130
|
+
async () => resourceContents("sg://self", registry.selfDescription),
|
|
131
|
+
);
|
|
132
|
+
server.registerResource(
|
|
133
|
+
"stylegallery-manifest",
|
|
134
|
+
"sg://manifest",
|
|
135
|
+
{
|
|
136
|
+
description: "Immutable manifest for the agent-native StyleGallery fixture.",
|
|
137
|
+
mimeType: "application/json",
|
|
138
|
+
},
|
|
139
|
+
async () => resourceContents("sg://manifest", registry.fixture.manifest),
|
|
140
|
+
);
|
|
141
|
+
server.registerResource(
|
|
142
|
+
"stylegallery-object",
|
|
143
|
+
new ResourceTemplate("sg://object/{reference}", {
|
|
144
|
+
list: async () => ({
|
|
145
|
+
resources: registry.fixture.records.map((record) => ({
|
|
146
|
+
description: `Immutable ${record.record_kind} record resolved from the common registry.`,
|
|
147
|
+
mimeType: "application/json",
|
|
148
|
+
name: record.stable_ref,
|
|
149
|
+
uri: objectUri(record.stable_ref),
|
|
150
|
+
})),
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
153
|
+
{
|
|
154
|
+
description: "Resolve one encoded StableRef through the immutable common registry.",
|
|
155
|
+
mimeType: "application/json",
|
|
156
|
+
},
|
|
157
|
+
async (uri, variables) => resourceContents(uri.toString(), resolveObject(registry, variables.reference)),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
server.server.registerCapabilities({ tools: { listChanged: false } });
|
|
161
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
162
|
+
tools: operations.map(toolDefinition),
|
|
163
|
+
}));
|
|
164
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
165
|
+
const operation = operationByName.get(request.params.name);
|
|
166
|
+
if (!operation) return deniedTool(request.params.name);
|
|
167
|
+
const input = normalizeInput(request.params.arguments ?? {});
|
|
168
|
+
return toolResponse(registry.invoke(operation.name, input));
|
|
169
|
+
});
|
|
170
|
+
return server;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const createMcpServer = createStyleGalleryMcpServer;
|