@ogygie/cli 0.9.0 → 0.10.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/cli.js +24 -0
- package/package.json +2 -2
- package/templates/project/scripts/ogygie-impact.ts +70 -261
package/dist/cli.js
CHANGED
|
@@ -15587,6 +15587,18 @@ var governedChecksumsByVersion = {
|
|
|
15587
15587
|
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15588
15588
|
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15589
15589
|
},
|
|
15590
|
+
"0.10.0": {
|
|
15591
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15592
|
+
"AGENTS.md": "e44f635c0b203a0040098425f217c5a29c47e57280a3545b60d640aea0d59a70",
|
|
15593
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15594
|
+
"scripts/lint-interdits.ts": "d859e132b7020942a5211f5419b10045170dee6afba3f50ca5642570a5c4a683",
|
|
15595
|
+
"scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
|
|
15596
|
+
"scripts/ogygie-impact.ts": "825a47a51e940b2d8473714a417b4a75bad4eede35e3f4b34258bf6e181e98f4",
|
|
15597
|
+
"scripts/verify-contracts.ts": "725cae269e78ecdaa1923553c3097d8d96277824b5bd79a9a40284c6246036b7",
|
|
15598
|
+
"scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
|
|
15599
|
+
"scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
|
|
15600
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15601
|
+
},
|
|
15590
15602
|
"0.2.0": {
|
|
15591
15603
|
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15592
15604
|
"AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
|
|
@@ -15664,6 +15676,18 @@ var governedChecksumsByVersion = {
|
|
|
15664
15676
|
"scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
|
|
15665
15677
|
"scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
|
|
15666
15678
|
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15679
|
+
},
|
|
15680
|
+
"0.9.0": {
|
|
15681
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15682
|
+
"AGENTS.md": "e44f635c0b203a0040098425f217c5a29c47e57280a3545b60d640aea0d59a70",
|
|
15683
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15684
|
+
"scripts/lint-interdits.ts": "d859e132b7020942a5211f5419b10045170dee6afba3f50ca5642570a5c4a683",
|
|
15685
|
+
"scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
|
|
15686
|
+
"scripts/ogygie-impact.ts": "0dd3ff8cdd57e6cff0dfec1c669fce53fde9ae43f0784ed114771c83d1f8b357",
|
|
15687
|
+
"scripts/verify-contracts.ts": "725cae269e78ecdaa1923553c3097d8d96277824b5bd79a9a40284c6246036b7",
|
|
15688
|
+
"scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
|
|
15689
|
+
"scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
|
|
15690
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15667
15691
|
}
|
|
15668
15692
|
};
|
|
15669
15693
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Ogygie framework CLI: deterministic scaffolding (init), governed-file upgrades (upgrade) and lockstep verification (check) for Ogygie products.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"tsx": "^4.20.0",
|
|
23
23
|
"typescript": "^5.8.0",
|
|
24
24
|
"zod": "^4.0.0",
|
|
25
|
-
"@ogygie/core": "0.
|
|
25
|
+
"@ogygie/core": "0.10.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -1,255 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @intent Session tool `pnpm ogygie:impact <
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* fs + textual import analysis, no new dependency, findings as OgygieError.
|
|
2
|
+
* @intent Session tool `pnpm ogygie:impact <target>` (spec
|
|
3
|
+
* impact-analyzer-core): prints the blast radius of a change to delivered
|
|
4
|
+
* code — who depends on it, which symbols they use, its routes, the tables it
|
|
5
|
+
* owns and any outside reader of them, the tests to re-run, and WHAT THE
|
|
6
|
+
* ANALYSIS COULD NOT SEE (D12).
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* The judgement lives in `@ogygie/core/impact`, where it is tested against an
|
|
9
|
+
* in-memory repo and callable by other tools; this script is its collecting
|
|
10
|
+
* half — read the manifest, open the repo, print. Everything it used to
|
|
11
|
+
* decide by itself (which app carries which feature, what counts as an
|
|
12
|
+
* import) now comes from the map and from the core.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - The owned-tables scan ignores *.test.ts files: populating ANOTHER feature's
|
|
16
|
-
* table as a fixture is the canonical bench pattern ("the socle feature owns
|
|
17
|
-
* that model" — see any service.test.ts); flagging it would make every run
|
|
18
|
-
* red and the tool useless. Production code referencing a foreign table
|
|
19
|
-
* remains a potential violation and IS flagged.
|
|
14
|
+
* Output contract: default prints the JSON report THEN the human summary;
|
|
15
|
+
* `--json` prints the JSON only. Exit 0 when findings are empty, 1 otherwise.
|
|
20
16
|
*/
|
|
21
|
-
import {
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
22
18
|
import { join } from "node:path";
|
|
23
|
-
import {
|
|
19
|
+
import { analyzeImpact, type ImpactReport } from "@ogygie/core/impact";
|
|
24
20
|
import { OgygieManifestSchema } from "@ogygie/core/manifest";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
21
|
+
import { createFilesystemRepoAdapter } from "@ogygie/core/repo-adapter";
|
|
22
|
+
import { OgygieFailure, ogygieError } from "@ogygie/core/errors";
|
|
27
23
|
|
|
28
24
|
const ROOT = new URL("..", import.meta.url).pathname;
|
|
29
25
|
const MANIFEST_PATH = "ogygie.manifest.json";
|
|
30
26
|
|
|
31
|
-
// ---------------------------------------------------------------- contract
|
|
32
|
-
/** In @ogygie/core once the pattern proves generic (spec §Contrats, D10 rule 3). */
|
|
33
|
-
export const ImpactReportSchema = z.object({
|
|
34
|
-
feature: z.string(),
|
|
35
|
-
dependents: z.array(
|
|
36
|
-
z.object({
|
|
37
|
-
name: z.string(),
|
|
38
|
-
importedSymbols: z.array(z.string()),
|
|
39
|
-
viaIndexOnly: z.boolean(),
|
|
40
|
-
}),
|
|
41
|
-
),
|
|
42
|
-
routes: z.array(
|
|
43
|
-
z.object({ method: z.string(), path: z.string(), description: z.string() }),
|
|
44
|
-
),
|
|
45
|
-
ownedTables: z.array(z.string()),
|
|
46
|
-
tableReadersOutsideOwner: z.array(
|
|
47
|
-
z.object({ table: z.string(), feature: z.string(), file: z.string() }),
|
|
48
|
-
),
|
|
49
|
-
findings: z.array(OgygieErrorSchema),
|
|
50
|
-
testCommand: z.string(),
|
|
51
|
-
});
|
|
52
|
-
export type ImpactReport = z.infer<typeof ImpactReportSchema>;
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------- helpers
|
|
55
|
-
function walkTsFiles(dir: string): string[] {
|
|
56
|
-
const files: string[] = [];
|
|
57
|
-
for (const entry of readdirSync(dir)) {
|
|
58
|
-
const fullPath = join(dir, entry);
|
|
59
|
-
if (statSync(fullPath).isDirectory()) files.push(...walkTsFiles(fullPath));
|
|
60
|
-
else if (entry.endsWith(".ts") || entry.endsWith(".tsx")) files.push(fullPath);
|
|
61
|
-
}
|
|
62
|
-
return files;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Named/default/namespace symbols of one import clause ("type" stripped). */
|
|
66
|
-
function symbolsOfImportClause(clause: string): string[] {
|
|
67
|
-
const symbols: string[] = [];
|
|
68
|
-
const braces = /\{([^}]*)\}/.exec(clause);
|
|
69
|
-
if (braces?.[1] !== undefined) {
|
|
70
|
-
for (const rawEntry of braces[1].split(",")) {
|
|
71
|
-
const name = rawEntry.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
|
|
72
|
-
if (name !== undefined && name !== "") symbols.push(name);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const outsideBraces = clause.replace(/\{[^}]*\}/, "").replace(/^type\s+/, "").trim();
|
|
76
|
-
if (/\*\s*as\s+/.test(outsideBraces)) symbols.push("*");
|
|
77
|
-
else if (outsideBraces.replace(/,/g, "").trim() !== "") symbols.push("default");
|
|
78
|
-
return symbols;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------- report
|
|
82
|
-
export function buildImpactReport(featureName: string): ImpactReport {
|
|
83
|
-
const manifest = OgygieManifestSchema.parse(
|
|
84
|
-
JSON.parse(readFileSync(join(ROOT, MANIFEST_PATH), "utf8")),
|
|
85
|
-
);
|
|
86
|
-
const apiApp = manifest.apps.find((app) => app.type === "api");
|
|
87
|
-
if (apiApp === undefined) {
|
|
88
|
-
throw new Error("no app of type api in the manifest");
|
|
89
|
-
}
|
|
90
|
-
const target = manifest.features.find((feature) => feature.name === featureName);
|
|
91
|
-
if (target === undefined) {
|
|
92
|
-
const failure = ogygieError({
|
|
93
|
-
code: "IMPACT_UNKNOWN_FEATURE",
|
|
94
|
-
message: `Feature "${featureName}" is unknown to the manifest. Valid features: ${manifest.features.map((feature) => feature.name).join(", ")}.`,
|
|
95
|
-
file: MANIFEST_PATH,
|
|
96
|
-
cause: `no features[] entry has name="${featureName}"`,
|
|
97
|
-
suggestedFix: "Pick one of the valid feature names listed in the message.",
|
|
98
|
-
});
|
|
99
|
-
console.error(JSON.stringify(failure, null, 2));
|
|
100
|
-
console.error("\nogygie:impact — feature inconnue.");
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const findings: OgygieError[] = [];
|
|
105
|
-
const targetDirName = target.folder.split("/").pop() ?? target.folder;
|
|
106
|
-
|
|
107
|
-
// (a)+(b) dependents: declared in the manifest AND/OR actually importing.
|
|
108
|
-
const importersByFeature = new Map<
|
|
109
|
-
string,
|
|
110
|
-
{ importedSymbols: Set<string>; viaIndexOnly: boolean }
|
|
111
|
-
>();
|
|
112
|
-
for (const feature of manifest.features) {
|
|
113
|
-
if (feature.name === target.name) continue;
|
|
114
|
-
const featureDir = join(ROOT, apiApp.path, feature.folder);
|
|
115
|
-
if (!existsSync(featureDir)) continue;
|
|
116
|
-
for (const filePath of walkTsFiles(featureDir)) {
|
|
117
|
-
const content = readFileSync(filePath, "utf8");
|
|
118
|
-
const relativeFile = filePath.slice(ROOT.length);
|
|
119
|
-
for (const match of content.matchAll(
|
|
120
|
-
/import\s+([^'"]+?)\s+from\s+"([^"]+)"/g,
|
|
121
|
-
)) {
|
|
122
|
-
const clause = match[1];
|
|
123
|
-
const source = match[2];
|
|
124
|
-
if (clause === undefined || source === undefined) continue;
|
|
125
|
-
const sibling = new RegExp(`^\\.\\./${targetDirName}/(.+)$`).exec(source);
|
|
126
|
-
if (sibling === null || sibling[1] === undefined) continue;
|
|
127
|
-
const entry = importersByFeature.get(feature.name) ?? {
|
|
128
|
-
importedSymbols: new Set<string>(),
|
|
129
|
-
viaIndexOnly: true,
|
|
130
|
-
};
|
|
131
|
-
for (const symbol of symbolsOfImportClause(clause)) {
|
|
132
|
-
entry.importedSymbols.add(symbol);
|
|
133
|
-
}
|
|
134
|
-
if (sibling[1] !== "index.js") {
|
|
135
|
-
entry.viaIndexOnly = false;
|
|
136
|
-
findings.push(
|
|
137
|
-
ogygieError({
|
|
138
|
-
code: "FEATURE_BOUNDARY_VIOLATION",
|
|
139
|
-
message: `Feature "${feature.name}" imports an internal file of "${target.name}" instead of its index.ts.`,
|
|
140
|
-
file: relativeFile,
|
|
141
|
-
cause: `import from "${source}" bypasses ${targetDirName}/index.ts`,
|
|
142
|
-
suggestedFix: `Export the needed symbols from ${targetDirName}/index.ts and import them from "../${targetDirName}/index.js".`,
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
importersByFeature.set(feature.name, entry);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const declaredDependents = manifest.features.filter((feature) =>
|
|
152
|
-
feature.dependsOn.includes(target.name),
|
|
153
|
-
);
|
|
154
|
-
for (const declared of declaredDependents) {
|
|
155
|
-
if (!importersByFeature.has(declared.name)) {
|
|
156
|
-
findings.push(
|
|
157
|
-
ogygieError({
|
|
158
|
-
code: "MANIFEST_DEPENDENCY_STALE",
|
|
159
|
-
message: `Feature "${declared.name}" declares dependsOn "${target.name}" but never imports it.`,
|
|
160
|
-
file: `${MANIFEST_PATH}#features.${declared.name}`,
|
|
161
|
-
cause: `dependsOn contains "${target.name}", no import of ../${targetDirName}/ found in its code`,
|
|
162
|
-
suggestedFix:
|
|
163
|
-
"Remove the stale dependsOn entry, or make the dependency real through the feature's index.ts.",
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
for (const importerName of importersByFeature.keys()) {
|
|
169
|
-
if (!declaredDependents.some((declared) => declared.name === importerName)) {
|
|
170
|
-
findings.push(
|
|
171
|
-
ogygieError({
|
|
172
|
-
code: "MANIFEST_DEPENDENCY_UNDECLARED",
|
|
173
|
-
message: `Feature "${importerName}" imports "${target.name}" but does not declare it in dependsOn.`,
|
|
174
|
-
file: `${MANIFEST_PATH}#features.${importerName}`,
|
|
175
|
-
cause: `imports of ../${targetDirName}/ found in its code, "${target.name}" absent from its dependsOn`,
|
|
176
|
-
suggestedFix: `Add "${target.name}" to the dependsOn of "${importerName}" — the manifest must never lie by omission.`,
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const dependentNames = [
|
|
183
|
-
...new Set([
|
|
184
|
-
...declaredDependents.map((feature) => feature.name),
|
|
185
|
-
...importersByFeature.keys(),
|
|
186
|
-
]),
|
|
187
|
-
].sort();
|
|
188
|
-
const dependents = dependentNames.map((name) => {
|
|
189
|
-
const imports = importersByFeature.get(name);
|
|
190
|
-
return {
|
|
191
|
-
name,
|
|
192
|
-
importedSymbols: [...(imports?.importedSymbols ?? [])].sort(),
|
|
193
|
-
viaIndexOnly: imports?.viaIndexOnly ?? true,
|
|
194
|
-
};
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// (d) owned tables referenced by another feature's PRODUCTION code.
|
|
198
|
-
const tableReadersOutsideOwner: ImpactReport["tableReadersOutsideOwner"] = [];
|
|
199
|
-
for (const feature of manifest.features) {
|
|
200
|
-
if (feature.name === target.name) continue;
|
|
201
|
-
const featureDir = join(ROOT, apiApp.path, feature.folder);
|
|
202
|
-
if (!existsSync(featureDir)) continue;
|
|
203
|
-
for (const filePath of walkTsFiles(featureDir)) {
|
|
204
|
-
if (filePath.endsWith(".test.ts") || filePath.endsWith(".test.tsx")) continue;
|
|
205
|
-
const content = readFileSync(filePath, "utf8");
|
|
206
|
-
const relativeFile = filePath.slice(ROOT.length);
|
|
207
|
-
for (const table of dbTableNamesOf(target)) {
|
|
208
|
-
if (!content.includes(`"${table}"`)) continue;
|
|
209
|
-
tableReadersOutsideOwner.push({
|
|
210
|
-
table,
|
|
211
|
-
feature: feature.name,
|
|
212
|
-
file: relativeFile,
|
|
213
|
-
});
|
|
214
|
-
findings.push(
|
|
215
|
-
ogygieError({
|
|
216
|
-
code: "TABLE_REFERENCED_OUTSIDE_OWNER",
|
|
217
|
-
message: `Table "${table}" is owned by "${target.name}" but referenced by production code of "${feature.name}".`,
|
|
218
|
-
file: relativeFile,
|
|
219
|
-
cause: `"${table}" found in a non-test file outside its owner feature`,
|
|
220
|
-
suggestedFix: `Go through the public contracts of ${targetDirName}/index.ts instead of touching the table — one table = one owner.`,
|
|
221
|
-
}),
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const dependentDirNames = dependentNames.map((name) => {
|
|
228
|
-
const feature = manifest.features.find((candidate) => candidate.name === name);
|
|
229
|
-
return feature?.folder.split("/").pop() ?? name;
|
|
230
|
-
});
|
|
231
|
-
const testCommand = ["pnpm test --", `features/${targetDirName}`, ...dependentDirNames.map((dirName) => `features/${dirName}`)].join(" ");
|
|
232
|
-
|
|
233
|
-
return ImpactReportSchema.parse({
|
|
234
|
-
feature: target.name,
|
|
235
|
-
dependents,
|
|
236
|
-
routes: target.routes.map((route) => ({
|
|
237
|
-
method: route.method,
|
|
238
|
-
path: route.path,
|
|
239
|
-
description: route.description,
|
|
240
|
-
})),
|
|
241
|
-
ownedTables: dbTableNamesOf(target),
|
|
242
|
-
tableReadersOutsideOwner,
|
|
243
|
-
findings,
|
|
244
|
-
testCommand,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ---------------------------------------------------------------- CLI
|
|
249
27
|
function printSummary(report: ImpactReport): void {
|
|
250
|
-
console.log(`\n── ogygie:impact ${report.
|
|
28
|
+
console.log(`\n── ogygie:impact ${report.target.name} (${report.target.kind}) ──`);
|
|
251
29
|
if (report.dependents.length === 0) {
|
|
252
|
-
console.log("Dépendants : aucun.");
|
|
30
|
+
console.log("Dépendants : aucun DANS CE PÉRIMÈTRE.");
|
|
253
31
|
} else {
|
|
254
32
|
console.log(`Dépendants (${report.dependents.length}) :`);
|
|
255
33
|
for (const dependent of report.dependents) {
|
|
@@ -258,7 +36,7 @@ function printSummary(report: ImpactReport): void {
|
|
|
258
36
|
? `importe { ${dependent.importedSymbols.join(", ")} }`
|
|
259
37
|
: "n'importe aucun symbole";
|
|
260
38
|
const boundary = dependent.viaIndexOnly ? "via index.ts uniquement" : "⚠ contourne index.ts";
|
|
261
|
-
console.log(` • ${dependent.name} — ${symbols} — ${boundary}`);
|
|
39
|
+
console.log(` • ${dependent.name} (${dependent.kind}) — ${symbols} — ${boundary}`);
|
|
262
40
|
}
|
|
263
41
|
}
|
|
264
42
|
const routes = report.routes.map((route) => `${route.method} ${route.path}`).join(" · ");
|
|
@@ -266,36 +44,67 @@ function printSummary(report: ImpactReport): void {
|
|
|
266
44
|
console.log(
|
|
267
45
|
`Tables possédées : ${report.ownedTables.length === 0 ? "aucune" : report.ownedTables.join(", ")}`,
|
|
268
46
|
);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
console.log(` ⚠ "${reader.table}" référencée par ${reader.feature} (${reader.file})`);
|
|
272
|
-
}
|
|
47
|
+
for (const reader of report.tableReadersOutsideOwner) {
|
|
48
|
+
console.log(` ⚠ "${reader.table}" référencée par ${reader.feature} (${reader.file})`);
|
|
273
49
|
}
|
|
274
50
|
console.log(
|
|
275
51
|
report.findings.length === 0
|
|
276
52
|
? "Findings : aucun."
|
|
277
53
|
: `Findings : ${report.findings.length} (détail dans le JSON ci-dessus).`,
|
|
278
54
|
);
|
|
279
|
-
console.log(`Tests à relancer : ${report.testCommand}
|
|
55
|
+
console.log(`Tests à relancer : ${report.testCommand}`);
|
|
56
|
+
// Always printed, ESPECIALLY when nothing was found: "no dependents" and
|
|
57
|
+
// "I did not look there" read the same without it (D12).
|
|
58
|
+
console.log(`\nPérimètre — dossiers scannés : ${report.scope.scannedFolders.length}`);
|
|
59
|
+
console.log(`Hors de portée de cette analyse :`);
|
|
60
|
+
for (const limit of report.scope.outOfReach) console.log(` · ${limit}`);
|
|
61
|
+
console.log("");
|
|
280
62
|
}
|
|
281
63
|
|
|
282
64
|
const cliArguments = process.argv.slice(2);
|
|
283
65
|
const jsonOnly = cliArguments.includes("--json");
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
66
|
+
const targetArgument = cliArguments.find((argument) => !argument.startsWith("--"));
|
|
67
|
+
|
|
68
|
+
function fail(error: unknown): never {
|
|
69
|
+
const payload =
|
|
70
|
+
error instanceof OgygieFailure
|
|
71
|
+
? error.error
|
|
72
|
+
: ogygieError({
|
|
73
|
+
code: "IMPACT_FAILED",
|
|
74
|
+
message: "The impact analysis could not run.",
|
|
75
|
+
file: MANIFEST_PATH,
|
|
76
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
77
|
+
suggestedFix: "Run `pnpm ogygie:verify-manifest` — the analysis reads the map first.",
|
|
78
|
+
});
|
|
79
|
+
console.error(JSON.stringify(payload, null, 2));
|
|
295
80
|
process.exit(1);
|
|
296
81
|
}
|
|
297
82
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
83
|
+
if (targetArgument === undefined) {
|
|
84
|
+
fail(
|
|
85
|
+
new OgygieFailure(
|
|
86
|
+
ogygieError({
|
|
87
|
+
code: "IMPACT_UNKNOWN_FEATURE",
|
|
88
|
+
message: "No target given. Usage: pnpm ogygie:impact <feature|package> [--json].",
|
|
89
|
+
file: null,
|
|
90
|
+
cause: "process.argv carries no non-flag argument",
|
|
91
|
+
suggestedFix: "Pass a name declared in ogygie.manifest.json.",
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const manifest = OgygieManifestSchema.parse(
|
|
99
|
+
JSON.parse(readFileSync(join(ROOT, MANIFEST_PATH), "utf8")),
|
|
100
|
+
);
|
|
101
|
+
const report = await analyzeImpact(
|
|
102
|
+
{ manifest, adapter: createFilesystemRepoAdapter(ROOT) },
|
|
103
|
+
targetArgument,
|
|
104
|
+
);
|
|
105
|
+
console.log(JSON.stringify(report, null, 2));
|
|
106
|
+
if (!jsonOnly) printSummary(report);
|
|
107
|
+
process.exit(report.findings.length === 0 ? 0 : 1);
|
|
108
|
+
} catch (thrown) {
|
|
109
|
+
fail(thrown);
|
|
110
|
+
}
|