@ogygie/core 0.13.0 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/core",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Ogygie framework core: manifest/spec/error schemas, generic tenancy and gate tooling. Ships TypeScript sources — Ogygie products run tsx/vitest by construction (D1).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,28 +13,29 @@
13
13
  "access": "public"
14
14
  },
15
15
  "exports": {
16
- "./manifest": "./src/manifest.schema.ts",
17
- "./manifest-owners": "./src/manifest-owners.ts",
18
- "./vocabulary": "./src/vocabulary.ts",
19
- "./vocabulary-audit": "./src/vocabulary-audit.ts",
20
- "./web-lint": "./src/web-lint.ts",
21
16
  "./bundle-budget": "./src/bundle-budget.ts",
22
- "./spec": "./src/spec.schema.ts",
23
- "./impact": "./src/impact.ts",
24
- "./repo-adapter": "./src/repo-adapter.ts",
17
+ "./client": "./src/manifest-client.ts",
18
+ "./client-react": "./src/manifest-client-react.ts",
19
+ "./clock": "./src/clock.ts",
20
+ "./contract-purity": "./src/contract-purity.ts",
21
+ "./db": "./src/db.ts",
25
22
  "./decision-refs": "./src/decision-refs.ts",
26
23
  "./design-brief": "./src/design-brief.ts",
27
- "./module-purity": "./src/module-purity.ts",
28
- "./contract-purity": "./src/contract-purity.ts",
29
24
  "./errors": "./src/error.schema.ts",
30
- "./db": "./src/db.ts",
31
- "./clock": "./src/clock.ts",
32
- "./tenant": "./src/tenant.ts",
25
+ "./gateway": "./src/manifest-gateway.ts",
26
+ "./impact": "./src/impact.ts",
27
+ "./manifest": "./src/manifest.schema.ts",
28
+ "./manifest-owners": "./src/manifest-owners.ts",
29
+ "./module-purity": "./src/module-purity.ts",
30
+ "./repo-adapter": "./src/repo-adapter.ts",
31
+ "./scan": "./src/scan.ts",
33
32
  "./shared-tables": "./src/shared-tables.ts",
33
+ "./spec": "./src/spec.schema.ts",
34
+ "./tenant": "./src/tenant.ts",
34
35
  "./test-kit": "./src/test-kit.ts",
35
- "./gateway": "./src/manifest-gateway.ts",
36
- "./client": "./src/manifest-client.ts",
37
- "./client-react": "./src/manifest-client-react.ts"
36
+ "./vocabulary": "./src/vocabulary.ts",
37
+ "./vocabulary-audit": "./src/vocabulary-audit.ts",
38
+ "./web-lint": "./src/web-lint.ts"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "@electric-sql/pglite": "^0.3.0",
@@ -5,7 +5,8 @@
5
5
  * confirm, and that neither shadows the other.
6
6
  */
7
7
  import { describe, expect, it } from "vitest";
8
- import { declaredEntries, findSpecOwner } from "./manifest-owners.js";
8
+ import { declaredEntries, findSpecOwner, hasImplementation } from "./manifest-owners.js";
9
+ import type { SpecOwner, SpecOwnerKind } from "./manifest-owners.js";
9
10
  import { OgygieManifestSchema } from "./manifest.schema.js";
10
11
 
11
12
  function manifestWith(extra: Record<string, unknown>) {
@@ -250,3 +251,30 @@ describe("declaredEntries — tout ce que la carte déclare, à un seul endroit"
250
251
  expect(declaredEntries(withApps({}))).toEqual([]);
251
252
  });
252
253
  });
254
+
255
+ describe("hasImplementation — ce qui prouve qu'une spec draft a déjà du code", () => {
256
+ const owner = (
257
+ kind: SpecOwnerKind,
258
+ status: "skeleton" | "in-progress" | "delivered",
259
+ ): SpecOwner => ({ kind, name: "peu-importe", status, folder: "un/chemin" });
260
+
261
+ it("ÉTANT DONNÉ un statut au-delà de skeleton ALORS l'implémentation existe, dossier ou pas", () => {
262
+ for (const kind of ["feature", "package", "framework-package"] as SpecOwnerKind[]) {
263
+ expect(hasImplementation(owner(kind, "in-progress"), false)).toBe(true);
264
+ expect(hasImplementation(owner(kind, "delivered"), false)).toBe(true);
265
+ }
266
+ });
267
+
268
+ it("une FEATURE skeleton dont le dossier existe a bien du code — c'est la preuve historique", () => {
269
+ expect(hasImplementation(owner("feature", "skeleton"), true)).toBe(true);
270
+ expect(hasImplementation(owner("feature", "skeleton"), false)).toBe(false);
271
+ });
272
+
273
+ it("MUTATION : un PACKAGE skeleton dont le dossier existe n'a PAS de code — le manifeste EXIGE ce dossier", () => {
274
+ // Sans cette règle, une spec draft et un package skeleton ne peuvent pas
275
+ // coexister : verify-manifest réclame le dossier, verify-specs y lit du
276
+ // code, et le projet est coincé entre deux fichiers gouvernés.
277
+ expect(hasImplementation(owner("package", "skeleton"), true)).toBe(false);
278
+ expect(hasImplementation(owner("framework-package", "skeleton"), true)).toBe(false);
279
+ });
280
+ });
@@ -158,6 +158,28 @@ export function findSpecOwner(
158
158
  return { kind: owner.kind, name: owner.name, status: owner.status, folder: owner.folder };
159
159
  }
