@thinkingsage/kanon 0.8.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/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- package/templates/knowledge/mcp-servers.yaml.njk +2 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rosetta Stone — Transactional Translation Registry
|
|
3
|
+
*
|
|
4
|
+
* Provides `TranslationRegistryBuilder` for atomic format contract registration
|
|
5
|
+
* and `TranslationRegistrySnapshot` as a deeply-frozen immutable query surface.
|
|
6
|
+
*
|
|
7
|
+
* CONSTRAINTS:
|
|
8
|
+
* - NO filesystem, process, clock, random, Git, or network imports
|
|
9
|
+
* - ALL public data shapes remain owned by `src/schemas.ts` — only re-export, never redefine
|
|
10
|
+
* - Pure functions only
|
|
11
|
+
*
|
|
12
|
+
* Requirements: 2.1, 2.2, 2.3, 2.5, 2.7, 15.1, 15.4, 15.5
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
Direction,
|
|
17
|
+
FormatContract,
|
|
18
|
+
FormatIdentifier,
|
|
19
|
+
JsonValue,
|
|
20
|
+
LifecycleStatus,
|
|
21
|
+
NormalizedRelativePath,
|
|
22
|
+
RegistryFailure,
|
|
23
|
+
SourceDocument,
|
|
24
|
+
TranslationDiagnostic,
|
|
25
|
+
} from "../schemas";
|
|
26
|
+
|
|
27
|
+
import { codePointCompare, deepFreeze } from "./contracts";
|
|
28
|
+
import { createDiagnostic, createRegistryFailure } from "./diagnostics";
|
|
29
|
+
import type { ImmutableTemplateBundle } from "./templates";
|
|
30
|
+
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
// Translator Function Type Aliases
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Context supplied to source translators during inbound translation.
|
|
37
|
+
*/
|
|
38
|
+
export interface SourceTranslatorContext {
|
|
39
|
+
readonly format: FormatContract;
|
|
40
|
+
readonly canonicalSchemaVersion: string;
|
|
41
|
+
readonly options: Readonly<Record<string, JsonValue>>;
|
|
42
|
+
readonly callerContext: Readonly<Record<string, JsonValue>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Output from a source translator: a candidate artifact and diagnostics.
|
|
47
|
+
*/
|
|
48
|
+
export interface SourceTranslationOutput {
|
|
49
|
+
candidate?: Record<string, unknown>;
|
|
50
|
+
diagnostics: readonly TranslationDiagnostic[];
|
|
51
|
+
consumedPaths: readonly NormalizedRelativePath[];
|
|
52
|
+
preservedPaths: readonly NormalizedRelativePath[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A pure source translator that converts in-memory documents into a canonical candidate.
|
|
57
|
+
*/
|
|
58
|
+
export type SourceTranslator = (
|
|
59
|
+
documents: readonly SourceDocument[],
|
|
60
|
+
context: SourceTranslatorContext,
|
|
61
|
+
) => SourceTranslationOutput;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Output from a pretty-printer: source-formatted documents and diagnostics.
|
|
65
|
+
*/
|
|
66
|
+
export interface SourcePrintOutput {
|
|
67
|
+
documents: readonly SourceDocument[];
|
|
68
|
+
diagnostics: readonly TranslationDiagnostic[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A pure pretty-printer that renders a canonical artifact back into source format.
|
|
73
|
+
*/
|
|
74
|
+
export type PrettyPrinter = (
|
|
75
|
+
artifact: Record<string, unknown>,
|
|
76
|
+
context: SourceTranslatorContext,
|
|
77
|
+
) => SourcePrintOutput;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Context supplied to target translators during outbound translation.
|
|
81
|
+
*/
|
|
82
|
+
export interface TargetTranslatorContext {
|
|
83
|
+
readonly format: FormatContract;
|
|
84
|
+
readonly variant: string;
|
|
85
|
+
readonly canonicalSchemaVersion: string;
|
|
86
|
+
readonly options: Readonly<Record<string, JsonValue>>;
|
|
87
|
+
readonly callerContext: Readonly<Record<string, JsonValue>>;
|
|
88
|
+
readonly templates: ImmutableTemplateBundle;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A pure target translator that converts a canonical artifact into a translation plan.
|
|
93
|
+
*/
|
|
94
|
+
export type TargetTranslator = (
|
|
95
|
+
artifact: Record<string, unknown>,
|
|
96
|
+
context: TargetTranslatorContext,
|
|
97
|
+
) => TargetTranslationOutput;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Output from a target translator: plan files and diagnostics.
|
|
101
|
+
*/
|
|
102
|
+
export interface TargetTranslationOutput {
|
|
103
|
+
plan: Record<string, unknown>;
|
|
104
|
+
diagnostics: readonly TranslationDiagnostic[];
|
|
105
|
+
degradations: readonly Record<string, unknown>[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
109
|
+
// Registry Extension
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A bundle of a format contract with optional translator implementations
|
|
114
|
+
* registered by trusted host code.
|
|
115
|
+
*/
|
|
116
|
+
export interface RegistryExtension {
|
|
117
|
+
readonly contract: FormatContract;
|
|
118
|
+
readonly sourceTranslator?: SourceTranslator;
|
|
119
|
+
readonly prettyPrinter?: PrettyPrinter;
|
|
120
|
+
readonly targetTranslator?: TargetTranslator;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
124
|
+
// Registration Result
|
|
125
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
126
|
+
|
|
127
|
+
export type RegistrationResult =
|
|
128
|
+
| { ok: true; contract: FormatContract }
|
|
129
|
+
| { ok: false; diagnostics: TranslationDiagnostic[] }
|
|
130
|
+
| { ok: false; registryFailure: RegistryFailure };
|
|
131
|
+
|
|
132
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
133
|
+
// Registry Query and Resolution Types
|
|
134
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Requested direction for format resolution. Includes "any" for queries
|
|
138
|
+
* that do not restrict direction.
|
|
139
|
+
*/
|
|
140
|
+
export type RequestedDirection = Direction | "any";
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Query parameters for listing contracts from the snapshot.
|
|
144
|
+
*/
|
|
145
|
+
export interface RegistryQuery {
|
|
146
|
+
readonly direction?: RequestedDirection;
|
|
147
|
+
readonly harness?: string | null;
|
|
148
|
+
readonly lifecycle?: readonly LifecycleStatus[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Result of resolving an identifier or alias against a direction.
|
|
153
|
+
*/
|
|
154
|
+
export type FormatResolution =
|
|
155
|
+
| {
|
|
156
|
+
ok: true;
|
|
157
|
+
contract: FormatContract;
|
|
158
|
+
diagnostics: TranslationDiagnostic[];
|
|
159
|
+
}
|
|
160
|
+
| {
|
|
161
|
+
ok: false;
|
|
162
|
+
diagnostics: TranslationDiagnostic[];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
166
|
+
// Supported Contract Version
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
168
|
+
|
|
169
|
+
const SUPPORTED_CONTRACT_VERSION = "1.0" as const;
|
|
170
|
+
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
172
|
+
// TranslationRegistryBuilder
|
|
173
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Mutable builder that registers format contracts atomically and produces
|
|
177
|
+
* an immutable `TranslationRegistrySnapshot` on `freeze()`.
|
|
178
|
+
*/
|
|
179
|
+
export interface TranslationRegistryBuilder {
|
|
180
|
+
register(extension: RegistryExtension): RegistrationResult;
|
|
181
|
+
freeze(): TranslationRegistrySnapshot;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Options for format resolution.
|
|
186
|
+
*/
|
|
187
|
+
export interface ResolveOptions {
|
|
188
|
+
/** Allow retired formats to resolve (for migration-only use cases). */
|
|
189
|
+
readonly allowRetired?: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Immutable, deeply-frozen snapshot of registered format contracts
|
|
194
|
+
* and their translator implementations.
|
|
195
|
+
*/
|
|
196
|
+
export interface TranslationRegistrySnapshot {
|
|
197
|
+
readonly version: string;
|
|
198
|
+
readonly registrationCount: number;
|
|
199
|
+
listContracts(query?: RegistryQuery): readonly FormatContract[];
|
|
200
|
+
resolve(
|
|
201
|
+
identifierOrAlias: string,
|
|
202
|
+
direction: RequestedDirection,
|
|
203
|
+
options?: ResolveOptions,
|
|
204
|
+
): FormatResolution;
|
|
205
|
+
getSourceTranslator(id: FormatIdentifier): SourceTranslator | undefined;
|
|
206
|
+
getPrettyPrinter(id: FormatIdentifier): PrettyPrinter | undefined;
|
|
207
|
+
getTargetTranslator(id: FormatIdentifier): TargetTranslator | undefined;
|
|
208
|
+
getAliasHistory(): ReadonlyMap<string, FormatIdentifier>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
212
|
+
// Internal Builder State
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
214
|
+
|
|
215
|
+
interface InternalRegistryEntry {
|
|
216
|
+
contract: FormatContract;
|
|
217
|
+
sourceTranslator?: SourceTranslator;
|
|
218
|
+
prettyPrinter?: PrettyPrinter;
|
|
219
|
+
targetTranslator?: TargetTranslator;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
223
|
+
// Builder Implementation
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create a new `TranslationRegistryBuilder` instance.
|
|
228
|
+
*
|
|
229
|
+
* @param version - The version string for the resulting snapshot (e.g. "1.0.0")
|
|
230
|
+
*/
|
|
231
|
+
export function createRegistryBuilder(
|
|
232
|
+
version: string,
|
|
233
|
+
): TranslationRegistryBuilder {
|
|
234
|
+
// Mutable builder state — only mutated on successful registration
|
|
235
|
+
const entries: Map<FormatIdentifier, InternalRegistryEntry> = new Map();
|
|
236
|
+
const aliasIndex: Map<string, FormatIdentifier> = new Map();
|
|
237
|
+
// Track alias registrations in order for history snapshots
|
|
238
|
+
const aliasHistory: Map<string, FormatIdentifier> = new Map();
|
|
239
|
+
let frozen = false;
|
|
240
|
+
|
|
241
|
+
function register(extension: RegistryExtension): RegistrationResult {
|
|
242
|
+
if (frozen) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
registryFailure: createRegistryFailure(
|
|
246
|
+
"Cannot register after the registry has been frozen.",
|
|
247
|
+
),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Wrap the entire validation in a try/catch so that if diagnostic
|
|
252
|
+
// construction itself fails, we return a RegistryFailure.
|
|
253
|
+
try {
|
|
254
|
+
return validateAndRegister(extension);
|
|
255
|
+
} catch {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
registryFailure: createRegistryFailure(
|
|
259
|
+
"Registration failed: diagnostic construction unavailable.",
|
|
260
|
+
),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function validateAndRegister(
|
|
266
|
+
extension: RegistryExtension,
|
|
267
|
+
): RegistrationResult {
|
|
268
|
+
const { contract } = extension;
|
|
269
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
270
|
+
|
|
271
|
+
// --- Contract version check ---
|
|
272
|
+
if (contract.contractVersion !== SUPPORTED_CONTRACT_VERSION) {
|
|
273
|
+
diagnostics.push(
|
|
274
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
275
|
+
formatId: contract.id,
|
|
276
|
+
message: `Unsupported contract version "${contract.contractVersion}". Only "${SUPPORTED_CONTRACT_VERSION}" is supported.`,
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Duplicate identifier check ---
|
|
282
|
+
if (entries.has(contract.id)) {
|
|
283
|
+
diagnostics.push(
|
|
284
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
285
|
+
formatId: contract.id,
|
|
286
|
+
message: `Duplicate format identifier "${contract.id}" is already registered.`,
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- Internal alias uniqueness (no duplicate aliases within same registration) ---
|
|
292
|
+
const seenAliases = new Set<string>();
|
|
293
|
+
for (const alias of contract.aliases) {
|
|
294
|
+
if (seenAliases.has(alias)) {
|
|
295
|
+
diagnostics.push(
|
|
296
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
297
|
+
formatId: contract.id,
|
|
298
|
+
message: `Duplicate alias "${alias}" within the same registration.`,
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
seenAliases.add(alias);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// --- Alias checks: duplicates against existing identifiers and aliases ---
|
|
306
|
+
for (const alias of contract.aliases) {
|
|
307
|
+
if (entries.has(alias as FormatIdentifier)) {
|
|
308
|
+
diagnostics.push(
|
|
309
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
310
|
+
formatId: contract.id,
|
|
311
|
+
message: `Alias "${alias}" conflicts with an existing format identifier.`,
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
} else if (aliasIndex.has(alias)) {
|
|
315
|
+
const existingOwner = aliasIndex.get(alias) ?? alias;
|
|
316
|
+
diagnostics.push(
|
|
317
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
318
|
+
formatId: contract.id,
|
|
319
|
+
message: `Alias "${alias}" is already registered by format "${existingOwner}".`,
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
// An alias that matches the contract's own id is a self-reference (valid)
|
|
324
|
+
// but an alias that matches another alias in the same registration is fine too.
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- Check alias does not duplicate the registrant's own id across existing aliases ---
|
|
328
|
+
if (aliasIndex.has(contract.id)) {
|
|
329
|
+
const existingOwner = aliasIndex.get(contract.id) ?? contract.id;
|
|
330
|
+
diagnostics.push(
|
|
331
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
332
|
+
formatId: contract.id,
|
|
333
|
+
message: `Format identifier "${contract.id}" conflicts with an alias registered by "${existingOwner}".`,
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --- Default variant validation ---
|
|
339
|
+
if (contract.defaultVariant !== undefined) {
|
|
340
|
+
const variantKeys = Object.keys(contract.variants);
|
|
341
|
+
if (!variantKeys.includes(contract.defaultVariant)) {
|
|
342
|
+
diagnostics.push(
|
|
343
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
344
|
+
formatId: contract.id,
|
|
345
|
+
message: `Default variant "${contract.defaultVariant}" is not declared in variants.`,
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Variant ID consistency: each variant's id must match its key ---
|
|
352
|
+
for (const [key, variant] of Object.entries(contract.variants)) {
|
|
353
|
+
if (variant.id !== key) {
|
|
354
|
+
diagnostics.push(
|
|
355
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
356
|
+
formatId: contract.id,
|
|
357
|
+
message: `Variant key "${key}" does not match its declared id "${variant.id}".`,
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// --- Detection rules non-empty for source-capable contracts ---
|
|
364
|
+
const direction = contract.direction;
|
|
365
|
+
if (
|
|
366
|
+
(direction === "source" || direction === "bidirectional") &&
|
|
367
|
+
contract.detection.rules.length === 0
|
|
368
|
+
) {
|
|
369
|
+
diagnostics.push(
|
|
370
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
371
|
+
formatId: contract.id,
|
|
372
|
+
message: `Source-capable contract must have at least one detection rule.`,
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- Normalization rules must have unique IDs ---
|
|
378
|
+
const normRuleIds = new Set<string>();
|
|
379
|
+
for (const rule of contract.normalizationRules) {
|
|
380
|
+
if (normRuleIds.has(rule.id)) {
|
|
381
|
+
diagnostics.push(
|
|
382
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
383
|
+
formatId: contract.id,
|
|
384
|
+
message: `Duplicate normalization rule id "${rule.id}".`,
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
normRuleIds.add(rule.id);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- Option definitions: enum values must be non-empty when type is "enum" ---
|
|
392
|
+
for (const [optionKey, optionDef] of Object.entries(
|
|
393
|
+
contract.optionDefinitions,
|
|
394
|
+
)) {
|
|
395
|
+
if (optionDef.type === "enum") {
|
|
396
|
+
if (!optionDef.enumValues || optionDef.enumValues.length === 0) {
|
|
397
|
+
diagnostics.push(
|
|
398
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
399
|
+
formatId: contract.id,
|
|
400
|
+
message: `Option "${optionKey}" has type "enum" but declares no enum values.`,
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --- Compatibility profile completeness is already enforced by Zod schema ---
|
|
408
|
+
// (RosettaCompatibilityProfileSchema has a refinement checking every capability)
|
|
409
|
+
|
|
410
|
+
// --- Direction-implied translator presence ---
|
|
411
|
+
if (
|
|
412
|
+
(direction === "source" || direction === "bidirectional") &&
|
|
413
|
+
!extension.sourceTranslator
|
|
414
|
+
) {
|
|
415
|
+
diagnostics.push(
|
|
416
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
417
|
+
formatId: contract.id,
|
|
418
|
+
message: `Direction "${direction}" requires a source translator, but none was provided.`,
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (
|
|
423
|
+
(direction === "target" || direction === "bidirectional") &&
|
|
424
|
+
!extension.targetTranslator
|
|
425
|
+
) {
|
|
426
|
+
diagnostics.push(
|
|
427
|
+
createDiagnostic("RS_INVALID_CONTRACT", {
|
|
428
|
+
formatId: contract.id,
|
|
429
|
+
message: `Direction "${direction}" requires a target translator, but none was provided.`,
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// --- If any diagnostics were collected, reject without mutating state ---
|
|
435
|
+
if (diagnostics.length > 0) {
|
|
436
|
+
return { ok: false, diagnostics };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// --- Atomic commit: all checks passed, now mutate builder state ---
|
|
440
|
+
entries.set(contract.id, {
|
|
441
|
+
contract,
|
|
442
|
+
sourceTranslator: extension.sourceTranslator,
|
|
443
|
+
prettyPrinter: extension.prettyPrinter,
|
|
444
|
+
targetTranslator: extension.targetTranslator,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
for (const alias of contract.aliases) {
|
|
448
|
+
aliasIndex.set(alias, contract.id);
|
|
449
|
+
aliasHistory.set(alias, contract.id);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { ok: true, contract };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function freeze(): TranslationRegistrySnapshot {
|
|
456
|
+
frozen = true;
|
|
457
|
+
return createSnapshot(version, entries, aliasIndex, aliasHistory);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { register, freeze };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
464
|
+
// Snapshot Implementation
|
|
465
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
466
|
+
|
|
467
|
+
function createSnapshot(
|
|
468
|
+
version: string,
|
|
469
|
+
entries: Map<FormatIdentifier, InternalRegistryEntry>,
|
|
470
|
+
aliasIndex: Map<string, FormatIdentifier>,
|
|
471
|
+
aliasHistory: Map<string, FormatIdentifier>,
|
|
472
|
+
): TranslationRegistrySnapshot {
|
|
473
|
+
// Build sorted contract list once (deterministic by FormatIdentifier)
|
|
474
|
+
const sortedIds = [...entries.keys()].sort(codePointCompare);
|
|
475
|
+
const frozenContracts: FormatContract[] = sortedIds
|
|
476
|
+
.map((id) => entries.get(id))
|
|
477
|
+
.filter((entry): entry is InternalRegistryEntry => entry !== undefined)
|
|
478
|
+
.map((entry) => deepFreeze(structuredClone(entry.contract)));
|
|
479
|
+
|
|
480
|
+
// Build lookup maps for translators (not frozen — functions can't be frozen)
|
|
481
|
+
const sourceTranslators = new Map<FormatIdentifier, SourceTranslator>();
|
|
482
|
+
const prettyPrinters = new Map<FormatIdentifier, PrettyPrinter>();
|
|
483
|
+
const targetTranslators = new Map<FormatIdentifier, TargetTranslator>();
|
|
484
|
+
|
|
485
|
+
for (const [id, entry] of entries) {
|
|
486
|
+
if (entry.sourceTranslator) {
|
|
487
|
+
sourceTranslators.set(id, entry.sourceTranslator);
|
|
488
|
+
}
|
|
489
|
+
if (entry.prettyPrinter) {
|
|
490
|
+
prettyPrinters.set(id, entry.prettyPrinter);
|
|
491
|
+
}
|
|
492
|
+
if (entry.targetTranslator) {
|
|
493
|
+
targetTranslators.set(id, entry.targetTranslator);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Freeze alias index into a plain object for lookup
|
|
498
|
+
const frozenAliasMap = new Map<string, FormatIdentifier>(aliasIndex);
|
|
499
|
+
|
|
500
|
+
// Freeze alias history for snapshot reporting
|
|
501
|
+
const frozenAliasHistory = new Map<string, FormatIdentifier>(aliasHistory);
|
|
502
|
+
|
|
503
|
+
// Registration count is the number of successfully registered contracts
|
|
504
|
+
const registrationCount = entries.size;
|
|
505
|
+
|
|
506
|
+
function listContracts(query?: RegistryQuery): readonly FormatContract[] {
|
|
507
|
+
if (!query) {
|
|
508
|
+
return frozenContracts;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return frozenContracts.filter((contract) => {
|
|
512
|
+
// Direction filter
|
|
513
|
+
if (query.direction && query.direction !== "any") {
|
|
514
|
+
if (!directionMatches(contract.direction, query.direction)) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Harness filter
|
|
520
|
+
if (query.harness !== undefined) {
|
|
521
|
+
if (query.harness === null) {
|
|
522
|
+
if (contract.harness !== null) return false;
|
|
523
|
+
} else {
|
|
524
|
+
if (contract.harness !== query.harness) return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Lifecycle filter
|
|
529
|
+
if (query.lifecycle && query.lifecycle.length > 0) {
|
|
530
|
+
if (!query.lifecycle.includes(contract.lifecycle.status)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return true;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function resolve(
|
|
540
|
+
identifierOrAlias: string,
|
|
541
|
+
direction: RequestedDirection,
|
|
542
|
+
options?: ResolveOptions,
|
|
543
|
+
): FormatResolution {
|
|
544
|
+
// Resolve the identifier: direct lookup first, then alias lookup
|
|
545
|
+
let formatId: FormatIdentifier | undefined;
|
|
546
|
+
|
|
547
|
+
if (entries.has(identifierOrAlias as FormatIdentifier)) {
|
|
548
|
+
formatId = identifierOrAlias as FormatIdentifier;
|
|
549
|
+
} else if (frozenAliasMap.has(identifierOrAlias)) {
|
|
550
|
+
formatId = frozenAliasMap.get(identifierOrAlias);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!formatId) {
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
diagnostics: [
|
|
557
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
558
|
+
formatId: identifierOrAlias,
|
|
559
|
+
message: `Format "${identifierOrAlias}" is not registered.`,
|
|
560
|
+
}),
|
|
561
|
+
],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const entry = entries.get(formatId);
|
|
566
|
+
if (!entry) {
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
diagnostics: [
|
|
570
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
571
|
+
formatId,
|
|
572
|
+
message: `Format "${formatId}" could not be resolved.`,
|
|
573
|
+
}),
|
|
574
|
+
],
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const contract = entry.contract;
|
|
578
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
579
|
+
|
|
580
|
+
// Lifecycle checks
|
|
581
|
+
const lifecycle = contract.lifecycle;
|
|
582
|
+
if (lifecycle.status === "retired") {
|
|
583
|
+
if (options?.allowRetired) {
|
|
584
|
+
// Migration-only: resolve with a warning instead of an error
|
|
585
|
+
diagnostics.push(
|
|
586
|
+
createDiagnostic("RS_LIFECYCLE_DEPRECATED", {
|
|
587
|
+
formatId: contract.id,
|
|
588
|
+
message: `Format "${contract.id}" has lifecycle status "retired" (resolved with allowRetired).`,
|
|
589
|
+
remediation: lifecycle.replacement
|
|
590
|
+
? `Migrate to "${lifecycle.replacement}".`
|
|
591
|
+
: "No replacement is declared.",
|
|
592
|
+
}),
|
|
593
|
+
);
|
|
594
|
+
} else {
|
|
595
|
+
return {
|
|
596
|
+
ok: false,
|
|
597
|
+
diagnostics: [
|
|
598
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
599
|
+
formatId: contract.id,
|
|
600
|
+
message: `Format "${contract.id}" has lifecycle status "retired" and is not selectable.`,
|
|
601
|
+
remediation: lifecycle.replacement
|
|
602
|
+
? `Migrate to "${lifecycle.replacement}".`
|
|
603
|
+
: "No replacement is declared.",
|
|
604
|
+
}),
|
|
605
|
+
],
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (lifecycle.status === "deprecated") {
|
|
611
|
+
diagnostics.push(
|
|
612
|
+
createDiagnostic("RS_LIFECYCLE_DEPRECATED", {
|
|
613
|
+
formatId: contract.id,
|
|
614
|
+
message: `Format "${contract.id}" is deprecated.`,
|
|
615
|
+
remediation: lifecycle.replacement
|
|
616
|
+
? `Migrate to "${lifecycle.replacement}".`
|
|
617
|
+
: "No replacement is declared.",
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (lifecycle.status === "experimental") {
|
|
623
|
+
diagnostics.push(
|
|
624
|
+
createDiagnostic("RS_LIFECYCLE_DEPRECATED", {
|
|
625
|
+
formatId: contract.id,
|
|
626
|
+
message: `Format "${contract.id}" has lifecycle status "experimental".`,
|
|
627
|
+
severityOverride: "info",
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Direction check
|
|
633
|
+
if (direction !== "any") {
|
|
634
|
+
if (!directionMatches(contract.direction, direction)) {
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
diagnostics: [
|
|
638
|
+
createDiagnostic("RS_DIRECTION_MISMATCH", {
|
|
639
|
+
formatId: contract.id,
|
|
640
|
+
message: `Format "${contract.id}" declares direction "${contract.direction}" but "${direction}" was requested.`,
|
|
641
|
+
}),
|
|
642
|
+
],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Return frozen clone of contract
|
|
648
|
+
const frozenContract = frozenContracts.find((c) => c.id === formatId);
|
|
649
|
+
if (!frozenContract) {
|
|
650
|
+
return {
|
|
651
|
+
ok: false,
|
|
652
|
+
diagnostics: [
|
|
653
|
+
createDiagnostic("RS_REGISTRATION_FAILED", {
|
|
654
|
+
formatId,
|
|
655
|
+
message: `Format "${formatId}" could not be resolved from snapshot.`,
|
|
656
|
+
}),
|
|
657
|
+
],
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return { ok: true, contract: frozenContract, diagnostics };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function getSourceTranslator(
|
|
665
|
+
id: FormatIdentifier,
|
|
666
|
+
): SourceTranslator | undefined {
|
|
667
|
+
return sourceTranslators.get(id);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function getPrettyPrinter(id: FormatIdentifier): PrettyPrinter | undefined {
|
|
671
|
+
return prettyPrinters.get(id);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function getTargetTranslator(
|
|
675
|
+
id: FormatIdentifier,
|
|
676
|
+
): TargetTranslator | undefined {
|
|
677
|
+
return targetTranslators.get(id);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function getAliasHistory(): ReadonlyMap<string, FormatIdentifier> {
|
|
681
|
+
return frozenAliasHistory;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const snapshot: TranslationRegistrySnapshot = {
|
|
685
|
+
version,
|
|
686
|
+
registrationCount,
|
|
687
|
+
listContracts,
|
|
688
|
+
resolve,
|
|
689
|
+
getSourceTranslator,
|
|
690
|
+
getPrettyPrinter,
|
|
691
|
+
getTargetTranslator,
|
|
692
|
+
getAliasHistory,
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
return Object.freeze(snapshot);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
699
|
+
// Direction Matching Helper
|
|
700
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Check whether a contract's declared direction satisfies a requested direction.
|
|
704
|
+
* "bidirectional" satisfies both "source" and "target" requests.
|
|
705
|
+
*/
|
|
706
|
+
function directionMatches(
|
|
707
|
+
contractDirection: Direction,
|
|
708
|
+
requested: Direction,
|
|
709
|
+
): boolean {
|
|
710
|
+
if (contractDirection === "bidirectional") return true;
|
|
711
|
+
return contractDirection === requested;
|
|
712
|
+
}
|