@ogygie/cli 0.5.3 → 0.6.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
|
@@ -14602,6 +14602,7 @@ import { join } from "node:path";
|
|
|
14602
14602
|
|
|
14603
14603
|
// ../ogygie-core/src/manifest.schema.ts
|
|
14604
14604
|
var FeatureNameSchema = external_exports.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "kebab-case required");
|
|
14605
|
+
var PackageDependencySchema = external_exports.string().regex(/^package:[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "expected package:<kebab-case-name>");
|
|
14605
14606
|
var HttpRouteSchema = external_exports.object({
|
|
14606
14607
|
method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
14607
14608
|
path: external_exports.string().startsWith("/api/"),
|
|
@@ -14626,7 +14627,9 @@ var FeatureManifestSchema = external_exports.object({
|
|
|
14626
14627
|
dbTables: external_exports.array(DbTableDeclarationSchema).describe(
|
|
14627
14628
|
"Drizzle tables owned by this feature (one table = one owner). String = tenant-scoped; { name, shared: true } = shared referential"
|
|
14628
14629
|
),
|
|
14629
|
-
dependsOn: external_exports.array(FeatureNameSchema).describe(
|
|
14630
|
+
dependsOn: external_exports.array(external_exports.union([FeatureNameSchema, PackageDependencySchema])).describe(
|
|
14631
|
+
"Features imported through their index.ts, and product packages as package:<name> \u2014 nothing else"
|
|
14632
|
+
),
|
|
14630
14633
|
status: external_exports.enum(["skeleton", "in-progress", "delivered"])
|
|
14631
14634
|
});
|
|
14632
14635
|
var OgygieVersionSchema = external_exports.string().regex(/^\d+\.\d+\.\d+$/, "expected a plain x.y.z version");
|
|
@@ -14654,6 +14657,14 @@ var VocabularyEntrySchema = external_exports.object({
|
|
|
14654
14657
|
var ProductVocabularySchema = external_exports.object({
|
|
14655
14658
|
entries: external_exports.array(VocabularyEntrySchema)
|
|
14656
14659
|
});
|
|
14660
|
+
var PackageManifestSchema = external_exports.object({
|
|
14661
|
+
name: FeatureNameSchema,
|
|
14662
|
+
path: external_exports.string().describe("Folder relative to the repo root, e.g. packages/solver"),
|
|
14663
|
+
intent: external_exports.string().min(20).max(300).describe("The package's 'why' in 1-3 sentences"),
|
|
14664
|
+
spec: external_exports.string().nullable(),
|
|
14665
|
+
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"),
|
|
14666
|
+
status: external_exports.enum(["skeleton", "in-progress", "delivered"])
|
|
14667
|
+
});
|
|
14657
14668
|
var OgygieManifestSchema = external_exports.object({
|
|
14658
14669
|
version: external_exports.literal(1),
|
|
14659
14670
|
product: external_exports.object({
|
|
@@ -14684,6 +14695,7 @@ var OgygieManifestSchema = external_exports.object({
|
|
|
14684
14695
|
})
|
|
14685
14696
|
),
|
|
14686
14697
|
features: external_exports.array(FeatureManifestSchema),
|
|
14698
|
+
packages: external_exports.array(PackageManifestSchema).optional().describe("Product code living outside every app \u2014 libraries, engines. Absent = the project has none"),
|
|
14687
14699
|
vocabulary: ProductVocabularySchema.optional().describe(
|
|
14688
14700
|
"The product's closed vocabulary \u2014 declared here, enforced by `ogygie verify-vocabulary`. Absent = gate silent"
|
|
14689
14701
|
),
|
|
@@ -14693,6 +14705,44 @@ var OgygieManifestSchema = external_exports.object({
|
|
|
14693
14705
|
specsFolder: external_exports.literal("specs/")
|
|
14694
14706
|
})
|
|
14695
14707
|
}).superRefine((manifest, context) => {
|
|
14708
|
+
const featureNames = new Set(manifest.features.map((feature) => feature.name));
|
|
14709
|
+
const packageNames = /* @__PURE__ */ new Set();
|
|
14710
|
+
(manifest.packages ?? []).forEach((packageEntry, index) => {
|
|
14711
|
+
if (featureNames.has(packageEntry.name)) {
|
|
14712
|
+
context.addIssue({
|
|
14713
|
+
code: "custom",
|
|
14714
|
+
path: ["packages", index, "name"],
|
|
14715
|
+
message: `"${packageEntry.name}" is already the name of a feature \u2014 one name designates one thing`
|
|
14716
|
+
});
|
|
14717
|
+
}
|
|
14718
|
+
if (packageNames.has(packageEntry.name)) {
|
|
14719
|
+
context.addIssue({
|
|
14720
|
+
code: "custom",
|
|
14721
|
+
path: ["packages", index, "name"],
|
|
14722
|
+
message: `package "${packageEntry.name}" is declared twice`
|
|
14723
|
+
});
|
|
14724
|
+
}
|
|
14725
|
+
packageNames.add(packageEntry.name);
|
|
14726
|
+
});
|
|
14727
|
+
const checkPackageDependencies = (dependsOn, path) => {
|
|
14728
|
+
dependsOn.forEach((dependency, dependencyIndex) => {
|
|
14729
|
+
if (!dependency.startsWith("package:")) return;
|
|
14730
|
+
const target = dependency.slice("package:".length);
|
|
14731
|
+
if (!packageNames.has(target)) {
|
|
14732
|
+
context.addIssue({
|
|
14733
|
+
code: "custom",
|
|
14734
|
+
path: [...path, dependencyIndex],
|
|
14735
|
+
message: `dependency "${dependency}" matches no declared package`
|
|
14736
|
+
});
|
|
14737
|
+
}
|
|
14738
|
+
});
|
|
14739
|
+
};
|
|
14740
|
+
manifest.features.forEach((feature, index) => {
|
|
14741
|
+
checkPackageDependencies(feature.dependsOn, ["features", index, "dependsOn"]);
|
|
14742
|
+
});
|
|
14743
|
+
(manifest.packages ?? []).forEach((packageEntry, index) => {
|
|
14744
|
+
checkPackageDependencies(packageEntry.dependsOn, ["packages", index, "dependsOn"]);
|
|
14745
|
+
});
|
|
14696
14746
|
const canonicalOwners = /* @__PURE__ */ new Map();
|
|
14697
14747
|
const aliasOwners = /* @__PURE__ */ new Map();
|
|
14698
14748
|
(manifest.vocabulary?.entries ?? []).forEach((entry, index) => {
|
|
@@ -14963,16 +15013,21 @@ function initProject(deps, productName2) {
|
|
|
14963
15013
|
}
|
|
14964
15014
|
|
|
14965
15015
|
// src/commands/studio.ts
|
|
15016
|
+
import { createRequire } from "node:module";
|
|
15017
|
+
import { join as join4 } from "node:path";
|
|
15018
|
+
import { pathToFileURL } from "node:url";
|
|
14966
15019
|
var PUBLISHED_STUDIO_SERVER = "@ogygie/studio/server";
|
|
14967
|
-
async function
|
|
14968
|
-
|
|
15020
|
+
async function loadStudioFrom(projectRoot) {
|
|
15021
|
+
const resolveFromProject = createRequire(join4(projectRoot, "package.json"));
|
|
15022
|
+
const entryPoint = resolveFromProject.resolve(PUBLISHED_STUDIO_SERVER);
|
|
15023
|
+
return await import(pathToFileURL(entryPoint).href);
|
|
14969
15024
|
}
|
|
14970
15025
|
function isModuleNotFound(thrown) {
|
|
14971
15026
|
const code = thrown?.code;
|
|
14972
15027
|
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || typeof thrown === "object" && thrown !== null && thrown instanceof Error && /Cannot find (package|module)/i.test(thrown.message);
|
|
14973
15028
|
}
|
|
14974
15029
|
async function runStudio(deps) {
|
|
14975
|
-
const importStudio = deps.importStudio ??
|
|
15030
|
+
const importStudio = deps.importStudio ?? (() => loadStudioFrom(deps.projectRoot));
|
|
14976
15031
|
let studioServer;
|
|
14977
15032
|
try {
|
|
14978
15033
|
studioServer = await importStudio();
|
|
@@ -15003,7 +15058,7 @@ async function runStudio(deps) {
|
|
|
15003
15058
|
|
|
15004
15059
|
// src/commands/upgrade.ts
|
|
15005
15060
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
15006
|
-
import { dirname as dirname2, join as
|
|
15061
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
15007
15062
|
|
|
15008
15063
|
// src/diff.ts
|
|
15009
15064
|
function diffLines(oldText, newText) {
|
|
@@ -15071,7 +15126,7 @@ function compareVersions(a, b) {
|
|
|
15071
15126
|
|
|
15072
15127
|
// src/commands/upgrade.ts
|
|
15073
15128
|
function reportMissingGates(deps) {
|
|
15074
|
-
const packageJsonPath =
|
|
15129
|
+
const packageJsonPath = join5(deps.projectRoot, "package.json");
|
|
15075
15130
|
if (!existsSync4(packageJsonPath)) return;
|
|
15076
15131
|
let scripts;
|
|
15077
15132
|
try {
|
|
@@ -15126,7 +15181,7 @@ async function upgradeProject(deps) {
|
|
|
15126
15181
|
for (const governed of governedFiles) {
|
|
15127
15182
|
const expected = installedChecksums[governed.targetPath];
|
|
15128
15183
|
if (expected === void 0) continue;
|
|
15129
|
-
const absolutePath =
|
|
15184
|
+
const absolutePath = join5(deps.projectRoot, governed.targetPath);
|
|
15130
15185
|
const actual = existsSync4(absolutePath) ? sha256Hex(readFileSync4(absolutePath, "utf8")) : "<absent>";
|
|
15131
15186
|
if (actual !== expected) {
|
|
15132
15187
|
throwOgygieError({
|
|
@@ -15142,10 +15197,10 @@ async function upgradeProject(deps) {
|
|
|
15142
15197
|
const diffBlocks = [];
|
|
15143
15198
|
for (const governed of governedFiles) {
|
|
15144
15199
|
const content = readFileSync4(
|
|
15145
|
-
|
|
15200
|
+
join5(deps.templatesDir, governed.templatePath),
|
|
15146
15201
|
"utf8"
|
|
15147
15202
|
);
|
|
15148
|
-
const absolutePath =
|
|
15203
|
+
const absolutePath = join5(deps.projectRoot, governed.targetPath);
|
|
15149
15204
|
const current = existsSync4(absolutePath) ? readFileSync4(absolutePath, "utf8") : "";
|
|
15150
15205
|
const diff = diffLines(current, content);
|
|
15151
15206
|
if (diff !== "") {
|
|
@@ -15166,7 +15221,7 @@ ${diff}`);
|
|
|
15166
15221
|
};
|
|
15167
15222
|
}
|
|
15168
15223
|
for (const write of plannedWrites) {
|
|
15169
|
-
const absolutePath =
|
|
15224
|
+
const absolutePath = join5(deps.projectRoot, write.targetPath);
|
|
15170
15225
|
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
15171
15226
|
writeFileSync3(absolutePath, write.content);
|
|
15172
15227
|
}
|
|
@@ -15188,7 +15243,7 @@ ${diff}`);
|
|
|
15188
15243
|
|
|
15189
15244
|
// src/commands/verify-vocabulary.ts
|
|
15190
15245
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
15191
|
-
import { join as
|
|
15246
|
+
import { join as join6, relative, sep } from "node:path";
|
|
15192
15247
|
|
|
15193
15248
|
// ../ogygie-core/src/vocabulary.ts
|
|
15194
15249
|
var TOKEN_PATTERN = new RegExp("\\p{L}[\\p{L}\\p{N}]*(?:[-_$][\\p{L}\\p{N}]+)*", "gu");
|
|
@@ -15295,7 +15350,7 @@ function auditProductVocabulary(input) {
|
|
|
15295
15350
|
var CODE_EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"];
|
|
15296
15351
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
15297
15352
|
function readManifest(projectRoot) {
|
|
15298
|
-
const manifestPath =
|
|
15353
|
+
const manifestPath = join6(projectRoot, MANIFEST_FILE_NAME);
|
|
15299
15354
|
if (!existsSync5(manifestPath)) {
|
|
15300
15355
|
throwOgygieError({
|
|
15301
15356
|
code: "OGYGIE_CLI_USAGE",
|
|
@@ -15320,13 +15375,13 @@ function readManifest(projectRoot) {
|
|
|
15320
15375
|
return parsed.data;
|
|
15321
15376
|
}
|
|
15322
15377
|
function collectCodeFiles(projectRoot, folder) {
|
|
15323
|
-
const absoluteFolder =
|
|
15378
|
+
const absoluteFolder = join6(projectRoot, folder);
|
|
15324
15379
|
if (!existsSync5(absoluteFolder)) return [];
|
|
15325
15380
|
const files = [];
|
|
15326
15381
|
const walk = (directory) => {
|
|
15327
15382
|
for (const entry of readdirSync2(directory)) {
|
|
15328
15383
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
15329
|
-
const entryPath =
|
|
15384
|
+
const entryPath = join6(directory, entry);
|
|
15330
15385
|
if (statSync2(entryPath).isDirectory()) {
|
|
15331
15386
|
walk(entryPath);
|
|
15332
15387
|
continue;
|
|
@@ -15362,9 +15417,9 @@ function verifyVocabulary(deps) {
|
|
|
15362
15417
|
const sourceFiles = productCodeFoldersOf(manifest).flatMap(
|
|
15363
15418
|
(folder) => collectCodeFiles(deps.projectRoot, folder)
|
|
15364
15419
|
);
|
|
15365
|
-
const governed = governedFiles.filter((file2) => existsSync5(
|
|
15420
|
+
const governed = governedFiles.filter((file2) => existsSync5(join6(deps.projectRoot, file2.targetPath))).map((file2) => ({
|
|
15366
15421
|
path: file2.targetPath,
|
|
15367
|
-
content: readFileSync5(
|
|
15422
|
+
content: readFileSync5(join6(deps.projectRoot, file2.targetPath), "utf8")
|
|
15368
15423
|
}));
|
|
15369
15424
|
return auditProductVocabulary({
|
|
15370
15425
|
vocabulary: manifest.vocabulary,
|
|
@@ -15436,27 +15491,39 @@ var governedChecksumsByVersion = {
|
|
|
15436
15491
|
"AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
|
|
15437
15492
|
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15438
15493
|
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15494
|
+
},
|
|
15495
|
+
"0.5.4": {
|
|
15496
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15497
|
+
"AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
|
|
15498
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15499
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15500
|
+
},
|
|
15501
|
+
"0.6.0": {
|
|
15502
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15503
|
+
"AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
|
|
15504
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15505
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15439
15506
|
}
|
|
15440
15507
|
};
|
|
15441
15508
|
|
|
15442
15509
|
// src/paths.ts
|
|
15443
15510
|
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
|
|
15444
|
-
import { dirname as dirname3, join as
|
|
15511
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
15445
15512
|
import { fileURLToPath } from "node:url";
|
|
15446
|
-
var PACKAGE_ROOT =
|
|
15513
|
+
var PACKAGE_ROOT = join7(dirname3(fileURLToPath(import.meta.url)), "..");
|
|
15447
15514
|
var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
|
|
15448
15515
|
function cliVersion() {
|
|
15449
15516
|
return PackageVersionSchema2.parse(
|
|
15450
|
-
JSON.parse(readFileSync6(
|
|
15517
|
+
JSON.parse(readFileSync6(join7(PACKAGE_ROOT, "package.json"), "utf8"))
|
|
15451
15518
|
).version;
|
|
15452
15519
|
}
|
|
15453
15520
|
function templatesDir() {
|
|
15454
|
-
const embedded =
|
|
15521
|
+
const embedded = join7(PACKAGE_ROOT, "templates");
|
|
15455
15522
|
if (existsSync6(embedded)) return embedded;
|
|
15456
15523
|
return repoTemplatesDir();
|
|
15457
15524
|
}
|
|
15458
15525
|
function repoTemplatesDir() {
|
|
15459
|
-
return
|
|
15526
|
+
return join7(PACKAGE_ROOT, "..", "..", "templates");
|
|
15460
15527
|
}
|
|
15461
15528
|
|
|
15462
15529
|
// src/cli.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.6.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -84,6 +84,20 @@ for (const app of manifest.apps) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// A declared package whose folder is absent is a lying map, exactly like a
|
|
88
|
+
// phantom app (spec manifest-packages B1).
|
|
89
|
+
for (const productPackage of manifest.packages ?? []) {
|
|
90
|
+
if (!existsSync(join(ROOT, productPackage.path))) {
|
|
91
|
+
report({
|
|
92
|
+
code: "MANIFEST_PACKAGE_NOT_FOUND",
|
|
93
|
+
message: `Package "${productPackage.name}" declares folder ${productPackage.path} which does not exist.`,
|
|
94
|
+
file: MANIFEST_PATH,
|
|
95
|
+
cause: `packages[].path=${productPackage.path} absent from disk`,
|
|
96
|
+
suggestedFix: "Create the folder or fix the manifest entry.",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
const apiApp = manifest.apps.find((app) => app.type === "api");
|
|
88
102
|
const featureNames = new Set(manifest.features.map((feature) => feature.name));
|
|
89
103
|
|
|
@@ -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
|
}
|