@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.
- package/dist/pipeline/auto-chain.d.ts +117 -0
- package/dist/pipeline/auto-chain.d.ts.map +1 -1
- package/dist/pipeline/auto-chain.js +1047 -35
- package/dist/pipeline/auto-chain.js.map +1 -1
- package/dist/pipeline/phase2/phases/prompt-generator.d.ts.map +1 -1
- package/dist/pipeline/phase2/phases/prompt-generator.js +152 -6
- package/dist/pipeline/phase2/phases/prompt-generator.js.map +1 -1
- package/dist/pipeline/phase4-5-pre-render/financial-model.d.ts +51 -0
- package/dist/pipeline/phase4-5-pre-render/financial-model.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/financial-model.js +118 -0
- package/dist/pipeline/phase4-5-pre-render/financial-model.js.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts +53 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js +130 -0
- package/dist/pipeline/phase4-5-pre-render/post-render-reconciler.js.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts +47 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js +105 -0
- package/dist/pipeline/phase4-5-pre-render/pre-render-coordinator.js.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts +42 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.d.ts.map +1 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.js +117 -0
- package/dist/pipeline/phase4-5-pre-render/sector-baselines.js.map +1 -0
- package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts.map +1 -1
- package/dist/pipeline/phase5-build/phases/post-generation-validator.js +341 -1
- package/dist/pipeline/phase5-build/phases/post-generation-validator.js.map +1 -1
- package/dist/pipeline/types.d.ts +4 -1
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/pipeline/types.js +9 -1
- package/dist/pipeline/types.js.map +1 -1
- package/dist/synthesis/domain-unit-registry.d.ts +1 -1
- package/dist/synthesis/domain-unit-registry.d.ts.map +1 -1
- package/dist/synthesis/domain-unit-registry.js +26 -0
- package/dist/synthesis/domain-unit-registry.js.map +1 -1
- package/dist/synthesis/financial-claim-extractor.d.ts +20 -0
- package/dist/synthesis/financial-claim-extractor.d.ts.map +1 -1
- package/dist/synthesis/financial-claim-extractor.js +31 -0
- package/dist/synthesis/financial-claim-extractor.js.map +1 -1
- package/dist/synthesis/financial-consistency-rules.d.ts +4 -0
- package/dist/synthesis/financial-consistency-rules.d.ts.map +1 -1
- package/dist/synthesis/financial-consistency-rules.js +51 -0
- package/dist/synthesis/financial-consistency-rules.js.map +1 -1
- package/dist/synthesis/roadmap-dates.d.ts +72 -0
- package/dist/synthesis/roadmap-dates.d.ts.map +1 -0
- package/dist/synthesis/roadmap-dates.js +203 -0
- package/dist/synthesis/roadmap-dates.js.map +1 -0
- package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
- package/dist/synthesis/simulation-artifact-generator.js +46 -0
- package/dist/synthesis/simulation-artifact-generator.js.map +1 -1
- package/dist/synthesis/simulation-renderers.d.ts.map +1 -1
- package/dist/synthesis/simulation-renderers.js +139 -34
- package/dist/synthesis/simulation-renderers.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-PIPELINE-072 §1 — Pre-Render Analysis Pass coordinator.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates:
|
|
5
|
+
* 1. Classify sector from query + domain analysis
|
|
6
|
+
* 2. Compute bottom-up unit economics via `computePreRenderUnitEconomics`
|
|
7
|
+
* 3. Write `<runDir>/unit-economics.json` with `source='prototype-preview'`
|
|
8
|
+
*
|
|
9
|
+
* Runs BEFORE `renderExecutiveSummary`, `renderDecisionMemo`, and
|
|
10
|
+
* `renderFinancialAnalysis` so the ADR-066 tier-1 manifest path fires on
|
|
11
|
+
* every run.
|
|
12
|
+
*
|
|
13
|
+
* The module is purely additive: if it fails (unknown sector, I/O error,
|
|
14
|
+
* schema validation) the renderers fall back to ADR-066 tier-3 heuristic +
|
|
15
|
+
* mandatory warning banner — which is the exact behavior before this ADR.
|
|
16
|
+
* Never blocks the pipeline.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { UnitEconomicsSchema } from '../types.js';
|
|
21
|
+
import { computePreRenderUnitEconomics, } from './financial-model.js';
|
|
22
|
+
/**
|
|
23
|
+
* Run the pre-render pass. Never throws. Returns an `emitted` result on
|
|
24
|
+
* success or a `skipped` result with a reason when the pass can't produce
|
|
25
|
+
* a preview manifest.
|
|
26
|
+
*/
|
|
27
|
+
export function executePreRender(input) {
|
|
28
|
+
const manifestPath = path.join(input.runDir, 'unit-economics.json');
|
|
29
|
+
// If a prototype-sourced manifest already exists at this path (e.g. the
|
|
30
|
+
// user re-ran the pipeline and an earlier prototype demo landed a real
|
|
31
|
+
// manifest in the same runDir), leave it alone. Pre-render never
|
|
32
|
+
// overwrites a real prototype manifest.
|
|
33
|
+
if (existingPrototypeManifest(manifestPath)) {
|
|
34
|
+
return {
|
|
35
|
+
status: 'skipped',
|
|
36
|
+
reason: 'existing-prototype-manifest',
|
|
37
|
+
detail: 'A prototype-sourced unit-economics.json already exists; pre-render left it untouched.',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const sectorHint = input.domainAnalysis?.domain ?? null;
|
|
41
|
+
const financial = computePreRenderUnitEconomics({
|
|
42
|
+
runId: input.runId,
|
|
43
|
+
query: input.query,
|
|
44
|
+
sectorHint,
|
|
45
|
+
});
|
|
46
|
+
if (!financial) {
|
|
47
|
+
return {
|
|
48
|
+
status: 'skipped',
|
|
49
|
+
reason: 'unknown-sector',
|
|
50
|
+
detail: 'classifySector returned "unknown" — no ADR-066 registry entry.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const parsed = UnitEconomicsSchema.safeParse(financial.manifest);
|
|
54
|
+
if (!parsed.success) {
|
|
55
|
+
return {
|
|
56
|
+
status: 'skipped',
|
|
57
|
+
reason: 'schema-rejected',
|
|
58
|
+
detail: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(input.runDir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(manifestPath, JSON.stringify(parsed.data, null, 2) + '\n', 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return {
|
|
67
|
+
status: 'skipped',
|
|
68
|
+
reason: 'io-error',
|
|
69
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
status: 'emitted',
|
|
74
|
+
manifestPath,
|
|
75
|
+
manifest: parsed.data,
|
|
76
|
+
metrics: financial.metrics,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function existingPrototypeManifest(manifestPath) {
|
|
80
|
+
if (!fs.existsSync(manifestPath))
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
84
|
+
const parsed = JSON.parse(raw);
|
|
85
|
+
return parsed['source'] === 'prototype';
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Corrupt file — pre-render will overwrite it
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Exported for tests + reconciler module.
|
|
93
|
+
export function readManifestIfExists(manifestPath) {
|
|
94
|
+
if (!fs.existsSync(manifestPath))
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
98
|
+
const parsed = UnitEconomicsSchema.safeParse(JSON.parse(raw));
|
|
99
|
+
return parsed.success ? parsed.data : null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=pre-render-coordinator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-render-coordinator.js","sourceRoot":"","sources":["../../../src/pipeline/phase4-5-pre-render/pre-render-coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-PIPELINE-072 §6: Sector baseline waste / unit-cost / achievable-reduction
|
|
3
|
+
* table. Consumed by the pre-render financial model to produce an initial
|
|
4
|
+
* `unit-economics.json` BEFORE the prototype runs, so the ADR-066 tier-1
|
|
5
|
+
* renderer path fires on the executive summary / decision memo / financial
|
|
6
|
+
* analysis instead of falling to the per-employee heuristic.
|
|
7
|
+
*
|
|
8
|
+
* Numbers are conservative industry rules-of-thumb. The prototype is expected
|
|
9
|
+
* to produce a more accurate manifest when its demo runs, and the ADR-072 §5
|
|
10
|
+
* reconciler emits a warning on any drift >25%.
|
|
11
|
+
*
|
|
12
|
+
* Every entry must correspond to an entry in the ADR-066 sector registry
|
|
13
|
+
* (`src/synthesis/domain-unit-registry.ts`). The integration test in
|
|
14
|
+
* `tests/unit/pipeline/pre-render/sector-baselines-072.test.ts` enforces the
|
|
15
|
+
* lockstep invariant.
|
|
16
|
+
*/
|
|
17
|
+
import type { Sector } from '../../synthesis/domain-unit-registry.js';
|
|
18
|
+
export interface SectorBaseline {
|
|
19
|
+
/** Sector key — must match the ADR-066 registry. */
|
|
20
|
+
readonly sector: Exclude<Sector, 'unknown'>;
|
|
21
|
+
/** Human-readable label for the operational unit. */
|
|
22
|
+
readonly unitLabel: string;
|
|
23
|
+
/** Savings key (must match the registry's unitSavingsKey). */
|
|
24
|
+
readonly unitSavingsKey: string;
|
|
25
|
+
/** USD saved per operational unit per year, at baseline achievable reduction. */
|
|
26
|
+
readonly usdPerUnitPerYear: number;
|
|
27
|
+
/** Baseline waste rate (0..1) — what fraction of the unit cost is wasted today. */
|
|
28
|
+
readonly baselineWasteRate: number;
|
|
29
|
+
/** Achievable reduction (0..1) — what fraction of the waste can reasonably be eliminated. */
|
|
30
|
+
readonly achievableReduction: number;
|
|
31
|
+
/** Methodology string displayed in the rendered financial analysis. */
|
|
32
|
+
readonly methodology: string;
|
|
33
|
+
/** Short description of the unit cost basis (rooms × occupancy, etc.). */
|
|
34
|
+
readonly unitCostBasis: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* ADR-PIPELINE-072 §6 — baseline table. Conservative industry rules-of-thumb.
|
|
38
|
+
* The prototype's measured numbers will overwrite these during the real demo.
|
|
39
|
+
*/
|
|
40
|
+
export declare const SECTOR_BASELINES: readonly SectorBaseline[];
|
|
41
|
+
export declare function getSectorBaseline(sector: Sector): SectorBaseline | null;
|
|
42
|
+
//# sourceMappingURL=sector-baselines.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sector-baselines.d.ts","sourceRoot":"","sources":["../../../src/pipeline/phase4-5-pre-render/sector-baselines.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AAEtE,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,iFAAiF;IACjF,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,mFAAmF;IACnF,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,6FAA6F;IAC7F,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,uEAAuE;IACvE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,cAAc,EA2FrD,CAAC;AAMF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAEvE"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-PIPELINE-072 §6: Sector baseline waste / unit-cost / achievable-reduction
|
|
3
|
+
* table. Consumed by the pre-render financial model to produce an initial
|
|
4
|
+
* `unit-economics.json` BEFORE the prototype runs, so the ADR-066 tier-1
|
|
5
|
+
* renderer path fires on the executive summary / decision memo / financial
|
|
6
|
+
* analysis instead of falling to the per-employee heuristic.
|
|
7
|
+
*
|
|
8
|
+
* Numbers are conservative industry rules-of-thumb. The prototype is expected
|
|
9
|
+
* to produce a more accurate manifest when its demo runs, and the ADR-072 §5
|
|
10
|
+
* reconciler emits a warning on any drift >25%.
|
|
11
|
+
*
|
|
12
|
+
* Every entry must correspond to an entry in the ADR-066 sector registry
|
|
13
|
+
* (`src/synthesis/domain-unit-registry.ts`). The integration test in
|
|
14
|
+
* `tests/unit/pipeline/pre-render/sector-baselines-072.test.ts` enforces the
|
|
15
|
+
* lockstep invariant.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* ADR-PIPELINE-072 §6 — baseline table. Conservative industry rules-of-thumb.
|
|
19
|
+
* The prototype's measured numbers will overwrite these during the real demo.
|
|
20
|
+
*/
|
|
21
|
+
export const SECTOR_BASELINES = [
|
|
22
|
+
{
|
|
23
|
+
sector: 'hospitality',
|
|
24
|
+
unitLabel: 'occupied room night',
|
|
25
|
+
unitSavingsKey: 'usd_per_orn',
|
|
26
|
+
usdPerUnitPerYear: 0.82,
|
|
27
|
+
baselineWasteRate: 0.32,
|
|
28
|
+
achievableReduction: 0.18,
|
|
29
|
+
methodology: 'rooms × occupancy × $/occupied-room-night linen + water reduction',
|
|
30
|
+
unitCostBasis: '0.32 kg linen/orn × $2.10/kg laundry',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
sector: 'airline-catering',
|
|
34
|
+
unitLabel: 'meal loaded',
|
|
35
|
+
unitSavingsKey: 'usd_per_meal_loaded',
|
|
36
|
+
usdPerUnitPerYear: 0.92,
|
|
37
|
+
baselineWasteRate: 0.18,
|
|
38
|
+
achievableReduction: 0.22,
|
|
39
|
+
methodology: 'meals × flights × 365 × $/meal-loaded waste reduction',
|
|
40
|
+
unitCostBasis: '0.18 kg/meal × $4.20/meal loaded (loaded-to-consumed gap)',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
sector: 'commercial-real-estate',
|
|
44
|
+
unitLabel: 'square foot-year',
|
|
45
|
+
unitSavingsKey: 'usd_per_sqft_year',
|
|
46
|
+
usdPerUnitPerYear: 0.25,
|
|
47
|
+
baselineWasteRate: 0.20,
|
|
48
|
+
achievableReduction: 0.15,
|
|
49
|
+
methodology: 'sqft × $/sqft-year energy + HVAC reduction',
|
|
50
|
+
unitCostBasis: '12 kWh/sqft-year × $0.14/kWh',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
sector: 'fleet',
|
|
54
|
+
unitLabel: 'vehicle-mile',
|
|
55
|
+
unitSavingsKey: 'usd_per_vehicle_mile',
|
|
56
|
+
usdPerUnitPerYear: 0.28,
|
|
57
|
+
baselineWasteRate: 0.20,
|
|
58
|
+
achievableReduction: 0.12,
|
|
59
|
+
methodology: 'fleet × miles × $/mile fuel + maintenance reduction',
|
|
60
|
+
unitCostBasis: '0.65 gal/vehicle-mile × $3.80/gal',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
sector: 'retail',
|
|
64
|
+
unitLabel: 'store-year',
|
|
65
|
+
unitSavingsKey: 'usd_per_transaction',
|
|
66
|
+
usdPerUnitPerYear: 14250,
|
|
67
|
+
baselineWasteRate: 0.30,
|
|
68
|
+
achievableReduction: 0.15,
|
|
69
|
+
methodology: 'stores × $95K/yr energy × achievable reduction',
|
|
70
|
+
unitCostBasis: '$95K/store/yr energy × 15% reduction ceiling',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
sector: 'manufacturing',
|
|
74
|
+
unitLabel: 'unit produced',
|
|
75
|
+
unitSavingsKey: 'usd_per_unit_produced',
|
|
76
|
+
usdPerUnitPerYear: 0.85,
|
|
77
|
+
baselineWasteRate: 0.023,
|
|
78
|
+
achievableReduction: 0.25,
|
|
79
|
+
methodology: 'throughput × $/unit scrap reduction',
|
|
80
|
+
unitCostBasis: '2.3% scrap rate × $/unit',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
sector: 'healthcare',
|
|
84
|
+
unitLabel: 'patient-day',
|
|
85
|
+
unitSavingsKey: 'usd_per_patient_day',
|
|
86
|
+
usdPerUnitPerYear: 0.22,
|
|
87
|
+
baselineWasteRate: 0.18,
|
|
88
|
+
achievableReduction: 0.20,
|
|
89
|
+
methodology: 'census × $/patient-day disposal + material reduction',
|
|
90
|
+
unitCostBasis: '0.8 kg waste/patient-day × $1.40/kg disposal',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
sector: 'data-center',
|
|
94
|
+
unitLabel: 'kilowatt-hour',
|
|
95
|
+
unitSavingsKey: 'usd_per_kwh',
|
|
96
|
+
usdPerUnitPerYear: 0.018,
|
|
97
|
+
baselineWasteRate: 0.23,
|
|
98
|
+
achievableReduction: 0.18,
|
|
99
|
+
methodology: 'load × 8760 × $/kWh PUE optimization',
|
|
100
|
+
unitCostBasis: 'PUE 1.6 → 1.3 opportunity × $0.08/kWh',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
sector: 'call-center',
|
|
104
|
+
unitLabel: 'contact minute',
|
|
105
|
+
unitSavingsKey: 'usd_per_contact_minute',
|
|
106
|
+
usdPerUnitPerYear: 0.09,
|
|
107
|
+
baselineWasteRate: 0.12,
|
|
108
|
+
achievableReduction: 0.14,
|
|
109
|
+
methodology: 'agents × shifts × $/contact-minute overstaffing reduction',
|
|
110
|
+
unitCostBasis: '12% overstaffing × $38/hr loaded',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const BASELINE_BY_SECTOR = new Map(SECTOR_BASELINES.map(b => [b.sector, b]));
|
|
114
|
+
export function getSectorBaseline(sector) {
|
|
115
|
+
return BASELINE_BY_SECTOR.get(sector) ?? null;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=sector-baselines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sector-baselines.js","sourceRoot":"","sources":["../../../src/pipeline/phase4-5-pre-render/sector-baselines.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAuBH;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAA8B;IACzD;QACE,MAAM,EAAE,aAAa;QACrB,SAAS,EAAE,qBAAqB;QAChC,cAAc,EAAE,aAAa;QAC7B,iBAAiB,EAAE,IAAI;QACvB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,mEAAmE;QAChF,aAAa,EAAE,sCAAsC;KACtD;IACD;QACE,MAAM,EAAE,kBAAkB;QAC1B,SAAS,EAAE,aAAa;QACxB,cAAc,EAAE,qBAAqB;QACrC,iBAAiB,EAAE,IAAI;QACvB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,uDAAuD;QACpE,aAAa,EAAE,2DAA2D;KAC3E;IACD;QACE,MAAM,EAAE,wBAAwB;QAChC,SAAS,EAAE,kBAAkB;QAC7B,cAAc,EAAE,mBAAmB;QACnC,iBAAiB,EAAE,IAAI;QACvB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,4CAA4C;QACzD,aAAa,EAAE,8BAA8B;KAC9C;IACD;QACE,MAAM,EAAE,OAAO;QACf,SAAS,EAAE,cAAc;QACzB,cAAc,EAAE,sBAAsB;QACtC,iBAAiB,EAAE,IAAI;QACvB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,qDAAqD;QAClE,aAAa,EAAE,mCAAmC;KACnD;IACD;QACE,MAAM,EAAE,QAAQ;QAChB,SAAS,EAAE,YAAY;QACvB,cAAc,EAAE,qBAAqB;QACrC,iBAAiB,EAAE,KAAK;QACxB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,gDAAgD;QAC7D,aAAa,EAAE,8CAA8C;KAC9D;IACD;QACE,MAAM,EAAE,eAAe;QACvB,SAAS,EAAE,eAAe;QAC1B,cAAc,EAAE,uBAAuB;QACvC,iBAAiB,EAAE,IAAI;QACvB,iBAAiB,EAAE,KAAK;QACxB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,qCAAqC;QAClD,aAAa,EAAE,0BAA0B;KAC1C;IACD;QACE,MAAM,EAAE,YAAY;QACpB,SAAS,EAAE,aAAa;QACxB,cAAc,EAAE,qBAAqB;QACrC,iBAAiB,EAAE,IAAI;QACvB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,sDAAsD;QACnE,aAAa,EAAE,8CAA8C;KAC9D;IACD;QACE,MAAM,EAAE,aAAa;QACrB,SAAS,EAAE,eAAe;QAC1B,cAAc,EAAE,aAAa;QAC7B,iBAAiB,EAAE,KAAK;QACxB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,sCAAsC;QACnD,aAAa,EAAE,uCAAuC;KACvD;IACD;QACE,MAAM,EAAE,aAAa;QACrB,SAAS,EAAE,gBAAgB;QAC3B,cAAc,EAAE,wBAAwB;QACxC,iBAAiB,EAAE,IAAI;QACvB,iBAAiB,EAAE,IAAI;QACvB,mBAAmB,EAAE,IAAI;QACzB,WAAW,EAAE,2DAA2D;QACxE,aAAa,EAAE,kCAAkC;KAClD;CACF,CAAC;AAEF,MAAM,kBAAkB,GAAwC,IAAI,GAAG,CACrE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAgB,EAAE,CAAC,CAAC,CAAC,CACnD,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,OAAO,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;AAChD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-generation-validator.d.ts","sourceRoot":"","sources":["../../../../src/pipeline/phase5-build/phases/post-generation-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,aAAa,CAAC;AAQxE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,KAAK,EAAE,4BAA4B,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;CACrC;AAMD;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,iBAAiB,EAAE,CAAC;CAC7E;
|
|
1
|
+
{"version":3,"file":"post-generation-validator.d.ts","sourceRoot":"","sources":["../../../../src/pipeline/phase5-build/phases/post-generation-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,aAAa,CAAC;AAQxE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,KAAK,EAAE,4BAA4B,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;CACrC;AAMD;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,iBAAiB,EAAE,CAAC;CAC7E;AAg6BD,eAAO,MAAM,KAAK,EAAE,SAAS,2BAA2B,EAAc,CAAC;AAMvE;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,CAsC9E;AAqBD;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,kBAAkB,GAC1B;IAAE,WAAW,EAAE,yBAAyB,CAAA;CAAE,CAgG5C"}
|
|
@@ -558,10 +558,350 @@ const RULE_PGV_012 = {
|
|
|
558
558
|
return findings;
|
|
559
559
|
},
|
|
560
560
|
};
|
|
561
|
+
/** PGV-016: Ban module-level correlation-ID mutable state (ADR-PIPELINE-074). */
|
|
562
|
+
const RULE_PGV_016 = {
|
|
563
|
+
id: 'PGV-016',
|
|
564
|
+
title: 'No module-level `let currentCorrelationId` in logger / logging files',
|
|
565
|
+
severity: 'error',
|
|
566
|
+
adrReference: 'ADR-PIPELINE-074',
|
|
567
|
+
check: (files) => {
|
|
568
|
+
const findings = [];
|
|
569
|
+
// Target: any variable binding named correlation* at module scope in
|
|
570
|
+
// logger-shaped files. Inside functions / classes the pattern is fine
|
|
571
|
+
// (AsyncLocalStorage stores are allowed); it's only the MODULE-LEVEL
|
|
572
|
+
// mutable that breaks under concurrent requests.
|
|
573
|
+
const pattern = /^(?:let|var)\s+(\w*[cC]orrelation[A-Za-z0-9_]*)\b/gm;
|
|
574
|
+
for (const [filePath, content] of files) {
|
|
575
|
+
const n = filePath.replace(/\\/g, '/').toLowerCase();
|
|
576
|
+
// Match scaffolded logger files + any generator-emitted logging module
|
|
577
|
+
if (!(n.endsWith('/logger.ts') || n.includes('/logging/') || n.endsWith('/correlation.ts')))
|
|
578
|
+
continue;
|
|
579
|
+
if (isTestFile(filePath))
|
|
580
|
+
continue;
|
|
581
|
+
let match;
|
|
582
|
+
const re = new RegExp(pattern.source, 'gm');
|
|
583
|
+
while ((match = re.exec(content)) !== null) {
|
|
584
|
+
findings.push({
|
|
585
|
+
ruleId: 'PGV-016',
|
|
586
|
+
filePath,
|
|
587
|
+
line: lineAt(content, match.index),
|
|
588
|
+
severity: 'error',
|
|
589
|
+
message: `Module-level mutable \`${match[1]}\` in logger/logging file — concurrent requests will cross-contaminate log lines. ` +
|
|
590
|
+
'Use `runWithCorrelation` + AsyncLocalStorage from the scaffolded `src/logger.ts` instead (ADR-PIPELINE-074).',
|
|
591
|
+
});
|
|
592
|
+
if (match.index === re.lastIndex)
|
|
593
|
+
re.lastIndex++;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return findings;
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
/** PGV-017: Ban Map-backed service stores (ADR-PIPELINE-075). */
|
|
600
|
+
const RULE_PGV_017 = {
|
|
601
|
+
id: 'PGV-017',
|
|
602
|
+
title: 'Service layers must use Repository<T>, not new Map<string, T>',
|
|
603
|
+
severity: 'error',
|
|
604
|
+
adrReference: 'ADR-PIPELINE-075',
|
|
605
|
+
check: (files) => {
|
|
606
|
+
const findings = [];
|
|
607
|
+
// Match a class field declared as `new Map<string, T>`. We look for
|
|
608
|
+
// the field-binding form specifically (private/public/readonly/protected
|
|
609
|
+
// prefix + identifier + assignment) so local variables and request-
|
|
610
|
+
// scoped caches inside methods don't trigger.
|
|
611
|
+
const fieldPattern = /^\s*(?:private|public|protected)\s+(?:readonly\s+)?\w+\s*(?::\s*[^=]+)?=\s*new\s+Map\s*<\s*string\s*,/gm;
|
|
612
|
+
for (const [filePath, content] of files) {
|
|
613
|
+
if (isTestFile(filePath))
|
|
614
|
+
continue;
|
|
615
|
+
// Scope: only flag inside service / application layers
|
|
616
|
+
const n = filePath.replace(/\\/g, '/').toLowerCase();
|
|
617
|
+
const isServiceLayer = n.includes('/services/') ||
|
|
618
|
+
n.includes('/application/') ||
|
|
619
|
+
n.includes('/domain/services/');
|
|
620
|
+
if (!isServiceLayer)
|
|
621
|
+
continue;
|
|
622
|
+
// Only fire when the file defines a *Service class (heuristic).
|
|
623
|
+
// This avoids flagging repositories, controllers, and utility classes
|
|
624
|
+
// that legitimately use Map for in-memory state.
|
|
625
|
+
const serviceClassMatch = content.match(/\bclass\s+(\w*(?:Service|Store|Manager))\b/);
|
|
626
|
+
if (!serviceClassMatch)
|
|
627
|
+
continue;
|
|
628
|
+
const serviceName = serviceClassMatch[1];
|
|
629
|
+
// Soften to a warning when the service clearly depends on a
|
|
630
|
+
// Repository — the Map is probably a request-scoped cache, not the
|
|
631
|
+
// primary store.
|
|
632
|
+
const hasRepositoryDep = /\b(?:private|public|protected|readonly)\s+\w+\s*:\s*\w*Repository\b/.test(content);
|
|
633
|
+
let match;
|
|
634
|
+
const re = new RegExp(fieldPattern.source, 'gm');
|
|
635
|
+
while ((match = re.exec(content)) !== null) {
|
|
636
|
+
findings.push({
|
|
637
|
+
ruleId: 'PGV-017',
|
|
638
|
+
filePath,
|
|
639
|
+
line: lineAt(content, match.index),
|
|
640
|
+
severity: hasRepositoryDep ? 'warn' : 'error',
|
|
641
|
+
message: `Service class \`${serviceName}\` stores state in a Map<string, T> field — use \`Repository<T>\` from ` +
|
|
642
|
+
'`src/persistence/repository.ts` instead. InMemoryRepository for tests, SqliteRepository for production (ADR-PIPELINE-075).',
|
|
643
|
+
});
|
|
644
|
+
if (match.index === re.lastIndex)
|
|
645
|
+
re.lastIndex++;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return findings;
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
/** PGV-018: /metrics + /health/live + /health/ready routes must exist (ADR-PIPELINE-076). */
|
|
652
|
+
const RULE_PGV_018 = {
|
|
653
|
+
id: 'PGV-018',
|
|
654
|
+
title: 'Generated project must expose /metrics, /health/live, /health/ready',
|
|
655
|
+
severity: 'error',
|
|
656
|
+
adrReference: 'ADR-PIPELINE-076',
|
|
657
|
+
check: (files) => {
|
|
658
|
+
const findings = [];
|
|
659
|
+
// Collect server-side files across common layouts. Exclude the
|
|
660
|
+
// scaffold's own api/base-app.ts because those routes are emitted
|
|
661
|
+
// there — PGV-018 is verifying downstream integration, not the
|
|
662
|
+
// scaffold file itself.
|
|
663
|
+
const serverFiles = Array.from(files.entries()).filter(([filePath]) => {
|
|
664
|
+
const n = filePath.replace(/\\/g, '/').toLowerCase();
|
|
665
|
+
if (isTestFile(filePath))
|
|
666
|
+
return false;
|
|
667
|
+
return (n.includes('/server/') ||
|
|
668
|
+
n.includes('/api/') ||
|
|
669
|
+
n.endsWith('/app.ts') ||
|
|
670
|
+
n.endsWith('/server.ts') ||
|
|
671
|
+
n.endsWith('/index.ts'));
|
|
672
|
+
});
|
|
673
|
+
if (serverFiles.length === 0)
|
|
674
|
+
return findings;
|
|
675
|
+
// Two paths satisfy the rule:
|
|
676
|
+
// (a) The project imports createBaseApp from the scaffold (every
|
|
677
|
+
// generated project inherits the three routes automatically).
|
|
678
|
+
// (b) The project defines the three routes explicitly in a server file.
|
|
679
|
+
const anyImportsBaseApp = serverFiles.some(([, content]) => /\bcreateBaseApp\b/.test(content) &&
|
|
680
|
+
/from\s+['"][^'"]*base-app(\.js)?['"]/.test(content));
|
|
681
|
+
if (anyImportsBaseApp)
|
|
682
|
+
return findings;
|
|
683
|
+
const aggregatedContent = serverFiles.map(([, c]) => c).join('\n');
|
|
684
|
+
const missing = [];
|
|
685
|
+
if (!/['"]\/metrics['"]/.test(aggregatedContent))
|
|
686
|
+
missing.push('/metrics');
|
|
687
|
+
if (!/['"]\/health\/live['"]/.test(aggregatedContent))
|
|
688
|
+
missing.push('/health/live');
|
|
689
|
+
if (!/['"]\/health\/ready['"]/.test(aggregatedContent))
|
|
690
|
+
missing.push('/health/ready');
|
|
691
|
+
if (missing.length > 0) {
|
|
692
|
+
const sample = serverFiles[0];
|
|
693
|
+
findings.push({
|
|
694
|
+
ruleId: 'PGV-018',
|
|
695
|
+
filePath: sample[0],
|
|
696
|
+
line: 0,
|
|
697
|
+
severity: 'error',
|
|
698
|
+
message: `Generated project does not expose ${missing.join(', ')}. ` +
|
|
699
|
+
'Import `createBaseApp` from `src/api/base-app.ts` and call `app.route()` ' +
|
|
700
|
+
'for your domain routes; the base app wires /metrics, /health/live, and /health/ready automatically (ADR-PIPELINE-076).',
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return findings;
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
/** PGV-019: createBaseApp readiness list must cover declared dependencies (ADR-PIPELINE-076). */
|
|
707
|
+
const RULE_PGV_019 = {
|
|
708
|
+
id: 'PGV-019',
|
|
709
|
+
title: 'createBaseApp readiness list should cover every external dependency',
|
|
710
|
+
severity: 'warn',
|
|
711
|
+
adrReference: 'ADR-PIPELINE-076',
|
|
712
|
+
check: (files) => {
|
|
713
|
+
const findings = [];
|
|
714
|
+
// Match createBaseApp({...}) invocation blocks and inspect the
|
|
715
|
+
// readiness array. Because the rule is regex-based, we only flag
|
|
716
|
+
// the clearly-wrong cases: readiness is the empty array alongside
|
|
717
|
+
// clear dependencies on db, erp, auditRepo, or circuit-breaker.
|
|
718
|
+
const invocationPattern = /createBaseApp\s*\(\s*\{([\s\S]*?)readiness\s*:\s*\[([\s\S]*?)\]([\s\S]*?)\}\s*\)/g;
|
|
719
|
+
for (const [filePath, content] of files) {
|
|
720
|
+
if (isTestFile(filePath))
|
|
721
|
+
continue;
|
|
722
|
+
let match;
|
|
723
|
+
const re = new RegExp(invocationPattern.source, 'g');
|
|
724
|
+
while ((match = re.exec(content)) !== null) {
|
|
725
|
+
const body = match[2] ?? '';
|
|
726
|
+
const trimmedBody = body.trim();
|
|
727
|
+
// Empty readiness list
|
|
728
|
+
const isEmpty = trimmedBody.length === 0;
|
|
729
|
+
// Only contains unconditional-pass checks like `{ ok: true }`
|
|
730
|
+
const onlyUnconditional = !isEmpty &&
|
|
731
|
+
/ok\s*:\s*true/.test(trimmedBody) &&
|
|
732
|
+
!/await|await\s+\w/.test(trimmedBody);
|
|
733
|
+
if (!isEmpty && !onlyUnconditional)
|
|
734
|
+
continue;
|
|
735
|
+
// Does the file clearly declare downstream dependencies?
|
|
736
|
+
const hasDbDep = /\bDatabase\s*\(|new\s+Database\s*\(|SqliteRepository|drizzle|sqlite/i.test(content);
|
|
737
|
+
const hasErpDep = /ErpAdapter|erp[_-]?client|Ramco|OPERA|Workday|SAP\b/i.test(content);
|
|
738
|
+
const hasCircuitDep = /CircuitBreaker/.test(content);
|
|
739
|
+
const depCount = [hasDbDep, hasErpDep, hasCircuitDep].filter(Boolean).length;
|
|
740
|
+
if (depCount < 1)
|
|
741
|
+
continue;
|
|
742
|
+
findings.push({
|
|
743
|
+
ruleId: 'PGV-019',
|
|
744
|
+
filePath,
|
|
745
|
+
line: lineAt(content, match.index),
|
|
746
|
+
severity: 'warn',
|
|
747
|
+
message: `createBaseApp readiness list is ${isEmpty ? 'empty' : 'unconditional-pass-only'} but the file has ${depCount} external dependencies (db/erp/circuit). ` +
|
|
748
|
+
'Add a readiness check for each dependency so /health/ready fails when they are unavailable (ADR-PIPELINE-076).',
|
|
749
|
+
});
|
|
750
|
+
if (match.index === re.lastIndex)
|
|
751
|
+
re.lastIndex++;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return findings;
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
/** PGV-020: Every ERP adapter/schema file exports ERP_SCHEMA_PROVENANCE (ADR-PIPELINE-077). */
|
|
758
|
+
const RULE_PGV_020 = {
|
|
759
|
+
id: 'PGV-020',
|
|
760
|
+
title: 'ERP adapter/schema files must export ERP_SCHEMA_PROVENANCE',
|
|
761
|
+
severity: 'error',
|
|
762
|
+
adrReference: 'ADR-PIPELINE-077',
|
|
763
|
+
check: (files) => {
|
|
764
|
+
const findings = [];
|
|
765
|
+
for (const [filePath, content] of files) {
|
|
766
|
+
const n = filePath.replace(/\\/g, '/').toLowerCase();
|
|
767
|
+
// Scope: files under src/erp/ that are NOT tests and NOT the
|
|
768
|
+
// scaffolded helper itself (schema-provenance.ts).
|
|
769
|
+
if (!n.includes('/erp/'))
|
|
770
|
+
continue;
|
|
771
|
+
if (isTestFile(filePath))
|
|
772
|
+
continue;
|
|
773
|
+
if (n.endsWith('/schema-provenance.ts'))
|
|
774
|
+
continue;
|
|
775
|
+
// Skip obvious non-adapter files (enums, constants, utilities).
|
|
776
|
+
// We only require the block on files that export a Zod schema OR
|
|
777
|
+
// look like an adapter class — these are the code paths that talk
|
|
778
|
+
// to the real ERP.
|
|
779
|
+
const looksLikeAdapter = /\bz\.object\s*\(/.test(content) ||
|
|
780
|
+
/\bclass\s+\w*(?:Adapter|Client|Schema)\b/.test(content) ||
|
|
781
|
+
/\bexport\s+const\s+\w+Schema\s*=/.test(content);
|
|
782
|
+
if (!looksLikeAdapter)
|
|
783
|
+
continue;
|
|
784
|
+
const hasBlock = /export\s+const\s+ERP_SCHEMA_PROVENANCE\b/.test(content);
|
|
785
|
+
if (!hasBlock) {
|
|
786
|
+
findings.push({
|
|
787
|
+
ruleId: 'PGV-020',
|
|
788
|
+
filePath,
|
|
789
|
+
line: 0,
|
|
790
|
+
severity: 'error',
|
|
791
|
+
message: 'ERP adapter/schema file does not export `ERP_SCHEMA_PROVENANCE`. ' +
|
|
792
|
+
"Import `ErpSchemaProvenance` from `src/erp/schema-provenance.ts` and export a constant with `source: 'invented'` as the default (ADR-PIPELINE-077).",
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return findings;
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
/** PGV-021: source='validated' requires non-null reviewer + catalog_version (ADR-PIPELINE-077). */
|
|
800
|
+
const RULE_PGV_021 = {
|
|
801
|
+
id: 'PGV-021',
|
|
802
|
+
title: "ERP_SCHEMA_PROVENANCE source='validated' requires reviewer + catalog_version",
|
|
803
|
+
severity: 'error',
|
|
804
|
+
adrReference: 'ADR-PIPELINE-077',
|
|
805
|
+
check: (files) => {
|
|
806
|
+
const findings = [];
|
|
807
|
+
// Regex over the provenance literal. We look for the object body
|
|
808
|
+
// between `ERP_SCHEMA_PROVENANCE` and the next top-level `};` so
|
|
809
|
+
// multi-line declarations are captured.
|
|
810
|
+
const literalPattern = /export\s+const\s+ERP_SCHEMA_PROVENANCE[^=]*=\s*\{([\s\S]*?)\}\s*;/g;
|
|
811
|
+
for (const [filePath, content] of files) {
|
|
812
|
+
const n = filePath.replace(/\\/g, '/').toLowerCase();
|
|
813
|
+
if (!n.includes('/erp/'))
|
|
814
|
+
continue;
|
|
815
|
+
if (isTestFile(filePath))
|
|
816
|
+
continue;
|
|
817
|
+
if (n.endsWith('/schema-provenance.ts'))
|
|
818
|
+
continue;
|
|
819
|
+
let match;
|
|
820
|
+
const re = new RegExp(literalPattern.source, 'g');
|
|
821
|
+
while ((match = re.exec(content)) !== null) {
|
|
822
|
+
const body = match[1] ?? '';
|
|
823
|
+
// Extract source value — tolerate single / double quotes + backticks
|
|
824
|
+
const sourceMatch = body.match(/\bsource\s*:\s*['"`](\w+)['"`]/);
|
|
825
|
+
if (!sourceMatch || sourceMatch[1] !== 'validated')
|
|
826
|
+
continue;
|
|
827
|
+
// Under validated, reviewer and catalog_version must be non-null
|
|
828
|
+
const reviewerMatch = body.match(/\breviewer\s*:\s*([^,}]+)/);
|
|
829
|
+
const catalogMatch = body.match(/\bcatalog_version\s*:\s*([^,}]+)/);
|
|
830
|
+
const reviewerVal = (reviewerMatch?.[1] ?? '').trim();
|
|
831
|
+
const catalogVal = (catalogMatch?.[1] ?? '').trim();
|
|
832
|
+
const reviewerOk = reviewerVal !== '' && reviewerVal !== 'null' && reviewerVal !== 'undefined';
|
|
833
|
+
const catalogOk = catalogVal !== '' && catalogVal !== 'null' && catalogVal !== 'undefined';
|
|
834
|
+
if (!reviewerOk || !catalogOk) {
|
|
835
|
+
const missing = [];
|
|
836
|
+
if (!reviewerOk)
|
|
837
|
+
missing.push('reviewer');
|
|
838
|
+
if (!catalogOk)
|
|
839
|
+
missing.push('catalog_version');
|
|
840
|
+
findings.push({
|
|
841
|
+
ruleId: 'PGV-021',
|
|
842
|
+
filePath,
|
|
843
|
+
line: lineAt(content, match.index),
|
|
844
|
+
severity: 'error',
|
|
845
|
+
message: `ERP_SCHEMA_PROVENANCE has source='validated' but ${missing.join(' and ')} is null/missing. ` +
|
|
846
|
+
"Record the SME review details (reviewer name + role, catalog version) — don't rubber-stamp the validation status (ADR-PIPELINE-077).",
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
if (match.index === re.lastIndex)
|
|
850
|
+
re.lastIndex++;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return findings;
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
/** PGV-022: Audit services must use hashAuditEntry from the scaffold (ADR-PIPELINE-078). */
|
|
857
|
+
const RULE_PGV_022 = {
|
|
858
|
+
id: 'PGV-022',
|
|
859
|
+
title: 'Audit services must hash with hashAuditEntry, not raw JSON.stringify + SHA-256',
|
|
860
|
+
severity: 'error',
|
|
861
|
+
adrReference: 'ADR-PIPELINE-078',
|
|
862
|
+
check: (files) => {
|
|
863
|
+
const findings = [];
|
|
864
|
+
// Match the anti-pattern: createHash('sha256').update(JSON.stringify(...))
|
|
865
|
+
// in a file that looks like an audit service.
|
|
866
|
+
const antiPattern = /createHash\s*\(\s*['"]sha256['"]\s*\)[\s\S]{0,80}\.update\s*\(\s*JSON\.stringify/g;
|
|
867
|
+
for (const [filePath, content] of files) {
|
|
868
|
+
if (isTestFile(filePath))
|
|
869
|
+
continue;
|
|
870
|
+
const n = filePath.replace(/\\/g, '/').toLowerCase();
|
|
871
|
+
// Only audit-shaped files: audit*.ts, services/audit*.ts, or files
|
|
872
|
+
// that declare a class with Audit in the name.
|
|
873
|
+
const isAuditShaped = n.endsWith('/audit.ts') ||
|
|
874
|
+
n.includes('/audit') ||
|
|
875
|
+
/\bclass\s+\w*Audit\w*\b/.test(content);
|
|
876
|
+
if (!isAuditShaped)
|
|
877
|
+
continue;
|
|
878
|
+
// Skip the scaffolded helper itself
|
|
879
|
+
if (n.endsWith('/audit-hash.ts') || n.endsWith('/canonical-json.ts'))
|
|
880
|
+
continue;
|
|
881
|
+
let match;
|
|
882
|
+
const re = new RegExp(antiPattern.source, 'g');
|
|
883
|
+
while ((match = re.exec(content)) !== null) {
|
|
884
|
+
findings.push({
|
|
885
|
+
ruleId: 'PGV-022',
|
|
886
|
+
filePath,
|
|
887
|
+
line: lineAt(content, match.index),
|
|
888
|
+
severity: 'error',
|
|
889
|
+
message: 'Raw `JSON.stringify` + `createHash("sha256")` in audit code — use ' +
|
|
890
|
+
'`hashAuditEntry` from `src/persistence/audit-hash.ts` instead. ' +
|
|
891
|
+
'JSON.stringify does not sort keys and nested payload mutations escape tamper detection (ADR-PIPELINE-078).',
|
|
892
|
+
});
|
|
893
|
+
if (match.index === re.lastIndex)
|
|
894
|
+
re.lastIndex++;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return findings;
|
|
898
|
+
},
|
|
899
|
+
};
|
|
561
900
|
const ALL_RULES = [
|
|
562
901
|
RULE_PGV_001, RULE_PGV_002, RULE_PGV_003, RULE_PGV_004, RULE_PGV_005,
|
|
563
902
|
RULE_PGV_006, RULE_PGV_007, RULE_PGV_008, RULE_PGV_009, RULE_PGV_010,
|
|
564
|
-
RULE_PGV_011, RULE_PGV_012,
|
|
903
|
+
RULE_PGV_011, RULE_PGV_012, RULE_PGV_016, RULE_PGV_017, RULE_PGV_018, RULE_PGV_019,
|
|
904
|
+
RULE_PGV_020, RULE_PGV_021, RULE_PGV_022,
|
|
565
905
|
];
|
|
566
906
|
// Exported for unit tests that want to run individual rules
|
|
567
907
|
export const RULES = ALL_RULES;
|