Skip to content

feat: scaffold the feature standard the platform actually enforces - #6

Merged
pyramation merged 2 commits into
mainfrom
feat/feature-image-and-declared-inputs
Aug 9, 2026
Merged

feat: scaffold the feature standard the platform actually enforces#6
pyramation merged 2 commits into
mainfrom
feat/feature-image-and-declared-inputs

Conversation

@pyramation

@pyramation pyramation commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Whatever fun init emits is the standard, whatever the docs say — and it was
emitting four things the platform disagrees with.

The image named nothing. Both feature templates declared
"image": "fn-____name____", while constructive-functions publishes
ghcr.io/constructive-io/features/<name>. A scaffold therefore named an image
nothing builds; it is features/____name____ now, in the manifests and in the
IMAGE constants beside them.

The gql manifest declared "inputs": [] — the one thing the manifest exists
for. The runtime compiles declared inputs into a JSON Schema and answers a bad
request with a 400 before the handler is entered, so a scaffold declaring none
taught its author to hand-check payloads, which AGENTS.md spends a section
forbidding. Every method now declares one, and the sql handler stops re-checking
a port the platform already rejected:

-export interface Params { [key: string]: unknown; }
-if (typeof subject !== 'string') throw new Error('payload.subject must be a string');
+export interface Params { subject: string; }

The sql surface no longer owns a schema. It scaffolded a pgpm module —
pgpm.plan, ____name____.control, Makefile, deploy/ revert/ verify/, a
____schema____ prompt — which constructive-functions/AGENTS.md forbids in as
many words, and which is how eight features end up inventing eight documents
tables that nothing in production has. All of it is deleted, along with the
language teaching it (both READMEs and the root one). What remains is the only
thing the surface ever was: a database connection.

The example query is now the one statement true of every database a feature might
run against — what the transaction is, rather than a table the template had to
invent to have something to read:

ctx.db(async (db) => {
  const { rows } = await db.query(
    "SELECT current_user AS role, current_setting('jwt.claims.database_id', true) AS database_id"
  );
  return { answer: `${params.subject} read for ${databaseId}`, role };
});

Its behaviour suite is the point of the surface as a fixture: the same handler on
a job carrying an actor reads authenticated, and without one reads anonymous.
role is a declared output, so the manifest carries it too.

Two drifts a fresh scaffold hit before it could run, found by scaffolding into
constructive-functions and running it rather than by reading:

  • createFunctionContext() now requires the job every invocation carries;
  • export const methods: Record<string, FunctionHandler<Record<string, unknown>>>
    erased each handler's declared params, so a typed Params failed to assign.
    The map infers its own type now, as features/greeting does.

scripts/check-templates.mjs pins the invariants on every push (node builtins
only, no package to install): feature images are features/<name>, every template
declares a method, every method declares at least one input, task identifiers stay
____name____:____method____, IMAGE agrees with the manifest, invalid JSON is a
hard failure — and no feature template carries pgpm.plan, a Makefile, a
.control, or deploy/ revert/ verify/, so the module cannot come back the way
it arrived. It reads both manifest shapes (methods[] and the top-level
single-method form a platform handler uses), so the two are not forced to converge
to satisfy the checker.

Verified by scaffolding both surfaces into constructive-functions with
pnpm fun init <name> --surface gql|sql against this branch and running them: 4
tests each, green, no manual repair.

Companions: constructive-io/constructive-db#2970 (fun root/port resolution, and
--schema retired from fun init) and constructive-io/constructive-functions#52.
Planning: constructive-io/constructive-planning#1535.

Link to Devin session: https://app.devin.ai/sessions/12ca02ecefab4a49a7d375452a86efdb
Requested by: @pyramation

A scaffolded feature named an image nothing publishes (fn-<name>, while
constructive-functions pushes ghcr.io/constructive-io/features/<name>), and the
gql template declared no inputs at all — so the runtime had nothing to enforce
and the author's first instinct was the presence check the declaration exists to
make unnecessary. Every method now declares one, the sql handler stops
re-checking a port the runtime already 400s on, and both queue suites pass
featureDir so a locally run image holds a request to the same declaration a
deployed one does.

Two drifts a fresh scaffold hit before it could run: createFunctionContext now
requires the job every invocation carries, and annotating methods as one common
payload type erased each handler's declared params.

scripts/check-templates.mjs pins the invariants on every push: what fun init
emits is the standard, whatever the docs say.

Verified by scaffolding both surfaces into constructive-functions and running
them: 4 tests each, green, no manual repair.
@pyramation pyramation self-assigned this Aug 9, 2026
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit 2666f4b into main Aug 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant