@llm-dev-ops/agentics-cli 2.3.0 → 2.4.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.
Files changed (53) hide show
  1. package/dist/pipeline/auto-chain.d.ts +117 -0
  2. package/dist/pipeline/auto-chain.d.ts.map +1 -1
  3. package/dist/pipeline/auto-chain.js +1047 -35
  4. package/dist/pipeline/auto-chain.js.map +1 -1
  5. package/dist/pipeline/phase2/phases/prompt-generator.d.ts.map +1 -1
  6. package/dist/pipeline/phase2/phases/prompt-generator.js +152 -6
  7. package/dist/pipeline/phase2/phases/prompt-generator.js.map +1 -1
  8. package/dist/pipeline/phase4-5-pre-render/financial-model.d.ts +51 -0
  9. package/dist/pipeline/phase4-5-pre-render/financial-model.d.ts.map +1 -0
  10. package/dist/pipeline/phase4-5-pre-render/financial-model.js +118 -0
  11. package/dist/pipeline/phase4-5-pre-render/financial-model.js.map +1 -0
  12. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts +53 -0
  13. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts.map +1 -0
  14. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js +130 -0
  15. package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js.map +1 -0
  16. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts +47 -0
  17. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts.map +1 -0
  18. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js +105 -0
  19. package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js.map +1 -0
  20. package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts +42 -0
  21. package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts.map +1 -0
  22. package/dist/pipeline/phase4-5-pre-render/sector-baselines.js +117 -0
  23. package/dist/pipeline/phase4-5-pre-render/sector-baselines.js.map +1 -0
  24. package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts.map +1 -1
  25. package/dist/pipeline/phase5-build/phases/post-generation-validator.js +341 -1
  26. package/dist/pipeline/phase5-build/phases/post-generation-validator.js.map +1 -1
  27. package/dist/pipeline/types.d.ts +4 -1
  28. package/dist/pipeline/types.d.ts.map +1 -1
  29. package/dist/pipeline/types.js +9 -1
  30. package/dist/pipeline/types.js.map +1 -1
  31. package/dist/synthesis/domain-unit-registry.d.ts +1 -1
  32. package/dist/synthesis/domain-unit-registry.d.ts.map +1 -1
  33. package/dist/synthesis/domain-unit-registry.js +26 -0
  34. package/dist/synthesis/domain-unit-registry.js.map +1 -1
  35. package/dist/synthesis/financial-claim-extractor.d.ts +20 -0
  36. package/dist/synthesis/financial-claim-extractor.d.ts.map +1 -1
  37. package/dist/synthesis/financial-claim-extractor.js +31 -0
  38. package/dist/synthesis/financial-claim-extractor.js.map +1 -1
  39. package/dist/synthesis/financial-consistency-rules.d.ts +4 -0
  40. package/dist/synthesis/financial-consistency-rules.d.ts.map +1 -1
  41. package/dist/synthesis/financial-consistency-rules.js +51 -0
  42. package/dist/synthesis/financial-consistency-rules.js.map +1 -1
  43. package/dist/synthesis/roadmap-dates.d.ts +72 -0
  44. package/dist/synthesis/roadmap-dates.d.ts.map +1 -0
  45. package/dist/synthesis/roadmap-dates.js +203 -0
  46. package/dist/synthesis/roadmap-dates.js.map +1 -0
  47. package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
  48. package/dist/synthesis/simulation-artifact-generator.js +46 -0
  49. package/dist/synthesis/simulation-artifact-generator.js.map +1 -1
  50. package/dist/synthesis/simulation-renderers.d.ts.map +1 -1
  51. package/dist/synthesis/simulation-renderers.js +139 -34
  52. package/dist/synthesis/simulation-renderers.js.map +1 -1
  53. package/package.json +1 -1