160
160
 
161
+ /**
162
+ * Does an implementation of this spec exist? The question the `draft` rule of
163
+ * the spec lifecycle rests on ("draft = no code yet").
164
+ *
165
+ * A declared status above `skeleton` always answers yes. A FOLDER on disk only
166
+ * answers yes for a FEATURE, whose folder is born with its code and is
167
+ * therefore evidence of it. A package's folder is NOT evidence: the manifest
168
+ * gate REQUIRES it to exist (MANIFEST_PACKAGE_NOT_FOUND), so a package
169
+ * declared `skeleton` whose spec is still `draft` could satisfy neither gate —
170
+ * one demands the folder, the other reads it as code, and the project is stuck
171
+ * between two governed files it may not edit. Observed on a consumer project
172
+ * the day it declared its first package before writing it.
173
+ *
174
+ * Lives here rather than in the gate script for the reason the whole module
175
+ * exists: the rule travels with the versioned package, not with a copy each
176
+ * project keeps.
177
+ */
178
+ export function hasImplementation(owner: SpecOwner, folderExists: boolean): boolean {
179
+ if (owner.status !== "skeleton") return true;
180
+ return owner.kind === "feature" && folderExists;
181
+ }
182
+
161
183
  /** What a refusal should suggest — both natures, never one that cannot exist. */
162
184
  export const SPEC_OWNER_FIX =
163
185
  "Set the owning entry to delivered in the manifest — a feature under `features`, a library under `packages`, or a published package under `framework.packages` (which names its spec explicitly) — or keep the spec in validated.";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @intent Prouve la règle de parcours des gates, avec le cas qui l'a imposée :
3
+ * un projet Nuxt dont `typecheck` génère `.nuxt/` que `lint` refusait ensuite.
4
+ */
5
+ import { describe, expect, it } from "vitest";
6
+ import { isScannableDirectory, NEVER_SCANNED_DIRECTORIES } from "./scan.js";
7
+
8
+ describe("isScannableDirectory", () => {
9
+ it("parcourt le code du projet", () => {
10
+ for (const name of ["apps", "packages", "src", "scripts", "features", "specs"]) {
11
+ expect(isScannableDirectory(name)).toBe(true);
12
+ }
13
+ });
14
+
15
+ it("ne parcourt AUCUN dossier en point — c'est la règle qui ne vieillit pas", () => {
16
+ for (const name of [".git", ".nuxt", ".output", ".next", ".svelte-kit", ".turbo", ".venv"]) {
17
+ expect(isScannableDirectory(name)).toBe(false);
18
+ }
19
+ });
20
+
21
+ it("ne parcourt pas les artefacts au nom ordinaire", () => {
22
+ for (const name of NEVER_SCANNED_DIRECTORIES) expect(isScannableDirectory(name)).toBe(false);
23
+ });
24
+
25
+ it("MUTATION : un dossier dont le nom COMMENCE par celui d'un artefact reste parcouru", () => {
26
+ // `distribution/` n'est pas `dist/`, et `buildings/` n'est pas `build/`.
27
+ expect(isScannableDirectory("distribution")).toBe(true);
28
+ expect(isScannableDirectory("buildings")).toBe(true);
29
+ expect(isScannableDirectory("node_modules_backup")).toBe(true);
30
+ });
31
+ });
package/src/scan.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @intent Which directories a gate WALKS — the one rule, for every gate.
3
+ *
4
+ * It existed five times, slightly differently, in as many scripts. That is how
5
+ * it fell behind: the lists were written when a build produced `dist/`, and a
6
+ * modern framework generates `.nuxt/`, `.output/`, `.next/`, `.svelte-kit/`,
7
+ * while a Python package carries `.venv/`. A gate that walks into them reports
8
+ * bans and forbidden words found in code NOBODY WROTE — and worse, it does so
9
+ * only after a build, so the loop passes in CI and fails on the machine of
10
+ * whoever just ran it. A consumer project hit exactly that: `typecheck`
11
+ * generated `.nuxt/`, and `lint` then refused a type-suppression directive the
12
+ * framework had written into its own generated types.
13
+ *
14
+ * The rule is deliberately blunt, because a subtle one would fall behind again:
15
+ * a DOT-directory is never source. Everything a project actually writes lives
16
+ * in a plainly-named folder; tooling, caches, virtual environments and build
17
+ * output all hide behind a dot, by a convention older than any of these tools.
18
+ */
19
+
20
+ /** Plainly-named directories that are still artefacts, not source. */
21
+ export const NEVER_SCANNED_DIRECTORIES = [
22
+ "node_modules",
23
+ "dist",
24
+ "dist-ui",
25
+ "build",
26
+ "coverage",
27
+ ] as const;
28
+
29
+ /** Does a gate walk into this directory? Its NAME is the whole question. */
30
+ export function isScannableDirectory(name: string): boolean {
31
+ // `.git`, `.nuxt`, `.output`, `.next`, `.svelte-kit`, `.turbo`, `.venv`,
32
+ // `.pytest_cache`… — one rule instead of a list that ages.
33
+ if (name.startsWith(".")) return false;
34
+ return !(NEVER_SCANNED_DIRECTORIES as readonly string[]).includes(name);
35
+ }