dna-sdk 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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/adapters/embedding/onnx.d.ts +32 -0
- package/dist/adapters/embedding/onnx.js +51 -0
- package/dist/adapters/filesystem/cache.d.ts +16 -0
- package/dist/adapters/filesystem/cache.js +113 -0
- package/dist/adapters/filesystem/index.d.ts +2 -0
- package/dist/adapters/filesystem/index.js +2 -0
- package/dist/adapters/filesystem/source.d.ts +82 -0
- package/dist/adapters/filesystem/source.js +357 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.d.ts +14 -0
- package/dist/adapters/postgres/migrations.js +88 -0
- package/dist/adapters/postgres/source.d.ts +172 -0
- package/dist/adapters/postgres/source.js +569 -0
- package/dist/adapters/resolvers/github.d.ts +12 -0
- package/dist/adapters/resolvers/github.js +47 -0
- package/dist/adapters/resolvers/http.d.ts +22 -0
- package/dist/adapters/resolvers/http.js +145 -0
- package/dist/adapters/resolvers/index.d.ts +3 -0
- package/dist/adapters/resolvers/index.js +3 -0
- package/dist/adapters/resolvers/local.d.ts +24 -0
- package/dist/adapters/resolvers/local.js +132 -0
- package/dist/adapters/search/driver.d.ts +45 -0
- package/dist/adapters/search/driver.js +138 -0
- package/dist/adapters/search/migrations.d.ts +26 -0
- package/dist/adapters/search/migrations.js +58 -0
- package/dist/adapters/search/rrf.d.ts +23 -0
- package/dist/adapters/search/rrf.js +42 -0
- package/dist/adapters/search/sqlite-vec.d.ts +66 -0
- package/dist/adapters/search/sqlite-vec.js +294 -0
- package/dist/bootstrap.d.ts +45 -0
- package/dist/bootstrap.js +134 -0
- package/dist/extensions/agentskills/DOCS.md +18 -0
- package/dist/extensions/agentskills.d.ts +28 -0
- package/dist/extensions/agentskills.js +252 -0
- package/dist/extensions/agentsmd/DOCS.md +18 -0
- package/dist/extensions/agentsmd.d.ts +22 -0
- package/dist/extensions/agentsmd.js +170 -0
- package/dist/extensions/audit/kinds/audit-log.kind.yaml +148 -0
- package/dist/extensions/audit.d.ts +25 -0
- package/dist/extensions/audit.js +62 -0
- package/dist/extensions/automation/kinds/automation.kind.yaml +296 -0
- package/dist/extensions/automation/query.d.ts +49 -0
- package/dist/extensions/automation/query.js +77 -0
- package/dist/extensions/automation/write-guards.d.ts +59 -0
- package/dist/extensions/automation/write-guards.js +153 -0
- package/dist/extensions/automation.d.ts +41 -0
- package/dist/extensions/automation.js +54 -0
- package/dist/extensions/collab.d.ts +16 -0
- package/dist/extensions/collab.js +79 -0
- package/dist/extensions/doc/kinds/doc.kind.yaml +118 -0
- package/dist/extensions/doc.d.ts +25 -0
- package/dist/extensions/doc.js +10 -0
- package/dist/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
- package/dist/extensions/eval/kinds/eval-case.kind.yaml +140 -0
- package/dist/extensions/eval/kinds/eval-run.kind.yaml +134 -0
- package/dist/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
- package/dist/extensions/eval.d.ts +24 -0
- package/dist/extensions/eval.js +31 -0
- package/dist/extensions/evidence/kinds/evidence.kind.yaml +92 -0
- package/dist/extensions/evidence.d.ts +17 -0
- package/dist/extensions/evidence.js +94 -0
- package/dist/extensions/federation.d.ts +22 -0
- package/dist/extensions/federation.js +173 -0
- package/dist/extensions/guardrails/DOCS.md +20 -0
- package/dist/extensions/guardrails.d.ts +31 -0
- package/dist/extensions/guardrails.js +228 -0
- package/dist/extensions/helix/DOCS-Actor.md +20 -0
- package/dist/extensions/helix/DOCS-Agent.md +20 -0
- package/dist/extensions/helix/DOCS-UseCase.md +20 -0
- package/dist/extensions/helix/layers.d.ts +12 -0
- package/dist/extensions/helix/layers.js +18 -0
- package/dist/extensions/helix/write-guards.d.ts +70 -0
- package/dist/extensions/helix/write-guards.js +172 -0
- package/dist/extensions/helix.d.ts +29 -0
- package/dist/extensions/helix.js +963 -0
- package/dist/extensions/helix_extras.d.ts +124 -0
- package/dist/extensions/helix_extras.js +328 -0
- package/dist/extensions/hooks.d.ts +31 -0
- package/dist/extensions/hooks.js +247 -0
- package/dist/extensions/kinddef/DOCS.md +24 -0
- package/dist/extensions/kinddef.d.ts +40 -0
- package/dist/extensions/kinddef.js +171 -0
- package/dist/extensions/lesson.d.ts +6 -0
- package/dist/extensions/lesson.js +194 -0
- package/dist/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
- package/dist/extensions/modelreg.d.ts +30 -0
- package/dist/extensions/modelreg.js +38 -0
- package/dist/extensions/recognizer.d.ts +34 -0
- package/dist/extensions/recognizer.js +272 -0
- package/dist/extensions/research.d.ts +32 -0
- package/dist/extensions/research.js +292 -0
- package/dist/extensions/safety.d.ts +33 -0
- package/dist/extensions/safety.js +230 -0
- package/dist/extensions/sdlc/kinds/adr.kind.yaml +136 -0
- package/dist/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
- package/dist/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
- package/dist/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
- package/dist/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
- package/dist/extensions/sdlc/kinds/insight.kind.yaml +87 -0
- package/dist/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
- package/dist/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
- package/dist/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
- package/dist/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
- package/dist/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
- package/dist/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
- package/dist/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
- package/dist/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
- package/dist/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
- package/dist/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
- package/dist/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
- package/dist/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
- package/dist/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
- package/dist/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
- package/dist/extensions/sdlc.d.ts +32 -0
- package/dist/extensions/sdlc.js +1318 -0
- package/dist/extensions/soulspec/DOCS.md +19 -0
- package/dist/extensions/soulspec.d.ts +22 -0
- package/dist/extensions/soulspec.js +287 -0
- package/dist/extensions/tenant.d.ts +6 -0
- package/dist/extensions/tenant.js +291 -0
- package/dist/extensions/testkit.d.ts +24 -0
- package/dist/extensions/testkit.js +211 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +102 -0
- package/dist/kernel/_text.d.ts +28 -0
- package/dist/kernel/_text.js +42 -0
- package/dist/kernel/bundle-handle.d.ts +83 -0
- package/dist/kernel/bundle-handle.js +184 -0
- package/dist/kernel/capabilities.d.ts +157 -0
- package/dist/kernel/capabilities.js +109 -0
- package/dist/kernel/collaborator-ports.d.ts +91 -0
- package/dist/kernel/collaborator-ports.js +1 -0
- package/dist/kernel/composition-resolver.d.ts +215 -0
- package/dist/kernel/composition-resolver.js +558 -0
- package/dist/kernel/composition.d.ts +10 -0
- package/dist/kernel/composition.js +1 -0
- package/dist/kernel/descriptor-loader.d.ts +13 -0
- package/dist/kernel/descriptor-loader.js +48 -0
- package/dist/kernel/document.d.ts +43 -0
- package/dist/kernel/document.js +71 -0
- package/dist/kernel/embedding.d.ts +28 -0
- package/dist/kernel/embedding.js +78 -0
- package/dist/kernel/errors.d.ts +62 -0
- package/dist/kernel/errors.js +82 -0
- package/dist/kernel/events.d.ts +6 -0
- package/dist/kernel/events.js +21 -0
- package/dist/kernel/evidence-capture.d.ts +69 -0
- package/dist/kernel/evidence-capture.js +138 -0
- package/dist/kernel/fs.d.ts +20 -0
- package/dist/kernel/fs.js +128 -0
- package/dist/kernel/generic-rw.d.ts +33 -0
- package/dist/kernel/generic-rw.js +235 -0
- package/dist/kernel/hooks.d.ts +172 -0
- package/dist/kernel/hooks.js +256 -0
- package/dist/kernel/index.d.ts +624 -0
- package/dist/kernel/index.js +1638 -0
- package/dist/kernel/instance.d.ts +211 -0
- package/dist/kernel/instance.js +487 -0
- package/dist/kernel/kind-registry.d.ts +183 -0
- package/dist/kernel/kind-registry.js +710 -0
- package/dist/kernel/kind_base.d.ts +106 -0
- package/dist/kernel/kind_base.js +244 -0
- package/dist/kernel/layer-resolver.d.ts +48 -0
- package/dist/kernel/layer-resolver.js +284 -0
- package/dist/kernel/lock-manager.d.ts +21 -0
- package/dist/kernel/lock-manager.js +43 -0
- package/dist/kernel/lock.d.ts +38 -0
- package/dist/kernel/lock.js +115 -0
- package/dist/kernel/meta.d.ts +135 -0
- package/dist/kernel/meta.js +817 -0
- package/dist/kernel/models.d.ts +3089 -0
- package/dist/kernel/models.js +764 -0
- package/dist/kernel/navigator.d.ts +36 -0
- package/dist/kernel/navigator.js +132 -0
- package/dist/kernel/port-surface.d.ts +33 -0
- package/dist/kernel/port-surface.js +180 -0
- package/dist/kernel/preview.d.ts +60 -0
- package/dist/kernel/preview.js +54 -0
- package/dist/kernel/prompt-budget.d.ts +39 -0
- package/dist/kernel/prompt-budget.js +43 -0
- package/dist/kernel/prompt-builder.d.ts +25 -0
- package/dist/kernel/prompt-builder.js +237 -0
- package/dist/kernel/protocols.d.ts +751 -0
- package/dist/kernel/protocols.js +493 -0
- package/dist/kernel/reports.d.ts +26 -0
- package/dist/kernel/reports.js +215 -0
- package/dist/kernel/resolver.d.ts +146 -0
- package/dist/kernel/resolver.js +232 -0
- package/dist/kernel/resource.d.ts +57 -0
- package/dist/kernel/resource.js +97 -0
- package/dist/kernel/runtime.d.ts +16 -0
- package/dist/kernel/runtime.js +18 -0
- package/dist/kernel/safety-scanner.d.ts +38 -0
- package/dist/kernel/safety-scanner.js +228 -0
- package/dist/kernel/serialize-to-files.d.ts +29 -0
- package/dist/kernel/serialize-to-files.js +23 -0
- package/dist/kernel/spec-access.d.ts +23 -0
- package/dist/kernel/spec-access.js +40 -0
- package/dist/kernel/studio_ui.d.ts +88 -0
- package/dist/kernel/studio_ui.js +135 -0
- package/dist/kernel/templates.d.ts +77 -0
- package/dist/kernel/templates.js +116 -0
- package/dist/kernel/tool-registry.d.ts +52 -0
- package/dist/kernel/tool-registry.js +89 -0
- package/dist/kernel/write-pipeline.d.ts +65 -0
- package/dist/kernel/write-pipeline.js +271 -0
- package/dist/kernel/writer-helpers.d.ts +24 -0
- package/dist/kernel/writer-helpers.js +42 -0
- package/dist/memory/decay.d.ts +31 -0
- package/dist/memory/decay.js +97 -0
- package/dist/memory/ecphory.d.ts +25 -0
- package/dist/memory/ecphory.js +178 -0
- package/dist/memory/encodingContext.d.ts +21 -0
- package/dist/memory/encodingContext.js +62 -0
- package/dist/memory/index.d.ts +23 -0
- package/dist/memory/index.js +23 -0
- package/dist/memory/memoryType.d.ts +7 -0
- package/dist/memory/memoryType.js +24 -0
- package/dist/memory/policy.d.ts +35 -0
- package/dist/memory/policy.js +35 -0
- package/dist/memory/retrieval.d.ts +21 -0
- package/dist/memory/retrieval.js +145 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/recordSearchConformance.d.ts +43 -0
- package/dist/testing/recordSearchConformance.js +147 -0
- package/dist/viz/ascii.d.ts +7 -0
- package/dist/viz/ascii.js +60 -0
- package/dist/viz/health.d.ts +8 -0
- package/dist/viz/health.js +161 -0
- package/dist/viz/index.d.ts +10 -0
- package/dist/viz/index.js +10 -0
- package/dist/viz/matrix.d.ts +8 -0
- package/dist/viz/matrix.js +84 -0
- package/dist/viz/mermaid.d.ts +38 -0
- package/dist/viz/mermaid.js +706 -0
- package/package.json +82 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kind base class with sensible defaults.
|
|
3
|
+
*
|
|
4
|
+
* 1:1 parity with python/dna/kernel/kind_base.py.
|
|
5
|
+
*
|
|
6
|
+
* Cuts the boilerplate that every Kind implementation duplicated.
|
|
7
|
+
* Subclassing pattern:
|
|
8
|
+
*
|
|
9
|
+
* class AssetKind extends KindBase {
|
|
10
|
+
* readonly apiVersion = "github.com/ruinosus/dna/asset/v1";
|
|
11
|
+
* readonly kind = "Asset";
|
|
12
|
+
* readonly alias = "asset-asset";
|
|
13
|
+
* readonly storage = StorageDescriptorImpl.bundle(
|
|
14
|
+
* "assets", "ASSET.md",
|
|
15
|
+
* );
|
|
16
|
+
* readonly asciiIcon = "🖼️";
|
|
17
|
+
* readonly displayLabel = "Assets";
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* That's the minimum-viable Kind. Everything else inherits sensible
|
|
21
|
+
* defaults from this base.
|
|
22
|
+
*
|
|
23
|
+
* The base is **purely additive**: existing Kind impls keep working
|
|
24
|
+
* as plain objects (they declare every field explicitly). Migrating
|
|
25
|
+
* a Kind to `KindBase` is a strict simplification — no API changes.
|
|
26
|
+
*
|
|
27
|
+
* Mandatory overrides (no default available):
|
|
28
|
+
* - `apiVersion` — globally unique namespace string
|
|
29
|
+
* - `kind` — CamelCase Kind name
|
|
30
|
+
* - `alias` — globally unique alias
|
|
31
|
+
* - `storage` — StorageDescriptor (call StorageDescriptorImpl.bundle/yaml/root)
|
|
32
|
+
*
|
|
33
|
+
* Everything else has defaults that match the most common case.
|
|
34
|
+
*/
|
|
35
|
+
import Ajv from "ajv";
|
|
36
|
+
import type { Document } from "./document.js";
|
|
37
|
+
import type { KindPort, LayerPolicy, StorageDescriptor } from "./protocols.js";
|
|
38
|
+
/** Default volatile spec fields shared by every Kind (s-sync-s1). Exported
|
|
39
|
+
* so DeclarativeKindPort (meta.ts) unions the SAME set instead of
|
|
40
|
+
* re-hardcoding it — twin of Python's KindBase.VOLATILE_SPEC_FIELDS
|
|
41
|
+
* class attribute (C1 review carry-over). */
|
|
42
|
+
export declare const DEFAULT_VOLATILE_SPEC_FIELDS: ReadonlySet<string>;
|
|
43
|
+
/** JSON Schema `format` values that appear in real Kind schemas across the
|
|
44
|
+
* repo (builtin kind descriptors, KindDefinition YAMLs, tests). ajv v8 ships
|
|
45
|
+
* ZERO built-in formats, and the Python twin (`jsonschema`) treats `format`
|
|
46
|
+
* as annotation-only by default — so parity is "formats never validate".
|
|
47
|
+
* Registering each as `true` keeps exactly that semantics (always-valid
|
|
48
|
+
* annotation) while silencing ajv's per-compile
|
|
49
|
+
* `unknown format "X" ignored in schema` warning (s-public-ci).
|
|
50
|
+
* NOTE: deliberately NOT ajv-formats — that would start rejecting values and
|
|
51
|
+
* break Py↔TS validation parity. */
|
|
52
|
+
export declare const SCHEMA_ANNOTATION_FORMATS: readonly string[];
|
|
53
|
+
/** Canonical Ajv factory for the SDK — every `new Ajv(...)` in the kernel
|
|
54
|
+
* goes through here so options stay in one place. strict:false accepts the
|
|
55
|
+
* lenient JSON Schema shapes users author in YAML. */
|
|
56
|
+
export declare function createAjv(): Ajv;
|
|
57
|
+
export declare abstract class KindBase implements Omit<KindPort, "apiVersion" | "kind" | "alias" | "storage"> {
|
|
58
|
+
readonly origin?: string;
|
|
59
|
+
readonly isPromptTarget: boolean;
|
|
60
|
+
readonly promptTargetPriority: number;
|
|
61
|
+
readonly flattenInContext: boolean;
|
|
62
|
+
get isRoot(): boolean;
|
|
63
|
+
readonly isRuntimeArtifact: boolean;
|
|
64
|
+
readonly plane: "record" | "composition";
|
|
65
|
+
readonly isSchemaAffecting: boolean;
|
|
66
|
+
readonly isOverlayable: boolean;
|
|
67
|
+
readonly scopeInheritable: boolean;
|
|
68
|
+
readonly isCatalogIdentity: boolean;
|
|
69
|
+
readonly embedFields: string[] | null;
|
|
70
|
+
readonly validateOnParse: boolean;
|
|
71
|
+
private _parseValidator?;
|
|
72
|
+
private _parseAjv?;
|
|
73
|
+
readonly docs?: string;
|
|
74
|
+
readonly descriptionFallbackField?: string;
|
|
75
|
+
readonly graphStyle?: {
|
|
76
|
+
fill: string;
|
|
77
|
+
stroke: string;
|
|
78
|
+
textColor: string;
|
|
79
|
+
};
|
|
80
|
+
readonly asciiIcon?: string;
|
|
81
|
+
readonly displayLabel?: string;
|
|
82
|
+
readonly uiSchema?: Record<string, Record<string, unknown>>;
|
|
83
|
+
readonly markerSharedAllowed: boolean;
|
|
84
|
+
readonly volatileSpecFields: ReadonlySet<string>;
|
|
85
|
+
depFilters(): Record<string, string> | null;
|
|
86
|
+
dependencies(): Record<string, string> | null;
|
|
87
|
+
schema(): Record<string, unknown> | null;
|
|
88
|
+
getDefaultAgentName(_doc: Document): string | null;
|
|
89
|
+
getLayerPolicies(_doc: Document): Record<string, LayerPolicy | string> | null;
|
|
90
|
+
parse(raw: Record<string, unknown>): unknown;
|
|
91
|
+
/** Validate `raw.spec` against `schema()` (ajv). No-op when no schema.
|
|
92
|
+
* Throws a clear error for a malformed spec (s-typed-models-for-dict-kinds). */
|
|
93
|
+
private _validateSpec;
|
|
94
|
+
describe(_doc: Document): string | null;
|
|
95
|
+
summary(doc: Document): Record<string, unknown> | null;
|
|
96
|
+
promptTemplate(): string | null;
|
|
97
|
+
protected canonicalSpec(spec: Record<string, unknown>): Record<string, unknown>;
|
|
98
|
+
/** Stable SHA-256 of the doc's authored identity — basis for source
|
|
99
|
+
* diff/sync. Invariant to key order, formatting, volatile stamps, and
|
|
100
|
+
* instruction_file-vs-inline; sensitive to real content. */
|
|
101
|
+
canonicalDigest(doc: Document): string;
|
|
102
|
+
abstract readonly apiVersion: string;
|
|
103
|
+
abstract readonly kind: string;
|
|
104
|
+
abstract readonly alias: string;
|
|
105
|
+
abstract readonly storage: StorageDescriptor;
|
|
106
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kind base class with sensible defaults.
|
|
3
|
+
*
|
|
4
|
+
* 1:1 parity with python/dna/kernel/kind_base.py.
|
|
5
|
+
*
|
|
6
|
+
* Cuts the boilerplate that every Kind implementation duplicated.
|
|
7
|
+
* Subclassing pattern:
|
|
8
|
+
*
|
|
9
|
+
* class AssetKind extends KindBase {
|
|
10
|
+
* readonly apiVersion = "github.com/ruinosus/dna/asset/v1";
|
|
11
|
+
* readonly kind = "Asset";
|
|
12
|
+
* readonly alias = "asset-asset";
|
|
13
|
+
* readonly storage = StorageDescriptorImpl.bundle(
|
|
14
|
+
* "assets", "ASSET.md",
|
|
15
|
+
* );
|
|
16
|
+
* readonly asciiIcon = "🖼️";
|
|
17
|
+
* readonly displayLabel = "Assets";
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* That's the minimum-viable Kind. Everything else inherits sensible
|
|
21
|
+
* defaults from this base.
|
|
22
|
+
*
|
|
23
|
+
* The base is **purely additive**: existing Kind impls keep working
|
|
24
|
+
* as plain objects (they declare every field explicitly). Migrating
|
|
25
|
+
* a Kind to `KindBase` is a strict simplification — no API changes.
|
|
26
|
+
*
|
|
27
|
+
* Mandatory overrides (no default available):
|
|
28
|
+
* - `apiVersion` — globally unique namespace string
|
|
29
|
+
* - `kind` — CamelCase Kind name
|
|
30
|
+
* - `alias` — globally unique alias
|
|
31
|
+
* - `storage` — StorageDescriptor (call StorageDescriptorImpl.bundle/yaml/root)
|
|
32
|
+
*
|
|
33
|
+
* Everything else has defaults that match the most common case.
|
|
34
|
+
*/
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
import Ajv from "ajv";
|
|
37
|
+
/** Default volatile spec fields shared by every Kind (s-sync-s1). Exported
|
|
38
|
+
* so DeclarativeKindPort (meta.ts) unions the SAME set instead of
|
|
39
|
+
* re-hardcoding it — twin of Python's KindBase.VOLATILE_SPEC_FIELDS
|
|
40
|
+
* class attribute (C1 review carry-over). */
|
|
41
|
+
export const DEFAULT_VOLATILE_SPEC_FIELDS = new Set([
|
|
42
|
+
"updated_at",
|
|
43
|
+
"version",
|
|
44
|
+
"created_at",
|
|
45
|
+
]);
|
|
46
|
+
/** JSON Schema `format` values that appear in real Kind schemas across the
|
|
47
|
+
* repo (builtin kind descriptors, KindDefinition YAMLs, tests). ajv v8 ships
|
|
48
|
+
* ZERO built-in formats, and the Python twin (`jsonschema`) treats `format`
|
|
49
|
+
* as annotation-only by default — so parity is "formats never validate".
|
|
50
|
+
* Registering each as `true` keeps exactly that semantics (always-valid
|
|
51
|
+
* annotation) while silencing ajv's per-compile
|
|
52
|
+
* `unknown format "X" ignored in schema` warning (s-public-ci).
|
|
53
|
+
* NOTE: deliberately NOT ajv-formats — that would start rejecting values and
|
|
54
|
+
* break Py↔TS validation parity. */
|
|
55
|
+
export const SCHEMA_ANNOTATION_FORMATS = [
|
|
56
|
+
"date-time",
|
|
57
|
+
"date",
|
|
58
|
+
"email",
|
|
59
|
+
"markdown",
|
|
60
|
+
];
|
|
61
|
+
/** Canonical Ajv factory for the SDK — every `new Ajv(...)` in the kernel
|
|
62
|
+
* goes through here so options stay in one place. strict:false accepts the
|
|
63
|
+
* lenient JSON Schema shapes users author in YAML. */
|
|
64
|
+
export function createAjv() {
|
|
65
|
+
const formats = {};
|
|
66
|
+
for (const f of SCHEMA_ANNOTATION_FORMATS)
|
|
67
|
+
formats[f] = true;
|
|
68
|
+
return new Ajv({ strict: false, allErrors: true, formats });
|
|
69
|
+
}
|
|
70
|
+
/** Stable, sorted-key, compact JSON — twin of Python's
|
|
71
|
+
* `json.dumps(sort_keys=True, separators=(",",":"))`. Logically-equal objects
|
|
72
|
+
* serialize identically regardless of insertion order (s-sync-s1). */
|
|
73
|
+
function _stableStringify(value) {
|
|
74
|
+
if (value === null || typeof value !== "object") {
|
|
75
|
+
return JSON.stringify(value) ?? "null";
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
return "[" + value.map(_stableStringify).join(",") + "]";
|
|
79
|
+
}
|
|
80
|
+
const obj = value;
|
|
81
|
+
const keys = Object.keys(obj).sort();
|
|
82
|
+
return ("{" +
|
|
83
|
+
keys.map((k) => JSON.stringify(k) + ":" + _stableStringify(obj[k])).join(",") +
|
|
84
|
+
"}");
|
|
85
|
+
}
|
|
86
|
+
export class KindBase {
|
|
87
|
+
// ---- Identity (subclasses MUST set these) -----------------------
|
|
88
|
+
// abstract apiVersion: string;
|
|
89
|
+
// abstract kind: string;
|
|
90
|
+
// abstract alias: string;
|
|
91
|
+
// abstract storage: StorageDescriptor;
|
|
92
|
+
// ---- Optional identity ------------------------------------------
|
|
93
|
+
origin;
|
|
94
|
+
// ---- Behavior flags (sensible defaults) -------------------------
|
|
95
|
+
// ``isRoot`` was an explicit boolean Phase 0–15. Phase 16 derives
|
|
96
|
+
// it from the storage descriptor: a Kind is the scope root iff its
|
|
97
|
+
// storage pattern is ROOT (single file at scope root). GenomeKind
|
|
98
|
+
// is the only one today; the getter keeps the existing read API
|
|
99
|
+
// (``kp.isRoot``) working for all consumers.
|
|
100
|
+
isPromptTarget = false;
|
|
101
|
+
promptTargetPriority = 0;
|
|
102
|
+
flattenInContext = false;
|
|
103
|
+
get isRoot() {
|
|
104
|
+
const storage = this.storage;
|
|
105
|
+
return storage?.pattern === "root";
|
|
106
|
+
}
|
|
107
|
+
// See KindPort.isRuntimeArtifact in protocols.ts. Override to `true`
|
|
108
|
+
// only on Kinds whose documents are generated by runtime workflows
|
|
109
|
+
// (EvalRun, Finding, Evidence, AssessmentRun, ...).
|
|
110
|
+
isRuntimeArtifact = false;
|
|
111
|
+
// ---- Two-planes (spec 2026-06-09-kinds-two-planes-design) --------
|
|
112
|
+
// "composition" (default) — participates in agent composition.
|
|
113
|
+
// "record" — pure typed document; writes never invalidate scope
|
|
114
|
+
// caches (Python kernel branches on this; TS has no cache machinery,
|
|
115
|
+
// the attr exists for 1:1 parity + the registration lint below).
|
|
116
|
+
plane = "composition";
|
|
117
|
+
// ---- Kernel classification (s-kernel-kindport-classification-attrs) ----
|
|
118
|
+
// The kernel DERIVES its classification sets from these per-Kind attributes
|
|
119
|
+
// instead of matching hardcoded Kind names. 1:1 parity with the Python
|
|
120
|
+
// KindBase. Defaults match the common case; the few Kinds that differ override.
|
|
121
|
+
// `isSchemaAffecting`: writing this Kind invalidates the schema cache.
|
|
122
|
+
isSchemaAffecting = false;
|
|
123
|
+
// `isOverlayable`: a tenant overlay may fork this Kind. False only for the
|
|
124
|
+
// structural bootstrap Kinds (scope identity / schema / policy).
|
|
125
|
+
isOverlayable = true;
|
|
126
|
+
// `scopeInheritable`: documents inherit across scopes by default. False for the
|
|
127
|
+
// per-scope SDLC ledger + structural Kinds.
|
|
128
|
+
scopeInheritable = true;
|
|
129
|
+
// `isCatalogIdentity`: writing a doc of this Kind changes the Catalog
|
|
130
|
+
// tier's scope/mandatory set (s-write-path-despecialize — replaces the
|
|
131
|
+
// hardcoded `kind == "Genome"` check in the Python write path; the TS
|
|
132
|
+
// kernel keeps no catalog cache, the attribute exists for 1:1 Kind
|
|
133
|
+
// metadata parity). True only on the scope-root identity Kind.
|
|
134
|
+
isCatalogIdentity = false;
|
|
135
|
+
// ---- Embedding source fields (F3 spec D4) ------------------------
|
|
136
|
+
// Fields of `spec` that compose the doc's embedding text. null =
|
|
137
|
+
// not embeddable (or still covered by the legacy EMBEDDABLE_KINDS
|
|
138
|
+
// frozenset in harness-shared). Declared here so the D4 derivation
|
|
139
|
+
// covers still-class Kinds as well as descriptor-synthesized ports.
|
|
140
|
+
embedFields = null;
|
|
141
|
+
// ---- Validation (s-typed-models-for-dict-kinds) -----------------
|
|
142
|
+
// When true, parse() validates the doc's `spec` against schema() (ajv) before
|
|
143
|
+
// returning — a malformed doc throws a clear error. 1:1 parity with the
|
|
144
|
+
// Python KindBase.validate_on_parse. Opt-in (default false).
|
|
145
|
+
validateOnParse = false;
|
|
146
|
+
_parseValidator;
|
|
147
|
+
_parseAjv;
|
|
148
|
+
// ---- Optional rendering hints (duck-typed by Studio) -----------
|
|
149
|
+
docs;
|
|
150
|
+
descriptionFallbackField;
|
|
151
|
+
graphStyle;
|
|
152
|
+
asciiIcon;
|
|
153
|
+
displayLabel;
|
|
154
|
+
uiSchema;
|
|
155
|
+
// ---- H1 opt-in for shared bundle markers -----------------------
|
|
156
|
+
markerSharedAllowed = false;
|
|
157
|
+
// ---- Source-sync identity (s-sync-s1) --------------------------
|
|
158
|
+
// Write-/runtime-stamped spec fields, NOT part of authored identity —
|
|
159
|
+
// excluded from canonicalDigest so the same doc in two sources hashes
|
|
160
|
+
// identically. Kinds override to extend (e.g. a Forecast adds generated_at).
|
|
161
|
+
volatileSpecFields = DEFAULT_VOLATILE_SPEC_FIELDS;
|
|
162
|
+
// ---- Behavior methods (default implementations) ----------------
|
|
163
|
+
depFilters() {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
dependencies() {
|
|
167
|
+
return this.depFilters();
|
|
168
|
+
}
|
|
169
|
+
schema() {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
getDefaultAgentName(_doc) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
getLayerPolicies(_doc) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
parse(raw) {
|
|
179
|
+
if (this.validateOnParse)
|
|
180
|
+
this._validateSpec(raw);
|
|
181
|
+
return raw;
|
|
182
|
+
}
|
|
183
|
+
/** Validate `raw.spec` against `schema()` (ajv). No-op when no schema.
|
|
184
|
+
* Throws a clear error for a malformed spec (s-typed-models-for-dict-kinds). */
|
|
185
|
+
_validateSpec(raw) {
|
|
186
|
+
const schema = this.schema();
|
|
187
|
+
if (!schema)
|
|
188
|
+
return;
|
|
189
|
+
if (this._parseValidator === undefined) {
|
|
190
|
+
this._parseAjv = createAjv();
|
|
191
|
+
this._parseValidator = this._parseAjv.compile(schema);
|
|
192
|
+
}
|
|
193
|
+
if (!this._parseValidator)
|
|
194
|
+
return;
|
|
195
|
+
// Accept a full envelope ({apiVersion, kind, spec}) OR a flat spec dict
|
|
196
|
+
// (mirrors the gaia Kinds' defensive parse + the Python twin).
|
|
197
|
+
const spec = "apiVersion" in raw && raw.spec && typeof raw.spec === "object"
|
|
198
|
+
? raw.spec
|
|
199
|
+
: raw;
|
|
200
|
+
if (!this._parseValidator(spec)) {
|
|
201
|
+
const errs = this._parseAjv?.errorsText(this._parseValidator.errors);
|
|
202
|
+
throw new Error(`Kind '${this.kind}' spec validation failed: ${errs}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
describe(_doc) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
summary(doc) {
|
|
209
|
+
const spec = doc.spec;
|
|
210
|
+
if (spec && typeof spec === "object" && !Array.isArray(spec)) {
|
|
211
|
+
return { ...spec };
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
promptTemplate() {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
// ---- Source-sync digest (s-sync-s1; twin of Python canonical_digest) ----
|
|
219
|
+
canonicalSpec(spec) {
|
|
220
|
+
const out = {};
|
|
221
|
+
for (const [k, v] of Object.entries(spec ?? {})) {
|
|
222
|
+
if (this.volatileSpecFields.has(k))
|
|
223
|
+
continue;
|
|
224
|
+
out[k] = v;
|
|
225
|
+
}
|
|
226
|
+
delete out["source_files"]; // pure transport, never identity
|
|
227
|
+
if (out["instruction_file"] && out["instruction"]) {
|
|
228
|
+
delete out["instruction_file"]; // file-backed == inline once resolved
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
/** Stable SHA-256 of the doc's authored identity — basis for source
|
|
233
|
+
* diff/sync. Invariant to key order, formatting, volatile stamps, and
|
|
234
|
+
* instruction_file-vs-inline; sensitive to real content. */
|
|
235
|
+
canonicalDigest(doc) {
|
|
236
|
+
const spec = (doc.spec ?? {});
|
|
237
|
+
const payload = {
|
|
238
|
+
kind: doc.kind ?? this.kind,
|
|
239
|
+
name: doc.name ?? null,
|
|
240
|
+
spec: this.canonicalSpec({ ...spec }),
|
|
241
|
+
};
|
|
242
|
+
return createHash("sha256").update(_stableStringify(payload), "utf8").digest("hex");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DefaultLayerResolver — overlay resolution with per-kind policies.
|
|
3
|
+
*
|
|
4
|
+
* 1:1 parity with Python dna.kernel.layer_resolver.
|
|
5
|
+
*
|
|
6
|
+
* Kernel-owned since s-invert-layer-resolver-dep (2026-07-07): layer
|
|
7
|
+
* resolution is a core kernel responsibility, so the default resolver
|
|
8
|
+
* lives in the kernel package — the kernel must work with ZERO
|
|
9
|
+
* extensions loaded. Previously at src/extensions/helix/layers.ts
|
|
10
|
+
* (which remains as a deprecated reexport shim).
|
|
11
|
+
*
|
|
12
|
+
* Iterates layer dimensions, loads overlays from the source,
|
|
13
|
+
* and merges with per-kind policies (open/restricted/locked).
|
|
14
|
+
* The resolver is fully generic: it depends only on
|
|
15
|
+
* kernel/protocols.LayerPolicy — no extension models or constants.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Deep merge two plain objects. Overlay wins.
|
|
19
|
+
* Lists are replaced (not concatenated), dicts are merged recursively.
|
|
20
|
+
*/
|
|
21
|
+
export declare function deepMerge(base: Record<string, unknown>, overlay: Record<string, unknown>): Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Concat + dedup + sort base.spec.timeline and overlay.spec.timeline
|
|
24
|
+
* (per ADR 2026-05-10). Returns the merged list when EITHER side has a
|
|
25
|
+
* timeline array; ``null`` when neither does.
|
|
26
|
+
*
|
|
27
|
+
* Dedup key: `at + actor + type + (from + to | summary)`.
|
|
28
|
+
* Sort: descending by ``at`` (newest first).
|
|
29
|
+
*/
|
|
30
|
+
export declare function mergeTimelineArrays(baseSpec: Record<string, unknown>, overlaySpec: Record<string, unknown>): Record<string, unknown>[] | null;
|
|
31
|
+
export interface LayerSource {
|
|
32
|
+
loadLayer(scope: string, layerId: string, layerValue: string): Record<string, unknown>[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Merges layer overlay documents into base documents, applying policies by kind alias.
|
|
36
|
+
*
|
|
37
|
+
* Policies map: `{ kind_alias_or_kind_name: LayerPolicy }`
|
|
38
|
+
* - open: deep merge spec (or add new documents)
|
|
39
|
+
* - restricted: only override existing keys in spec
|
|
40
|
+
* - locked: block changes (warn only)
|
|
41
|
+
*/
|
|
42
|
+
export declare class DefaultLayerResolver {
|
|
43
|
+
resolve(baseDocuments: Record<string, unknown>[], layers: Record<string, string>, source: LayerSource, scope: string, policies: Record<string, string>): Record<string, unknown>[];
|
|
44
|
+
private _mergeAll;
|
|
45
|
+
private _policyForKind;
|
|
46
|
+
private _applyMerge;
|
|
47
|
+
private _applyRestrictedMerge;
|
|
48
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DefaultLayerResolver — overlay resolution with per-kind policies.
|
|
3
|
+
*
|
|
4
|
+
* 1:1 parity with Python dna.kernel.layer_resolver.
|
|
5
|
+
*
|
|
6
|
+
* Kernel-owned since s-invert-layer-resolver-dep (2026-07-07): layer
|
|
7
|
+
* resolution is a core kernel responsibility, so the default resolver
|
|
8
|
+
* lives in the kernel package — the kernel must work with ZERO
|
|
9
|
+
* extensions loaded. Previously at src/extensions/helix/layers.ts
|
|
10
|
+
* (which remains as a deprecated reexport shim).
|
|
11
|
+
*
|
|
12
|
+
* Iterates layer dimensions, loads overlays from the source,
|
|
13
|
+
* and merges with per-kind policies (open/restricted/locked).
|
|
14
|
+
* The resolver is fully generic: it depends only on
|
|
15
|
+
* kernel/protocols.LayerPolicy — no extension models or constants.
|
|
16
|
+
*/
|
|
17
|
+
import { LayerPolicy } from "./protocols.js";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Deep merge
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Deep merge two plain objects. Overlay wins.
|
|
23
|
+
* Lists are replaced (not concatenated), dicts are merged recursively.
|
|
24
|
+
*/
|
|
25
|
+
export function deepMerge(base, overlay) {
|
|
26
|
+
const result = structuredClone(base);
|
|
27
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
28
|
+
if (key in result &&
|
|
29
|
+
typeof result[key] === "object" &&
|
|
30
|
+
result[key] !== null &&
|
|
31
|
+
!Array.isArray(result[key]) &&
|
|
32
|
+
typeof value === "object" &&
|
|
33
|
+
value !== null &&
|
|
34
|
+
!Array.isArray(value)) {
|
|
35
|
+
result[key] = deepMerge(result[key], value);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
result[key] = structuredClone(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Timeline merger (cross-overlay append-only)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Concat + dedup + sort base.spec.timeline and overlay.spec.timeline
|
|
48
|
+
* (per ADR 2026-05-10). Returns the merged list when EITHER side has a
|
|
49
|
+
* timeline array; ``null`` when neither does.
|
|
50
|
+
*
|
|
51
|
+
* Dedup key: `at + actor + type + (from + to | summary)`.
|
|
52
|
+
* Sort: descending by ``at`` (newest first).
|
|
53
|
+
*/
|
|
54
|
+
export function mergeTimelineArrays(baseSpec, overlaySpec) {
|
|
55
|
+
const baseTl = Array.isArray(baseSpec.timeline)
|
|
56
|
+
? baseSpec.timeline
|
|
57
|
+
: [];
|
|
58
|
+
const overlayTl = Array.isArray(overlaySpec.timeline)
|
|
59
|
+
? overlaySpec.timeline
|
|
60
|
+
: [];
|
|
61
|
+
if (baseTl.length === 0 && overlayTl.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const merged = [];
|
|
65
|
+
for (const ev of [...baseTl, ...overlayTl]) {
|
|
66
|
+
if (typeof ev !== "object" || ev === null || Array.isArray(ev))
|
|
67
|
+
continue;
|
|
68
|
+
const key = JSON.stringify([
|
|
69
|
+
ev.at ?? null,
|
|
70
|
+
ev.actor ?? null,
|
|
71
|
+
ev.type ?? null,
|
|
72
|
+
ev.from ?? null,
|
|
73
|
+
ev.to ?? null,
|
|
74
|
+
ev.summary ?? null,
|
|
75
|
+
]);
|
|
76
|
+
if (seen.has(key))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
merged.push(structuredClone(ev));
|
|
80
|
+
}
|
|
81
|
+
// Stable sort: newest first.
|
|
82
|
+
merged.sort((a, b) => {
|
|
83
|
+
const ax = String(a.at ?? "");
|
|
84
|
+
const bx = String(b.at ?? "");
|
|
85
|
+
if (ax < bx)
|
|
86
|
+
return 1;
|
|
87
|
+
if (ax > bx)
|
|
88
|
+
return -1;
|
|
89
|
+
return 0;
|
|
90
|
+
});
|
|
91
|
+
return merged;
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Overlay metadata stamper (Phase 2 overlay UX)
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
/**
|
|
97
|
+
* In-place: write ``has_overlay`` + ``overlay_fields`` into ``doc.metadata``.
|
|
98
|
+
* Studio's editor banners + per-field markers read these.
|
|
99
|
+
*
|
|
100
|
+
* ``overlayFields=null`` is the sentinel for "the entire doc came from
|
|
101
|
+
* the overlay" (overlay-only add, no base to diff against). Frontend
|
|
102
|
+
* treats this distinctly from an empty list.
|
|
103
|
+
*
|
|
104
|
+
* Idempotent across multiple layer dimensions: subsequent calls UNION
|
|
105
|
+
* the field list so the user sees every field touched across all
|
|
106
|
+
* applied layers.
|
|
107
|
+
*/
|
|
108
|
+
function stampOverlayMetadata(doc, overlayFields) {
|
|
109
|
+
const md = doc.metadata ?? {};
|
|
110
|
+
doc.metadata = md;
|
|
111
|
+
md.has_overlay = true;
|
|
112
|
+
if (overlayFields === null) {
|
|
113
|
+
md.overlay_fields = null;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const existing = md.overlay_fields;
|
|
117
|
+
if (existing === undefined || existing === null) {
|
|
118
|
+
md.overlay_fields = [...overlayFields];
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(existing)) {
|
|
122
|
+
const merged = [...existing];
|
|
123
|
+
for (const f of overlayFields) {
|
|
124
|
+
if (!merged.includes(f))
|
|
125
|
+
merged.push(f);
|
|
126
|
+
}
|
|
127
|
+
md.overlay_fields = merged;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// DefaultLayerResolver
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* Merges layer overlay documents into base documents, applying policies by kind alias.
|
|
135
|
+
*
|
|
136
|
+
* Policies map: `{ kind_alias_or_kind_name: LayerPolicy }`
|
|
137
|
+
* - open: deep merge spec (or add new documents)
|
|
138
|
+
* - restricted: only override existing keys in spec
|
|
139
|
+
* - locked: block changes (warn only)
|
|
140
|
+
*/
|
|
141
|
+
export class DefaultLayerResolver {
|
|
142
|
+
resolve(baseDocuments, layers, source, scope, policies) {
|
|
143
|
+
let result = baseDocuments.map((d) => structuredClone(d));
|
|
144
|
+
for (const [layerId, value] of Object.entries(layers)) {
|
|
145
|
+
const overlayDocs = source.loadLayer(scope, layerId, value);
|
|
146
|
+
if (overlayDocs.length === 0)
|
|
147
|
+
continue;
|
|
148
|
+
result = this._mergeAll(result, overlayDocs, policies, layerId, value);
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
_mergeAll(baseDocs, overlayDocs, policies, layerId, value) {
|
|
154
|
+
const result = baseDocs.map((d) => structuredClone(d));
|
|
155
|
+
// Build index: "kind\0name" -> position in result
|
|
156
|
+
const baseIndex = new Map();
|
|
157
|
+
for (let i = 0; i < result.length; i++) {
|
|
158
|
+
const kind = result[i].kind ?? "";
|
|
159
|
+
const name = result[i].metadata?.name ?? "";
|
|
160
|
+
if (kind && name)
|
|
161
|
+
baseIndex.set(`${kind}\0${name}`, i);
|
|
162
|
+
}
|
|
163
|
+
for (const overlay of overlayDocs) {
|
|
164
|
+
const oKind = overlay.kind ?? "";
|
|
165
|
+
const oName = overlay.metadata?.name ?? "";
|
|
166
|
+
const policy = this._policyForKind(oKind, policies);
|
|
167
|
+
const key = `${oKind}\0${oName}`;
|
|
168
|
+
if (baseIndex.has(key)) {
|
|
169
|
+
const idx = baseIndex.get(key);
|
|
170
|
+
result[idx] = this._applyMerge(result[idx], overlay, policy, layerId, value);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// New document — only allowed under OPEN policy
|
|
174
|
+
if (policy === LayerPolicy.LOCKED ||
|
|
175
|
+
policy === LayerPolicy.RESTRICTED) {
|
|
176
|
+
console.warn(`Layer '${layerId}=${value}' tried to add '${oKind}/${oName}' but policy is ${policy}. Ignored.`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Overlay-only add — stamp metadata so the frontend knows
|
|
180
|
+
// "the whole doc is your overlay" (Phase 2 UX).
|
|
181
|
+
const newDoc = structuredClone(overlay);
|
|
182
|
+
stampOverlayMetadata(newDoc, null);
|
|
183
|
+
result.push(newDoc);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
_policyForKind(kind, policies) {
|
|
190
|
+
if (kind in policies)
|
|
191
|
+
return policies[kind];
|
|
192
|
+
const kindLower = kind.toLowerCase();
|
|
193
|
+
for (const [alias, policy] of Object.entries(policies)) {
|
|
194
|
+
if (alias.endsWith(`-${kindLower}`) || alias === kindLower)
|
|
195
|
+
return policy;
|
|
196
|
+
}
|
|
197
|
+
return LayerPolicy.OPEN;
|
|
198
|
+
}
|
|
199
|
+
_applyMerge(base, overlay, policy, layerId, value) {
|
|
200
|
+
const specOverlay = overlay.spec ?? {};
|
|
201
|
+
const specBase = base.spec ?? {};
|
|
202
|
+
// Timeline is append-only across overlays — concat+sort regardless
|
|
203
|
+
// of policy (even LOCKED). ADR 2026-05-10.
|
|
204
|
+
const mergedTimeline = mergeTimelineArrays(specBase, specOverlay);
|
|
205
|
+
if (Object.keys(specOverlay).length === 0 && mergedTimeline === null) {
|
|
206
|
+
return base;
|
|
207
|
+
}
|
|
208
|
+
if (policy === LayerPolicy.LOCKED) {
|
|
209
|
+
if (mergedTimeline === null) {
|
|
210
|
+
const name = base.metadata?.name ?? "";
|
|
211
|
+
console.warn(`Layer '${layerId}=${value}' tried to modify locked document '${name}'. Ignored.`);
|
|
212
|
+
return base;
|
|
213
|
+
}
|
|
214
|
+
// Timeline-only overlay on a LOCKED doc still appends events.
|
|
215
|
+
const result = structuredClone(base);
|
|
216
|
+
const sp = result.spec ?? {};
|
|
217
|
+
sp.timeline = mergedTimeline;
|
|
218
|
+
result.spec = sp;
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
if (policy === LayerPolicy.RESTRICTED) {
|
|
222
|
+
// Strip `timeline` from the restricted-merge call so the
|
|
223
|
+
// "unknown key" warning doesn't fire for timeline-only changes.
|
|
224
|
+
const specOverlayNoTl = {};
|
|
225
|
+
for (const [k, v] of Object.entries(specOverlay)) {
|
|
226
|
+
if (k !== "timeline")
|
|
227
|
+
specOverlayNoTl[k] = v;
|
|
228
|
+
}
|
|
229
|
+
const result = this._applyRestrictedMerge(base, specOverlayNoTl, layerId, value);
|
|
230
|
+
if (mergedTimeline !== null) {
|
|
231
|
+
const sp = result.spec ?? {};
|
|
232
|
+
sp.timeline = mergedTimeline;
|
|
233
|
+
result.spec = sp;
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
// OPEN — deep merge spec
|
|
238
|
+
const result = structuredClone(base);
|
|
239
|
+
result.spec = deepMerge(specBase, specOverlay);
|
|
240
|
+
if (mergedTimeline !== null) {
|
|
241
|
+
result.spec.timeline = mergedTimeline;
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(specOverlay).length > 0) {
|
|
244
|
+
// Phase 2 overlay UX: top-level spec keys the overlay provided
|
|
245
|
+
// are "overridden" under OPEN policy. Exclude `timeline` since
|
|
246
|
+
// it's append-only, not an override.
|
|
247
|
+
const overridden = Object.keys(specOverlay).filter((k) => k !== "timeline");
|
|
248
|
+
if (overridden.length > 0) {
|
|
249
|
+
stampOverlayMetadata(result, overridden);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
_applyRestrictedMerge(base, specOverlay, layerId, value) {
|
|
255
|
+
const result = structuredClone(base);
|
|
256
|
+
const specBase = result.spec ?? {};
|
|
257
|
+
const applied = [];
|
|
258
|
+
for (const [key, val] of Object.entries(specOverlay)) {
|
|
259
|
+
if (!(key in specBase)) {
|
|
260
|
+
console.warn(`Layer '${layerId}=${value}' tried to add key '${key}' to restricted document. Ignored.`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (typeof specBase[key] === "object" &&
|
|
264
|
+
specBase[key] !== null &&
|
|
265
|
+
!Array.isArray(specBase[key]) &&
|
|
266
|
+
typeof val === "object" &&
|
|
267
|
+
val !== null &&
|
|
268
|
+
!Array.isArray(val)) {
|
|
269
|
+
specBase[key] = deepMerge(specBase[key], val);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
specBase[key] = structuredClone(val);
|
|
273
|
+
}
|
|
274
|
+
applied.push(key);
|
|
275
|
+
}
|
|
276
|
+
result.spec = specBase;
|
|
277
|
+
// Restricted policy drops unknown keys — stamp only the ones
|
|
278
|
+
// that actually merged.
|
|
279
|
+
if (applied.length > 0) {
|
|
280
|
+
stampOverlayMetadata(result, applied);
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
}
|