@@ -87,6 +87,133 @@ function copyDirRecursive(src, dest) {
87
87
  * Called after every phase so even if later phases fail, earlier artifacts
88
88
  * are still available.
89
89
  */
90
+ /**
91
+ * ADR-PIPELINE-074: scaffold body for `src/logger.ts`.
92
+ *
93
+ * Uses `node:async_hooks` AsyncLocalStorage so concurrent Express/Hono
94
+ * requests NEVER cross-contaminate log lines. The legacy
95
+ * `let currentCorrelationId` module-level mutable has been removed; any
96
+ * generator that reintroduces it is caught by PGV-016.
97
+ *
98
+ * Exports:
99
+ * - `Logger` interface with info/warn/error/child
100
+ * - `createLogger(service)` factory
101
+ * - `runWithCorrelation(id, fn)` — wraps an async scope with the ID
102
+ * - `getCorrelationId()` — returns the ID for the current async scope
103
+ * - `setCorrelationId(id)` — legacy shim; mutates the current store
104
+ */
105
+ export const LOGGER_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-074)
106
+ // Correlation IDs flow through an AsyncLocalStorage so concurrent requests
107
+ // never cross-contaminate log lines. Import runWithCorrelation + createLogger
108
+ // from here; NEVER reach for a module-level mutable to store the ID.
109
+ import { AsyncLocalStorage } from 'node:async_hooks';
110
+
111
+ export interface Logger {
112
+ info(event: string, data?: Record<string, unknown>): void;
113
+ warn(event: string, data?: Record<string, unknown>): void;
114
+ error(event: string, error: Error, data?: Record<string, unknown>): void;
115
+ child(bindings: Record<string, unknown>): Logger;
116
+ }
117
+
118
+ interface LoggerContext {
119
+ correlationId?: string;
120
+ bindings: Record<string, unknown>;
121
+ }
122
+
123
+ const storage = new AsyncLocalStorage<LoggerContext>();
124
+
125
+ /**
126
+ * Run \`fn\` with \`correlationId\` installed in the current async scope.
127
+ * Every log line emitted inside \`fn\` and its async descendants will carry
128
+ * this ID, and it will NOT leak to other concurrent requests.
129
+ */
130
+ export function runWithCorrelation<T>(correlationId: string, fn: () => T): T {
131
+ const parent = storage.getStore();
132
+ const ctx: LoggerContext = {
133
+ correlationId,
134
+ bindings: parent?.bindings ?? {},
135
+ };
136
+ return storage.run(ctx, fn);
137
+ }
138
+
139
+ /** Returns the correlation ID for the current async scope, if any. */
140
+ export function getCorrelationId(): string | undefined {
141
+ return storage.getStore()?.correlationId;
142
+ }
143
+
144
+ /**
145
+ * Back-compat shim — deprecated. Retained for legacy callers, but
146
+ * SHOULD NOT be used inside request handlers. Use runWithCorrelation.
147
+ * Under AsyncLocalStorage, this mutates the current store in place;
148
+ * outside an async scope it is a silent no-op.
149
+ */
150
+ export function setCorrelationId(id: string | undefined): void {
151
+ const ctx = storage.getStore();
152
+ if (ctx) ctx.correlationId = id;
153
+ }
154
+
155
+ export function createLogger(service: string): Logger {
156
+ const build = (bindings: Record<string, unknown>): Logger => {
157
+ const emit = (level: string, event: string, extra?: Record<string, unknown>): void => {
158
+ const ctx = storage.getStore();
159
+ const entry = {
160
+ timestamp: new Date().toISOString(),
161
+ level,
162
+ service,
163
+ event,
164
+ ...(ctx?.correlationId ? { correlationId: ctx.correlationId } : {}),
165
+ ...bindings,
166
+ ...(ctx?.bindings ?? {}),
167
+ ...extra,
168
+ };
169
+ process.stderr.write(JSON.stringify(entry) + '\\n');
170
+ };
171
+ return {
172
+ info: (event, data) => emit('info', event, data),
173
+ warn: (event, data) => emit('warn', event, data),
174
+ error: (event, err, data) => emit('error', event, {
175
+ error: err.message,
176
+ stack: err.stack,
177
+ ...data,
178
+ }),
179
+ child: (childBindings) => build({ ...bindings, ...childBindings }),
180
+ };
181
+ };
182
+ return build({});
183
+ }
184
+ `;
185
+ /**
186
+ * ADR-PIPELINE-074: concurrency-correctness test emitted alongside
187
+ * `src/logger.ts` in every generated project. Fails loudly if
188
+ * AsyncLocalStorage wiring regresses.
189
+ */
190
+ export const LOGGER_CONCURRENCY_TEST_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-074)
191
+ // Proves correlation IDs do NOT leak across concurrent async tasks.
192
+ // Works with vitest and jest — both expose describe/it/expect as globals.
193
+ /* eslint-disable @typescript-eslint/no-explicit-any */
194
+ import { describe, it, expect } from 'vitest';
195
+ import { runWithCorrelation, getCorrelationId } from './logger.js';
196
+
197
+ describe('logger correlation-id async scoping (ADR-PIPELINE-074)', () => {
198
+ it('does not leak correlation IDs across concurrent async tasks', async () => {
199
+ const results: Array<{ requested: string; observed: string | undefined }> = [];
200
+ const tasks: Promise<void>[] = [];
201
+ for (let i = 0; i < 50; i++) {
202
+ const id = \`req-\${i}\`;
203
+ tasks.push(
204
+ runWithCorrelation(id, async () => {
205
+ await new Promise((r) => setTimeout(r, Math.random() * 10));
206
+ results.push({ requested: id, observed: getCorrelationId() });
207
+ }),
208
+ );
209
+ }
210
+ await Promise.all(tasks);
211
+ for (const r of results) {
212
+ expect(r.observed).toBe(r.requested);
213
+ }
214
+ });
215
+ });
216
+ `;
90
217
  /**
91
218
  * ADR-PIPELINE-068: scaffold body for `src/simulation-lineage.ts`.
92
219
  *
@@ -289,10 +416,796 @@ export class CircuitBreaker {
289
416
  getState(): string { return this.state; }
290
417
  }
291
418
  `;
419
+ /**
420
+ * ADR-PIPELINE-075: scaffold body for `src/persistence/repository.ts`.
421
+ *
422
+ * Defines the `Repository<T>` interface every stateful service should
423
+ * depend on. Each entity is identified by `idOf(entity)`, validated by a
424
+ * Zod schema on read, and bound to a table name for the SQLite backend.
425
+ */
426
+ export const REPOSITORY_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-075)
427
+ // Repository interface every stateful service should depend on.
428
+ // Generators: use this instead of storing state in a Map<string, T>.
429
+ import type { ZodType } from 'zod';
430
+
431
+ export interface Repository<T> {
432
+ save(entity: T): Promise<void>;
433
+ get(id: string): Promise<T | null>;
434
+ list(filter?: Partial<T>): Promise<readonly T[]>;
435
+ delete(id: string): Promise<void>;
436
+ }
437
+
438
+ export interface RepositoryOptions<T> {
439
+ /** How to extract the primary key from an entity. */
440
+ readonly idOf: (entity: T) => string;
441
+ /** Zod schema used to re-validate rows on load (SQLite JSON columns are untrusted). */
442
+ readonly schema: ZodType<T>;
443
+ /** Table name for SQLite backend. Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/. */
444
+ readonly tableName: string;
445
+ }
446
+
447
+ /** Errors thrown by all Repository implementations. */
448
+ export class RepositoryError extends Error {
449
+ constructor(message: string, public readonly code: string) {
450
+ super(\`\${code}: \${message}\`);
451
+ this.name = 'RepositoryError';
452
+ }
453
+ }
454
+ `;
455
+ /**
456
+ * ADR-PIPELINE-075: scaffold body for `src/persistence/in-memory-repository.ts`.
457
+ *
458
+ * Test-friendly `Repository<T>` implementation backed by a Map. Validates
459
+ * on write AND read so test fixtures catch schema drift. Production
460
+ * services should swap in SqliteRepository via the composition root.
461
+ */
462
+ export const IN_MEMORY_REPO_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-075)
463
+ // In-memory Repository<T> for tests. Not for production use.
464
+ import type { Repository, RepositoryOptions } from './repository.js';
465
+
466
+ export class InMemoryRepository<T> implements Repository<T> {
467
+ private readonly rows = new Map<string, T>();
468
+ constructor(private readonly options: RepositoryOptions<T>) {}
469
+
470
+ async save(entity: T): Promise<void> {
471
+ const validated = this.options.schema.parse(entity);
472
+ this.rows.set(this.options.idOf(validated), validated);
473
+ }
474
+
475
+ async get(id: string): Promise<T | null> {
476
+ const row = this.rows.get(id);
477
+ return row === undefined ? null : row;
478
+ }
479
+
480
+ async list(filter?: Partial<T>): Promise<readonly T[]> {
481
+ const all = Array.from(this.rows.values());
482
+ if (!filter) return all;
483
+ return all.filter(row =>
484
+ Object.entries(filter).every(
485
+ ([k, v]) => (row as Record<string, unknown>)[k] === v,
486
+ ),
487
+ );
488
+ }
489
+
490
+ async delete(id: string): Promise<void> {
491
+ this.rows.delete(id);
492
+ }
493
+
494
+ /** Test helper — never call from production code. */
495
+ clear(): void {
496
+ this.rows.clear();
497
+ }
498
+
499
+ /** Test helper — returns the current row count. */
500
+ get size(): number {
501
+ return this.rows.size;
502
+ }
503
+ }
504
+ `;
505
+ /**
506
+ * ADR-PIPELINE-075: scaffold body for `src/persistence/sqlite-repository.ts`.
507
+ *
508
+ * Production `Repository<T>` backed by better-sqlite3. Stores each entity
509
+ * as a JSON column keyed by its primary key. Zod re-validates on read
510
+ * because the JSON column is untrusted at runtime.
511
+ *
512
+ * Generated projects must add better-sqlite3 + @types/better-sqlite3 to
513
+ * their package.json — the cross-cutting prompt footer reminds the coding
514
+ * agent to include these deps.
515
+ */
516
+ export const SQLITE_REPO_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-075)
517
+ // SQLite-backed Repository<T> using better-sqlite3.
518
+ // Requires: better-sqlite3 in dependencies, @types/better-sqlite3 in devDependencies.
519
+ /* eslint-disable @typescript-eslint/no-explicit-any */
520
+ import type { Repository, RepositoryOptions } from './repository.js';
521
+ import { RepositoryError } from './repository.js';
522
+
523
+ // Minimal structural type — works with better-sqlite3 without importing it
524
+ // into the scaffold (so the scaffold compiles even when the dependency
525
+ // isn't installed in the pipeline's own test environment).
526
+ export interface BetterSqliteDatabase {
527
+ exec(sql: string): void;
528
+ prepare(sql: string): {
529
+ run(...params: any[]): { changes: number; lastInsertRowid: number | bigint };
530
+ get(...params: any[]): any;
531
+ all(...params: any[]): any[];
532
+ };
533
+ }
534
+
535
+ export interface SqliteRepositoryDeps {
536
+ readonly db: BetterSqliteDatabase;
537
+ }
538
+
539
+ export class SqliteRepository<T> implements Repository<T> {
540
+ constructor(
541
+ private readonly deps: SqliteRepositoryDeps,
542
+ private readonly options: RepositoryOptions<T>,
543
+ ) {
544
+ this.assertSafeTableName();
545
+ this.deps.db.exec(\`
546
+ CREATE TABLE IF NOT EXISTS \${this.options.tableName} (
547
+ id TEXT PRIMARY KEY,
548
+ data TEXT NOT NULL,
549
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
550
+ );
551
+ \`);
552
+ }
553
+
554
+ private assertSafeTableName(): void {
555
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(this.options.tableName)) {
556
+ throw new RepositoryError(\`unsafe table name: \${this.options.tableName}\`, 'ECLI-REPO-001');
557
+ }
558
+ }
559
+
560
+ async save(entity: T): Promise<void> {
561
+ const validated = this.options.schema.parse(entity);
562
+ const id = this.options.idOf(validated);
563
+ const data = JSON.stringify(validated);
564
+ this.deps.db
565
+ .prepare(
566
+ \`INSERT INTO \${this.options.tableName} (id, data) VALUES (?, ?)
567
+ ON CONFLICT(id) DO UPDATE SET data = excluded.data,
568
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')\`,
569
+ )
570
+ .run(id, data);
571
+ }
572
+
573
+ async get(id: string): Promise<T | null> {
574
+ const row = this.deps.db
575
+ .prepare(\`SELECT data FROM \${this.options.tableName} WHERE id = ?\`)
576
+ .get(id) as { data: string } | undefined;
577
+ if (!row) return null;
578
+ return this.options.schema.parse(JSON.parse(row.data));
579
+ }
580
+
581
+ async list(filter?: Partial<T>): Promise<readonly T[]> {
582
+ const rows = this.deps.db
583
+ .prepare(\`SELECT data FROM \${this.options.tableName}\`)
584
+ .all() as Array<{ data: string }>;
585
+ const parsed = rows.map(r => this.options.schema.parse(JSON.parse(r.data)));
586
+ if (!filter) return parsed;
587
+ return parsed.filter(row =>
588
+ Object.entries(filter).every(
589
+ ([k, v]) => (row as Record<string, unknown>)[k] === v,
590
+ ),
591
+ );
592
+ }
593
+
594
+ async delete(id: string): Promise<void> {
595
+ this.deps.db.prepare(\`DELETE FROM \${this.options.tableName} WHERE id = ?\`).run(id);
596
+ }
597
+ }
598
+ `;
599
+ /**
600
+ * ADR-PIPELINE-075: scaffold body for `src/persistence/audit-repository.ts`.
601
+ *
602
+ * Append-only audit repository. NO update, NO delete — an audit entry is
603
+ * committed and lives forever. Services that need to mutate a past entry
604
+ * have to bypass the repository and talk raw SQL, which PGV and code
605
+ * review will catch.
606
+ */
607
+ export const AUDIT_REPO_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-075)
608
+ // Append-only audit repository. No update, no delete by design.
609
+ /* eslint-disable @typescript-eslint/no-explicit-any */
610
+ import type { BetterSqliteDatabase } from './sqlite-repository.js';
611
+
612
+ export interface AuditEntry {
613
+ readonly entryId: string;
614
+ readonly prevHash: string;
615
+ readonly hash: string;
616
+ readonly payload: Record<string, unknown>;
617
+ readonly actor: string;
618
+ readonly action: string;
619
+ readonly entityType: string;
620
+ readonly entityId: string;
621
+ readonly createdAt: string;
622
+ }
623
+
624
+ export interface AuditRepositoryDeps {
625
+ readonly db: BetterSqliteDatabase;
626
+ }
627
+
628
+ export class AppendOnlyAuditRepository {
629
+ constructor(private readonly deps: AuditRepositoryDeps) {
630
+ this.deps.db.exec(\`
631
+ CREATE TABLE IF NOT EXISTS audit_log (
632
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
633
+ entry_id TEXT NOT NULL UNIQUE,
634
+ prev_hash TEXT NOT NULL,
635
+ hash TEXT NOT NULL,
636
+ payload TEXT NOT NULL,
637
+ actor TEXT NOT NULL,
638
+ action TEXT NOT NULL,
639
+ entity_type TEXT NOT NULL,
640
+ entity_id TEXT NOT NULL,
641
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
642
+ );
643
+ CREATE INDEX IF NOT EXISTS idx_audit_log_entity
644
+ ON audit_log(entity_type, entity_id);
645
+ \`);
646
+ }
647
+
648
+ append(entry: AuditEntry): void {
649
+ this.deps.db
650
+ .prepare(
651
+ \`INSERT INTO audit_log
652
+ (entry_id, prev_hash, hash, payload, actor, action, entity_type, entity_id, created_at)
653
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\`,
654
+ )
655
+ .run(
656
+ entry.entryId,
657
+ entry.prevHash,
658
+ entry.hash,
659
+ JSON.stringify(entry.payload),
660
+ entry.actor,
661
+ entry.action,
662
+ entry.entityType,
663
+ entry.entityId,
664
+ entry.createdAt,
665
+ );
666
+ }
667
+
668
+ list(): readonly AuditEntry[] {
669
+ const rows = this.deps.db
670
+ .prepare(\`SELECT * FROM audit_log ORDER BY sequence ASC\`)
671
+ .all() as Array<{
672
+ entry_id: string;
673
+ prev_hash: string;
674
+ hash: string;
675
+ payload: string;
676
+ actor: string;
677
+ action: string;
678
+ entity_type: string;
679
+ entity_id: string;
680
+ created_at: string;
681
+ }>;
682
+ return rows.map(r => ({
683
+ entryId: r.entry_id,
684
+ prevHash: r.prev_hash,
685
+ hash: r.hash,
686
+ payload: JSON.parse(r.payload),
687
+ actor: r.actor,
688
+ action: r.action,
689
+ entityType: r.entity_type,
690
+ entityId: r.entity_id,
691
+ createdAt: r.created_at,
692
+ }));
693
+ }
694
+
695
+ listByEntity(entityType: string, entityId: string): readonly AuditEntry[] {
696
+ const rows = this.deps.db
697
+ .prepare(
698
+ \`SELECT * FROM audit_log
699
+ WHERE entity_type = ? AND entity_id = ?
700
+ ORDER BY sequence ASC\`,
701
+ )
702
+ .all(entityType, entityId) as Array<{
703
+ entry_id: string;
704
+ prev_hash: string;
705
+ hash: string;
706
+ payload: string;
707
+ actor: string;
708
+ action: string;
709
+ entity_type: string;
710
+ entity_id: string;
711
+ created_at: string;
712
+ }>;
713
+ return rows.map(r => ({
714
+ entryId: r.entry_id,
715
+ prevHash: r.prev_hash,
716
+ hash: r.hash,
717
+ payload: JSON.parse(r.payload),
718
+ actor: r.actor,
719
+ action: r.action,
720
+ entityType: r.entity_type,
721
+ entityId: r.entity_id,
722
+ createdAt: r.created_at,
723
+ }));
724
+ }
725
+
726
+ /** Total row count — used by verify() and reconciliation jobs. */
727
+ size(): number {
728
+ const row = this.deps.db
729
+ .prepare('SELECT COUNT(*) AS n FROM audit_log')
730
+ .get() as { n: number };
731
+ return row.n;
732
+ }
733
+ }
734
+ `;
735
+ /**
736
+ * ADR-PIPELINE-078: scaffold body for `src/persistence/canonical-json.ts`.
737
+ *
738
+ * Deterministic JSON serialization for audit hash chains and idempotency
739
+ * keys. Follows JCS (RFC 8785) closely enough for the pipeline's needs:
740
+ * object keys are sorted lexicographically at every depth; arrays preserve
741
+ * order; strings, numbers, booleans, and null use standard JSON encoding.
742
+ *
743
+ * Rejects undefined, BigInt, and circular references. Date objects should
744
+ * be serialized as ISO strings at the boundary BEFORE passing to this
745
+ * function.
746
+ */
747
+ export const CANONICAL_JSON_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-078)
748
+ // Deterministic JSON for audit hash chains. Keys sorted at every depth.
749
+
750
+ export class CanonicalJsonError extends Error {
751
+ constructor(message: string) {
752
+ super(\`ECLI-CJ-078: \${message}\`);
753
+ this.name = 'CanonicalJsonError';
754
+ }
755
+ }
756
+
757
+ /**
758
+ * Canonicalize \`value\` into a deterministic JSON string. Keys are sorted
759
+ * at every level; arrays preserve order; primitives use standard encoding.
760
+ */
761
+ export function canonicalJson(value: unknown): string {
762
+ const visited = new WeakSet<object>();
763
+ return serialize(value, visited);
764
+ }
765
+
766
+ function serialize(value: unknown, visited: WeakSet<object>): string {
767
+ if (value === undefined) {
768
+ throw new CanonicalJsonError('undefined is not canonicalizable — pass null instead');
769
+ }
770
+ if (value === null) return 'null';
771
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
772
+ if (typeof value === 'number') {
773
+ if (!Number.isFinite(value)) {
774
+ throw new CanonicalJsonError(\`non-finite number: \${String(value)}\`);
775
+ }
776
+ return JSON.stringify(value);
777
+ }
778
+ if (typeof value === 'string') {
779
+ return JSON.stringify(value);
780
+ }
781
+ if (typeof value === 'bigint') {
782
+ throw new CanonicalJsonError('BigInt is not canonicalizable — convert to string at the boundary');
783
+ }
784
+ if (Array.isArray(value)) {
785
+ if (visited.has(value)) throw new CanonicalJsonError('circular reference in array');
786
+ visited.add(value);
787
+ const parts = value.map(item => serialize(item, visited));
788
+ visited.delete(value);
789
+ return '[' + parts.join(',') + ']';
790
+ }
791
+ if (typeof value === 'object') {
792
+ if (visited.has(value as object)) throw new CanonicalJsonError('circular reference in object');
793
+ visited.add(value as object);
794
+ const record = value as Record<string, unknown>;
795
+ const keys = Object.keys(record).sort();
796
+ const pairs: string[] = [];
797
+ for (const key of keys) {
798
+ const v = record[key];
799
+ if (v === undefined) continue;
800
+ pairs.push(JSON.stringify(key) + ':' + serialize(v, visited));
801
+ }
802
+ visited.delete(value as object);
803
+ return '{' + pairs.join(',') + '}';
804
+ }
805
+ throw new CanonicalJsonError(\`unsupported value type: \${typeof value}\`);
806
+ }
807
+ `;
808
+ /**
809
+ * ADR-PIPELINE-078: scaffold body for `src/persistence/audit-hash.ts`.
810
+ *
811
+ * Wraps `canonicalJson` with audit-log-specific conventions. Every
812
+ * generated `AuditService.append()` method should call `hashAuditEntry`
813
+ * rather than rolling its own `JSON.stringify + createHash` chain.
814
+ */
815
+ export const AUDIT_HASH_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-078)
816
+ // Audit hash-chain helper. Import hashAuditEntry instead of hand-rolling
817
+ // createHash('sha256').update(JSON.stringify(...)).
818
+ import { createHash } from 'node:crypto';
819
+ import { canonicalJson } from './canonical-json.js';
820
+
821
+ export interface HashChainInput {
822
+ readonly prevHash: string;
823
+ readonly entryId: string;
824
+ readonly actor: string;
825
+ readonly action: string;
826
+ readonly entityType: string;
827
+ readonly entityId: string;
828
+ readonly payload: unknown;
829
+ readonly createdAt: string;
830
+ }
831
+
832
+ /**
833
+ * Deterministically hash an audit entry's content + its link to the
834
+ * previous entry. Payload keys are sorted at every depth so mutations
835
+ * at any nesting level change the hash.
836
+ */
837
+ export function hashAuditEntry(input: HashChainInput): string {
838
+ const body = canonicalJson({
839
+ prevHash: input.prevHash,
840
+ entryId: input.entryId,
841
+ actor: input.actor,
842
+ action: input.action,
843
+ entityType: input.entityType,
844
+ entityId: input.entityId,
845
+ payload: input.payload,
846
+ createdAt: input.createdAt,
847
+ });
848
+ return createHash('sha256').update(body).digest('hex');
849
+ }
850
+ `;
851
+ /**
852
+ * ADR-PIPELINE-077: scaffold body for `src/erp/schema-provenance.ts`.
853
+ *
854
+ * Every generated ERP adapter file (schema.ts, <system>-adapter.ts, etc.)
855
+ * must export an `ERP_SCHEMA_PROVENANCE` constant of type
856
+ * `ErpSchemaProvenance` declaring whether the schema was invented,
857
+ * pulled from a catalog, or validated by an SME. Under
858
+ * `AGENTICS_ERP_STRICT=true`, `assertErpProvenanceOrFail` refuses to
859
+ * start when source='invented'. PGV-020 enforces presence, PGV-021
860
+ * enforces reviewer + catalog_version on validated entries.
861
+ */
862
+ export const ERP_SCHEMA_PROVENANCE_SCAFFOLD = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-077)
863
+ // Provenance tag for generated ERP schemas. MANDATORY on every file
864
+ // under src/erp/ that exports Zod schemas targeting an external ERP.
865
+
866
+ export type ErpProvenanceSource = 'invented' | 'catalog' | 'validated';
867
+
868
+ export interface ErpSchemaProvenance {
869
+ /** Target ERP system — e.g. 'Ramco Aviation', 'Oracle OPERA', 'SAP S/4HANA'. */
870
+ readonly erp_system: string;
871
+ /** Specific module the schema targets — 'Flight Catering Order'. */
872
+ readonly module: string;
873
+ /** Validation stage: invented (no review), catalog (fields match), validated (SME-reviewed). */
874
+ readonly source: ErpProvenanceSource;
875
+ /** Version identifier of the catalog source, when known. */
876
+ readonly catalog_version: string | null;
877
+ /** ISO timestamp of SME review when source='validated'. */
878
+ readonly validated_at: string | null;
879
+ /** Reviewer name + role when source='validated'. */
880
+ readonly reviewer: { name: string; role: string } | null;
881
+ /** Free-text notes visible in startup logs and generated README. */
882
+ readonly notes: string;
883
+ }
884
+
885
+ /**
886
+ * Assert an ERP adapter is allowed to run in the current environment.
887
+ * Under AGENTICS_ERP_STRICT=true, source='invented' throws ECLI-ERP-077
888
+ * and the process exits with code 77.
889
+ */
890
+ export function assertErpProvenanceOrFail(
891
+ provenance: ErpSchemaProvenance,
892
+ env: NodeJS.ProcessEnv = process.env,
893
+ ): void {
894
+ const strict = env['AGENTICS_ERP_STRICT'] === 'true';
895
+ if (strict && provenance.source === 'invented') {
896
+ throw new Error(
897
+ \`ECLI-ERP-077: ERP adapter for \${provenance.erp_system}/\${provenance.module} has source='invented' \` +
898
+ \`and AGENTICS_ERP_STRICT=true. Pilot deployment requires an SME review — set source='validated' \` +
899
+ \`and provide reviewer + catalog_version before enabling strict mode.\`,
900
+ );
901
+ }
902
+ }
903
+
904
+ /** Console-friendly banner for startup logs + demo output. */
905
+ export function formatProvenanceBanner(provenance: ErpSchemaProvenance): string {
906
+ const icon = provenance.source === 'validated' ? '✅' :
907
+ provenance.source === 'catalog' ? '🟡' :
908
+ '⚠️ ';
909
+ const lines: string[] = [
910
+ \`\${icon} ERP SCHEMA PROVENANCE — \${provenance.erp_system} / \${provenance.module}\`,
911
+ \` source: \${provenance.source}\`,
912
+ ];
913
+ if (provenance.catalog_version) lines.push(\` catalog: \${provenance.catalog_version}\`);
914
+ if (provenance.validated_at) lines.push(\` validated: \${provenance.validated_at}\`);
915
+ if (provenance.reviewer) lines.push(\` reviewer: \${provenance.reviewer.name} (\${provenance.reviewer.role})\`);
916
+ if (provenance.source === 'invented') {
917
+ lines.push(
918
+ ' ',
919
+ ' WARNING: This schema has not been validated against a real ERP document catalog.',
920
+ ' Pilot deployment requires an SME review before the first live call.',
921
+ " Set AGENTICS_ERP_STRICT=true in production to block runs until source='validated'.",
922
+ );
923
+ }
924
+ return lines.join('\\n');
925
+ }
926
+
927
+ /** Convenience wrapper — prints the banner to stderr. */
928
+ export function printProvenanceBanner(provenance: ErpSchemaProvenance): void {
929
+ process.stderr.write(formatProvenanceBanner(provenance) + '\\n');
930
+ }
931
+ `;
932
+ /**
933
+ * ADR-PIPELINE-076: scaffold body for `src/api/base-app.ts` (Hono variant).
934
+ *
935
+ * Ships a wire-complete base app so the coding agent never writes
936
+ * middleware/metrics/health plumbing from scratch. Generators extend
937
+ * via `createBaseApp({...}).route('/api/<domain>', yourRouter)` instead
938
+ * of rebuilding.
939
+ *
940
+ * Every generated project that imports `createBaseApp` gets:
941
+ * - correlation middleware (AsyncLocalStorage-scoped, ADR-074)
942
+ * - request logging with duration histogram
943
+ * - GET /health/live (process up check)
944
+ * - GET /health/ready (every readiness check passes)
945
+ * - GET /metrics (Prometheus text, ADR-051)
946
+ * - structured error handler (last)
947
+ */
948
+ export const BASE_APP_SCAFFOLD_HONO = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-076)
949
+ // Wire-complete Hono app base. DO NOT rewrite — extend via
950
+ // \`createBaseApp(deps).route('/api/...', yourRouter)\` from your routes module.
951
+ import { Hono } from 'hono';
952
+ import { randomUUID } from 'node:crypto';
953
+ import { runWithCorrelation, createLogger } from '../logger.js';
954
+ import { incrementCounter, recordHistogram, metricsHandler } from '../middleware.js';
955
+
956
+ export interface HealthCheck {
957
+ readonly name: string;
958
+ readonly check: () => Promise<{ ok: boolean; detail?: string }>;
959
+ }
960
+
961
+ export interface BaseAppDeps {
962
+ readonly serviceName: string;
963
+ readonly version: string;
964
+ readonly readiness: readonly HealthCheck[];
965
+ }
966
+
967
+ export interface BaseAppContextVariables {
968
+ correlationId: string;
969
+ startedAt: number;
970
+ }
971
+
972
+ const baseLogger = createLogger('base-app');
973
+
974
+ /**
975
+ * Build the wire-complete base app.
976
+ *
977
+ * Returned Hono instance already has:
978
+ * - correlation ID middleware (AsyncLocalStorage-scoped)
979
+ * - request logging with duration histogram
980
+ * - GET /health/live process-up check
981
+ * - GET /health/ready runs every registered check
982
+ * - GET /metrics Prometheus text format
983
+ * - structured error handler
984
+ *
985
+ * Add domain routes with \`.route('/api/<domain>', yourRouter)\`.
986
+ */
987
+ export function createBaseApp(
988
+ deps: BaseAppDeps,
989
+ ): Hono<{ Variables: BaseAppContextVariables }> {
990
+ const app = new Hono<{ Variables: BaseAppContextVariables }>();
991
+
992
+ // Correlation ID — MUST be first so every downstream log carries it
993
+ app.use('*', async (c, next) => {
994
+ const id = c.req.header('x-correlation-id') ?? randomUUID();
995
+ c.set('correlationId', id);
996
+ c.set('startedAt', Date.now());
997
+ c.header('X-Correlation-Id', id);
998
+ await runWithCorrelation(id, async () => {
999
+ await next();
1000
+ });
1001
+ });
1002
+
1003
+ // Request logging + metrics
1004
+ app.use('*', async (c, next) => {
1005
+ await next();
1006
+ const durationMs = Date.now() - c.get('startedAt');
1007
+ const status = c.res.status;
1008
+ baseLogger.info('http.request', {
1009
+ method: c.req.method,
1010
+ path: c.req.path,
1011
+ status,
1012
+ durationMs,
1013
+ });
1014
+ incrementCounter('http_requests_total', {
1015
+ method: c.req.method,
1016
+ status: String(status),
1017
+ });
1018
+ recordHistogram('http_request_duration_ms', durationMs, {
1019
+ method: c.req.method,
1020
+ });
1021
+ });
1022
+
1023
+ // Liveness — always returns OK if the process is running
1024
+ app.get('/health/live', (c) => {
1025
+ return c.json({
1026
+ status: 'ok',
1027
+ service: deps.serviceName,
1028
+ version: deps.version,
1029
+ uptime_sec: Math.round(process.uptime()),
1030
+ });
1031
+ });
1032
+
1033
+ // Readiness — runs every registered check
1034
+ app.get('/health/ready', async (c) => {
1035
+ const results: Array<{ name: string; ok: boolean; detail?: string }> = [];
1036
+ for (const check of deps.readiness) {
1037
+ try {
1038
+ const r = await check.check();
1039
+ results.push({ name: check.name, ok: r.ok, ...(r.detail ? { detail: r.detail } : {}) });
1040
+ } catch (err) {
1041
+ results.push({
1042
+ name: check.name,
1043
+ ok: false,
1044
+ detail: err instanceof Error ? err.message : String(err),
1045
+ });
1046
+ }
1047
+ }
1048
+ const allOk = results.every((r) => r.ok);
1049
+ return c.json(
1050
+ { status: allOk ? 'ready' : 'not-ready', checks: results },
1051
+ allOk ? 200 : 503,
1052
+ );
1053
+ });
1054
+
1055
+ // Metrics — Prometheus text format
1056
+ app.get('/metrics', (c) => {
1057
+ return c.text(metricsHandler(), 200, {
1058
+ 'Content-Type': 'text/plain; version=0.0.4',
1059
+ });
1060
+ });
1061
+
1062
+ // Error handler — last, so it catches everything above
1063
+ app.onError((err, c) => {
1064
+ baseLogger.error('http.error', err as Error, {
1065
+ method: c.req.method,
1066
+ path: c.req.path,
1067
+ });
1068
+ const status = (err as { httpStatus?: number }).httpStatus ?? 500;
1069
+ return c.json(
1070
+ {
1071
+ error: {
1072
+ message: err.message,
1073
+ code: (err as { code?: string }).code ?? 'INTERNAL_ERROR',
1074
+ },
1075
+ },
1076
+ status as 500,
1077
+ );
1078
+ });
1079
+
1080
+ return app;
1081
+ }
1082
+ `;
1083
+ /**
1084
+ * ADR-PIPELINE-076: scaffold body for `src/api/base-app.ts` (Express variant).
1085
+ *
1086
+ * Used when the generator detects Express in Phase 4. Same guarantees as
1087
+ * the Hono variant — wires correlation / logging / liveness / readiness
1088
+ * / metrics / error handler in the correct order.
1089
+ */
1090
+ export const BASE_APP_SCAFFOLD_EXPRESS = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-076)
1091
+ // Wire-complete Express app base. DO NOT rewrite — mount your routes
1092
+ // onto the returned \`app\` instance instead.
1093
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1094
+ import express from 'express';
1095
+ import { randomUUID } from 'node:crypto';
1096
+ import { runWithCorrelation, createLogger } from '../logger.js';
1097
+ import { incrementCounter, recordHistogram, metricsHandlerExpress } from '../middleware.js';
1098
+
1099
+ export interface HealthCheck {
1100
+ readonly name: string;
1101
+ readonly check: () => Promise<{ ok: boolean; detail?: string }>;
1102
+ }
1103
+
1104
+ export interface BaseAppDeps {
1105
+ readonly serviceName: string;
1106
+ readonly version: string;
1107
+ readonly readiness: readonly HealthCheck[];
1108
+ }
1109
+
1110
+ const baseLogger = createLogger('base-app');
1111
+
1112
+ export function createBaseApp(deps: BaseAppDeps): express.Express {
1113
+ const app = express();
1114
+ app.use(express.json({ limit: '1mb' }));
1115
+
1116
+ // Correlation ID — AsyncLocalStorage-scoped for the request's
1117
+ // entire async continuation.
1118
+ app.use((req: any, res: any, next: () => void) => {
1119
+ const id = (req.headers['x-correlation-id'] as string) || randomUUID();
1120
+ req.correlationId = id;
1121
+ res.setHeader('X-Correlation-Id', id);
1122
+ runWithCorrelation(id, next);
1123
+ });
1124
+
1125
+ // Request logging + metrics
1126
+ app.use((req: any, res: any, next: () => void) => {
1127
+ const started = Date.now();
1128
+ res.on('finish', () => {
1129
+ const durationMs = Date.now() - started;
1130
+ baseLogger.info('http.request', {
1131
+ method: req.method,
1132
+ path: req.path,
1133
+ status: res.statusCode,
1134
+ durationMs,
1135
+ });
1136
+ incrementCounter('http_requests_total', {
1137
+ method: req.method,
1138
+ status: String(res.statusCode),
1139
+ });
1140
+ recordHistogram('http_request_duration_ms', durationMs, {
1141
+ method: req.method,
1142
+ });
1143
+ });
1144
+ next();
1145
+ });
1146
+
1147
+ app.get('/health/live', (_req, res) => {
1148
+ res.json({
1149
+ status: 'ok',
1150
+ service: deps.serviceName,
1151
+ version: deps.version,
1152
+ uptime_sec: Math.round(process.uptime()),
1153
+ });
1154
+ });
1155
+
1156
+ app.get('/health/ready', async (_req, res) => {
1157
+ const results: Array<{ name: string; ok: boolean; detail?: string }> = [];
1158
+ for (const check of deps.readiness) {
1159
+ try {
1160
+ const r = await check.check();
1161
+ results.push({ name: check.name, ok: r.ok, ...(r.detail ? { detail: r.detail } : {}) });
1162
+ } catch (err) {
1163
+ results.push({
1164
+ name: check.name,
1165
+ ok: false,
1166
+ detail: err instanceof Error ? err.message : String(err),
1167
+ });
1168
+ }
1169
+ }
1170
+ const allOk = results.every((r) => r.ok);
1171
+ res.status(allOk ? 200 : 503).json({
1172
+ status: allOk ? 'ready' : 'not-ready',
1173
+ checks: results,
1174
+ });
1175
+ });
1176
+
1177
+ app.get('/metrics', metricsHandlerExpress);
1178
+
1179
+ app.use((err: any, req: any, res: any, _next: () => void) => {
1180
+ baseLogger.error('http.error', err as Error, {
1181
+ method: req.method,
1182
+ path: req.path,
1183
+ });
1184
+ const status = err?.httpStatus ?? 500;
1185
+ res.status(status).json({
1186
+ error: {
1187
+ message: err?.message ?? 'Internal error',
1188
+ code: err?.code ?? 'INTERNAL_ERROR',
1189
+ },
1190
+ });
1191
+ });
1192
+
1193
+ return app;
1194
+ }
1195
+ `;
292
1196
  export const OWNED_SCAFFOLD_MODULES = [
293
1197
  {
294
1198
  path: 'src/logger.ts',
295
- exports: ['Logger', 'createLogger', 'setCorrelationId', 'getCorrelationId'],
1199
+ // ADR-PIPELINE-074: runWithCorrelation is now the authoritative
1200
+ // way to scope a correlation ID; setCorrelationId is retained as a
1201
+ // legacy shim but must not be used inside request handlers.
1202
+ exports: [
1203
+ 'Logger',
1204
+ 'createLogger',
1205
+ 'runWithCorrelation',
1206
+ 'getCorrelationId',
1207
+ 'setCorrelationId',
1208
+ ],
296
1209
  },
297
1210
  {
298
1211
  path: 'src/config.ts',
@@ -304,12 +1217,16 @@ export const OWNED_SCAFFOLD_MODULES = [
304
1217
  },
305
1218
  {
306
1219
  path: 'src/middleware.ts',
1220
+ // ADR-PIPELINE-076: metricsHandler is now a pure renderer; the
1221
+ // Express-compat wrapper is metricsHandlerExpress. The Hono wrapper
1222
+ // lives in the base-app scaffold.
307
1223
  exports: [
308
1224
  'correlationId',
309
1225
  'requestLogger',
310
1226
  'incrementCounter',
311
1227
  'recordHistogram',
312
1228
  'metricsHandler',
1229
+ 'metricsHandlerExpress',
313
1230
  ],
314
1231
  },
315
1232
  {
@@ -337,6 +1254,53 @@ export const OWNED_SCAFFOLD_MODULES = [
337
1254
  'loadSimulationId',
338
1255
  ],
339
1256
  },
1257
+ // ADR-PIPELINE-075: scaffolded persistence layer
1258
+ {
1259
+ path: 'src/persistence/repository.ts',
1260
+ exports: ['Repository', 'RepositoryOptions', 'RepositoryError'],
1261
+ },
1262
+ {
1263
+ path: 'src/persistence/in-memory-repository.ts',
1264
+ exports: ['InMemoryRepository'],
1265
+ },
1266
+ {
1267
+ path: 'src/persistence/sqlite-repository.ts',
1268
+ exports: ['SqliteRepository', 'SqliteRepositoryDeps', 'BetterSqliteDatabase'],
1269
+ },
1270
+ {
1271
+ path: 'src/persistence/audit-repository.ts',
1272
+ exports: ['AppendOnlyAuditRepository', 'AuditRepositoryDeps', 'AuditEntry'],
1273
+ },
1274
+ // ADR-PIPELINE-076: wire-complete server scaffold
1275
+ {
1276
+ path: 'src/api/base-app.ts',
1277
+ exports: [
1278
+ 'createBaseApp',
1279
+ 'BaseAppDeps',
1280
+ 'BaseAppContextVariables',
1281
+ 'HealthCheck',
1282
+ ],
1283
+ },
1284
+ // ADR-PIPELINE-077: ERP schema provenance tag
1285
+ {
1286
+ path: 'src/erp/schema-provenance.ts',
1287
+ exports: [
1288
+ 'ErpProvenanceSource',
1289
+ 'ErpSchemaProvenance',
1290
+ 'assertErpProvenanceOrFail',
1291
+ 'formatProvenanceBanner',
1292
+ 'printProvenanceBanner',
1293
+ ],
1294
+ },
1295
+ // ADR-PIPELINE-078: deep canonical JSON for audit hash chains
1296
+ {
1297
+ path: 'src/persistence/canonical-json.ts',
1298
+ exports: ['CanonicalJsonError', 'canonicalJson'],
1299
+ },
1300
+ {
1301
+ path: 'src/persistence/audit-hash.ts',
1302
+ exports: ['HashChainInput', 'hashAuditEntry'],
1303
+ },
340
1304
  ];
341
1305
  export function buildOwnedModulesManifest(now = new Date()) {
342
1306
  return {
@@ -492,33 +1456,16 @@ function copyPlanningArtifacts(runDir, targetRoot) {
492
1456
  // Instead of hoping the coding agent reads the prompts, we deliver the files.
493
1457
  const scaffoldDir = path.join(plansDir, 'scaffold', 'src');
494
1458
  fs.mkdirSync(scaffoldDir, { recursive: true });
495
- const loggerCode = `// Auto-generated by Agentics pipeline (ADR-039)
496
- export interface Logger {
497
- info(event: string, data?: Record<string, unknown>): void;
498
- warn(event: string, data?: Record<string, unknown>): void;
499
- error(event: string, error: Error, data?: Record<string, unknown>): void;
500
- }
501
-
502
- let currentCorrelationId: string | undefined;
503
-
504
- export function setCorrelationId(id: string): void { currentCorrelationId = id; }
505
- export function getCorrelationId(): string | undefined { return currentCorrelationId; }
506
-
507
- export function createLogger(service: string): Logger {
508
- const emit = (level: string, event: string, extra?: Record<string, unknown>) => {
509
- const entry = { timestamp: new Date().toISOString(), level, service, event, ...(currentCorrelationId ? { correlationId: currentCorrelationId } : {}), ...extra };
510
- process.stderr.write(JSON.stringify(entry) + '\\n');
511
- };
512
- return {
513
- info: (event, data) => emit('info', event, data),
514
- warn: (event, data) => emit('warn', event, data),
515
- error: (event, err, data) => emit('error', event, { error: err.message, stack: err.stack, ...data }),
516
- };
517
- }
518
- `;
519
- const configCode = `// Auto-generated by Agentics pipeline (ADR-039)
1459
+ // ADR-PIPELINE-074: logger scaffold now uses AsyncLocalStorage for
1460
+ // concurrent-request correctness. Body lives in LOGGER_SCAFFOLD at
1461
+ // module scope so it can be unit-tested independently.
1462
+ const loggerCode = LOGGER_SCAFFOLD;
1463
+ // ADR-PIPELINE-039 + ADR-PIPELINE-075: AppConfig now includes a `db`
1464
+ // block so the composition root can pick sqlite (production) vs
1465
+ // in-memory (tests) without code changes.
1466
+ const configCode = `// Auto-generated by Agentics pipeline (ADR-039 + ADR-PIPELINE-075)
520
1467
  export interface AppConfig {
521
- env: 'development' | 'staging' | 'production';
1468
+ env: 'development' | 'staging' | 'production' | 'test';
522
1469
  port: number;
523
1470
  logLevel: 'debug' | 'info' | 'warn' | 'error';
524
1471
  erp: {
@@ -527,6 +1474,10 @@ export interface AppConfig {
527
1474
  timeoutMs: number;
528
1475
  maxRetries: number;
529
1476
  };
1477
+ db: {
1478
+ driver: 'sqlite' | 'in-memory';
1479
+ path: string;
1480
+ };
530
1481
  }
531
1482
 
532
1483
  export const config: AppConfig = {
@@ -539,6 +1490,10 @@ export const config: AppConfig = {
539
1490
  timeoutMs: parseInt(process.env['ERP_TIMEOUT_MS'] ?? '30000', 10),
540
1491
  maxRetries: parseInt(process.env['ERP_MAX_RETRIES'] ?? '3', 10),
541
1492
  },
1493
+ db: {
1494
+ driver: (process.env['DB_DRIVER'] ?? 'sqlite') as AppConfig['db']['driver'],
1495
+ path: process.env['DB_PATH'] ?? './data/app.db',
1496
+ },
542
1497
  };
543
1498
  `;
544
1499
  const errorsCode = `// Auto-generated by Agentics pipeline (ADR-039)
@@ -569,21 +1524,49 @@ export class ERPError extends AppError {
569
1524
  `;
570
1525
  // TypeScript scaffold (default)
571
1526
  fs.writeFileSync(path.join(scaffoldDir, 'logger.ts'), loggerCode, 'utf-8');
1527
+ // ADR-PIPELINE-074: ship a concurrency regression test next to the
1528
+ // logger so generated projects fail loudly if someone reintroduces
1529
+ // a module-level correlation-ID store.
1530
+ fs.writeFileSync(path.join(scaffoldDir, 'logger.concurrency.test.ts'), LOGGER_CONCURRENCY_TEST_SCAFFOLD, 'utf-8');
572
1531
  fs.writeFileSync(path.join(scaffoldDir, 'config.ts'), configCode, 'utf-8');
573
1532
  fs.writeFileSync(path.join(scaffoldDir, 'errors.ts'), errorsCode, 'utf-8');
574
- // ADR-051: Correlation ID middleware + metrics scaffold
575
- const middlewareCode = `// Auto-generated by Agentics pipeline (ADR-051)
1533
+ // ADR-PIPELINE-075: Scaffolded persistence layer Repository<T> +
1534
+ // InMemoryRepository + SqliteRepository + AppendOnlyAuditRepository.
1535
+ // Every stateful service should import these instead of rolling a
1536
+ // Map<string, T> store. PGV-017 flags the anti-pattern at post-gen time.
1537
+ const persistenceDir = path.join(scaffoldDir, 'persistence');
1538
+ fs.mkdirSync(persistenceDir, { recursive: true });
1539
+ fs.writeFileSync(path.join(persistenceDir, 'repository.ts'), REPOSITORY_SCAFFOLD, 'utf-8');
1540
+ fs.writeFileSync(path.join(persistenceDir, 'in-memory-repository.ts'), IN_MEMORY_REPO_SCAFFOLD, 'utf-8');
1541
+ fs.writeFileSync(path.join(persistenceDir, 'sqlite-repository.ts'), SQLITE_REPO_SCAFFOLD, 'utf-8');
1542
+ fs.writeFileSync(path.join(persistenceDir, 'audit-repository.ts'), AUDIT_REPO_SCAFFOLD, 'utf-8');
1543
+ // ADR-PIPELINE-078: deep canonical JSON + audit hash helper.
1544
+ // Every generated AuditService should import hashAuditEntry from this
1545
+ // helper instead of hand-rolling JSON.stringify + createHash chains.
1546
+ // PGV-022 flags the anti-pattern at post-gen time.
1547
+ fs.writeFileSync(path.join(persistenceDir, 'canonical-json.ts'), CANONICAL_JSON_SCAFFOLD, 'utf-8');
1548
+ fs.writeFileSync(path.join(persistenceDir, 'audit-hash.ts'), AUDIT_HASH_SCAFFOLD, 'utf-8');
1549
+ // ADR-051 + ADR-PIPELINE-074: Correlation ID middleware now wraps next()
1550
+ // in runWithCorrelation so the ID lives in AsyncLocalStorage for the
1551
+ // request's entire async continuation. Concurrent requests cannot
1552
+ // cross-contaminate log lines.
1553
+ const middlewareCode = `// Auto-generated by Agentics pipeline (ADR-051 + ADR-PIPELINE-074)
576
1554
  import { randomUUID } from 'node:crypto';
577
- import { createLogger } from './logger.js';
1555
+ import { createLogger, runWithCorrelation } from './logger.js';
578
1556
 
579
1557
  const logger = createLogger('middleware');
580
1558
 
581
- /** Correlation ID middleware — attaches to every request, threads through logs */
1559
+ /**
1560
+ * Correlation ID middleware — attaches to every request, threads through
1561
+ * logs via AsyncLocalStorage. The request handler + all its async
1562
+ * descendants run inside runWithCorrelation so getCorrelationId() returns
1563
+ * this request's ID, not a sibling's.
1564
+ */
582
1565
  export function correlationId(req: any, res: any, next: () => void): void {
583
1566
  const id = (req.headers['x-correlation-id'] as string) || randomUUID();
584
1567
  req.correlationId = id;
585
1568
  res.setHeader('X-Correlation-Id', id);
586
- next();
1569
+ runWithCorrelation(id, next);
587
1570
  }
588
1571
 
589
1572
  /** Request logging middleware — logs every request with timing */
@@ -613,7 +1596,13 @@ export function recordHistogram(name: string, value: number, labels: Record<stri
613
1596
  histograms[key]!.push(value);
614
1597
  }
615
1598
 
616
- export function metricsHandler(_req: any, res: any): void {
1599
+ /**
1600
+ * ADR-PIPELINE-076: Pure metrics renderer — returns Prometheus text.
1601
+ * Framework-specific wrappers (metricsHandlerExpress, metricsHandlerHono)
1602
+ * delegate here. Call this directly from any framework that isn't
1603
+ * Express or Hono.
1604
+ */
1605
+ export function metricsHandler(): string {
617
1606
  const lines: string[] = [];
618
1607
  for (const [key, val] of Object.entries(counters)) {
619
1608
  lines.push(\`# TYPE \${key.split('{')[0]} counter\`);
@@ -626,8 +1615,13 @@ export function metricsHandler(_req: any, res: any): void {
626
1615
  lines.push(\`\${key}_sum \${sum}\`);
627
1616
  lines.push(\`\${key}_count \${count}\`);
628
1617
  }
629
- res.setHeader('Content-Type', 'text/plain');
630
- res.end(lines.join('\\n'));
1618
+ return lines.join('\\n');
1619
+ }
1620
+
1621
+ /** Express-compatible metrics handler — wraps metricsHandler(). */
1622
+ export function metricsHandlerExpress(_req: any, res: any): void {
1623
+ res.setHeader('Content-Type', 'text/plain; version=0.0.4');
1624
+ res.end(metricsHandler());
631
1625
  }
632
1626
 
633
1627
  // ADR-PIPELINE-069: CircuitBreaker now lives in its own scaffold module.
@@ -642,6 +1636,24 @@ export { CircuitBreaker } from './circuit-breaker.js';
642
1636
  // middleware.ts. Owned by the scaffold (see OWNED_SCAFFOLD_MODULES).
643
1637
  fs.writeFileSync(path.join(scaffoldDir, 'circuit-breaker.ts'), CIRCUIT_BREAKER_SCAFFOLD, 'utf-8');
644
1638
  totalCopied += 1;
1639
+ // ADR-PIPELINE-076: wire-complete Hono base app. Generators extend
1640
+ // via createBaseApp(deps).route('/api/<domain>', router) instead of
1641
+ // rebuilding the middleware/metrics/health plumbing from scratch.
1642
+ // PGV-018 will fail the build if /metrics, /health/live, /health/ready
1643
+ // are missing from the final project tree.
1644
+ const apiDir = path.join(scaffoldDir, 'api');
1645
+ fs.mkdirSync(apiDir, { recursive: true });
1646
+ fs.writeFileSync(path.join(apiDir, 'base-app.ts'), BASE_APP_SCAFFOLD_HONO, 'utf-8');
1647
+ totalCopied += 1;
1648
+ // ADR-PIPELINE-077: ERP schema provenance helper. Every generated
1649
+ // ERP adapter MUST export an ERP_SCHEMA_PROVENANCE constant and call
1650
+ // assertErpProvenanceOrFail at construction so strict-mode deployments
1651
+ // block on unreviewed schemas. PGV-020 enforces presence, PGV-021
1652
+ // enforces reviewer + catalog_version on validated entries.
1653
+ const erpDir = path.join(scaffoldDir, 'erp');
1654
+ fs.mkdirSync(erpDir, { recursive: true });
1655
+ fs.writeFileSync(path.join(erpDir, 'schema-provenance.ts'), ERP_SCHEMA_PROVENANCE_SCAFFOLD, 'utf-8');
1656
+ totalCopied += 1;
645
1657
  // ADR-PIPELINE-066: unit-economics helper. The demo script calls
646
1658
  // writeUnitEconomics() to emit a machine-readable manifest that the
647
1659
  // executive renderer prefers over per-employee heuristics.