@ogygie/cli 0.5.4 → 0.7.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
CHANGED
|
@@ -14588,6 +14588,42 @@ var governedFiles = [
|
|
|
14588
14588
|
templatePath: "project/tsconfig.base.json",
|
|
14589
14589
|
targetPath: "tsconfig.base.json",
|
|
14590
14590
|
description: "Strict TypeScript options decided by the framework."
|
|
14591
|
+
},
|
|
14592
|
+
// The gate scripts (spec ogygie-standalone-repo v3). They decide nothing by
|
|
14593
|
+
// themselves: they apply the framework's rules to what the manifest
|
|
14594
|
+
// declares, so they are identical in every project — and a gate corrected
|
|
14595
|
+
// upstream must reach every project, which recopying by hand never achieved.
|
|
14596
|
+
// What used to be product-specific in them now lives in the manifest
|
|
14597
|
+
// (`tenancy`).
|
|
14598
|
+
{
|
|
14599
|
+
templatePath: "project/scripts/verify-manifest.ts",
|
|
14600
|
+
targetPath: "scripts/verify-manifest.ts",
|
|
14601
|
+
description: "Gate: the manifest tells the truth about the actual code."
|
|
14602
|
+
},
|
|
14603
|
+
{
|
|
14604
|
+
templatePath: "project/scripts/verify-specs.ts",
|
|
14605
|
+
targetPath: "scripts/verify-specs.ts",
|
|
14606
|
+
description: "Gate: spec frontmatter, canonical sections, lifecycle, decision refs."
|
|
14607
|
+
},
|
|
14608
|
+
{
|
|
14609
|
+
templatePath: "project/scripts/verify-contracts.ts",
|
|
14610
|
+
targetPath: "scripts/verify-contracts.ts",
|
|
14611
|
+
description: "Gate: transitive purity of every contract entry point."
|
|
14612
|
+
},
|
|
14613
|
+
{
|
|
14614
|
+
templatePath: "project/scripts/lint-interdits.ts",
|
|
14615
|
+
targetPath: "scripts/lint-interdits.ts",
|
|
14616
|
+
description: "Gate: the absolute bans of AGENTS.md, plus the raw-db rule derived from the manifest."
|
|
14617
|
+
},
|
|
14618
|
+
{
|
|
14619
|
+
templatePath: "project/scripts/ogygie-impact.ts",
|
|
14620
|
+
targetPath: "scripts/ogygie-impact.ts",
|
|
14621
|
+
description: "Tool: blast radius of a feature, derived from the manifest."
|
|
14622
|
+
},
|
|
14623
|
+
{
|
|
14624
|
+
templatePath: "project/scripts/ogygie-design-brief.ts",
|
|
14625
|
+
targetPath: "scripts/ogygie-design-brief.ts",
|
|
14626
|
+
description: "Tool: design brief derived from tokens, patterns, contracts and specs."
|
|
14591
14627
|
}
|
|
14592
14628
|
];
|
|
14593
14629
|
|
|
@@ -14602,6 +14638,7 @@ import { join } from "node:path";
|
|
|
14602
14638
|
|
|
14603
14639
|
// ../ogygie-core/src/manifest.schema.ts
|
|
14604
14640
|
var FeatureNameSchema = external_exports.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "kebab-case required");
|
|
14641
|
+
var PackageDependencySchema = external_exports.string().regex(/^package:[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "expected package:<kebab-case-name>");
|
|
14605
14642
|
var HttpRouteSchema = external_exports.object({
|
|
14606
14643
|
method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
14607
14644
|
path: external_exports.string().startsWith("/api/"),
|
|
@@ -14626,7 +14663,9 @@ var FeatureManifestSchema = external_exports.object({
|
|
|
14626
14663
|
dbTables: external_exports.array(DbTableDeclarationSchema).describe(
|
|
14627
14664
|
"Drizzle tables owned by this feature (one table = one owner). String = tenant-scoped; { name, shared: true } = shared referential"
|
|
14628
14665
|
),
|
|
14629
|
-
dependsOn: external_exports.array(FeatureNameSchema).describe(
|
|
14666
|
+
dependsOn: external_exports.array(external_exports.union([FeatureNameSchema, PackageDependencySchema])).describe(
|
|
14667
|
+
"Features imported through their index.ts, and product packages as package:<name> \u2014 nothing else"
|
|
14668
|
+
),
|
|
14630
14669
|
status: external_exports.enum(["skeleton", "in-progress", "delivered"])
|
|
14631
14670
|
});
|
|
14632
14671
|
var OgygieVersionSchema = external_exports.string().regex(/^\d+\.\d+\.\d+$/, "expected a plain x.y.z version");
|
|
@@ -14654,6 +14693,20 @@ var VocabularyEntrySchema = external_exports.object({
|
|
|
14654
14693
|
var ProductVocabularySchema = external_exports.object({
|
|
14655
14694
|
entries: external_exports.array(VocabularyEntrySchema)
|
|
14656
14695
|
});
|
|
14696
|
+
var PackageManifestSchema = external_exports.object({
|
|
14697
|
+
name: FeatureNameSchema,
|
|
14698
|
+
path: external_exports.string().describe("Folder relative to the repo root, e.g. packages/solver"),
|
|
14699
|
+
intent: external_exports.string().min(20).max(300).describe("The package's 'why' in 1-3 sentences"),
|
|
14700
|
+
spec: external_exports.string().nullable(),
|
|
14701
|
+
dependsOn: external_exports.array(PackageDependencySchema).describe("Other PRODUCT packages, as package:<name> \u2014 never a feature: a package that depended on one would live under an app, and would be a feature"),
|
|
14702
|
+
status: external_exports.enum(["skeleton", "in-progress", "delivered"])
|
|
14703
|
+
});
|
|
14704
|
+
var TenancyManifestSchema = external_exports.object({
|
|
14705
|
+
column: external_exports.string().min(1).describe("SQL name of the tenant column, e.g. workspace_id \u2014 the one AGENTS.product.md announces"),
|
|
14706
|
+
rawDbAllowed: external_exports.array(FeatureNameSchema).default([]).describe(
|
|
14707
|
+
"Features allowed to reach the raw database: the tenancy socle, which BUILDS the contexts and therefore predates them. Every other feature goes through tenantDb"
|
|
14708
|
+
)
|
|
14709
|
+
});
|
|
14657
14710
|
var OgygieManifestSchema = external_exports.object({
|
|
14658
14711
|
version: external_exports.literal(1),
|
|
14659
14712
|
product: external_exports.object({
|
|
@@ -14684,6 +14737,10 @@ var OgygieManifestSchema = external_exports.object({
|
|
|
14684
14737
|
})
|
|
14685
14738
|
),
|
|
14686
14739
|
features: external_exports.array(FeatureManifestSchema),
|
|
14740
|
+
tenancy: TenancyManifestSchema.optional().describe(
|
|
14741
|
+
"Product values the framework gates need \u2014 absent, the tenancy checks do not run"
|
|
14742
|
+
),
|
|
14743
|
+
packages: external_exports.array(PackageManifestSchema).optional().describe("Product code living outside every app \u2014 libraries, engines. Absent = the project has none"),
|
|
14687
14744
|
vocabulary: ProductVocabularySchema.optional().describe(
|
|
14688
14745
|
"The product's closed vocabulary \u2014 declared here, enforced by `ogygie verify-vocabulary`. Absent = gate silent"
|
|
14689
14746
|
),
|
|
@@ -14693,6 +14750,53 @@ var OgygieManifestSchema = external_exports.object({
|
|
|
14693
14750
|
specsFolder: external_exports.literal("specs/")
|
|
14694
14751
|
})
|
|
14695
14752
|
}).superRefine((manifest, context) => {
|
|
14753
|
+
const featureNames = new Set(manifest.features.map((feature) => feature.name));
|
|
14754
|
+
const packageNames = /* @__PURE__ */ new Set();
|
|
14755
|
+
(manifest.packages ?? []).forEach((packageEntry, index) => {
|
|
14756
|
+
if (featureNames.has(packageEntry.name)) {
|
|
14757
|
+
context.addIssue({
|
|
14758
|
+
code: "custom",
|
|
14759
|
+
path: ["packages", index, "name"],
|
|
14760
|
+
message: `"${packageEntry.name}" is already the name of a feature \u2014 one name designates one thing`
|
|
14761
|
+
});
|
|
14762
|
+
}
|
|
14763
|
+
if (packageNames.has(packageEntry.name)) {
|
|
14764
|
+
context.addIssue({
|
|
14765
|
+
code: "custom",
|
|
14766
|
+
path: ["packages", index, "name"],
|
|
14767
|
+
message: `package "${packageEntry.name}" is declared twice`
|
|
14768
|
+
});
|
|
14769
|
+
}
|
|
14770
|
+
packageNames.add(packageEntry.name);
|
|
14771
|
+
});
|
|
14772
|
+
const checkPackageDependencies = (dependsOn, path) => {
|
|
14773
|
+
dependsOn.forEach((dependency, dependencyIndex) => {
|
|
14774
|
+
if (!dependency.startsWith("package:")) return;
|
|
14775
|
+
const target = dependency.slice("package:".length);
|
|
14776
|
+
if (!packageNames.has(target)) {
|
|
14777
|
+
context.addIssue({
|
|
14778
|
+
code: "custom",
|
|
14779
|
+
path: [...path, dependencyIndex],
|
|
14780
|
+
message: `dependency "${dependency}" matches no declared package`
|
|
14781
|
+
});
|
|
14782
|
+
}
|
|
14783
|
+
});
|
|
14784
|
+
};
|
|
14785
|
+
manifest.features.forEach((feature, index) => {
|
|
14786
|
+
checkPackageDependencies(feature.dependsOn, ["features", index, "dependsOn"]);
|
|
14787
|
+
});
|
|
14788
|
+
(manifest.packages ?? []).forEach((packageEntry, index) => {
|
|
14789
|
+
checkPackageDependencies(packageEntry.dependsOn, ["packages", index, "dependsOn"]);
|
|
14790
|
+
});
|
|
14791
|
+
(manifest.tenancy?.rawDbAllowed ?? []).forEach((featureName, index) => {
|
|
14792
|
+
if (!featureNames.has(featureName)) {
|
|
14793
|
+
context.addIssue({
|
|
14794
|
+
code: "custom",
|
|
14795
|
+
path: ["tenancy", "rawDbAllowed", index],
|
|
14796
|
+
message: `"${featureName}" matches no declared feature`
|
|
14797
|
+
});
|
|
14798
|
+
}
|
|
14799
|
+
});
|
|
14696
14800
|
const canonicalOwners = /* @__PURE__ */ new Map();
|
|
14697
14801
|
const aliasOwners = /* @__PURE__ */ new Map();
|
|
14698
14802
|
(manifest.vocabulary?.entries ?? []).forEach((entry, index) => {
|
|
@@ -15447,6 +15551,24 @@ var governedChecksumsByVersion = {
|
|
|
15447
15551
|
"AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
|
|
15448
15552
|
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15449
15553
|
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15554
|
+
},
|
|
15555
|
+
"0.6.0": {
|
|
15556
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15557
|
+
"AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
|
|
15558
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15559
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15560
|
+
},
|
|
15561
|
+
"0.7.0": {
|
|
15562
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15563
|
+
"AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
|
|
15564
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15565
|
+
"scripts/lint-interdits.ts": "d859e132b7020942a5211f5419b10045170dee6afba3f50ca5642570a5c4a683",
|
|
15566
|
+
"scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
|
|
15567
|
+
"scripts/ogygie-impact.ts": "0dd3ff8cdd57e6cff0dfec1c669fce53fde9ae43f0784ed114771c83d1f8b357",
|
|
15568
|
+
"scripts/verify-contracts.ts": "725cae269e78ecdaa1923553c3097d8d96277824b5bd79a9a40284c6246036b7",
|
|
15569
|
+
"scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
|
|
15570
|
+
"scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
|
|
15571
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15450
15572
|
}
|
|
15451
15573
|
};
|
|
15452
15574
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.7.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
- Tenant du produit : **à définir** (le mot du domaine qui possède les
|
|
20
20
|
données). L'alias produit de `TenantContext` est **`<Tenant>Context`**
|
|
21
21
|
(`apps/api/src/context.ts`) : clé TS de colonne `<tenant>Id`, colonne SQL
|
|
22
|
-
**`<tenant>_id`** — à déclarer aussi dans `
|
|
23
|
-
(`TENANT_COLUMN_NAMES`).
|
|
22
|
+
**`<tenant>_id`** — à déclarer aussi dans le bloc `tenancy` du manifeste).
|
|
24
23
|
- Liste fermée du socle autorisé à toucher la base brute : **à définir** (la
|
|
25
24
|
feature qui CONSTRUIT les contextes tenant) — déclarée dans
|
|
26
25
|
`scripts/lint-interdits.ts` (`RAW_DB_SOCLE_ALLOWLIST`). Toute autre feature
|
|
@@ -22,6 +22,13 @@ const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
|
22
22
|
const SELF = fileURLToPath(import.meta.url);
|
|
23
23
|
const SCANNED_DIRS = ["apps", "packages", "scripts"];
|
|
24
24
|
const IGNORED_DIRS = new Set(["node_modules", "dist", ".git"]);
|
|
25
|
+
// Build artifacts of a FRAMEWORK repo: a published package may embed a copy of
|
|
26
|
+
// templates/ before publishing (generated and gitignored, like dist/). Derived
|
|
27
|
+
// from the manifest rather than hard-coded, so this file stays identical in
|
|
28
|
+
// every project — a consumer manifest has no framework block, and the set is
|
|
29
|
+
// simply empty.
|
|
30
|
+
const embeddedTemplateDirs = (frameworkPackagePaths: readonly string[]): Set<string> =>
|
|
31
|
+
new Set(frameworkPackagePaths.map((packagePath) => join(ROOT, packagePath, "templates")));
|
|
25
32
|
const EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs"];
|
|
26
33
|
|
|
27
34
|
const TS_IGNORE_BAN = {
|
|
@@ -29,16 +36,12 @@ const TS_IGNORE_BAN = {
|
|
|
29
36
|
rule: "ts-ignore interdit — @ts-expect-error avec justification si inévitable",
|
|
30
37
|
};
|
|
31
38
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
39
|
+
// Socle exception (AGENTS.md §Multi-tenant): the tenancy feature BUILDS the
|
|
40
|
+
// contexts, so it predates them. Every other feature goes through tenantDb —
|
|
41
|
+
// unless the manifest declares that every table it owns is SHARED, which have
|
|
42
|
+
// no tenant column to filter on. The list is DECLARED at the manifest
|
|
43
|
+
// (`tenancy.rawDbAllowed`), never held here: this file is governed.
|
|
37
44
|
const FEATURES_PREFIX = "apps/api/src/features/";
|
|
38
|
-
// TODO(product): the ONE tenancy socle feature allowed to touch the raw
|
|
39
|
-
// database (it BUILDS the tenant contexts) — declare it in AGENTS.product.md
|
|
40
|
-
// and here. Empty until the socle exists.
|
|
41
|
-
const RAW_DB_SOCLE_ALLOWLIST = new Set<string>([]);
|
|
42
45
|
const RAW_DB_BAN = {
|
|
43
46
|
pattern: /\bctx\.db\b/,
|
|
44
47
|
rule: "ctx.db interdit hors du socle et des features à tables partagées — passer par tenantDb (AGENTS.md §Multi-tenant)",
|
|
@@ -53,11 +56,15 @@ const manifest = OgygieManifestSchema.parse(
|
|
|
53
56
|
JSON.parse(readFileSync(join(ROOT, "ogygie.manifest.json"), "utf8")),
|
|
54
57
|
);
|
|
55
58
|
/** Feature folder names allowed to touch the raw database, derived — not held. */
|
|
59
|
+
const socleAllowlist = new Set(manifest.tenancy?.rawDbAllowed ?? []);
|
|
56
60
|
const rawDbAllowedFolders = new Set(
|
|
57
61
|
manifest.features
|
|
58
|
-
.filter((feature) => mayUseRawDb(feature,
|
|
62
|
+
.filter((feature) => mayUseRawDb(feature, socleAllowlist))
|
|
59
63
|
.map((feature) => feature.folder.split("/").at(-1) ?? feature.name),
|
|
60
64
|
);
|
|
65
|
+
const ignoredBuildDirs = embeddedTemplateDirs(
|
|
66
|
+
(manifest.framework?.packages ?? []).map((packageEntry) => packageEntry.path),
|
|
67
|
+
);
|
|
61
68
|
|
|
62
69
|
const violations: string[] = [];
|
|
63
70
|
|
|
@@ -75,6 +82,7 @@ function scan(dir: string): void {
|
|
|
75
82
|
const entryPath = join(dir, entry);
|
|
76
83
|
const stats = statSync(entryPath);
|
|
77
84
|
if (stats.isDirectory()) {
|
|
85
|
+
if (ignoredBuildDirs.has(entryPath)) continue;
|
|
78
86
|
scan(entryPath);
|
|
79
87
|
continue;
|
|
80
88
|
}
|
|
@@ -28,12 +28,35 @@ import {
|
|
|
28
28
|
import { ogygieError, type OgygieError } from "@ogygie/core/errors";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Tenant column
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* Tenant column of the PRODUCT, read from the manifest (`tenancy.column`) —
|
|
32
|
+
* never a constant in this file, which is governed and therefore identical in
|
|
33
|
+
* every Ogygie project. A table declared `shared: true` must not carry it.
|
|
34
|
+
* No tenancy declared = nothing to guard, and the check stands down instead of
|
|
35
|
+
* pretending.
|
|
35
36
|
*/
|
|
36
|
-
const
|
|
37
|
+
const tenantColumnNames = (): readonly string[] =>
|
|
38
|
+
manifest.tenancy === undefined ? [] : [manifest.tenancy.column];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A project declaring SHARED tables without a tenant column is the one case
|
|
42
|
+
* where silence is dangerous: the check that guards those tables cannot run,
|
|
43
|
+
* and a passing gate would mean nothing. Said out loud rather than skipped —
|
|
44
|
+
* a gate that quietly guards nothing is worse than no gate.
|
|
45
|
+
*/
|
|
46
|
+
function reportUndeclaredTenancy(): void {
|
|
47
|
+
if (manifest.tenancy !== undefined) return;
|
|
48
|
+
const shared = manifest.features.flatMap((feature) => sharedTableNamesOf(feature));
|
|
49
|
+
if (shared.length === 0) return;
|
|
50
|
+
report({
|
|
51
|
+
code: "MANIFEST_TENANCY_UNDECLARED",
|
|
52
|
+
message:
|
|
53
|
+
"Shared tables are declared but the manifest declares no tenant column — the shared-table check cannot guard anything.",
|
|
54
|
+
file: MANIFEST_PATH,
|
|
55
|
+
cause: `tables ${shared.join(", ")} declared shared, tenancy block absent`,
|
|
56
|
+
suggestedFix:
|
|
57
|
+
'Declare `"tenancy": { "column": "<sql_tenant_column>", "rawDbAllowed": [...] }` in the manifest — the value AGENTS.product.md announces.',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
37
60
|
|
|
38
61
|
const ROOT = new URL("..", import.meta.url).pathname;
|
|
39
62
|
const MANIFEST_PATH = "ogygie.manifest.json";
|
|
@@ -71,6 +94,7 @@ if (!parsed.success) {
|
|
|
71
94
|
conclude();
|
|
72
95
|
}
|
|
73
96
|
const manifest = parsed.data;
|
|
97
|
+
reportUndeclaredTenancy();
|
|
74
98
|
|
|
75
99
|
for (const app of manifest.apps) {
|
|
76
100
|
if (!existsSync(join(ROOT, app.path))) {
|
|
@@ -84,6 +108,43 @@ for (const app of manifest.apps) {
|
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
110
|
|
|
111
|
+
// Framework self-description (spec ogygie-standalone-repo B1): every path the
|
|
112
|
+
// block advertises must exist — an agent trusts the manifest INSTEAD of
|
|
113
|
+
// exploring, so a phantom path here is a lie like any other.
|
|
114
|
+
if (manifest.framework !== undefined) {
|
|
115
|
+
const declaredPaths = [
|
|
116
|
+
...manifest.framework.packages.map((pkg) => pkg.path),
|
|
117
|
+
manifest.framework.templates.project,
|
|
118
|
+
manifest.framework.templates.feature,
|
|
119
|
+
manifest.framework.governedFiles,
|
|
120
|
+
];
|
|
121
|
+
for (const declaredPath of declaredPaths) {
|
|
122
|
+
if (!existsSync(join(ROOT, declaredPath))) {
|
|
123
|
+
report({
|
|
124
|
+
code: "MANIFEST_FRAMEWORK_PATH_NOT_FOUND",
|
|
125
|
+
message: `The framework block declares ${declaredPath}, which does not exist.`,
|
|
126
|
+
file: MANIFEST_PATH,
|
|
127
|
+
cause: `framework path ${declaredPath} absent from disk`,
|
|
128
|
+
suggestedFix: "Create the folder/module or fix the framework block.",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A declared package whose folder is absent is a lying map, exactly like a
|
|
135
|
+
// phantom app (spec manifest-packages B1).
|
|
136
|
+
for (const productPackage of manifest.packages ?? []) {
|
|
137
|
+
if (!existsSync(join(ROOT, productPackage.path))) {
|
|
138
|
+
report({
|
|
139
|
+
code: "MANIFEST_PACKAGE_NOT_FOUND",
|
|
140
|
+
message: `Package "${productPackage.name}" declares folder ${productPackage.path} which does not exist.`,
|
|
141
|
+
file: MANIFEST_PATH,
|
|
142
|
+
cause: `packages[].path=${productPackage.path} absent from disk`,
|
|
143
|
+
suggestedFix: "Create the folder or fix the manifest entry.",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
87
148
|
const apiApp = manifest.apps.find((app) => app.type === "api");
|
|
88
149
|
const featureNames = new Set(manifest.features.map((feature) => feature.name));
|
|
89
150
|
|
|
@@ -224,7 +285,7 @@ for (const feature of manifest.features) {
|
|
|
224
285
|
for (const finding of findTenantColumnsInSharedTables(
|
|
225
286
|
dbSchemaContent,
|
|
226
287
|
sharedTableNamesOf(feature),
|
|
227
|
-
|
|
288
|
+
tenantColumnNames(),
|
|
228
289
|
)) {
|
|
229
290
|
report({
|
|
230
291
|
code: "SHARED_TABLE_WITH_TENANT_COLUMN",
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
parseKnownDecisionIds,
|
|
30
30
|
} from "@ogygie/core/decision-refs";
|
|
31
31
|
import { OgygieManifestSchema } from "@ogygie/core/manifest";
|
|
32
|
+
import { findSpecOwner, SPEC_OWNER_FIX } from "@ogygie/core/manifest-owners";
|
|
32
33
|
import { ogygieError, type OgygieError } from "@ogygie/core/errors";
|
|
33
34
|
|
|
34
35
|
const ROOT = new URL("..", import.meta.url).pathname;
|
|
@@ -46,7 +47,6 @@ function report(input: Parameters<typeof ogygieError>[0]): void {
|
|
|
46
47
|
const manifest = OgygieManifestSchema.parse(
|
|
47
48
|
JSON.parse(readFileSync(join(ROOT, "ogygie.manifest.json"), "utf8")),
|
|
48
49
|
);
|
|
49
|
-
const apiApp = manifest.apps.find((app) => app.type === "api");
|
|
50
50
|
|
|
51
51
|
const specFiles = readdirSync(join(ROOT, SPECS_FOLDER)).filter(
|
|
52
52
|
(fileName) => fileName.endsWith(".spec.md") && !fileName.startsWith("_"),
|
|
@@ -124,18 +124,15 @@ for (const fileName of specFiles) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
130
|
-
const
|
|
131
|
-
apiApp !== undefined && manifestFeature !== undefined
|
|
132
|
-
? join(ROOT, apiApp.path, manifestFeature.folder)
|
|
133
|
-
: null;
|
|
127
|
+
// The map confirms a spec through a feature OR a product package: the rule
|
|
128
|
+
// is unchanged, the natures able to confirm are two (spec manifest-packages).
|
|
129
|
+
const owner = findSpecOwner(manifest, frontmatter.id);
|
|
130
|
+
const ownerDir = owner?.folder === null || owner === undefined ? null : join(ROOT, owner.folder);
|
|
134
131
|
|
|
135
132
|
if (frontmatter.status === "draft") {
|
|
136
133
|
const implemented =
|
|
137
|
-
(
|
|
138
|
-
(
|
|
134
|
+
(owner !== undefined && owner.status !== "skeleton") ||
|
|
135
|
+
(ownerDir !== null && existsSync(ownerDir));
|
|
139
136
|
if (implemented) {
|
|
140
137
|
report({
|
|
141
138
|
code: "SPEC_DRAFT_ALREADY_IMPLEMENTED",
|
|
@@ -160,14 +157,16 @@ for (const fileName of specFiles) {
|
|
|
160
157
|
"Implement the deferred behaviors (their blocking features now exist?) and empty `deferred`, or keep the spec in validated.",
|
|
161
158
|
});
|
|
162
159
|
}
|
|
163
|
-
if (
|
|
160
|
+
if (owner === undefined || owner.status !== "delivered") {
|
|
164
161
|
report({
|
|
165
162
|
code: "SPEC_IMPLEMENTED_NOT_DELIVERED",
|
|
166
163
|
message: `Spec ${fileName} is implemented but the manifest does not say status "delivered".`,
|
|
167
164
|
file: specPath,
|
|
168
|
-
cause:
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
cause:
|
|
166
|
+
owner === undefined
|
|
167
|
+
? "no feature and no package of that name in the manifest"
|
|
168
|
+
: `${owner.kind} "${owner.name}" status=${owner.status}`,
|
|
169
|
+
suggestedFix: SPEC_OWNER_FIX,
|
|
171
170
|
});
|
|
172
171
|
}
|
|
173
172
|
}
|