feat: vendor the public read contract, and give it its own source switch
The public surface — 17 of the 28 registered routes — reads from a 29KB
TypeScript fixture and never touches the backend. `TECH_LOG_STUDIO_SOURCE`
only ever switched the Studio gateways; `publicContent` was wired to the
static adapter unconditionally, so no configuration could make the public
site show published content. This is the first half of closing that: the
contract and the switch, with the adapter still to come.
The generator now vendors both canonical contracts instead of one. They are
independent — different services on different schedules — so each carries
its own digest and operation list, and updating one leaves the other's drift
gate quiet.
`TECH_LOG_PUBLIC_SOURCE` is deliberately a second flag rather than a rename
of the Studio one. The combination that matters right now is exactly the one
a single flag cannot express: the authoring backend is live while the public
read API does not exist yet. production stays on MOCK for that reason —
pointing it at HTTP today would empty the live site — and moves when the
backend serves /api/v1/public.
Also records the compatibility evidence the registry gate wanted for the
Studio access change in fff5e6f. That gate has been failing since, which is
on me: the change was real and breaking, and it shipped without the note
explaining that route ids and schemas are untouched and only the access
classification moves.
This commit is contained in:
@@ -280,6 +280,86 @@
|
||||
"compatibilityWindow": "Existing valid envelopes continue to decode; invalid values fail closed.",
|
||||
"rollback": "Remove the required codec field and runtime codec dispatch together.",
|
||||
"owner": "frontend-platform"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_HOME:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENTS:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_NEW:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_EDIT:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_VALIDATION:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_PREVIEW:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_DOCUMENT_PUBLISH:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_PUBLICATIONS:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_PUBLICATION_PREVIEW:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
},
|
||||
{
|
||||
"changeId": "FE-REG-ROUTE:TECH_LOG_STUDIO_NOT_FOUND:access:field-changed",
|
||||
"versionBump": "Studio routes move from access \"public\" to \"session-required\". Every route was registered public, so the router's auth gate was a no-op and a production build served the Studio shell to signed-out visitors.",
|
||||
"migration": "None for callers. The route ids, paths, params, and search schemas are unchanged; only the access classification moves, and the SPA resolves it from the route's own layoutGroup rather than a per-route literal.",
|
||||
"compatibilityWindow": "The deployed release consumes only its same-release registry snapshot; a signed-out visitor is redirected to the public shell instead of rendering Studio chrome.",
|
||||
"rollback": "Roll back the atomic release to 93ce86e; the route contract derives access from layoutGroup in one expression, so the previous value returns with the release.",
|
||||
"owner": "tech-log-frontend"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "HTTP",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "MOCK",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "MOCK",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "MOCK",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"TECH_LOG_PUBLIC_SOURCE": "HTTP",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -38,14 +38,14 @@
|
||||
},
|
||||
"contractSet": {
|
||||
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
||||
"setDigest": "sha256:261ac63030fcbcef8bd0b7087d29120996a9f580f455739dad245141860c1fe5",
|
||||
"setDigest": "sha256:5a67c3ce96f6be3a9314040f3039ca32a81f9eaf4402c88a745e235d183f23df",
|
||||
"packages": [
|
||||
{
|
||||
"packageId": "@tech-log/studio-contract",
|
||||
"version": "3.0.0",
|
||||
"digest": "sha256:6cae9924403d0761f401643a022980b8e04183eea0d890c143c9fbbbbc7431e4",
|
||||
"runtimeProtocolVersion": 1,
|
||||
"sourceRevision": "b20d7a2"
|
||||
"sourceRevision": "55a9599"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* canonical studio-v1.yaml을 vendor하고 타입을 생성한다.
|
||||
* canonical 계약(studio-v1, public-v1)을 vendor하고 타입을 생성한다.
|
||||
*
|
||||
* 생성기는 저장소 의존성에 넣지 않는다. `openapi-typescript`는 TypeScript 5의
|
||||
* classic compiler API를 요구하는데 이 저장소는 TypeScript 7.0.2를 고정하고
|
||||
@@ -12,15 +12,45 @@
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { argv, env, exit } from "node:process";
|
||||
|
||||
const CANONICAL_ROOT =
|
||||
env.TECH_LOG_DESIGN_PACKAGE ?? "/home/donghyeon/workspace/tech-log-design-package";
|
||||
const CANONICAL_YAML = `${CANONICAL_ROOT}/contracts/openapi/studio-v1.yaml`;
|
||||
const VENDOR_YAML = "src/features/tech-log/contracts/studio/studio-api.openapi.yaml";
|
||||
const GENERATED = "src/features/tech-log/contracts/studio/generated.ts";
|
||||
const SOURCE_RECORD = "src/features/tech-log/contracts/studio/canonical-source.json";
|
||||
|
||||
/**
|
||||
* 계약은 둘이고 서로 독립이다. Studio는 인증된 작성 표면이고, Public은 인증
|
||||
* 없는 조회 표면이다. 각자 자기 canonical yaml에서 나오고 자기 digest를 들고
|
||||
* 다니므로, 한쪽이 갱신돼도 다른 쪽 drift 게이트는 조용하다.
|
||||
*/
|
||||
type ContractTarget = Readonly<{
|
||||
name: string;
|
||||
packageId: string;
|
||||
canonicalYaml: string;
|
||||
vendorYaml: string;
|
||||
generated: string;
|
||||
sourceRecord: string;
|
||||
}>;
|
||||
|
||||
const CONTRACTS: readonly ContractTarget[] = Object.freeze([
|
||||
Object.freeze({
|
||||
name: "studio",
|
||||
packageId: "@tech-log/studio-contract",
|
||||
canonicalYaml: `${CANONICAL_ROOT}/contracts/openapi/studio-v1.yaml`,
|
||||
vendorYaml: "src/features/tech-log/contracts/studio/studio-api.openapi.yaml",
|
||||
generated: "src/features/tech-log/contracts/studio/generated.ts",
|
||||
sourceRecord: "src/features/tech-log/contracts/studio/canonical-source.json",
|
||||
}),
|
||||
Object.freeze({
|
||||
name: "public",
|
||||
packageId: "@tech-log/public-contract",
|
||||
canonicalYaml: `${CANONICAL_ROOT}/contracts/openapi/public-v1.yaml`,
|
||||
vendorYaml: "src/features/tech-log/contracts/public/public-api.openapi.yaml",
|
||||
generated: "src/features/tech-log/contracts/public/generated.ts",
|
||||
sourceRecord: "src/features/tech-log/contracts/public/canonical-source.json",
|
||||
}),
|
||||
]);
|
||||
|
||||
const OPENAPI_TYPESCRIPT = "openapi-typescript@7.9.1";
|
||||
const GENERATOR_TYPESCRIPT = "typescript@5.9.3";
|
||||
@@ -56,71 +86,84 @@ function fail(problems: readonly string[]): never {
|
||||
}
|
||||
|
||||
if (check) {
|
||||
const vendored = readFileSync(VENDOR_YAML, "utf8");
|
||||
const generated = readFileSync(GENERATED, "utf8");
|
||||
const record = JSON.parse(readFileSync(SOURCE_RECORD, "utf8")) as CanonicalRecord;
|
||||
const problems: string[] = [];
|
||||
const summaries: string[] = [];
|
||||
|
||||
if (digestOf(readFileSync(VENDOR_YAML)) !== record.digest) {
|
||||
problems.push(`${VENDOR_YAML} does not hash to the recorded digest`);
|
||||
}
|
||||
const vendoredOperations = operationIdsOf(vendored);
|
||||
if (vendoredOperations.join(" ") !== [...record.operationIds].join(" ")) {
|
||||
problems.push(`${SOURCE_RECORD} operationIds differ from ${VENDOR_YAML}`);
|
||||
}
|
||||
if (specVersionOf(vendored) !== record.version) {
|
||||
problems.push(`${SOURCE_RECORD} version differs from ${VENDOR_YAML}`);
|
||||
}
|
||||
// 생성물은 operationId로 키가 매겨진 `operations` 인터페이스를 노출한다.
|
||||
for (const operationId of record.operationIds) {
|
||||
if (!new RegExp(`^\\s{4}${operationId}:`, "mu").test(generated)) {
|
||||
problems.push(`${GENERATED} is missing operation ${operationId}`);
|
||||
for (const target of CONTRACTS) {
|
||||
const vendored = readFileSync(target.vendorYaml, "utf8");
|
||||
const generated = readFileSync(target.generated, "utf8");
|
||||
const record = JSON.parse(readFileSync(target.sourceRecord, "utf8")) as CanonicalRecord;
|
||||
|
||||
if (digestOf(readFileSync(target.vendorYaml)) !== record.digest) {
|
||||
problems.push(`${target.vendorYaml} does not hash to the recorded digest`);
|
||||
}
|
||||
if (operationIdsOf(vendored).join(" ") !== [...record.operationIds].join(" ")) {
|
||||
problems.push(`${target.sourceRecord} operationIds differ from ${target.vendorYaml}`);
|
||||
}
|
||||
if (specVersionOf(vendored) !== record.version) {
|
||||
problems.push(`${target.sourceRecord} version differs from ${target.vendorYaml}`);
|
||||
}
|
||||
if (record.packageId !== target.packageId) {
|
||||
problems.push(`${target.sourceRecord} packageId is not ${target.packageId}`);
|
||||
}
|
||||
// 생성물은 operationId로 키가 매겨진 `operations` 인터페이스를 노출한다.
|
||||
for (const operationId of record.operationIds) {
|
||||
if (!new RegExp(`^\\s{4}${operationId}:`, "mu").test(generated)) {
|
||||
problems.push(`${target.generated} is missing operation ${operationId}`);
|
||||
}
|
||||
}
|
||||
summaries.push(
|
||||
`${record.packageId}@${record.version} (${record.sourceRevision}), ${record.operationIds.length} operations`,
|
||||
);
|
||||
}
|
||||
|
||||
if (problems.length > 0) fail(problems);
|
||||
console.log(
|
||||
`tech-log contract is in sync: ${record.packageId}@${record.version} (${record.sourceRevision}), ${record.operationIds.length} operations.`,
|
||||
);
|
||||
console.log(`tech-log contracts are in sync:\n- ${summaries.join("\n- ")}`);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
const canonicalBytes = readFileSync(CANONICAL_YAML);
|
||||
const canonicalText = canonicalBytes.toString("utf8");
|
||||
const sourceRevision = execFileSync(
|
||||
"git",
|
||||
["-C", CANONICAL_ROOT, "rev-parse", "--short=7", "HEAD"],
|
||||
{ encoding: "utf8" },
|
||||
).trim();
|
||||
|
||||
const record: CanonicalRecord = {
|
||||
packageId: "@tech-log/studio-contract",
|
||||
version: specVersionOf(canonicalText),
|
||||
digest: digestOf(canonicalBytes),
|
||||
sourceRevision: execFileSync(
|
||||
"git",
|
||||
["-C", CANONICAL_ROOT, "rev-parse", "--short=7", "HEAD"],
|
||||
{ encoding: "utf8" },
|
||||
).trim(),
|
||||
operationIds: operationIdsOf(canonicalText),
|
||||
};
|
||||
for (const target of CONTRACTS) {
|
||||
const canonicalBytes = readFileSync(target.canonicalYaml);
|
||||
const canonicalText = canonicalBytes.toString("utf8");
|
||||
|
||||
// 격리 실행. 저장소의 node_modules와 lockfile은 그대로다.
|
||||
const generated = execFileSync(
|
||||
"corepack",
|
||||
[
|
||||
"pnpm",
|
||||
"dlx",
|
||||
"--package",
|
||||
GENERATOR_TYPESCRIPT,
|
||||
"--package",
|
||||
OPENAPI_TYPESCRIPT,
|
||||
"openapi-typescript",
|
||||
CANONICAL_YAML,
|
||||
],
|
||||
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
|
||||
);
|
||||
const record: CanonicalRecord = {
|
||||
packageId: target.packageId,
|
||||
version: specVersionOf(canonicalText),
|
||||
digest: digestOf(canonicalBytes),
|
||||
sourceRevision,
|
||||
operationIds: operationIdsOf(canonicalText),
|
||||
};
|
||||
|
||||
writeFileSync(VENDOR_YAML, canonicalText);
|
||||
writeFileSync(GENERATED, generated);
|
||||
writeFileSync(SOURCE_RECORD, `${JSON.stringify(record, null, 2)}\n`);
|
||||
console.log(
|
||||
`Generated from ${record.packageId}@${record.version} (${record.sourceRevision}), ${record.operationIds.length} operations.`,
|
||||
);
|
||||
// 격리 실행. 저장소의 node_modules와 lockfile은 그대로다.
|
||||
const generated = execFileSync(
|
||||
"corepack",
|
||||
[
|
||||
"pnpm",
|
||||
"dlx",
|
||||
"--package",
|
||||
GENERATOR_TYPESCRIPT,
|
||||
"--package",
|
||||
OPENAPI_TYPESCRIPT,
|
||||
"openapi-typescript",
|
||||
target.canonicalYaml,
|
||||
],
|
||||
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
|
||||
);
|
||||
|
||||
mkdirSync(dirname(target.vendorYaml), { recursive: true });
|
||||
writeFileSync(target.vendorYaml, canonicalText);
|
||||
writeFileSync(target.generated, generated);
|
||||
writeFileSync(target.sourceRecord, `${JSON.stringify(record, null, 2)}\n`);
|
||||
console.log(
|
||||
`Generated from ${record.packageId}@${record.version} (${record.sourceRevision}), ${record.operationIds.length} operations.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 재생성은 매번 package digest를 바꾼다. `pnpm dev`가 그대로 서빙하는
|
||||
// `public/release-manifest.json`은 build가 컴파일한 contract set을 그대로
|
||||
|
||||
@@ -599,6 +599,7 @@ export async function createRuntimeAdapters(
|
||||
const featureInputs = createInstalledFeatureInputs({
|
||||
contractOperations,
|
||||
studioSource: config.TECH_LOG_STUDIO_SOURCE,
|
||||
publicSource: config.TECH_LOG_PUBLIC_SOURCE,
|
||||
apiBaseUrl: config.API_BASE_URL,
|
||||
requestTimeoutMs: config.REQUEST_TIMEOUT_MS,
|
||||
csrf: techLogCsrf,
|
||||
|
||||
@@ -56,6 +56,12 @@ export type RuntimeConfig = Readonly<{
|
||||
* MOCK.
|
||||
*/
|
||||
TECH_LOG_STUDIO_SOURCE: "MOCK" | "HTTP";
|
||||
/**
|
||||
* §3.5-adjacent runtime switch: which public-read adapter this build talks
|
||||
* to. Independent of the Studio switch — the two surfaces are separate
|
||||
* services. A V1 document predates the key and normalizes to MOCK.
|
||||
*/
|
||||
TECH_LOG_PUBLIC_SOURCE: "MOCK" | "HTTP";
|
||||
/** Present only while a V1 document is still accepted. */
|
||||
LEGACY_API_CONTRACT_VERSION?: string;
|
||||
}>;
|
||||
@@ -143,6 +149,10 @@ export function validateRuntimeConfig(value: unknown): RuntimeConfigValidation {
|
||||
TECH_LOG_STUDIO_SOURCE: isV2
|
||||
? (parsed as RuntimeConfigV2).TECH_LOG_STUDIO_SOURCE
|
||||
: "MOCK",
|
||||
// Same treatment for the public-read switch.
|
||||
TECH_LOG_PUBLIC_SOURCE: isV2
|
||||
? (parsed as RuntimeConfigV2).TECH_LOG_PUBLIC_SOURCE
|
||||
: "MOCK",
|
||||
...(isV2
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -52,6 +52,11 @@ export const ENV_REGISTRY = Object.freeze({
|
||||
// TechLog Studio gateway adapter selection. Defaults to MOCK while the
|
||||
// backend does not exist yet.
|
||||
TECH_LOG_STUDIO_SOURCE: runtime("public", false, "MOCK"),
|
||||
// TechLog public-read adapter selection. Separate from the Studio switch on
|
||||
// purpose: the two surfaces are different services on different schedules,
|
||||
// and the combination that matters right now — an authoring backend that is
|
||||
// live while the public read API is not — is unreachable with one flag.
|
||||
TECH_LOG_PUBLIC_SOURCE: runtime("public", false, "MOCK"),
|
||||
// §3.5: build-time narrowing of the product manifest. A feature left out
|
||||
// here is not imported by any registry and never reaches the bundle.
|
||||
VITE_PRODUCT_FEATURES: build("compile-time", false, null),
|
||||
|
||||
@@ -159,6 +159,10 @@ export const runtimeConfigV2ArtifactSchema = z
|
||||
// TechLog Studio adapter selection. Backend is not live yet, so the
|
||||
// default is the in-memory mock; a document may opt a build into HTTP.
|
||||
TECH_LOG_STUDIO_SOURCE: z.enum(["MOCK", "HTTP"]).default("MOCK"),
|
||||
// TechLog public-read adapter selection, defaulted the same way and for
|
||||
// the same reason. Held apart from the Studio switch so one surface can
|
||||
// move to HTTP without dragging the other with it.
|
||||
TECH_LOG_PUBLIC_SOURCE: z.enum(["MOCK", "HTTP"]).default("MOCK"),
|
||||
})
|
||||
.strict()
|
||||
.superRefine(runtimeConfigArtifactInvariants);
|
||||
|
||||
@@ -23,6 +23,7 @@ export function createInstalledFeatureInputs(
|
||||
context: Parameters<typeof createReferenceFeatureInstalledInput>[0] &
|
||||
Readonly<{
|
||||
studioSource: "MOCK" | "HTTP";
|
||||
publicSource: "MOCK" | "HTTP";
|
||||
apiBaseUrl: string;
|
||||
requestTimeoutMs: number;
|
||||
csrf: CsrfTokenProvider;
|
||||
|
||||
@@ -27,6 +27,7 @@ import { publicContentQueries } from "./static/public-query.ts";
|
||||
*/
|
||||
export type TechLogInstallContext = Readonly<{
|
||||
studioSource: "MOCK" | "HTTP";
|
||||
publicSource: "MOCK" | "HTTP";
|
||||
contractOperations: StudioOperationExecutor;
|
||||
apiBaseUrl: string;
|
||||
requestTimeoutMs: number;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"packageId": "@tech-log/public-contract",
|
||||
"version": "2.0.0",
|
||||
"digest": "sha256:8ac71425b38658f34641102b4c2e6e21288c811efebdb92c0a46fb9d4790e23e",
|
||||
"sourceRevision": "55a9599",
|
||||
"operationIds": [
|
||||
"getPublicSite",
|
||||
"getPublicHome",
|
||||
"exploreKnowledge",
|
||||
"exploreQuestions",
|
||||
"listPublicTopics",
|
||||
"getPublicTopic",
|
||||
"getPublicCase",
|
||||
"getPublicReference",
|
||||
"getPublicQuestion",
|
||||
"listPublicProjects",
|
||||
"getPublicProject",
|
||||
"listPublicProjectDecisions",
|
||||
"listPublicProjectRecords",
|
||||
"listPublicProjectActivities",
|
||||
"listPublicReleases",
|
||||
"getPublicRelease",
|
||||
"getPublicProfile",
|
||||
"searchPublicResources"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"packageId": "@tech-log/studio-contract",
|
||||
"version": "3.0.0",
|
||||
"digest": "sha256:6cae9924403d0761f401643a022980b8e04183eea0d890c143c9fbbbbc7431e4",
|
||||
"sourceRevision": "b20d7a2",
|
||||
"sourceRevision": "55a9599",
|
||||
"operationIds": [
|
||||
"getStudioSession",
|
||||
"getStudioDashboard",
|
||||
|
||||
Reference in New Issue
Block a user