Implement sync-business-contract.js wrapper
Implement sync-business-contract.js wrapper
Section titled “Implement sync-business-contract.js wrapper”Context
Section titled “Context”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.
Decision
Section titled “Decision”Implement tktspace-business/tools/gen/sync-business-contract.js as a
~80-line plain-Node.js orchestrator with three steps:
- Fetch canonical — read
_workflow/contracts/business.openapi.yamlvia theyamlpackage (v2), parse, write JSON totools/gen/swagger-api.json. - Apply patches — auto-discover
patch-*.jsfiles viafs.readdirSyncfiltered by/^patch-[^.]+\.js$/(no dotted segments before.js→ excludes*.spec.js/*.test.js), sort alphabetically, shell out to each viachild_process.execFileSync('node', [absPath], { stdio: 'inherit', cwd: repoRoot }). Hard-fail on first throw — no try/catch. - Codegen —
npx ng-openapi-gen -c tools/gen/api-gateway.jsonwithstdio: '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.
Considered alternatives
Section titled “Considered alternatives”- 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 andexecFileSyncexit 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 buildfor 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:contractsis the only place that reaches HTTP.
Design choices (with the WHY)
Section titled “Design choices (with the WHY)”- Plain Node, no TypeScript. Stays consistent with existing
patch-*.js. No transpile step. Smaller blast radius. yamlv2 package as explicitdevDependency. NOT currently installed —pnpm-lock.yamllistsyaml@^2.4.2only as an optional peer ofpostcss-load-config, no snapshot is resolved. AC-12 is load-bearing: the implementer MUSTnpm install --save-dev yaml. v2 API:yaml.parse(string)returns a plain JS value (correct for one-way YAML → JS object → JSON pipeline;parseDocumentis 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— sopatch-spheres-swagger.jsmatches, but a hypotheticalpatch-foo.spec.js/patch-foo.test.jsWOULD NOT (defends against accidental test-file execution as production code). Todaytools/gen/has no*.spec.js/*.test.jsfiles; 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 buildchain. No prettier post-step. No git-stash dance. Wrapper does fetch → patch → codegen, nothing else. - Keep
generate:apibare. Muscle memory + any CI scripts that call it continue to work. Use case: tweakingapi-gateway.jsonand 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 ofcwd.execSynccalls passcwd: repoRootexplicitly so patch scripts andng-openapi-genresolve 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, soswagger-api.jsonis untouched. IfwriteFileSyncitself 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.
File layout
Section titled “File layout”- 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"toscripts. - Add
"yaml": "^2.x"todevDependencies. - Leave
generate:apiunchanged.
- Add
- Edit.
tktspace-business/tools/gen/patch-spheres-swagger.js:18— replaceRun before ng-openapi-gen via npm run generate:api.withRun automatically via tools/gen/sync-business-contract.js before ng-openapi-gen.(matching the existing wording inpatch-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:apiwithtktspace-business → npm run sync:contractsand note the wrapper chains canonical-fetch + patches + codegen. - “API contracts” section: add one-liner about the web↔business
asymmetry —
tktspace-webconsumes the YAML directly vianpm run generate, whereastktspace-businessmust run the wrapper becausetools/gen/patch-*.jsscripts apply local enum/DTO patches between canonical and codegen.
- “Contract change rules” §4 (line 54): replace
- 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.
Wrapper script — reference shape
Section titled “Wrapper script — reference shape”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 siblingif (!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 -> JSONconst 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 spacesfor (const script of readdirSync(toolsGenDir) .filter((f) => /^patch-[^.]+\.js$/.test(f)) .sort()) { execFileSync('node', [join(toolsGenDir, script)], { stdio: 'inherit', cwd: repoRoot });}
// 4. codegenexecFileSync('npx', ['ng-openapi-gen', '-c', 'tools/gen/api-gateway.json'], { stdio: 'inherit', cwd: repoRoot });Risks / edge cases
Section titled “Risks / edge cases”- YAML parse failure mid-pipeline — throws before
writeFileSync, existingswagger-api.jsonis 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.jsonis 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 ofcwd. Safe. _workflow/not a sibling oftktspace-business/— explicitexistsSync(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 usesexecFileSync('node', [absPath], ...)andexecFileSync('npx', ['ng-openapi-gen', ...], ...)instead ofexecSyncwith 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.
Testing approach
Section titled “Testing approach”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.
Rollout / migration
Section titled “Rollout / migration”- Pure tooling addition. No deploy, no contract change, no runtime behaviour change.
- Developer muscle memory shift:
npm run generate:api→npm run sync:contractsfor the canonical-driven flow.generate:apiretained 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).
Open follow-ups
Section titled “Open follow-ups”- (Polish, separate ticket) Pin
ng-openapi-genversion + add a prettier post-step to suppress mechanical formatter drift in regen diffs. Surfaced as reviewer feedback during869dpa3zp. Out of scope here. - (Architecture, deferred) Migrate
tools/gen/swagger-api.jsonout of the repo (regen-on-build). Tracked separately — intentionally deferred per spec non-goals.
STATUS: READY_FOR_REVIEW