@ogygie/cli 0.7.0 → 0.9.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 +131 -30
- package/package.json +2 -2
- package/templates/project/AGENTS.md +27 -12
package/dist/cli.js
CHANGED
|
@@ -14649,9 +14649,14 @@ var HttpRouteSchema = external_exports.object({
|
|
|
14649
14649
|
"Cache roots this mutation stales BEYOND its own path root (cross-feature dependency, read by the manifest-derived client \u2014 never guessed in a screen). Each value must be an existing feature name or route root"
|
|
14650
14650
|
)
|
|
14651
14651
|
});
|
|
14652
|
+
var SharedTableKindSchema = external_exports.enum(["reference", "ownerless"]);
|
|
14652
14653
|
var DbTableDeclarationSchema = external_exports.union([
|
|
14653
14654
|
external_exports.string(),
|
|
14654
|
-
external_exports.object({
|
|
14655
|
+
external_exports.object({
|
|
14656
|
+
name: external_exports.string(),
|
|
14657
|
+
shared: external_exports.literal(true),
|
|
14658
|
+
sharedKind: SharedTableKindSchema.optional()
|
|
14659
|
+
})
|
|
14655
14660
|
]);
|
|
14656
14661
|
var FeatureManifestSchema = external_exports.object({
|
|
14657
14662
|
name: FeatureNameSchema,
|
|
@@ -14674,7 +14679,21 @@ var FrameworkManifestSchema = external_exports.object({
|
|
|
14674
14679
|
external_exports.object({
|
|
14675
14680
|
name: external_exports.string().describe("Published npm name, e.g. @ogygie/core"),
|
|
14676
14681
|
path: external_exports.string().describe("Folder relative to the repo root"),
|
|
14677
|
-
intent: external_exports.string().min(20)
|
|
14682
|
+
intent: external_exports.string().min(20),
|
|
14683
|
+
/**
|
|
14684
|
+
* The spec describing this package, when one does. Named EXPLICITLY
|
|
14685
|
+
* because a published npm name (`@ogygie/mcp`) is not a spec id
|
|
14686
|
+
* (`ogygie-mcp`) — the other two natures of entry match by name, this
|
|
14687
|
+
* one cannot. Several packages may point at the same spec: a spec of
|
|
14688
|
+
* the framework often spans the core and the CLI.
|
|
14689
|
+
*/
|
|
14690
|
+
spec: external_exports.string().nullable(),
|
|
14691
|
+
/**
|
|
14692
|
+
* How far along, in the SAME words as the other natures. Required, and
|
|
14693
|
+
* deliberately without default: a lifecycle gate that guesses a status
|
|
14694
|
+
* confirms specs nobody delivered.
|
|
14695
|
+
*/
|
|
14696
|
+
status: external_exports.enum(["skeleton", "in-progress", "delivered"])
|
|
14678
14697
|
})
|
|
14679
14698
|
),
|
|
14680
14699
|
templates: external_exports.object({
|
|
@@ -14769,6 +14788,22 @@ var OgygieManifestSchema = external_exports.object({
|
|
|
14769
14788
|
}
|
|
14770
14789
|
packageNames.add(packageEntry.name);
|
|
14771
14790
|
});
|
|
14791
|
+
const specIdOf = (specPath) => (specPath.split("/").pop() ?? specPath).replace(/\.spec\.md$/, "");
|
|
14792
|
+
const ownedSpecIds = /* @__PURE__ */ new Set([
|
|
14793
|
+
...manifest.features.map((feature) => feature.name),
|
|
14794
|
+
...packageNames
|
|
14795
|
+
]);
|
|
14796
|
+
(manifest.framework?.packages ?? []).forEach((packageEntry, index) => {
|
|
14797
|
+
if (packageEntry.spec === null) return;
|
|
14798
|
+
const specId = specIdOf(packageEntry.spec);
|
|
14799
|
+
if (ownedSpecIds.has(specId)) {
|
|
14800
|
+
context.addIssue({
|
|
14801
|
+
code: "custom",
|
|
14802
|
+
path: ["framework", "packages", index, "spec"],
|
|
14803
|
+
message: `spec "${specId}" is already owned by a feature or a product package \u2014 one spec, one owner`
|
|
14804
|
+
});
|
|
14805
|
+
}
|
|
14806
|
+
});
|
|
14772
14807
|
const checkPackageDependencies = (dependsOn, path) => {
|
|
14773
14808
|
dependsOn.forEach((dependency, dependencyIndex) => {
|
|
14774
14809
|
if (!dependency.startsWith("package:")) return;
|
|
@@ -15110,9 +15145,57 @@ async function runStudio(deps) {
|
|
|
15110
15145
|
});
|
|
15111
15146
|
}
|
|
15112
15147
|
|
|
15148
|
+
// src/commands/mcp.ts
|
|
15149
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
15150
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
15151
|
+
import { join as join5 } from "node:path";
|
|
15152
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
15153
|
+
var PUBLISHED_MCP_SERVER = "@ogygie/mcp/server";
|
|
15154
|
+
async function loadMcpFrom(projectRoot) {
|
|
15155
|
+
const resolveFromProject = createRequire2(join5(projectRoot, "package.json"));
|
|
15156
|
+
const entryPoint = resolveFromProject.resolve(PUBLISHED_MCP_SERVER);
|
|
15157
|
+
return await import(pathToFileURL2(entryPoint).href);
|
|
15158
|
+
}
|
|
15159
|
+
function isModuleNotFound2(thrown) {
|
|
15160
|
+
const code = thrown?.code;
|
|
15161
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || thrown instanceof Error && /Cannot find (package|module)/i.test(thrown.message);
|
|
15162
|
+
}
|
|
15163
|
+
async function runMcp(deps) {
|
|
15164
|
+
const importMcp = deps.importMcp ?? (() => loadMcpFrom(deps.projectRoot));
|
|
15165
|
+
let mcpServer;
|
|
15166
|
+
try {
|
|
15167
|
+
mcpServer = await importMcp();
|
|
15168
|
+
} catch (thrown) {
|
|
15169
|
+
const reason = thrown instanceof Error ? thrown.message : String(thrown);
|
|
15170
|
+
if (isModuleNotFound2(thrown)) {
|
|
15171
|
+
throwOgygieError({
|
|
15172
|
+
code: "MCP_NOT_INSTALLED",
|
|
15173
|
+
message: "The Ogygie MCP server is not installed in this project.",
|
|
15174
|
+
file: "package.json",
|
|
15175
|
+
cause: `${PUBLISHED_MCP_SERVER} cannot be resolved: ${reason}`,
|
|
15176
|
+
suggestedFix: `Install it at the framework's own version (lockstep D2): pnpm add -Dw @ogygie/mcp@${deps.cliVersion}`
|
|
15177
|
+
});
|
|
15178
|
+
}
|
|
15179
|
+
throwOgygieError({
|
|
15180
|
+
code: "MCP_LOAD_FAILED",
|
|
15181
|
+
message: "The Ogygie MCP server is installed but could not be loaded.",
|
|
15182
|
+
file: "package.json",
|
|
15183
|
+
cause: reason,
|
|
15184
|
+
suggestedFix: "Reinstalling will not help: this is a framework defect. Report it with this cause, and check that @ogygie/mcp matches @ogygie/cli (lockstep D2)."
|
|
15185
|
+
});
|
|
15186
|
+
}
|
|
15187
|
+
await mcpServer.startOgygieMcp({
|
|
15188
|
+
argv: deps.argv,
|
|
15189
|
+
cwd: deps.projectRoot,
|
|
15190
|
+
version: deps.cliVersion,
|
|
15191
|
+
token: process.env["OGYGIE_MCP_TOKEN"],
|
|
15192
|
+
readConfigFile: (absolutePath) => readFileSync4(absolutePath, "utf8")
|
|
15193
|
+
});
|
|
15194
|
+
}
|
|
15195
|
+
|
|
15113
15196
|
// src/commands/upgrade.ts
|
|
15114
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as
|
|
15115
|
-
import { dirname as dirname2, join as
|
|
15197
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
15198
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
15116
15199
|
|
|
15117
15200
|
// src/diff.ts
|
|
15118
15201
|
function diffLines(oldText, newText) {
|
|
@@ -15180,11 +15263,11 @@ function compareVersions(a, b) {
|
|
|
15180
15263
|
|
|
15181
15264
|
// src/commands/upgrade.ts
|
|
15182
15265
|
function reportMissingGates(deps) {
|
|
15183
|
-
const packageJsonPath =
|
|
15266
|
+
const packageJsonPath = join6(deps.projectRoot, "package.json");
|
|
15184
15267
|
if (!existsSync4(packageJsonPath)) return;
|
|
15185
15268
|
let scripts;
|
|
15186
15269
|
try {
|
|
15187
|
-
const parsed = JSON.parse(
|
|
15270
|
+
const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
|
|
15188
15271
|
const scriptsValue = typeof parsed === "object" && parsed !== null ? parsed.scripts : void 0;
|
|
15189
15272
|
if (typeof scriptsValue === "object" && scriptsValue !== null) {
|
|
15190
15273
|
scripts = scriptsValue;
|
|
@@ -15235,8 +15318,8 @@ async function upgradeProject(deps) {
|
|
|
15235
15318
|
for (const governed of governedFiles) {
|
|
15236
15319
|
const expected = installedChecksums[governed.targetPath];
|
|
15237
15320
|
if (expected === void 0) continue;
|
|
15238
|
-
const absolutePath =
|
|
15239
|
-
const actual = existsSync4(absolutePath) ? sha256Hex(
|
|
15321
|
+
const absolutePath = join6(deps.projectRoot, governed.targetPath);
|
|
15322
|
+
const actual = existsSync4(absolutePath) ? sha256Hex(readFileSync5(absolutePath, "utf8")) : "<absent>";
|
|
15240
15323
|
if (actual !== expected) {
|
|
15241
15324
|
throwOgygieError({
|
|
15242
15325
|
code: "OGYGIE_GOVERNED_FILE_MODIFIED",
|
|
@@ -15250,12 +15333,12 @@ async function upgradeProject(deps) {
|
|
|
15250
15333
|
const plannedWrites = [];
|
|
15251
15334
|
const diffBlocks = [];
|
|
15252
15335
|
for (const governed of governedFiles) {
|
|
15253
|
-
const content =
|
|
15254
|
-
|
|
15336
|
+
const content = readFileSync5(
|
|
15337
|
+
join6(deps.templatesDir, governed.templatePath),
|
|
15255
15338
|
"utf8"
|
|
15256
15339
|
);
|
|
15257
|
-
const absolutePath =
|
|
15258
|
-
const current = existsSync4(absolutePath) ?
|
|
15340
|
+
const absolutePath = join6(deps.projectRoot, governed.targetPath);
|
|
15341
|
+
const current = existsSync4(absolutePath) ? readFileSync5(absolutePath, "utf8") : "";
|
|
15259
15342
|
const diff = diffLines(current, content);
|
|
15260
15343
|
if (diff !== "") {
|
|
15261
15344
|
plannedWrites.push({ targetPath: governed.targetPath, content });
|
|
@@ -15275,7 +15358,7 @@ ${diff}`);
|
|
|
15275
15358
|
};
|
|
15276
15359
|
}
|
|
15277
15360
|
for (const write of plannedWrites) {
|
|
15278
|
-
const absolutePath =
|
|
15361
|
+
const absolutePath = join6(deps.projectRoot, write.targetPath);
|
|
15279
15362
|
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
15280
15363
|
writeFileSync3(absolutePath, write.content);
|
|
15281
15364
|
}
|
|
@@ -15296,8 +15379,8 @@ ${diff}`);
|
|
|
15296
15379
|
}
|
|
15297
15380
|
|
|
15298
15381
|
// src/commands/verify-vocabulary.ts
|
|
15299
|
-
import { existsSync as existsSync5, readFileSync as
|
|
15300
|
-
import { join as
|
|
15382
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
15383
|
+
import { join as join7, relative, sep } from "node:path";
|
|
15301
15384
|
|
|
15302
15385
|
// ../ogygie-core/src/vocabulary.ts
|
|
15303
15386
|
var TOKEN_PATTERN = new RegExp("\\p{L}[\\p{L}\\p{N}]*(?:[-_$][\\p{L}\\p{N}]+)*", "gu");
|
|
@@ -15404,7 +15487,7 @@ function auditProductVocabulary(input) {
|
|
|
15404
15487
|
var CODE_EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"];
|
|
15405
15488
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
15406
15489
|
function readManifest(projectRoot) {
|
|
15407
|
-
const manifestPath =
|
|
15490
|
+
const manifestPath = join7(projectRoot, MANIFEST_FILE_NAME);
|
|
15408
15491
|
if (!existsSync5(manifestPath)) {
|
|
15409
15492
|
throwOgygieError({
|
|
15410
15493
|
code: "OGYGIE_CLI_USAGE",
|
|
@@ -15415,7 +15498,7 @@ function readManifest(projectRoot) {
|
|
|
15415
15498
|
});
|
|
15416
15499
|
}
|
|
15417
15500
|
const parsed = OgygieManifestSchema.safeParse(
|
|
15418
|
-
JSON.parse(
|
|
15501
|
+
JSON.parse(readFileSync6(manifestPath, "utf8"))
|
|
15419
15502
|
);
|
|
15420
15503
|
if (!parsed.success) {
|
|
15421
15504
|
throwOgygieError({
|
|
@@ -15429,13 +15512,13 @@ function readManifest(projectRoot) {
|
|
|
15429
15512
|
return parsed.data;
|
|
15430
15513
|
}
|
|
15431
15514
|
function collectCodeFiles(projectRoot, folder) {
|
|
15432
|
-
const absoluteFolder =
|
|
15515
|
+
const absoluteFolder = join7(projectRoot, folder);
|
|
15433
15516
|
if (!existsSync5(absoluteFolder)) return [];
|
|
15434
15517
|
const files = [];
|
|
15435
15518
|
const walk = (directory) => {
|
|
15436
15519
|
for (const entry of readdirSync2(directory)) {
|
|
15437
15520
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
15438
|
-
const entryPath =
|
|
15521
|
+
const entryPath = join7(directory, entry);
|
|
15439
15522
|
if (statSync2(entryPath).isDirectory()) {
|
|
15440
15523
|
walk(entryPath);
|
|
15441
15524
|
continue;
|
|
@@ -15443,7 +15526,7 @@ function collectCodeFiles(projectRoot, folder) {
|
|
|
15443
15526
|
if (!CODE_EXTENSIONS.some((extension) => entry.endsWith(extension))) continue;
|
|
15444
15527
|
files.push({
|
|
15445
15528
|
path: relative(projectRoot, entryPath).split(sep).join("/"),
|
|
15446
|
-
content:
|
|
15529
|
+
content: readFileSync6(entryPath, "utf8")
|
|
15447
15530
|
});
|
|
15448
15531
|
}
|
|
15449
15532
|
};
|
|
@@ -15471,9 +15554,9 @@ function verifyVocabulary(deps) {
|
|
|
15471
15554
|
const sourceFiles = productCodeFoldersOf(manifest).flatMap(
|
|
15472
15555
|
(folder) => collectCodeFiles(deps.projectRoot, folder)
|
|
15473
15556
|
);
|
|
15474
|
-
const governed = governedFiles.filter((file2) => existsSync5(
|
|
15557
|
+
const governed = governedFiles.filter((file2) => existsSync5(join7(deps.projectRoot, file2.targetPath))).map((file2) => ({
|
|
15475
15558
|
path: file2.targetPath,
|
|
15476
|
-
content:
|
|
15559
|
+
content: readFileSync6(join7(deps.projectRoot, file2.targetPath), "utf8")
|
|
15477
15560
|
}));
|
|
15478
15561
|
return auditProductVocabulary({
|
|
15479
15562
|
vocabulary: manifest.vocabulary,
|
|
@@ -15569,27 +15652,39 @@ var governedChecksumsByVersion = {
|
|
|
15569
15652
|
"scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
|
|
15570
15653
|
"scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
|
|
15571
15654
|
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15655
|
+
},
|
|
15656
|
+
"0.8.0": {
|
|
15657
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15658
|
+
"AGENTS.md": "e44f635c0b203a0040098425f217c5a29c47e57280a3545b60d640aea0d59a70",
|
|
15659
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15660
|
+
"scripts/lint-interdits.ts": "d859e132b7020942a5211f5419b10045170dee6afba3f50ca5642570a5c4a683",
|
|
15661
|
+
"scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
|
|
15662
|
+
"scripts/ogygie-impact.ts": "0dd3ff8cdd57e6cff0dfec1c669fce53fde9ae43f0784ed114771c83d1f8b357",
|
|
15663
|
+
"scripts/verify-contracts.ts": "725cae269e78ecdaa1923553c3097d8d96277824b5bd79a9a40284c6246036b7",
|
|
15664
|
+
"scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
|
|
15665
|
+
"scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
|
|
15666
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15572
15667
|
}
|
|
15573
15668
|
};
|
|
15574
15669
|
|
|
15575
15670
|
// src/paths.ts
|
|
15576
|
-
import { existsSync as existsSync6, readFileSync as
|
|
15577
|
-
import { dirname as dirname3, join as
|
|
15671
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
15672
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
15578
15673
|
import { fileURLToPath } from "node:url";
|
|
15579
|
-
var PACKAGE_ROOT =
|
|
15674
|
+
var PACKAGE_ROOT = join8(dirname3(fileURLToPath(import.meta.url)), "..");
|
|
15580
15675
|
var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
|
|
15581
15676
|
function cliVersion() {
|
|
15582
15677
|
return PackageVersionSchema2.parse(
|
|
15583
|
-
JSON.parse(
|
|
15678
|
+
JSON.parse(readFileSync7(join8(PACKAGE_ROOT, "package.json"), "utf8"))
|
|
15584
15679
|
).version;
|
|
15585
15680
|
}
|
|
15586
15681
|
function templatesDir() {
|
|
15587
|
-
const embedded =
|
|
15682
|
+
const embedded = join8(PACKAGE_ROOT, "templates");
|
|
15588
15683
|
if (existsSync6(embedded)) return embedded;
|
|
15589
15684
|
return repoTemplatesDir();
|
|
15590
15685
|
}
|
|
15591
15686
|
function repoTemplatesDir() {
|
|
15592
|
-
return
|
|
15687
|
+
return join8(PACKAGE_ROOT, "..", "..", "templates");
|
|
15593
15688
|
}
|
|
15594
15689
|
|
|
15595
15690
|
// src/cli.ts
|
|
@@ -15668,14 +15763,20 @@ ogygie verify-vocabulary \u2014 ${problems.length} probl\xE8me(s).`);
|
|
|
15668
15763
|
console.log("ogygie verify-vocabulary \u2014 OK, le vocabulaire d\xE9clar\xE9 est respect\xE9.");
|
|
15669
15764
|
} else if (command === "studio") {
|
|
15670
15765
|
await runStudio({ projectRoot: process.cwd(), cliVersion: cliVersion() });
|
|
15766
|
+
} else if (command === "mcp") {
|
|
15767
|
+
await runMcp({
|
|
15768
|
+
projectRoot: process.cwd(),
|
|
15769
|
+
cliVersion: cliVersion(),
|
|
15770
|
+
argv: process.argv.slice(3)
|
|
15771
|
+
});
|
|
15671
15772
|
} else {
|
|
15672
15773
|
printError(
|
|
15673
15774
|
ogygieError({
|
|
15674
15775
|
code: "OGYGIE_CLI_USAGE",
|
|
15675
15776
|
message: `Unknown command ${JSON.stringify(command ?? "")}.`,
|
|
15676
15777
|
file: null,
|
|
15677
|
-
cause: `expected one of: init <product-name>, upgrade, check, verify-vocabulary, studio (flags: --json, --yes)`,
|
|
15678
|
-
suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary` or `ogygie
|
|
15778
|
+
cause: `expected one of: init <product-name>, upgrade, check, verify-vocabulary, studio, mcp (flags: --json, --yes)`,
|
|
15779
|
+
suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary`, `ogygie studio` or `ogygie mcp`."
|
|
15679
15780
|
})
|
|
15680
15781
|
);
|
|
15681
15782
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.9.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -381,22 +381,37 @@ par vigilance.
|
|
|
381
381
|
tenant** (second rempart : même une requête buggée ne peut pas lire un
|
|
382
382
|
autre tenant). Toute migration créant une table tenant inclut sa policy RLS —
|
|
383
383
|
le template de migration l'impose.
|
|
384
|
-
4. Les tables
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
384
|
+
4. Les tables SANS PROPRIÉTAIRE TENANT sont déclarées explicitement au manifeste
|
|
385
|
+
sous la forme objet `{ "name": "<table>", "shared": true, "sharedKind": "..." }`
|
|
386
|
+
(clés anglaises : le manifeste est machine-lisible). Elles échappent alors à
|
|
387
|
+
la règle 3 (pas de colonne tenant, pas de policy RLS tenant) ET à la règle 2 :
|
|
388
|
+
n'ayant aucune colonne sur laquelle filtrer, elles se lisent hors de
|
|
389
|
+
`tenantDb`.
|
|
390
|
+
|
|
391
|
+
**Deux natures, et une seule est écrivable par l'API** (D8). La nature se
|
|
392
|
+
déclare au manifeste, parce qu'une information de sécurité vit sur la carte :
|
|
393
|
+
- **`reference`** — connaissance universelle qu'aucun tenant ne possède,
|
|
394
|
+
chargée par un script d'extraction depuis une source externe versionnée.
|
|
395
|
+
**Côté API elle est en LECTURE SEULE** : seul le script d'extraction
|
|
396
|
+
l'alimente. C'est le défaut quand `sharedKind` est absent, et le sens
|
|
397
|
+
historique de `shared: true`.
|
|
398
|
+
- **`ownerless`** — donnée produite par l'API elle-même, sans propriétaire
|
|
399
|
+
parce qu'elle naît avant tout compte. **Sa feature l'écrit.** N'ayant pas
|
|
400
|
+
de tenant pour rempart, elle en porte un autre : ses lignes ne se lisent
|
|
401
|
+
que par un **secret non devinable** (jeton), jamais par un identifiant
|
|
402
|
+
énumérable ni séquentiel. Une table `ownerless` sans ce secret est une
|
|
403
|
+
violation — et comme aucun gate ne sait le vérifier, la spec de la feature
|
|
404
|
+
doit dire OÙ vit ce secret et comment il est produit.
|
|
405
|
+
|
|
406
|
+
Ce droit se paie comptant, et les gates vérifient ce qu'ils peuvent au lieu
|
|
407
|
+
de faire confiance :
|
|
392
408
|
- une feature ne gagne l'accès brut à la base que si TOUTES ses tables sont
|
|
393
409
|
partagées (lint-interdits, dérivé du manifeste) — une seule table tenant
|
|
394
410
|
dans la même feature le lui retire ;
|
|
395
411
|
- une table déclarée partagée qui porte encore la colonne tenant casse
|
|
396
|
-
verify-manifest (`SHARED_TABLE_WITH_TENANT_COLUMN`)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
La liste des tables partagées du produit est dans `AGENTS.product.md`.
|
|
412
|
+
verify-manifest (`SHARED_TABLE_WITH_TENANT_COLUMN`).
|
|
413
|
+
La liste des tables partagées du produit, avec leur nature, est dans
|
|
414
|
+
`AGENTS.product.md`.
|
|
400
415
|
|
|
401
416
|
### Template repository (le SEUL accès DB autorisé)
|
|
402
417
|
```ts
|