Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAIM_INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ must not be inferred from a row here. See
| Only intervals a five-field cron can name are declarable; the rest are refused by name | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestTriggerScheduleSpellsOnlyTheIntervalsCronCanName` (`pkg/business`) | implemented | accounts |
| An occurrence under the reference arm is minted from current facts, and the next one after a revocation is refused | MODULE.md § Unattended work; WORK_CONTEXTS.md § Triggers | `TestTriggerOccurrenceMintsFromTheReferenceAndIsRefusedAfterRevocation` (`pkg/adapters`) | implemented | accounts |
| The installation-principal arm mints for a binding whose audience is the executing module's own name | WORK_CONTEXTS.md § Triggers | `TestTriggerOccurrenceMintsFromTheInstallationBinding`, `TestTriggerIdentityWithoutADeclarationRefusesBothArms` (`pkg/adapters`) | implemented | accounts |
| "Sync now" admits `ReconcileSource` under a kit-minted effect identity, and a double press yields one receipt and one job | MODULE.md § Unattended work; saas-ui README § Datasources | `reconcile-pass.test.tsx` (`packages/saas-ui`) drives the panel: one identity per pass, the repeat press recovers through `LookupReconcileSource` without re-admitting, the identity survives a failure | implemented | frontend |
| No surface in this repository reaches a source sync without an effect identity | MODULE.md § Unattended work; saas-ui README § Datasources | `reconcile-is-the-only-manual-pass.test.ts` walks the TypeScript AST of `src` and the published packages and fails on any `syncSource` on a datasource client | implemented | frontend |
| A person's pass is listed beside the declared schedule's occurrences, with its effect identity | MODULE.md § Unattended work | `source-trigger-schedules.container.test.tsx` asserts the manual row is in the SAME `admissions` table as the two scheduled rows, and that a recovered receipt replaces its row rather than adding one | implemented | frontend |
| Every pass a source ever took, listed | — | none, and none is possible here: a receipt is recovered by identity and never enumerated, so the panel shows only the passes this browser admitted and says so | not claimed | accounts |
| A schedule actually coming due, in a composed environment, under either arm | MODULE.md § Unattended work ("what is not proven") | none — nothing in this repository composes a module that executes declared work | planned | #1081 |

## Handbook and interface surfaces
Expand Down
16 changes: 16 additions & 0 deletions MODULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,22 @@ it, and a test walks the service's AST to keep that true. One occurrence admits
`DatasourceService/ReconcileSource`, the same host operation a person's manual
pass takes.

**"Sync now" is that admission, not a request beside it** (issue #1094). The
published kit points the button at `ReconcileSource` under an **effect identity
the kit mints**, so a person's pass and a scheduled occurrence are one admission
of one shape and the receipts cannot tell them apart. A press that repeats the
pass still being reported reads the original receipt back through
`LookupReconcileSource` and surfaces *that* — one receipt and one job for two
presses, rather than two full re-sends of the source — and the press after that
is a new pass. `syncSource`, which reached the same sync engines carrying no
effect identity, is **deleted** from the kit rather than deprecated, and a
TypeScript AST walk over this repository fails on any remaining caller
(`src/features/datasources/__tests__/reconcile-is-the-only-manual-pass.test.ts`).
The reconcile-schedules panel lists a person's pass in the **same table** as the
declared schedule's last and next occurrence, with its effect identity beside
them; the passes it can list are the ones this browser admitted, because a
receipt is recovered by identity and never enumerated.

**What is not proven:** no live occurrence exists. Nothing in this repository
composes a module that executes declared work, so the declaration is built
and held to the contract's field names by test, and the walk that has not been
Expand Down
24 changes: 18 additions & 6 deletions module/services/accounts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@ person pressing "Sync now", and by the **periodic reconcile**. Only the last of
the three is guaranteed to exist, so it is what makes "we sync on push" honest:
without it, a missed webhook is permanent and nothing ever notices.

Two of those three are now the **same admission**: a person's "Sync now" and one
occurrence of the declared schedule both admit `ReconcileSource` under an effect
identity their caller minted (issue #1094).

- **The schedule is DECLARED here and FIRED somewhere else** (issue #1081).
`business.DeclareSourceTrigger` turns a source's interval into a trigger an
declared release carries — a five-field cron in UTC, `"skip"` for both missed
Expand All @@ -959,16 +963,24 @@ without it, a missed webhook is permanent and nothing ever notices.
ticker in `work.go`, `enqueueScheduledReconcile`, `enqueueScheduledPullSync`,
`scheduledReconcileKey` and the due query they read
(`ListDatasourceSourcesDueForReconcile`).
- **One occurrence admits one host operation.** `DatasourceService/ReconcileSource`
(marked `codefly.runnable.v0.operation`, idempotency required, paired with
`LookupReconcileSource`) is what the declared trigger names, and it is the same
work a person asks for by hand. It routes each provider to the engine that
- **One occurrence admits one host operation, and so does a person's press.**
`DatasourceService/ReconcileSource` (marked `codefly.runnable.v0.operation`,
idempotency required, paired with `LookupReconcileSource`) is what the declared
trigger names, and since #1094 it is literally what "Sync now" calls as well —
the published kit's `syncSource`, which reached these engines carrying no
effect identity, is deleted. It routes each provider to the engine that
provider syncs on: GitHub to a conditional reconcile on the delivery queue (the
job resolves the head and snapshots only if it moved), everything else to the
same sync request "Sync now" produces. The enqueue is keyed by the occurrence's
pull its connector has. The enqueue is keyed by the occurrence's
effect identity, so two attempts of one occurrence are one job — which is what
the schedule-derived key had to stand in for while two replicas could both
sweep one due row.
sweep one due row, and which is now also what makes a person's double press
one pass. **The host has no notion of an identity being spent**: it returns the
same receipt for the same identity forever, so "when is my intent new" is the
CLIENT's question, and the kit answers it from presses rather than from a
clock. There is no enumerable list of past passes either — a receipt is read
back by identity, never listed — so a surface claiming to show every pass a
source ever took would be inventing one.
- **The interval is per provider, and it is the source's own configuration.** 30
minutes for GitHub; a day for the pull providers, whose connectors re-send
their whole content on every sync (the gap registered against each in
Expand Down
4 changes: 2 additions & 2 deletions module/services/frontend/code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codefly-dev/ui",
"version": "0.12.7",
"version": "0.12.8",
"description": "The shared, versioned Codefly SaaS frontend kit: the plugin host and the tokens-as-data skin mechanism, standalone so the host and Module-Federation remotes import and dedupe one copy.",
"repository": {
"type": "git",
Expand Down
35 changes: 30 additions & 5 deletions module/services/frontend/code/packages/saas-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,10 @@ const directory = usePrincipalDirectory(); // loading | ready | refused | failed
a **Connect GitHub** action. The last-sync cell shows whichever clock applies:
`last_synced_at` for a provider that pulls, or `last ingest <date, time> ·
<short commit>` for a github source, whose change sets the compiler enqueues
(webhook delivery, periodic reconcile, or a tenant's "Sync now" forced
reconcile) — it never sets `last_synced_at`, so "Never" is dropped rather than
shown above live provenance. Loading/error/empty are first-class. Connect,
(a webhook delivery, or a reconcile pass — a declared schedule's occurrence or
a tenant's "Sync now", which are the same admission) — it never sets
`last_synced_at`, so "Never" is dropped rather than shown above live
provenance. Loading/error/empty are first-class. Connect,
Sync, Reconnect, Use GitHub App and Delete are offered only to a viewer who
may use them (`canManage`; with a `gateway`, read from the viewer's
credential): anyone else keeps a row's History and is told who connects
Expand Down Expand Up @@ -258,7 +259,11 @@ const directory = usePrincipalDirectory(); // loading | ready | refused | failed
offers so the consumer can show the ingesting module's progress.
- **Error** — `DEGRADED` or `PAUSED`: the host's `status_reason` verbatim,
and Reconnect, which asks for the credential again (a replacement PAT, or
nothing on the App and public paths) and enqueues a sync.
nothing on the App and public paths) and enqueues a sync. Reconnect needs
the client's `reconnectSource`: a reconnect rebinds whose authority the
source's syncs run under, which is not a reconcile and has no idempotent
form, so a client without it is told who reconnects rather than offered a
control that would do something else.
There is no fourth state. A source whose status an **older host does not
report** (the field decodes to its proto default, which the gateway maps to
`unknown` rather than guessing) reads as **Connected** and says its state is
Expand Down Expand Up @@ -291,14 +296,34 @@ const directory = usePrincipalDirectory(); // loading | ready | refused | failed
Do not mount this and `<DatasourcesPanel>` on the same page: both redeem the
GitHub App's single-use return state, and the loser reports a rejection for an
installation that in fact succeeded.
- **"Sync now" is an admission, not a request.** It runs
`DatasourceService/ReconcileSource` — the host operation a source's *declared*
reconcile schedule admits one occurrence of — under an **effect identity the
kit mints**, so a person's pass and a scheduled pass are one admission of one
shape and are indistinguishable in the host's receipts. `useReconcileSource`
owns the protocol: it presents an identity from a `ReconcilePassLedger` it
holds for the mounted surface (`createReconcilePassLedger`), and a
press that **repeats** the pass still being reported reads the original
receipt back through `LookupReconcileSource` and surfaces *that* — one receipt
and one job for two presses, rather than two full re-sends of the source. The
press after a repeat is a new pass (the ledger retires the identity); a press
after a *failure* re-presents the same identity, which makes a retry a
recovery. A lookup answering `"unknown"` is **inconclusive**, never "it did
not happen". The panel and `<DeclaredSourceCard>` render the outcome the
operation returned — the identity, the status and the job.
`onSourceReconcileAdmitted((pass) => …)` hears each one, which is how a host
surface shows a person's pass beside the declared schedule's occurrences.
There is **no `syncSource`**: it reached the same engines carrying no effect
identity, so two presses were two passes, and it is deleted rather than
deprecated.
- `createDatasourceClient({ apiBase, getAccessToken, refreshAccessToken })` — builds the
gateway-bound `DatasourceClient` (with 401 refresh-and-retry) directly, for driving the
hooks outside the panel. `datasourceClientOverTransport(transport)` does the same over a
transport you already own. Its `getSourceSync(orgId, sourceId)` reads a source's latest
sync as typed phases (queued, fetching, compiled, handed off, done, failed) with a
timestamp for each, the compiled change set, and a typed failure.
- `onSourceSyncRequested((sourceId) => …)` — hears every sync the kit's client enqueues
(Sync now, a reconnect, the first sync a connect starts), so a view rendering a source's
(a reconcile pass, a reconnect, the first sync a connect starts), so a view rendering a source's
progress in the panel's per-source slot can watch closely at once. It returns the
unsubscribe. The registry lives on `globalThis`, so a remote's own copy of the kit hears
the host panel.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codefly-dev/saas-ui",
"version": "0.12.7",
"version": "0.12.8",
"description": "Reusable SaaS-domain frontend components (datasources, …) the portal and solutions both import, built on @codefly-dev/ui primitives and driven by an injected typed client.",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ const collection: CollectionAccessView = {
const client = (overrides: Partial<DatasourceClient>): DatasourceClient => ({
listSources: async () => [],
addGitHubSource: async () => {},
syncSource: async () => "job",
reconcileSource: async (_orgId, _id, effectId) => ({
effectId,
status: "committed" as const,
jobId: "job",
}),
lookupReconcileSource: async () => undefined,
deleteSource: async () => {},
...overrides,
});
Expand Down
Loading
Loading