diff --git a/.project/concepts/tech-stack/_debt.md b/.project/concepts/tech-stack/_debt.md new file mode 100644 index 0000000..654bbb7 --- /dev/null +++ b/.project/concepts/tech-stack/_debt.md @@ -0,0 +1,8 @@ +# Doc-debt — tech-stack + +> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file. +> One block = one "big change": `why` + `files` + `verify` (leaves to review). + +## Raw markers (consolidate into blocks, then delete) +- TOUCHED package.json @2026-08-17 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) +- TOUCHED scripts/link-polyfill.ts @2026-08-17 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) diff --git a/package.json b/package.json index a520e44..4f14fbc 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,14 @@ "build:orm": "rdf-orm build --input ./src/shared/shapes/shex --output ./src/shared/shapes/orm", "validate": "bun scripts/validate.ts", "build:ng": "bash scripts/build-ng-packages.sh", - "link:polyfill": "bun scripts/link-polyfill.ts", + "link:polyfill": "bun scripts/link-polyfill.ts polyfill", + "link:indexing": "bun scripts/link-polyfill.ts indexing", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, "dependencies": { "@ng-eventually/polyfill": "git+https://gitea.reconnexion.apps.gueraud.net/Reconnexion/ng-eventually.git#a8d53010c227462cc9317e9be499c2100ca8d533&path:/packages/polyfill", + "@ng-helpers/indexing": "git+https://gitea.reconnexion.apps.gueraud.net/Sylvain/ng-helpers.git#v1.0.1", "@ng-org/alien-deepsignals": "0.1.2-alpha.11", "@ng-org/orm": "0.1.2-alpha.18", "@ng-org/shex-orm": "0.1.2-alpha.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e2d0ef..09bc1f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ng-eventually/polyfill': specifier: git+https://gitea.reconnexion.apps.gueraud.net/Reconnexion/ng-eventually.git#a8d53010c227462cc9317e9be499c2100ca8d533&path:/packages/polyfill version: git+https://gitea.reconnexion.apps.gueraud.net/Reconnexion/ng-eventually.git#a8d53010c227462cc9317e9be499c2100ca8d533&path:/packages/polyfill(@ng-org/alien-deepsignals@0.1.2-alpha.11(react@19.2.7))(@ng-org/orm@0.1.2-alpha.18(react@19.2.7))(@ng-org/shex-orm@0.1.2-alpha.8(typescript@6.0.3))(@ng-org/web@0.1.2-alpha.13) + '@ng-helpers/indexing': + specifier: git+https://gitea.reconnexion.apps.gueraud.net/Sylvain/ng-helpers.git#v1.0.1 + version: git+https://gitea.reconnexion.apps.gueraud.net/Sylvain/ng-helpers.git#d615a72775cc9110de64b9b7fcc1d0d6c6d127ea(@ng-eventually/polyfill@git+https://gitea.reconnexion.apps.gueraud.net/Reconnexion/ng-eventually.git#a8d53010c227462cc9317e9be499c2100ca8d533&path:/packages/polyfill(@ng-org/alien-deepsignals@0.1.2-alpha.11(react@19.2.7))(@ng-org/orm@0.1.2-alpha.18(react@19.2.7))(@ng-org/shex-orm@0.1.2-alpha.8(typescript@6.0.3))(@ng-org/web@0.1.2-alpha.13)) '@ng-org/alien-deepsignals': specifier: 0.1.2-alpha.11 version: 0.1.2-alpha.11(react@19.2.7) @@ -506,6 +509,12 @@ packages: '@ng-org/web': optional: true + '@ng-helpers/indexing@git+https://gitea.reconnexion.apps.gueraud.net/Sylvain/ng-helpers.git#d615a72775cc9110de64b9b7fcc1d0d6c6d127ea': + resolution: {commit: d615a72775cc9110de64b9b7fcc1d0d6c6d127ea, repo: https://gitea.reconnexion.apps.gueraud.net/Sylvain/ng-helpers.git, type: git} + version: 1.0.1 + peerDependencies: + '@ng-eventually/polyfill': '*' + '@ng-org/alien-deepsignals@0.1.2-alpha.11': resolution: {integrity: sha512-nPgqOrheAda/pW5FHgSb45SrSZWuyMyEVqO683ijEsVPpD105bngfh92PPfcRoRnFzGSoKXa3CfuqUHi2+qVIQ==} peerDependencies: @@ -3734,6 +3743,10 @@ snapshots: '@ng-org/shex-orm': 0.1.2-alpha.8(typescript@6.0.3) '@ng-org/web': 0.1.2-alpha.13 + '@ng-helpers/indexing@git+https://gitea.reconnexion.apps.gueraud.net/Sylvain/ng-helpers.git#d615a72775cc9110de64b9b7fcc1d0d6c6d127ea(@ng-eventually/polyfill@git+https://gitea.reconnexion.apps.gueraud.net/Reconnexion/ng-eventually.git#a8d53010c227462cc9317e9be499c2100ca8d533&path:/packages/polyfill(@ng-org/alien-deepsignals@0.1.2-alpha.11(react@19.2.7))(@ng-org/orm@0.1.2-alpha.18(react@19.2.7))(@ng-org/shex-orm@0.1.2-alpha.8(typescript@6.0.3))(@ng-org/web@0.1.2-alpha.13))': + dependencies: + '@ng-eventually/polyfill': git+https://gitea.reconnexion.apps.gueraud.net/Reconnexion/ng-eventually.git#a8d53010c227462cc9317e9be499c2100ca8d533&path:/packages/polyfill(@ng-org/alien-deepsignals@0.1.2-alpha.11(react@19.2.7))(@ng-org/orm@0.1.2-alpha.18(react@19.2.7))(@ng-org/shex-orm@0.1.2-alpha.8(typescript@6.0.3))(@ng-org/web@0.1.2-alpha.13) + '@ng-org/alien-deepsignals@0.1.2-alpha.11(react@19.2.7)': dependencies: alien-signals: 2.0.8 diff --git a/scripts/link-polyfill.ts b/scripts/link-polyfill.ts index e7552b8..9230f74 100644 --- a/scripts/link-polyfill.ts +++ b/scripts/link-polyfill.ts @@ -1,67 +1,111 @@ #!/usr/bin/env bun /** - * link-polyfill.ts — Reactive local link for the @ng-eventually/polyfill polyfill. + * link-polyfill.ts — Reactive local overlay of a data-layer PROVIDER's checkout into + * node_modules. One script, one provider per run; only the paths differ between them. * - * WHY a copy-overlay and not a symlink: - * The committed prod dependency installs @ng-eventually/polyfill from Gitea (git+https) - * into pnpm's store WITHOUT its own node_modules/@ng-org → @ng-org/web resolves up to - * Festipod → ONE @ng-org instance (one verifier). The local polyfill CHECKOUT, however, - * carries its own node_modules/@ng-org/* (symlinks into the ng-eventually-js monorepo - * store). Symlinking node_modules/@ng-eventually/polyfill to that checkout puts the - * checkout's @ng-org in the resolution path → a SECOND @ng-org instance → broken SDK - * (two verifiers). So we overlay a real directory that contains ONLY the polyfill's - * source (no node_modules) and keep it in sync by copying — @ng-org still resolves to - * Festipod, single instance preserved. + * PROVIDERS (first non-flag argument; defaults to `polyfill`): + * polyfill → node_modules/@ng-eventually/polyfill override: NG_EVENTUALLY_LOCAL + * indexing → node_modules/@ng-helpers/indexing override: NG_HELPERS_LOCAL + * + * WHY a copy-overlay and not a symlink (identical for every provider): + * A committed prod dependency installs from Gitea (git+https) into pnpm's store WITHOUT + * its own node_modules/@ng-org, so @ng-org/web resolves UP to Festipod → ONE @ng-org + * instance (one verifier). A local CHECKOUT, however, carries its own node_modules/* + * (links into that provider's own dev tree). Symlinking node_modules/ to the + * checkout would put the checkout's copies in the resolution path → a SECOND @ng-org + * (and, for `indexing`, a second @ng-eventually/polyfill) → broken SDK, two verifiers. + * So we overlay a real directory containing ONLY the provider's source (no node_modules): + * shared packages still resolve up to Festipod, single instance preserved. * * WHAT IT DOES: - * 1. Replaces node_modules/@ng-eventually/polyfill (the pnpm store symlink) with a real - * directory holding the local polyfill's package.json + src (NO node_modules). - * 2. Asserts the single-instance invariant (same @ng-org/web realpath from Festipod and - * from the overlay) — aborts if it would break. - * 3. Watches the local polyfill src and copies each change into the overlay. + * 1. Replaces node_modules/ (the pnpm store symlink) with a real directory + * holding the local checkout's package.json + src (NO node_modules). + * 2. Asserts the single-instance invariant — every package this provider SHARES with + * Festipod must resolve to the same realpath from Festipod and from the overlay — + * and aborts if it would break. + * 3. Watches the local checkout's src and copies each change into the overlay. * - * ⚠️ RESTART `bun run dev` AFTER THIS SCRIPT WRITES. The overlay lives inside - * node_modules, which file watchers exclude by convention — so a running dev - * server keeps serving the polyfill it loaded at startup, however many times - * this script rewrites the files underneath it. VERIFIED the hard way: a dev - * server six days old served pre-fix code while the overlay on disk was current, - * and an hour went into hunting a defect that had already been fixed. Watching - * copies the files; it does not make anything reload them. + * ⚠️ RESTART `bun run dev` AFTER THIS SCRIPT WRITES — a rebuild is NOT a substitute. + * A running dev server NEVER picks up a package refreshed inside node_modules, not even + * across a genuine rebuild: VERIFIED in a controlled test, an application-source edit + * produced a new bundle hash and the rebuilt bundle STILL carried the stale dependency. + * The server's resolution of that import is pinned at process start and a rebuild does not + * re-resolve it. Only restarting serves the fresh copy, and nothing warns you — a stale + * server looks exactly like a current one. See + * .project/concepts/tech-stack/caveat_polyfill-overlay-needs-a-dev-restart.md, which cost + * an hour to learn. Watching copies the files; it does not make anything reload them. * * USAGE (reactive dev): - * Terminal 1: pnpm run link:polyfill # overlays local source, then watches - * Terminal 2: bun run dev # portless festipod bun --hot src/index.ts - * Edit files under packages/polyfill/src → they land in node_modules → RESTART dev to pick them up. + * Terminal 1: pnpm run link:polyfill # or: pnpm run link:indexing + * Terminal 2: bun run dev # portless festipod bun --hot src/index.ts + * Edit the checkout's src → it lands in node_modules → RESTART dev to pick it up. * - * pnpm run link:polyfill --once # overlay + verify, no watch (CI / one-shot) - * Return to the committed git-installed dependency: pnpm install - * - * Override the local checkout path with NG_EVENTUALLY_LOCAL=/path/to/packages/polyfill. + * pnpm run link:indexing --once # overlay + verify, no watch (CI / one-shot) + * Return to the committed git-installed dependencies: pnpm install */ import { existsSync, lstatSync, mkdirSync, rmSync, cpSync, copyFileSync, realpathSync } from "node:fs"; import { watch } from "node:fs"; import { join, dirname } from "node:path"; +interface Provider { + /** Package as installed, e.g. "@ng-eventually/polyfill" — also its node_modules path. */ + readonly packageName: string; + /** Local checkout used when the env override is unset. */ + readonly defaultLocal: string; + /** Env var overriding the local checkout path. */ + readonly envOverride: string; + /** + * Packages this provider SHARES with Festipod and that must stay single-instance. + * Each is resolved from Festipod and from the overlay; the realpaths must match. + */ + readonly singletons: readonly string[]; +} + +const PROVIDERS: Record = { + polyfill: { + packageName: "@ng-eventually/polyfill", + defaultLocal: "/home/sylvain/projects/nextgraph/ng-eventually-js/packages/polyfill", + envOverride: "NG_EVENTUALLY_LOCAL", + singletons: ["@ng-org/web"], + }, + indexing: { + packageName: "@ng-helpers/indexing", + defaultLocal: "/home/sylvain/projects/nextgraph/ng-helpers", + envOverride: "NG_HELPERS_LOCAL", + // Consumes the polyfill, so BOTH it and the verifier underneath must stay single. + singletons: ["@ng-eventually/polyfill", "@ng-org/web"], + }, +}; + const FESTIPOD = realpathSync(join(import.meta.dir, "..")); -const LOCAL = - process.env.NG_EVENTUALLY_LOCAL ?? - "/home/sylvain/projects/nextgraph/ng-eventually-js/packages/polyfill"; -const TARGET = join(FESTIPOD, "node_modules", "@ng-eventually", "polyfill"); -const SRC_LOCAL = join(LOCAL, "src"); -const SRC_TARGET = join(TARGET, "src"); -const ONCE = process.argv.includes("--once"); +const args = process.argv.slice(2); +const ONCE = args.includes("--once"); +const KEY = args.find((a) => !a.startsWith("-")) ?? "polyfill"; function fail(msg: string): never { - console.error(`✖ link:polyfill — ${msg}`); + console.error(`✖ link:${KEY} — ${msg}`); process.exit(1); } +const provider = PROVIDERS[KEY]; +if (!provider) { + fail(`unknown provider "${KEY}" — expected one of: ${Object.keys(PROVIDERS).join(", ")}`); +} + +const LOCAL = process.env[provider.envOverride] ?? provider.defaultLocal; +const TARGET = join(FESTIPOD, "node_modules", ...provider.packageName.split("/")); +const SRC_LOCAL = join(LOCAL, "src"); +const SRC_TARGET = join(TARGET, "src"); + if (!existsSync(join(LOCAL, "package.json"))) { - fail(`local polyfill not found at ${LOCAL} (set NG_EVENTUALLY_LOCAL to override)`); + fail(`local checkout not found at ${LOCAL} (set ${provider.envOverride} to override)`); +} +if (!existsSync(SRC_LOCAL)) { + fail(`local checkout has no src/ at ${SRC_LOCAL}`); } // 1. Replace the pnpm store symlink with a real overlay dir (metadata + src, NO node_modules). -console.log(`→ overlaying local polyfill: ${LOCAL}`); +console.log(`→ overlaying local ${provider.packageName}: ${LOCAL}`); if (existsSync(TARGET) || lstatSync(TARGET, { throwIfNoEntry: false })) { rmSync(TARGET, { recursive: true, force: true }); } @@ -70,30 +114,32 @@ for (const meta of ["package.json", "tsconfig.json", "README.md"]) { const from = join(LOCAL, meta); if (existsSync(from)) copyFileSync(from, join(TARGET, meta)); } -// Copy src fresh (NEVER a node_modules dir — that is what guarantees single @ng-org instance). +// Copy src fresh (NEVER a node_modules dir — that is what guarantees single instances). cpSync(SRC_LOCAL, SRC_TARGET, { recursive: true }); -// 2. Assert the single-instance invariant. -const fromFestipod = realpathSync(Bun.resolveSync("@ng-org/web", FESTIPOD)); +// 2. Assert the single-instance invariant for every package shared with Festipod. const overlayReal = realpathSync(TARGET); -const fromPolyfill = realpathSync(Bun.resolveSync("@ng-org/web", overlayReal)); -console.log(` @ng-org/web (Festipod): ${fromFestipod}`); -console.log(` @ng-org/web (overlay) : ${fromPolyfill}`); -if (fromFestipod !== fromPolyfill) { - fail( - "single-instance invariant BROKEN — @ng-org/web resolves to two different realpaths.\n" + - " The overlay must not contain its own node_modules/@ng-org. Aborting.", - ); +for (const spec of provider.singletons) { + const fromFestipod = realpathSync(Bun.resolveSync(spec, FESTIPOD)); + const fromOverlay = realpathSync(Bun.resolveSync(spec, overlayReal)); + console.log(` ${spec} (Festipod): ${fromFestipod}`); + console.log(` ${spec} (overlay) : ${fromOverlay}`); + if (fromFestipod !== fromOverlay) { + fail( + `single-instance invariant BROKEN — ${spec} resolves to two different realpaths.\n` + + " The overlay must not contain its own node_modules. Aborting.", + ); + } } -console.log("✓ single @ng-org/web instance preserved"); +console.log(`✓ single instance preserved for: ${provider.singletons.join(", ")}`); if (ONCE) { console.log("✓ overlay ready (--once, not watching)"); process.exit(0); } -// 3. Watch and copy on change. This keeps the overlay CURRENT; it does not make a -// running dev server notice (node_modules is outside the watcher) — restart it. +// 3. Watch and copy on change. This keeps the overlay CURRENT; it does NOT make a running +// dev server notice — not even across a rebuild (see the header). Restart it. console.log(`👀 watching ${SRC_LOCAL} → ${SRC_TARGET} (Ctrl-C to stop)`); watch(SRC_LOCAL, { recursive: true }, (_event, filename) => { if (!filename) return;