diff --git a/.project/concepts/app-contract/_overview.md b/.project/concepts/app-contract/_overview.md index adaa25f..378c7b1 100644 --- a/.project/concepts/app-contract/_overview.md +++ b/.project/concepts/app-contract/_overview.md @@ -29,10 +29,10 @@ This library exists so an application can be **written today against the NextGra The distinguishing question, asked at every choice — published signature as much as internal mechanism: **does the target do this?** If it does it differently, we do it their way, whatever that costs. *Would a caller have to unlearn it?* comes second, and measures how bad a divergence is — never whether one is allowed. See `rule_no-divergence-from-nextgraph`. -This repo is the **provider** of `contract_polyfill-surface`; consuming applications live in other repos and pull it. The per-symbol ruling, with an epistemic label on every target-side claim, stays here in `docs/api-contract.md` — that is maintainer material, not the engagement. +This repo is the **provider** of `polyfill-surface`; consuming applications live in other repos, pull the engagement, and declare back what they actually consume — one `usage_.md` beside the contract, in the interface folder. The per-symbol ruling, with an epistemic label on every target-side claim, stays here in `docs/api-contract.md` — that is maintainer material, not the engagement. ## Read first -- `contract_polyfill-surface` — the engagement itself, written from the caller's point of view. +- `polyfill-surface/contract_polyfill-surface` — the engagement itself, written from the caller's point of view. - `rule_no-divergence-from-nextgraph` — the test that decides what may be built at all. - `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have. diff --git a/.project/concepts/app-contract/contract_polyfill-surface.md b/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md similarity index 79% rename from .project/concepts/app-contract/contract_polyfill-surface.md rename to .project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md index 9d54776..389a02e 100644 --- a/.project/concepts/app-contract/contract_polyfill-surface.md +++ b/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md @@ -55,7 +55,17 @@ export const storeRegistry: { // no identity parameter — a session is export async function readUnion(docs: NuriLike[]): Promise; export interface UnionSubject { subject: string; graph: Nuri; props: Record } export function useShape(shapeType: unknown, scope: unknown): unknown; // read-filtered view -export function watchShape(query: ShapeQuery): ShapeObservable; +// TWO positional arguments — the same pair `useShape` takes. `ShapeQuery` is what you +// READ BACK (the snapshot), never what you pass in. +export function watchShape(shapeType: unknown, scope: Scope): ShapeObservable; +export interface ShapeObservable { + getSnapshot(): ShapeQuery; // stable reference until the state changes + subscribe(onChange: () => void): () => void; // returns its own unsubscribe + refetch(): void; // re-resolve and re-read now; never polls +} +export interface ShapeQuery { + data: T[]; isPending: boolean; isSuccess: boolean; isError: boolean; error: unknown; +} export function subscribeDoc(nuri: NuriLike, onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe; export function subscribeDocs(nuris: NuriLike[], onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe; @@ -153,6 +163,16 @@ The same rule reaches what a call hands BACK, not only what it looked up first: ## Change policy -This surface changes, and shrinks. The package does not offer semantic-version stability. +**Semver, and majors are the normal case.** This surface converges on a NextGraph that does not ship yet, so most steps toward the target remove or narrow something — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time. -Re-pull this contract at every upgrade. +What each level means here, in this package's own terms: + +- **major** — a published symbol is removed (`getSession`, `normalizeId`, `currentUser`, `RegistrySession` and `inbox.materialize` all left this way), **or** an existing call narrows: it now throws where it returned (`listMyEntityDocs` refuses a listing whose documents you could not open; `ensureIdentity` rejects rather than resolve on an incomplete restore), or it reports a state you did not have to handle before (`watchShape` publishing `isError` where it used to publish a synced-empty snapshot). A signature change a caller must react to counts; one that only accepts more than before does not. +- **minor** — a symbol is added and nothing existing moves (`inbox.readSyncedForDocument` arrived this way). +- **patch** — a fix that changes neither the published surface nor anything above under `## Guarantees`. + +**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.3`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you. + +`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything — but the next release very likely is a major. What exists today is `1.0.0-dev.1`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.1`, the string you pinned, never the version it is heading for. + +There is no changelog file and no deprecation window: **the sections above are the release note.** A removal or a narrowing lands in `## Surface` and `## Guarantees` in the same version that ships it, and a symbol is never left published-but-dead as a courtesy. Diff this leaf between two pulls — `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first. diff --git a/.project/concepts/e2e-harness/_overview.md b/.project/concepts/e2e-harness/_overview.md index fbf9e6d..40679b3 100644 --- a/.project/concepts/e2e-harness/_overview.md +++ b/.project/concepts/e2e-harness/_overview.md @@ -2,9 +2,10 @@ type: overview summary: What each end-to-end suite judges, the shortcut that kept two defects invisible, and the failure mode that is not ours to fix triggers: - keywords: [e2e, harness, playwright, chromium, journey, suite, profile, broker page, notebook, wallet profile] + keywords: [e2e, harness, playwright, chromium, journey, suite, profile, broker page, notebook, wallet profile, ng-e2e-helpers] paths: - "packages/polyfill/e2e/**" + - "packages/ng-e2e-helpers/**" vocabulary: - term: journey gloss: one named end-to-end story in the applicative suite, reported pass or fail as a whole @@ -20,8 +21,12 @@ Three suites drive real browsers against the real broker and the real wallet app They are also expensive and, on a machine whose network moves, unreliable in ways that look exactly like product defects. Most of what this concept holds exists so an agent does not spend hours diagnosing a product bug that is a transport failure — or, worse, trust a green run that measured nothing. +The generic half of that machinery is a package of its own, `ng-e2e-helpers`, and this repo **publishes** it: a NextGraph application in any repository pulls `ng-e2e-helpers/contract_ng-e2e-helpers` and declares back what it consumes. Our own suites are one of those consumers, and declare it in the same folder — same pair, no boundary to cross. + ## Read first +- `ng-e2e-helpers/contract_ng-e2e-helpers` — the engagement, written from the caller's point of view. +- `ng-e2e-helpers/usage_polyfill-e2e` — what our own suites actually call, and where the fit still costs them code. - `knowledge_what-each-suite-judges` — which suite answers which question. - `rule_never-shortcut-the-sign-in` — the shortcut that hid a 404 and a regression at once. - `caveat_a-dropped-pipe-kills-a-run` — the failure mode that is not ours. diff --git a/.project/concepts/e2e-harness/ng-e2e-helpers/contract_ng-e2e-helpers.md b/.project/concepts/e2e-harness/ng-e2e-helpers/contract_ng-e2e-helpers.md new file mode 100644 index 0000000..712a358 --- /dev/null +++ b/.project/concepts/e2e-harness/ng-e2e-helpers/contract_ng-e2e-helpers.md @@ -0,0 +1,161 @@ +--- +type: contract +summary: The machinery ng-e2e-helpers offers a NextGraph application's end-to-end suite — wallets, the broker crossing, per-run profiles, bounds, and a bounded report +--- + +# contract_ng-e2e-helpers — `ng-e2e-helpers` + +## Scope + +This package is the end-to-end testing machinery a NextGraph application needs to get a real person into itself: minting a wallet by driving the wallet application, crossing the broker, and coming back inside the iframe the application runs in — plus per-run browser profiles, bounds that turn a hang into a named failure, and a run report whose size does not depend on what failed. + +It covers **only what is generic to NextGraph**. It knows nothing about any one application, and nothing about any compatibility layer: an application calling the NextGraph SDK directly is its intended consumer. What is specific to your repository — the page that carries your application, how you build your bundle, which journeys you run — is yours to write, and this package deliberately offers no place to put it. + +It is not a test runner, not an assertion library, and not a fixture system. You keep your own runner and your own `main()`. + +### Deployment requirements + +`playwright` and `@ng-org/web` are **peer dependencies**, and you own both versions: browser binaries have to match the driver, and the SDK the export page opens a session with must be the one your application and your broker agree on. Import the browser types from the helpers that return them rather than from `playwright` directly — a second resolution of the driver produces a structurally different `BrowserContext`, and a context you opened then cannot be handed back to the helper that opens contexts. + +A machine running this needs a real Chromium, network reach to the wallet application and to the broker, and a writable temporary directory for the per-run profiles. + +## Surface + +Full typed shape: the package's `types` entry, `ng-e2e-helpers`. It also ships one executable, `ng-mint-wallet`, for provisioning a deployment's wallet file outside any run. The load-bearing signatures: + +```ts +// ── bounds: a hang becomes a named failure ─────────────────────────────────── +export function within(what: string, ms: number, task: () => Promise): Promise; +export class DeadlineExceeded extends Error {} // thrown by `within` — names `what` +export class BrowserGone extends Error {} // the browser died; not the task's fault +export function enclosingBound(steps: readonly number[], margin: number): number; +export function closeQuietly(what: string, close: () => Promise): Promise; +export function armSuiteDeadline(suite: string, ms: number, thenReport?: () => void): void; +export function browserLost(reason: string): void; // declare it, once +export function lossDeclared(): string | null; // has it been declared? +export function firstLine(e: unknown): string; +export const CLOSE_MS: number, CONTEXT_ACTION_MS: number, CONTEXT_NAVIGATION_MS: number; + +// ── measurement: how a bound gets its number ───────────────────────────────── +export function measured(what: string, bound: number, task: (ms: number) => Promise): Promise; +export function record(what: string, ms: number, ok: boolean, bound: number): void; +export function timingsWanted(): boolean; // true under E2E_TIMINGS=1 +export function printTimings(): void; + +// ── browser and profiles: one run owns its own ─────────────────────────────── +export interface RunProfile { readonly dir: string; readonly purpose: string; discard(): void } +export function newRunProfile(purpose: string): RunProfile; +export function isAlive(pid: number): boolean; +export function launchWatchedContext(label: string, dir: string): Promise; +export function closeContext(label: string, ctx: BrowserContext): Promise; +export function newPage(label: string, ctx: BrowserContext): Promise; +export const LAUNCH_MS: number, NEW_PAGE_MS: number; + +// ── the wallet: minted, carried, imported ──────────────────────────────────── +export interface WalletCredentials { readonly name: string; readonly password: string } +export const DEFAULT_WALLET_NAME: string; // the password never has a default +export function mintWalletProfile(purpose: string, c: WalletCredentials): Promise; +export function mintWalletProfileKeepingContext( + purpose: string, c: WalletCredentials): Promise<{ ctx: BrowserContext; profile: RunProfile }>; +export function mintWalletBytes(password: string, name?: string): Promise; +export function createWalletInContext(ctx: BrowserContext, c: WalletCredentials): Promise; +export function emptyProfileContext( + purpose: string): Promise<{ ctx: BrowserContext; profile: RunProfile }>; +export function exportWalletBytes(ctx: BrowserContext, walletPassword: string): Promise; +export function exportWalletFile(ctx: BrowserContext, ngwPath: string, pw: string): Promise; +export function importWalletFile(page: Page, ngwPath: string, password: string): Promise; + +// ── the broker crossing ────────────────────────────────────────────────────── +export function setupBrokerPage(page: Page, appUrl: string, walletPassword: string): Promise; +export function completeBrokerLogin(page: Page, appUrl: string, pw: string): Promise; +export const BROKER_LOGIN_MS: number, BROKER_ROUND_TRIP_MS: number; +export function brokerRedirectFor(appUrl: string): string; +export const BROKER_SCREENS: readonly BrokerScreenSpec[]; // the screens, in test order +export const WALLET_APP, WALLET_CREATION, WALLET_IMPORT; // the wallet application's own pages +export type BrokerScreen = "choose-broker" | "login-offered" | "wallet-list" + | "password" | "working" | "error"; +export type { BrokerScreenSpec, ScreenSignature, ScreenResponse, TextPattern }; + +// ── serving your application to the browser ────────────────────────────────── +export function serveOnEphemeralPort( + handler: (req: IncomingMessage, res: ServerResponse) => void, +): Promise<{ url: string; close: () => void }>; + +// ── failures that are not the application's ────────────────────────────────── +export function browserTrouble(label: string, ctx: BrowserContext): Promise; +export function frameTrouble(id: string, page: Page, frame: Frame, marker: string): Promise; +export const BROWSER_PROBE_MS: number, FRAME_PROBE_MS: number; + +// ── the report ─────────────────────────────────────────────────────────────── +export interface JourneyDeclaration { readonly name: string; readonly checks: readonly string[] } +export type Prerequisite = () => Promise | (string | null); +export interface JourneySpec { + readonly name: string; // must be a declared journey + readonly needs?: readonly Prerequisite[]; // each answers null, or why it cannot start + readonly run: () => Promise; +} +export interface SuiteOptions { + readonly label: string; + readonly journeys: readonly JourneyDeclaration[]; + readonly journeyBound: number; + readonly diagnose?: () => Promise; +} +export interface SuiteReport { + check(name: string, ok: boolean, detail?: string): void; // throws on an undeclared name + journey(spec: JourneySpec): Promise; + finish(fatal: string | null): never; // prints, then leaves +} +export function declareSuite(options: SuiteOptions): SuiteReport; +``` + +## Guarantees + +**A bound names what it was waiting for.** `within(what, ms, task)` either settles the task or throws a `DeadlineExceeded` carrying `what` verbatim — never a bare timeout with no subject. `armSuiteDeadline` does the same for a whole run, and reports before leaving. `enclosingBound(steps, margin)` returns a number no smaller than the sum of the bounds it encloses, so an enclosure can never fire before the step that actually hung. + +**A run owns its profile and discards it.** `newRunProfile` returns a directory belonging to this run alone; `discard()` kills whatever still holds it and removes it, is idempotent, and also runs when the process leaves — including when the run is killed. Two runs never share local state, so nothing a previous run left can make this one pass or fail. + +**A minted wallet is new.** `mintWalletProfile`/`mintWalletBytes` walk the wallet application to create a wallet that did not exist a moment ago. `mintWalletBytes` keeps only the bytes and discards the profile it minted in; `mintWalletProfile` keeps the profile for the length of the run. The password is always a parameter and never has a default; the name defaults to `DEFAULT_WALLET_NAME` because nothing in NextGraph keys off it. + +**The crossing dispatches on the screen it can see**, not on a fixed sequence: `setupBrokerPage` reads the page against `BROKER_SCREENS` in order, answers it, and returns the application's `Frame` once the crossing has completed. It identifies the application by ORIGIN. A terminal screen ends the crossing as a failure rather than an action. + +**An empty-profile context starts with nothing local.** `emptyProfileContext` gives a context whose profile holds no wallet and no repo cache, which is the reconnection cold start: the wallet's repos are on the broker and not in this profile. `importWalletFile` then puts the wallet in — but not the repos' cache — so the next session over it still hits that cold start. + +**A closed server is closed.** `serveOnEphemeralPort` tracks its open sockets and destroys them on `close()`, so a connection nobody hung up cannot be blamed on whatever goes wrong next. + +**A known failure mode is named instead of the innocent operation.** `browserTrouble`/`frameTrouble` answer a string when the browser or the application frame is the actual cause, and `null` when they are not. `SuiteOptions.diagnose` puts that answer in FRONT of a journey's own reason, never in place of it. A frame that is attached, on the right URL, and empty is reported as trouble — that is the state a naive check misses. + +**The report's size does not depend on what failed.** Journeys and their checks are declared up front to `declareSuite`; `check` throws on a name the journey did not declare, `journey` runs one journey bounded and isolated so it cannot change the report's shape, and `finish` reports every declared check the run did not get to before printing and exiting. A journey whose prerequisite is provably dead is reported as such instead of being driven. + +**Errors are values where a caller can act on them.** `browserTrouble`/`frameTrouble`/`Prerequisite` answer `string | null`, never throw for the condition they detect. `DeadlineExceeded` and `BrowserGone` are exported classes, so `instanceof` is a supported way to tell a hang from a dead browser. + +## Non-guarantees + +**No assertion library, no runner, no fixtures.** `SuiteReport.check` records a boolean you computed; nothing here decides what is true. + +**`finish` never returns** — it exits the process. Do not put cleanup after it; put it in the `discard()`/`closeQuietly` path. + +**No message text is stable.** The strings from `browserTrouble`, `frameTrouble`, `DeadlineExceeded` and the report are for a human reading a run. Do not parse them, match on them, or branch on them. + +**No timing promise.** Every exported `*_MS` constant is a bound sized from a measurement on one machine, not a service level: they change when the measurements change, and a run slower than one of them is a failed run, not a broken guarantee. `E2E_TIMINGS=1` reprints the measurements so you can resize your own. + +**Nothing survives a run.** Profiles are discarded, and no artifact, cache, or wallet is carried from one run to the next. The one exception is a file you write yourself with `exportWalletFile` or `ng-mint-wallet`. + +**No concurrency contract.** The suite machinery assumes one run per process. Two suites sharing a process share `armSuiteDeadline`, the timings table and the loss declaration. + +**No wallet-application version pinning.** The crossing is driven against the wallet application as deployed at the broker you point it at. When that application changes its screens, `BROKER_SCREENS` is what has to be updated — a crossing that fails there is not necessarily your application's fault, and is not this package's promise to hold across upstream redesigns. + +## Change policy + +**Semver, and majors are the normal case.** This machinery tracks a NextGraph and a wallet application that are both still moving, so a screen inventory, a bound, or a call shape changes whenever they do — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time. + +What each level means here, in this package's own terms: + +- **major** — an exported symbol is removed or renamed, **or** an existing call narrows: it now throws where it returned, requires an argument that was optional, or returns something a caller must newly handle. A change to what a `SuiteReport` accepts (a check name, a journey shape) is a major, because your suite is written against it. A signature change a caller must react to counts; one that only accepts more than before does not. +- **minor** — a symbol is added and nothing existing moves: a new helper, a new screen in `BROKER_SCREENS`, a new optional field on `SuiteOptions`. +- **patch** — a fix that changes neither the exported surface nor anything above under `## Guarantees` — including a re-sized `*_MS` constant, which is explicitly disclaimed above. + +**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.3`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you. + +`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it. What exists today is `1.0.0-dev.1`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: ng-e2e-helpers@1.0.0-dev.1`, the string you pinned, never the version it is heading for. + +There is no changelog file and no deprecation window: **the sections above are the release note.** A removal or a narrowing lands in `## Surface` and `## Guarantees` in the same version that ships it. Diff this leaf between two pulls — `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first. diff --git a/.project/concepts/e2e-harness/ng-e2e-helpers/usage_polyfill-e2e.md b/.project/concepts/e2e-harness/ng-e2e-helpers/usage_polyfill-e2e.md new file mode 100644 index 0000000..1f1c038 --- /dev/null +++ b/.project/concepts/e2e-harness/ng-e2e-helpers/usage_polyfill-e2e.md @@ -0,0 +1,55 @@ +--- +type: usage +summary: What the polyfill's own end-to-end suites call from ng-e2e-helpers, under which constraints, and where the fit still costs them code +against: ng-e2e-helpers@1.0.0-dev.1 +--- + +# usage_polyfill-e2e — the polyfill's end-to-end suites + +The consumer is the end-to-end suite of `@ng-eventually/polyfill`: four entry points under that package's `e2e/` — a batch of SDK checks against the broker, an applicative suite driving the example application, a reactivity probe, and a cold-start reproduction — plus the page module they share. + +This consumer lives in the same repository as the provider. That changes nothing about what is declared here: the engagement is what these suites are written against, and anything they call that is NOT listed below is theirs to fix if it moves. + +## Consumed surface + +**Bounds** — `within`, `armSuiteDeadline`, `closeQuietly`, `enclosingBound`, `firstLine`. `within` is the most-called entry of the whole package here: every bridge call into the application iframe goes through it. + +**Measurement** — `measured`. Nothing reads `record`, `timingsWanted` or `printTimings` directly; the `E2E_TIMINGS=1` output is consumed as printed output, not as a call. + +**Browser and profiles** — `launchWatchedContext`, `closeContext`, `newPage`, and the type `RunProfile`. `newRunProfile` and `isAlive` are never called: profiles are always obtained through a wallet call. + +**Wallet** — `mintWalletProfile` (the batch wallet, minted once per run), `mintWalletProfileKeepingContext` (the cold-start reproduction, which needs the very first application session over a never-used wallet), `emptyProfileContext` + `importWalletFile` (the reconnection journey), `exportWalletBytes` (serving the wallet from the application's own bundle), and the type `WalletCredentials`. + +**Broker crossing** — `setupBrokerPage` in all four suites, `completeBrokerLogin` in the applicative one. + +**Serving** — `serveOnEphemeralPort`, for both the SDK page and the example application's bundle. + +**Known failure modes** — `browserTrouble`, `frameTrouble`. + +**Report** — `declareSuite` and the types `JourneyDeclaration`, `Prerequisite`. Used by the applicative suite only. + +**Constants** — `BROKER_ROUND_TRIP_MS`, `NEW_PAGE_MS`, both as inputs to `enclosingBound` rather than as waits of their own. + +Everything else the package exports is offered and NOT consumed here — the screen inventory (`BROKER_SCREENS`, `WALLET_APP`, `WALLET_CREATION`, `WALLET_IMPORT`, `brokerRedirectFor`, and the screen types), `createWalletInContext`, `exportWalletFile`, `mintWalletBytes`, `DEFAULT_WALLET_NAME`, `browserLost`, `lossDeclared`, the exported error classes, and the remaining `*_MS` constants. It is safely evolvable as far as these suites are concerned. + +## Constraints + +**One Playwright, and the browser types come from the helpers.** These suites import `Frame`, `Page` and `BrowserContext` from `playwright` directly today, which holds only because the package resolves the same copy. The safer form — deriving them from the helpers that return them — is what a consumer in another repository must do, and what this one should adopt. + +**Every suite mints its own physical wallet, per run.** Nothing is carried between runs and no wallet is reused, so a run's result never depends on a previous one. The batch wallet's NAME is stable and its identity is not: two runs sharing the name share nothing else. + +**The password is supplied, never defaulted.** These suites also read the barrier's displayed password back off its own screen and pass it to `importWalletFile`, which is the only way to prove that what the barrier shows is what opens the file. A defaulted password would make that check vacuous. + +**An enclosing bound is at least the sum of what it encloses.** These suites compute every enclosure with `enclosingBound` rather than picking a round number, because an enclosure that fires first reports "the journey timed out" and never names the step that hung. + +**A wait must not be entered unbounded.** Playwright's `frame.evaluate` waits forever, so no bridge call is made outside `within`. + +## Frictions + +**`frame.evaluate` has no bound, and three suites each wrote the same wrapper.** A `sdk(frame, method, …args)` helper — `within(…, BRIDGE_MS, () => frame.evaluate(…))` — is duplicated verbatim in three of the four entry points. The bound is the single most important one in each of them, and the package offers nothing to obtain it from. A bounded `evaluate` helper here would delete the duplication and stop each new suite re-deriving it. + +**"Measured and bounded" is one intent and two calls.** Sizing a bound from its own measurement is the discipline this package prescribes, yet the applicative suite composes `measured(what, ms, (bound) => within(what, bound, task))` by hand — and the indexing layer's suite, in another repository, wrote the same line byte for byte. A helper that pairs them belongs on the engagement. + +**`declareSuite` fits a journey suite and not a check batch.** Only the applicative suite uses it; the other three declare a local `record`/`check` pair and print their own summary, because their shape is a flat list of checks discovered as the run goes rather than named journeys declared up front. The up-front declaration is exactly what makes the report bounded, so this is not a request to relax it — but three of four suites falling outside it suggests the package covers one of two real shapes. + +**The screen inventory is exported and unreachable in practice.** `BROKER_SCREENS` and the `WALLET_*` page descriptions are published, but a suite that crosses the broker with `setupBrokerPage` never needs them, and one that wanted to extend the crossing has no supported way to hand a modified inventory back in. As it stands they read as internals that happen to be exported. diff --git a/.project/contracts.yaml b/.project/contracts.yaml index 5b5256d..00a12a0 100644 --- a/.project/contracts.yaml +++ b/.project/contracts.yaml @@ -1,11 +1,42 @@ # Inter-repo contracts. `publish:` is this project's engagement toward its consumers — # listing a leaf here IS the act of publishing it; an unlisted `contract_` leaf is a draft. # -# This project is a PROVIDER only for now: the applications that consume the polyfill live in -# their own repos and pull `polyfill-surface` from here. Nothing is consumed in the other -# direction — the NextGraph clone this library aligns on is a read-only reference we -# verify against, not a contract anyone stamps. +# This project is a PROVIDER of two interfaces, each hosted in an interface folder inside +# its owning concept, with one `usage_.md` per consumer beside the engagement: +# +# polyfill-surface (concepts/app-contract/polyfill-surface/) +# consumers: the Festipod application (its own repo), `ng-helpers` +# ng-e2e-helpers (concepts/e2e-harness/ng-e2e-helpers/) +# consumers: this repo's own polyfill e2e suites (same repo — declared in place, +# nothing to pull), `ng-helpers`, any NextGraph application +# +# `consume:` here is the PROVIDER side of the pair: we pull each consumer's own declaration +# back, so we can see what is actually relied on and read its Frictions. We consume no +# engagement — the NextGraph clone this library aligns on is a read-only reference we verify +# against, not a contract anyone stamps. +# +# `pullFrom:` values are CANONICAL remote identities, because this file travels with the +# branch. Per-developer local access lives in `.project/contracts.local.yaml`, which is +# gitignored and must never be committed. publish: # paths are relative to `.project/` - polyfill-surface: concepts/app-contract/contract_polyfill-surface.md + polyfill-surface: concepts/app-contract/polyfill-surface/contract_polyfill-surface.md + ng-e2e-helpers: concepts/e2e-harness/ng-e2e-helpers/contract_ng-e2e-helpers.md + +consume: + # Festipod is NOT listed: it has not declared a usage leaf, and we do not author one for + # a consumer whose repo is not ours. An interface with no declared consumer degrades to + # the one-document mode, which is what `polyfill-surface` is for Festipod today. + + - contract: ng-helpers-uses-polyfill-surface + type: git + pullFrom: git@gitea.reconnexion.apps.gueraud.net:Sylvain/ng-helpers.git/.project/concepts/indexing/polyfill-surface/usage_ng-helpers.md + ref: main + into: concepts/app-contract/polyfill-surface/ + + - contract: ng-helpers-uses-ng-e2e-helpers + type: git + pullFrom: git@gitea.reconnexion.apps.gueraud.net:Sylvain/ng-helpers.git/.project/concepts/indexing/ng-e2e-helpers/usage_ng-helpers.md + ref: main + into: concepts/e2e-harness/ng-e2e-helpers/ diff --git a/packages/ng-e2e-helpers/package.json b/packages/ng-e2e-helpers/package.json index 1bd07d3..44bf095 100644 --- a/packages/ng-e2e-helpers/package.json +++ b/packages/ng-e2e-helpers/package.json @@ -1,6 +1,6 @@ { "name": "ng-e2e-helpers", - "version": "0.0.0", + "version": "1.0.0-dev.1", "type": "module", "description": "End-to-end testing machinery for a NextGraph application: mint and carry a wallet, cross the broker, per-run browser profiles, bounded waits that name what they were waiting for, and a run report whose size does not depend on what failed.", "main": "./src/index.ts", diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index 31e5890..b35844e 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -1,6 +1,6 @@ { "name": "@ng-eventually/polyfill", - "version": "0.0.0", + "version": "1.0.0-dev.1", "type": "module", "description": "Polyfill of the NextGraph JS SDK over @ng-org/web + @ng-org/orm, with emulated capabilities and inbox. Drop-in; remove at migration.", "main": "./src/index.ts",