Skip to content

Implement sync-business-contract.js wrapper

Implement sync-business-contract.js wrapper

Section titled “Implement sync-business-contract.js wrapper”

tktspace-business regenerates its typed Angular client from _workflow/contracts/business.openapi.yaml via ng-openapi-gen, but the orchestration is half-built. package.json:19 ships only generate:api (bare codegen), and tools/gen/patch-*.js files (which mutate swagger-api.json to add ~40 sphere-aware DTOs, customer-pass adjust fixes, covered-extras templates, etc.) are NOT wired into any script. patch-passes-swagger.js:16 already references a wrapper that does not exist. Three prior tickets (refundable #17, X-Target-App 869dpa32y, DINING cleanup 869dpa3zp) repeated brittle manual hand- edits of swagger-api.json. The next ticket in the lock chain (869dpxbj6 — activity-types collapse + hasSeatSelection) will compound that pain. This ADR formalises the wrapper before it lands.

Implement tktspace-business/tools/gen/sync-business-contract.js as a ~80-line plain-Node.js orchestrator with three steps:

  1. Fetch canonical — read _workflow/contracts/business.openapi.yaml via the yaml package (v2), parse, write JSON to tools/gen/swagger-api.json.
  2. Apply patches — auto-discover patch-*.js files via fs.readdirSync filtered by /^patch-[^.]+\.js$/ (no dotted segments before .js → excludes *.spec.js / *.test.js), sort alphabetically, shell out to each via child_process.execFileSync('node', [absPath], { stdio: 'inherit', cwd: repoRoot }). Hard-fail on first throw — no try/catch.
  3. Codegennpx ng-openapi-gen -c tools/gen/api-gateway.json with stdio: 'inherit'.

Wire as "sync:contracts": "node tools/gen/sync-business-contract.js" in package.json. Keep the bare generate:api script unchanged for back-compat.

  • TypeScript wrapper compiled via ts-node. Rejected. tools/gen/ is plain JS today (patch scripts are .js); adding a transpile step buys nothing for an 80-line orchestrator and slows the inner dev loop. Type safety isn’t load-bearing — the script’s I/O is files and execFileSync exit codes.
  • Manual ordering via an explicit array. Rejected. Locks the wrapper to today’s two patches; every new patch needs a wrapper edit (violates US-2). Alphabetical auto-discover with patch-NN-* priority-prefix escape hatch (e.g. patch-01-spheres.js) keeps ordering deterministic without coupling.
  • Auto-chain npm run build for smoke. Rejected per spec non-goals. Wrapper stays single-purpose; smoke is a developer responsibility (or a separate CI step).
  • Fetch from running backend (HTTP) instead of canonical YAML. Rejected. Two-layer caching (backend → canonical YAML → business swagger-api.json) is intentional architecture — see contract-snapshot ADR D9. Backend’s own sync:contracts is the only place that reaches HTTP.
  • Plain Node, no TypeScript. Stays consistent with existing patch-*.js. No transpile step. Smaller blast radius.
  • yaml v2 package as explicit devDependency. NOT currently installed — pnpm-lock.yaml lists yaml@^2.4.2 only as an optional peer of postcss-load-config, no snapshot is resolved. AC-12 is load-bearing: the implementer MUST npm install --save-dev yaml. v2 API: yaml.parse(string) returns a plain JS value (correct for one-way YAML → JS object → JSON pipeline; parseDocument is for round-tripping with comment/order preservation, not needed here).
  • Glob-style auto-discover (/^patch-[^.]+\.js$/). Future patches drop in without script edits. The [^.]+ segment matches patch filenames with no dots before .js — so patch-spheres-swagger.js matches, but a hypothetical patch-foo.spec.js / patch-foo.test.js WOULD NOT (defends against accidental test-file execution as production code). Today tools/gen/ has no *.spec.js / *.test.js files; the regex is a forward guard. Alphabetical sort gives stable order; priority is encoded via filename (patch-01-foo.js, patch-02-bar.js) if a future patch needs to run first.
  • stdio: 'inherit'. Streams patch + codegen output to the developer’s terminal. Failures are visible inline.
  • Hard-fail on first error. No try/catch wrapping. If a patch throws, the wrapper exits non-zero and later patches do not run. Easier to debug than silent fallthrough; matches AC-3.
  • Single-purpose. No npm run build chain. No prettier post-step. No git-stash dance. Wrapper does fetch → patch → codegen, nothing else.
  • Keep generate:api bare. Muscle memory + any CI scripts that call it continue to work. Use case: tweaking api-gateway.json and re-emitting without re-fetching canonical.
  • Idempotent (AC-2 + AC-10). Same canonical YAML + deterministic JSON.stringify(obj, null, 2) + deterministic patches (existing code, in-place mutations are pure functions of the input) + deterministic codegen → byte-for-byte stable output.
  • Path anchoring via path.resolve(__dirname, ...). Wrapper works regardless of cwd. execSync calls pass cwd: repoRoot explicitly so patch scripts and ng-openapi-gen resolve relative paths from a known anchor.
  • Atomicity is best-effort. Step 1 does a direct writeFileSync (not a tmp+rename). If YAML parse throws, the throw is before the write, so swagger-api.json is untouched. If writeFileSync itself fails mid-write, the file may be partial — but this is the same risk level as any of the existing patch scripts and is acceptable for a developer-local tool.
  • NEW. tktspace-business/tools/gen/sync-business-contract.js (~80 lines, plain Node.js, shape per spec §Wrapper script).
  • Edit. tktspace-business/package.json:
    • Add "sync:contracts": "node tools/gen/sync-business-contract.js" to scripts.
    • Add "yaml": "^2.x" to devDependencies.
    • Leave generate:api unchanged.
  • Edit. tktspace-business/tools/gen/patch-spheres-swagger.js:18 — replace Run before ng-openapi-gen via npm run generate:api. with Run automatically via tools/gen/sync-business-contract.js before ng-openapi-gen. (matching the existing wording in patch-passes-swagger.js:16, verified at line 16 today).
  • Edit. _workflow/CLAUDE.md:
    • “Contract change rules” §4 (line 54): replace tktspace-business → npm run generate:api with tktspace-business → npm run sync:contracts and note the wrapper chains canonical-fetch + patches + codegen.
    • “API contracts” section: add one-liner about the web↔business asymmetry — tktspace-web consumes the YAML directly via npm run generate, whereas tktspace-business must run the wrapper because tools/gen/patch-*.js scripts apply local enum/DTO patches between canonical and codegen.
  • Delete. tktspace-business/tools/gen/business.openapi.json (stale, dated 2026-05-17, predates canonical convention).
  • Delete. tktspace-business/tools/gen/business.openapi.yaml (stale, same provenance). Verified unreferenced via grep — see AC-11 + spec verification findings.

Use the snippet from spec §“Wrapper script — illustrative shape” verbatim. Key invariants:

const { existsSync } = require('fs');
const { execFileSync } = require('child_process');
const toolsGenDir = __dirname;
const repoRoot = resolve(toolsGenDir, '..', '..');
const canonicalPath = resolve(
repoRoot, '..', '_workflow', 'contracts', 'business.openapi.yaml'
);
const swaggerJsonPath = join(toolsGenDir, 'swagger-api.json');
// Layout precheck — surface a crisp error if _workflow isn't a sibling
if (!existsSync(canonicalPath)) {
console.error(
`[sync-business-contract] canonical contract not found at ${canonicalPath}\n` +
` This wrapper assumes _workflow/ is a sibling of tktspace-business/.\n` +
` See _workflow/CLAUDE.md → "Local setup from scratch" for the layout.`
);
process.exit(1);
}
// 1+2. canonical YAML -> JSON
const canonical = yaml.parse(readFileSync(canonicalPath, 'utf8'));
writeFileSync(swaggerJsonPath, JSON.stringify(canonical, null, 2) + '\n');
// 3. patches — execFileSync (NOT execSync) to avoid shell quoting on paths with spaces
for (const script of readdirSync(toolsGenDir)
.filter((f) => /^patch-[^.]+\.js$/.test(f))
.sort()) {
execFileSync('node', [join(toolsGenDir, script)], { stdio: 'inherit', cwd: repoRoot });
}
// 4. codegen
execFileSync('npx', ['ng-openapi-gen', '-c', 'tools/gen/api-gateway.json'],
{ stdio: 'inherit', cwd: repoRoot });
  • YAML parse failure mid-pipeline — throws before writeFileSync, existing swagger-api.json is untouched. Re-run after fixing canonical.
  • Patch script throws — wrapper exits non-zero, later patches skipped. Developer sees the error inline via stdio: 'inherit'. Re-run after fix.
  • ng-openapi-gen failure — propagates non-zero. swagger-api.json is already the new canonical; codegen output (src/api/) may be partial. Developer re-runs after fixing.
  • Wrapper invoked outside business repo root — anchored via path.resolve(__dirname, ...), so absolute paths are independent of cwd. Safe.
  • _workflow/ not a sibling of tktspace-business/ — explicit existsSync(canonicalPath) precheck at the top of the wrapper exits with a CRISP error pointing at the documented layout convention (_workflow/CLAUDE.md “Local setup from scratch”). No mysterious ENOENT later in the pipeline.
  • Paths with spaces in user’s clone (e.g., /Users/Some User/...) — wrapper uses execFileSync('node', [absPath], ...) and execFileSync('npx', ['ng-openapi-gen', ...], ...) instead of execSync with interpolated shell strings. No quoting hazard.
  • Patch ordering becomes order-dependent in the future — escape hatch is the patch-NN-* naming convention (alphabetical sort naturally honours numeric prefixes). No wrapper change needed.

Pure tooling. No unit tests. Phase B writes a single failing smoke that shells out to the wrapper from a small Node/Jest spec (or a bash smoke), asserting the resulting tools/gen/swagger-api.json:

  • Contains the canonical schemas (spot-check ActivityResponseDto, CompanyResponseDto).
  • Contains patched schemas after patches apply (spot-check SphereAdminDto, CategoryAdminDto, CoveredExtraTemplateDto).
  • Has the expected enum members: SphereTargetApp: [GYM_APP, TICKETS_APP, SERVICES_APP], ActivityTypeCode: [SHOW, MOVIE, SLOT_BASED, SERVICE], SphereCode: [SPORT, CINEMA, SHOWS, SERVICES] (verified present in canonical YAML at line 2228+).

Phase C makes the smoke pass. Manual smokes (idempotency, auto-discover with a temp patch-zzz-noop.js, npm run build green) follow the spec §Test plan.

  • Pure tooling addition. No deploy, no contract change, no runtime behaviour change.
  • Developer muscle memory shift: npm run generate:apinpm run sync:contracts for the canonical-driven flow. generate:api retained for the niche “tweak api-gateway.json and re-emit without re-fetching” use case.
  • Stale business.openapi.{json,yaml} deletion is the only file removal; both files are unreferenced (verified, see AC-11).
  • (Polish, separate ticket) Pin ng-openapi-gen version + add a prettier post-step to suppress mechanical formatter drift in regen diffs. Surfaced as reviewer feedback during 869dpa3zp. Out of scope here.
  • (Architecture, deferred) Migrate tools/gen/swagger-api.json out of the repo (regen-on-build). Tracked separately — intentionally deferred per spec non-goals.

STATUS: READY_FOR_REVIEW