loki-mode 7.121.2 → 7.121.4
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +33 -0
- package/autonomy/lib/contract-scaffold/README.md +83 -0
- package/autonomy/lib/contract-scaffold/generate.mjs +90 -0
- package/autonomy/lib/contract-scaffold/scaffold.sh +147 -0
- package/autonomy/lib/contract-scaffold/templates/server/index.mjs.tmpl +62 -0
- package/autonomy/lib/contract-scaffold/templates/server/package.json.tmpl +10 -0
- package/autonomy/lib/contract-scaffold/templates/ui/ListView.tsx.tmpl +75 -0
- package/autonomy/lib/contract-scaffold/templates/ui/theme.css.tmpl +99 -0
- package/autonomy/lib/functional-verify.py +246 -0
- package/autonomy/lib/proof-generator.py +97 -3
- package/autonomy/lib/scaffold-hook.sh +82 -0
- package/autonomy/run.sh +75 -6
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.121.
|
|
6
|
+
# Loki Mode v7.121.4
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.121.
|
|
411
|
+
**v7.121.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.121.
|
|
1
|
+
7.121.4
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -723,6 +723,11 @@ _detect_port() {
|
|
|
723
723
|
*make*)
|
|
724
724
|
_APP_RUNNER_PORT=8080
|
|
725
725
|
;;
|
|
726
|
+
*http.server*|static)
|
|
727
|
+
# Static site served by python3 -m http.server. 8000 is python's
|
|
728
|
+
# http.server convention; keep it distinct from the 8080 catch-all.
|
|
729
|
+
_APP_RUNNER_PORT=8000
|
|
730
|
+
;;
|
|
726
731
|
*)
|
|
727
732
|
_APP_RUNNER_PORT=8080
|
|
728
733
|
;;
|
|
@@ -914,6 +919,34 @@ app_runner_init() {
|
|
|
914
919
|
return 0
|
|
915
920
|
fi
|
|
916
921
|
|
|
922
|
+
# 9b. Static site: a web root with an index.html and no server-app signal
|
|
923
|
+
# above (no dev/start script, no Flask/Django/etc). A landing page / static
|
|
924
|
+
# site IS a serveable web app -- without this it fell through to "none" and
|
|
925
|
+
# got no preview, no health check, no screenshot (founder feedback: a hero/
|
|
926
|
+
# pricing/waitlist landing page showed "This app has no live server"). Serve
|
|
927
|
+
# it with python3 -m http.server (always present, zero deps). Guarded on a
|
|
928
|
+
# REAL web root index.html so genuine CLIs/libraries (no index.html) still
|
|
929
|
+
# honestly read "none" below -- this never green-washes a non-web artifact.
|
|
930
|
+
local static_root=""
|
|
931
|
+
if [ -f "$dir/index.html" ]; then
|
|
932
|
+
static_root="$dir"
|
|
933
|
+
elif [ -f "$dir/public/index.html" ]; then
|
|
934
|
+
static_root="$dir/public"
|
|
935
|
+
elif [ -f "$dir/dist/index.html" ]; then
|
|
936
|
+
static_root="$dir/dist"
|
|
937
|
+
elif [ -f "$dir/build/index.html" ]; then
|
|
938
|
+
static_root="$dir/build"
|
|
939
|
+
fi
|
|
940
|
+
if [ -n "$static_root" ]; then
|
|
941
|
+
_detect_port "static"
|
|
942
|
+
# Serve the static root on the detected port; bind localhost, no reload.
|
|
943
|
+
_APP_RUNNER_METHOD="python3 -m http.server ${_APP_RUNNER_PORT} --bind 127.0.0.1 --directory $static_root"
|
|
944
|
+
_write_detection "static" "$_APP_RUNNER_METHOD"
|
|
945
|
+
log_info "App Runner: detected static site (index.html) -> serving $static_root"
|
|
946
|
+
_APP_RUNNER_URL="http://localhost:${_APP_RUNNER_PORT}"
|
|
947
|
+
return 0
|
|
948
|
+
fi
|
|
949
|
+
|
|
917
950
|
# 10. Fallback: nothing detected
|
|
918
951
|
log_warn "App Runner: no application detected, continuing without app runner"
|
|
919
952
|
_write_detection "none" ""
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# contract-scaffold (M1: the contract-first codegen spine)
|
|
2
|
+
|
|
3
|
+
The single load-bearing mechanism behind "a real, wired app, not a static shell."
|
|
4
|
+
See `artifacts/beat-replit-engineering-plan.md` (studied from Replit's Adopt) and
|
|
5
|
+
`artifacts/replit-adopt-study.md`.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
`scaffold.sh <out_dir> <resource> [field:type ...]` emits a **contract-first**
|
|
10
|
+
project skeleton:
|
|
11
|
+
|
|
12
|
+
1. An **OpenAPI contract** (`openapi.yaml`) as a first-class artifact -- the single
|
|
13
|
+
source of truth for the API.
|
|
14
|
+
2. An **Orval config** that generates typed react-query hooks FROM the contract.
|
|
15
|
+
3. `package.json` + `tsconfig.json` with a real codegen + typecheck script.
|
|
16
|
+
|
|
17
|
+
The point: after `npm run codegen`, a page can only call hooks that the contract
|
|
18
|
+
defines. A page referencing an endpoint the backend does not implement **fails
|
|
19
|
+
typecheck** -- so a static-shell-passed-off-as-wired becomes impossible.
|
|
20
|
+
|
|
21
|
+
## Proven (tests/test-contract-scaffold.sh, 7/7)
|
|
22
|
+
|
|
23
|
+
- scaffold -> real `openapi.yaml`
|
|
24
|
+
- real Orval codegen -> `useListBookmark` / `useCreateBookmark` / `useDeleteBookmark`
|
|
25
|
+
- a page on a REAL endpoint typechecks
|
|
26
|
+
- a page on a NON-CONTRACT endpoint **fails typecheck** (drift blocked)
|
|
27
|
+
|
|
28
|
+
Proven on a throwaway `bookmark` resource -- **general by construction, no
|
|
29
|
+
knowledge of any specific PRD** (anti-teaching-to-the-test).
|
|
30
|
+
|
|
31
|
+
## Status: STANDALONE + ADDITIVE -- wired into NO build lane
|
|
32
|
+
|
|
33
|
+
This is a capability module, like FV-1 was. It changes zero existing build
|
|
34
|
+
behavior. **Wiring it into the default build lane is the gated next step** (the
|
|
35
|
+
M1->"M1-wire" fork, analogous to FV-1->FV-2): that step touches the parity-locked
|
|
36
|
+
`run.sh`/`build_prompt.ts` core, reclassifies what every build produces, and
|
|
37
|
+
requires bash+bun parity work + council review + founder sign-off. Do NOT wire it
|
|
38
|
+
in as a solo/rushed action.
|
|
39
|
+
|
|
40
|
+
## Scope
|
|
41
|
+
|
|
42
|
+
Gap A (the generated app's own quality). NOT Gap B (Replit's managed cloud:
|
|
43
|
+
hosted DB provisioning, auth, secrets, deploy infra -- multi-quarter, out).
|
|
44
|
+
|
|
45
|
+
## M2 -- real backend floor (DONE, standalone)
|
|
46
|
+
|
|
47
|
+
`generate.mjs <out_dir> <resource> [field:type ...]` produces a REAL Express +
|
|
48
|
+
SQLite backend (via `templates/server/`, substitution NOT heredocs) that persists:
|
|
49
|
+
POST creates a row that survives to a later GET, DELETE removes it, one seed row so
|
|
50
|
+
the first screen is never blank. Every declared field is always bound (a partial
|
|
51
|
+
POST persists cleanly, 201, never 500 -- a bug the FV harness caught).
|
|
52
|
+
|
|
53
|
+
Proven (tests/test-backend-floor.sh, 6/6) on a throwaway `note` resource: generates,
|
|
54
|
+
starts, POST persists, partial POST persists, and **FV-1 reports
|
|
55
|
+
functional_status=verified** -- the convergence. A static shell would fail this.
|
|
56
|
+
|
|
57
|
+
Architecture: template files + a Node substitutor, NOT bash heredocs emitting JS
|
|
58
|
+
(heredocs collide with JS `${...}`/backticks -- learned the hard way; see the plan).
|
|
59
|
+
|
|
60
|
+
## M3 -- design-system pass (DONE, standalone)
|
|
61
|
+
|
|
62
|
+
`generate.mjs` also emits (into `src/`, from `templates/ui/`) a design system so a
|
|
63
|
+
generated frontend looks DESIGNED, not default:
|
|
64
|
+
- `theme.css` -- ROLE tokens (bg/surface/text/primary/...), a confident modern
|
|
65
|
+
default palette, and a parallel dark theme honored from the OS (`--no-ui` skips).
|
|
66
|
+
- `<Resource>ListView.tsx` -- a data surface with all THREE UI states: a shimmer
|
|
67
|
+
skeleton (never a blank flash), an inviting empty state with a CTA, and a
|
|
68
|
+
first-run "you're viewing sample data" banner (backed by M2's seed row). Wired
|
|
69
|
+
to the M1 contract hook, so it cannot reference a non-contract endpoint.
|
|
70
|
+
|
|
71
|
+
Small touches that make users smile: friendly copy, a clear primary action,
|
|
72
|
+
graceful loading/empty/error states, no raw hex (re-theme from one block).
|
|
73
|
+
|
|
74
|
+
Proven (tests/test-design-system.sh, 8/8): no raw hex in components, all three
|
|
75
|
+
states, dark theme, AND the tokenized ListView TYPECHECKS against the generated
|
|
76
|
+
contract hooks (the contract even caught a wrong response-type assumption in the
|
|
77
|
+
UI -- the spine working as designed).
|
|
78
|
+
|
|
79
|
+
## Next (wiring, see the plan)
|
|
80
|
+
|
|
81
|
+
- Wire M1+M2+M3 into the build lane (GATED: touches parity-locked run.sh/
|
|
82
|
+
build_prompt.ts core -> bash+bun parity + council + founder sign-off).
|
|
83
|
+
- Wire the FV harness into the completion verdict (FV-2, founder-gated).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// generate.mjs -- M2 backend-floor generator (template substitution, NOT heredocs).
|
|
3
|
+
//
|
|
4
|
+
// Reads the server template files and substitutes tokens derived from a portable
|
|
5
|
+
// {resource, fields} descriptor, producing a REAL Express + SQLite backend that
|
|
6
|
+
// persists. General by construction: no knowledge of any specific PRD.
|
|
7
|
+
//
|
|
8
|
+
// Why a Node generator and not a bash heredoc: a heredoc emitting JS collides with
|
|
9
|
+
// JS `${...}` template literals and backticks (bit both the M1 yaml and a first
|
|
10
|
+
// M2 js heredoc). Templates + substitution keeps generated code reviewable as real
|
|
11
|
+
// code. See artifacts/beat-replit-engineering-plan.md (M2 architecture decision).
|
|
12
|
+
//
|
|
13
|
+
// Usage: node generate.mjs <out_dir> <resource> [field:type ...]
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const TPL = join(__dir, "templates", "server");
|
|
20
|
+
|
|
21
|
+
const [, , outDir, resource, ...fieldArgs] = process.argv;
|
|
22
|
+
if (!outDir || !resource) {
|
|
23
|
+
console.error("usage: generate.mjs <out_dir> <resource> [field:type ...]");
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fields = (fieldArgs.length ? fieldArgs : ["title:string"]).map((f) => {
|
|
28
|
+
const [name, type = "string"] = f.split(":");
|
|
29
|
+
return { name, type };
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const cap = resource.charAt(0).toUpperCase() + resource.slice(1);
|
|
33
|
+
const coll = resource.endsWith("s") ? resource : resource + "s";
|
|
34
|
+
|
|
35
|
+
const sqlType = (t) =>
|
|
36
|
+
["int", "integer", "number", "bool", "boolean"].includes(t) ? "INTEGER" : "TEXT";
|
|
37
|
+
const seedVal = (t) =>
|
|
38
|
+
["int", "integer", "number"].includes(t) ? "1"
|
|
39
|
+
: ["bool", "boolean"].includes(t) ? "0"
|
|
40
|
+
: "'sample value'";
|
|
41
|
+
|
|
42
|
+
const tokens = {
|
|
43
|
+
__COLL__: coll,
|
|
44
|
+
__RESOURCE_CAP__: cap,
|
|
45
|
+
__COLUMNS_SQL__: fields.map((f) => ` ${f.name} ${sqlType(f.type)},`).join("\n"),
|
|
46
|
+
__SEED_COLS__: fields.length ? fields.map((f) => f.name).join(", ") + "," : "",
|
|
47
|
+
__SEED_VALS__: fields.length ? fields.map((f) => seedVal(f.type)).join(", ") + "," : "",
|
|
48
|
+
__INSERT_COLS__: fields.length ? fields.map((f) => f.name).join(", ") + "," : "",
|
|
49
|
+
__INSERT_PLACEHOLDERS__: fields.length ? fields.map((f) => "@" + f.name).join(", ") + "," : "",
|
|
50
|
+
__BODY_FIELDS__: fields.map((f) => `"${f.name}"`).join(", "),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function render(tpl) {
|
|
54
|
+
let out = tpl;
|
|
55
|
+
// Replace longer tokens first so __RESOURCE_CAP__ isn't partially hit, etc.
|
|
56
|
+
for (const key of Object.keys(tokens).sort((a, b) => b.length - a.length)) {
|
|
57
|
+
out = out.split(key).join(tokens[key]);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
mkdirSync(join(outDir, "server"), { recursive: true });
|
|
63
|
+
for (const [tpl, dest] of [
|
|
64
|
+
["index.mjs.tmpl", "index.mjs"],
|
|
65
|
+
["package.json.tmpl", "package.json"],
|
|
66
|
+
]) {
|
|
67
|
+
const src = readFileSync(join(TPL, tpl), "utf8");
|
|
68
|
+
writeFileSync(join(outDir, "server", dest), render(src));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// M3: the design-system UI templates (tokens + dark + three states), wired to the
|
|
72
|
+
// M1 contract hooks. Emitted into src/ alongside the generated client so a
|
|
73
|
+
// generated frontend looks DESIGNED (not default) and cannot reference a
|
|
74
|
+
// non-contract endpoint. Optional: skip with --no-ui.
|
|
75
|
+
if (!process.argv.includes("--no-ui")) {
|
|
76
|
+
const UI = join(__dir, "templates", "ui");
|
|
77
|
+
mkdirSync(join(outDir, "src"), { recursive: true });
|
|
78
|
+
for (const [tpl, dest] of [
|
|
79
|
+
["theme.css.tmpl", "theme.css"],
|
|
80
|
+
["ListView.tsx.tmpl", `${cap}ListView.tsx`],
|
|
81
|
+
]) {
|
|
82
|
+
const src = readFileSync(join(UI, tpl), "utf8");
|
|
83
|
+
writeFileSync(join(outDir, "src", dest), render(src));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`generated real backend floor at ${outDir}/server (Express + SQLite, /${coll})`);
|
|
88
|
+
if (!process.argv.includes("--no-ui"))
|
|
89
|
+
console.log(`generated design-system UI at ${outDir}/src (tokens + dark + 3 states, wired to the contract)`);
|
|
90
|
+
console.log(`next: (cd ${outDir}/server && npm install && PORT=3000 npm start)`);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# contract-scaffold/scaffold.sh -- M1: the contract-first codegen spine.
|
|
3
|
+
#
|
|
4
|
+
# The single load-bearing mechanism behind "a real wired app, not a static shell"
|
|
5
|
+
# (see artifacts/beat-replit-engineering-plan.md, studied from Replit's Adopt).
|
|
6
|
+
# It emits an OpenAPI contract as a first-class artifact, then generates typed
|
|
7
|
+
# client hooks + validators FROM it, so a page PHYSICALLY CANNOT call an endpoint
|
|
8
|
+
# the backend does not implement -- it will not typecheck. A static mock stops
|
|
9
|
+
# being possible to pass off as a wired app.
|
|
10
|
+
#
|
|
11
|
+
# STATUS: STANDALONE + ADDITIVE. Wired into NO build lane. It is a capability
|
|
12
|
+
# module proven on throwaway specs; wiring it into the default build is the
|
|
13
|
+
# gated next step (like FV-1 -> FV-2), requiring bash+bun parity work + council.
|
|
14
|
+
#
|
|
15
|
+
# GENERAL by construction: it takes a portable {name, entity, fields} descriptor
|
|
16
|
+
# and produces a contract-first skeleton for ANY simple REST resource. It has NO
|
|
17
|
+
# knowledge of any specific PRD (anti-teaching-to-the-test).
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# scaffold.sh <out_dir> <resource_name> [field:type ...]
|
|
21
|
+
# Example:
|
|
22
|
+
# scaffold.sh /tmp/bookmarks bookmark url:string title:string
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
OUT="${1:?usage: scaffold.sh <out_dir> <resource> [field:type ...]}"
|
|
26
|
+
RES="${2:?resource name required}"
|
|
27
|
+
shift 2 || true
|
|
28
|
+
FIELDS=("$@")
|
|
29
|
+
[ "${#FIELDS[@]}" -eq 0 ] && FIELDS=("title:string")
|
|
30
|
+
|
|
31
|
+
# Plural collection path (naive but sufficient: append 's' unless already plural).
|
|
32
|
+
case "$RES" in *s) COLL="$RES" ;; *) COLL="${RES}s" ;; esac
|
|
33
|
+
|
|
34
|
+
mkdir -p "$OUT"
|
|
35
|
+
|
|
36
|
+
# ---- 1. The OpenAPI contract (the single source of truth) -------------------
|
|
37
|
+
# Build the schema properties + a create-body from the field descriptors.
|
|
38
|
+
_props=""; _required=""; _create_props=""
|
|
39
|
+
for f in "${FIELDS[@]}"; do
|
|
40
|
+
fname="${f%%:*}"; ftype="${f##*:}"
|
|
41
|
+
case "$ftype" in
|
|
42
|
+
int|integer|number) otype="integer" ;;
|
|
43
|
+
bool|boolean) otype="boolean" ;;
|
|
44
|
+
*) otype="string" ;;
|
|
45
|
+
esac
|
|
46
|
+
_props+=" ${fname}: { type: ${otype} }
|
|
47
|
+
"
|
|
48
|
+
_create_props+=" ${fname}: { type: ${otype} }
|
|
49
|
+
"
|
|
50
|
+
_required+=" - ${fname}
|
|
51
|
+
"
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
cat > "$OUT/openapi.yaml" <<YAML
|
|
55
|
+
openapi: 3.1.0
|
|
56
|
+
info: { title: Api, version: 1.0.0 }
|
|
57
|
+
servers: [{ url: /api }]
|
|
58
|
+
paths:
|
|
59
|
+
/${COLL}:
|
|
60
|
+
get:
|
|
61
|
+
operationId: list${RES^}
|
|
62
|
+
responses:
|
|
63
|
+
'200':
|
|
64
|
+
description: OK
|
|
65
|
+
content:
|
|
66
|
+
application/json:
|
|
67
|
+
schema: { type: array, items: { \$ref: '#/components/schemas/${RES^}' } }
|
|
68
|
+
post:
|
|
69
|
+
operationId: create${RES^}
|
|
70
|
+
requestBody:
|
|
71
|
+
required: true
|
|
72
|
+
content:
|
|
73
|
+
application/json:
|
|
74
|
+
schema: { \$ref: '#/components/schemas/${RES^}Create' }
|
|
75
|
+
responses:
|
|
76
|
+
'201':
|
|
77
|
+
description: Created
|
|
78
|
+
content:
|
|
79
|
+
application/json:
|
|
80
|
+
schema: { \$ref: '#/components/schemas/${RES^}' }
|
|
81
|
+
/${COLL}/{id}:
|
|
82
|
+
delete:
|
|
83
|
+
operationId: delete${RES^}
|
|
84
|
+
parameters:
|
|
85
|
+
- { name: id, in: path, required: true, schema: { type: string } }
|
|
86
|
+
responses:
|
|
87
|
+
'204': { description: No Content }
|
|
88
|
+
components:
|
|
89
|
+
schemas:
|
|
90
|
+
${RES^}:
|
|
91
|
+
type: object
|
|
92
|
+
required:
|
|
93
|
+
- id
|
|
94
|
+
${_required} properties:
|
|
95
|
+
id: { type: string }
|
|
96
|
+
${_props} ${RES^}Create:
|
|
97
|
+
type: object
|
|
98
|
+
required:
|
|
99
|
+
${_required} properties:
|
|
100
|
+
${_create_props}
|
|
101
|
+
YAML
|
|
102
|
+
|
|
103
|
+
# ---- 2. Orval config: contract -> typed react-query hooks -------------------
|
|
104
|
+
cat > "$OUT/orval.config.ts" <<'TS'
|
|
105
|
+
import { defineConfig } from "orval";
|
|
106
|
+
export default defineConfig({
|
|
107
|
+
client: {
|
|
108
|
+
input: "./openapi.yaml",
|
|
109
|
+
output: {
|
|
110
|
+
target: "./src/generated/api.ts",
|
|
111
|
+
client: "react-query",
|
|
112
|
+
mode: "single",
|
|
113
|
+
clean: true,
|
|
114
|
+
override: { header: () => ["// GENERATED by orval -- do not edit manually."] },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
TS
|
|
119
|
+
|
|
120
|
+
# ---- 3. package.json (real deps, real codegen script) ----------------------
|
|
121
|
+
cat > "$OUT/package.json" <<JSON
|
|
122
|
+
{
|
|
123
|
+
"name": "contract-first-${COLL}",
|
|
124
|
+
"private": true,
|
|
125
|
+
"type": "module",
|
|
126
|
+
"scripts": {
|
|
127
|
+
"codegen": "orval --config ./orval.config.ts",
|
|
128
|
+
"typecheck": "tsc --noEmit"
|
|
129
|
+
},
|
|
130
|
+
"dependencies": { "@tanstack/react-query": "^5.0.0", "react": "^18.0.0" },
|
|
131
|
+
"devDependencies": { "orval": "^8.0.0", "typescript": "^5.4.0", "@types/react": "^18.0.0" }
|
|
132
|
+
}
|
|
133
|
+
JSON
|
|
134
|
+
|
|
135
|
+
cat > "$OUT/tsconfig.json" <<'JSON'
|
|
136
|
+
{
|
|
137
|
+
"compilerOptions": {
|
|
138
|
+
"strict": true, "noEmit": true, "jsx": "react-jsx",
|
|
139
|
+
"module": "ESNext", "moduleResolution": "Bundler",
|
|
140
|
+
"target": "ES2022", "lib": ["ES2022", "DOM"], "skipLibCheck": true
|
|
141
|
+
},
|
|
142
|
+
"include": ["src"]
|
|
143
|
+
}
|
|
144
|
+
JSON
|
|
145
|
+
|
|
146
|
+
echo "scaffolded contract-first skeleton at $OUT (resource=$RES, collection=/$COLL)"
|
|
147
|
+
echo "next: (cd $OUT && npm install && npm run codegen && npm run typecheck)"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Real backend FLOOR: Express + SQLite persistence implementing the M1 contract.
|
|
2
|
+
// POST persists; GET reflects it; DELETE removes it -- a working data pipeline,
|
|
3
|
+
// not a mock. Generated by contract-scaffold (template-substitution, general).
|
|
4
|
+
// (Token reference intentionally omitted from this comment so the substitutor
|
|
5
|
+
// does not rewrite it; see generate.mjs for the token list.)
|
|
6
|
+
import express from "express";
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
const db = new Database(process.env.DB_PATH || ":memory:");
|
|
11
|
+
db.exec(`CREATE TABLE IF NOT EXISTS __COLL__ (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
__COLUMNS_SQL__
|
|
14
|
+
created_at TEXT NOT NULL
|
|
15
|
+
);`);
|
|
16
|
+
|
|
17
|
+
// Seed one row so the very first screen is never blank (the empty vs first-run
|
|
18
|
+
// UI-state discipline: a real app greets the user with sample data, not a void).
|
|
19
|
+
const seedCount = db.prepare("SELECT COUNT(*) AS c FROM __COLL__").get().c;
|
|
20
|
+
if (seedCount === 0) {
|
|
21
|
+
db.prepare(
|
|
22
|
+
"INSERT INTO __COLL__ (id, __SEED_COLS__ created_at) VALUES (?, __SEED_VALS__ ?)"
|
|
23
|
+
).run(randomUUID(), new Date().toISOString());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const app = express();
|
|
27
|
+
app.use(express.json());
|
|
28
|
+
|
|
29
|
+
// Only these keys are accepted from a POST body -- never arbitrary columns.
|
|
30
|
+
// EVERY declared field is always bound (defaulting to null when the caller omits
|
|
31
|
+
// it), so a partial POST persists cleanly instead of throwing "missing named
|
|
32
|
+
// parameter". A real app tolerates optional fields; it does not 500 on them.
|
|
33
|
+
const BODY_FIELDS = [__BODY_FIELDS__];
|
|
34
|
+
function pickBody(body) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const k of BODY_FIELDS) out[k] = body != null && body[k] !== undefined ? body[k] : null;
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// GET /api/__COLL__ (list__RESOURCE_CAP__)
|
|
41
|
+
app.get("/api/__COLL__", (_req, res) => {
|
|
42
|
+
res.json(db.prepare("SELECT * FROM __COLL__ ORDER BY created_at DESC").all());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// POST /api/__COLL__ (create__RESOURCE_CAP__) -- PERSISTS
|
|
46
|
+
app.post("/api/__COLL__", (req, res) => {
|
|
47
|
+
const fields = pickBody(req.body);
|
|
48
|
+
const row = { id: randomUUID(), ...fields, created_at: new Date().toISOString() };
|
|
49
|
+
db.prepare(
|
|
50
|
+
"INSERT INTO __COLL__ (id, __INSERT_COLS__ created_at) VALUES (@id, __INSERT_PLACEHOLDERS__ @created_at)"
|
|
51
|
+
).run(row);
|
|
52
|
+
res.status(201).json(row);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// DELETE /api/__COLL__/:id (delete__RESOURCE_CAP__)
|
|
56
|
+
app.delete("/api/__COLL__/:id", (req, res) => {
|
|
57
|
+
db.prepare("DELETE FROM __COLL__ WHERE id = ?").run(req.params.id);
|
|
58
|
+
res.status(204).end();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
|
|
62
|
+
app.listen(port, () => console.log(`__RESOURCE_CAP__ API listening on port ${port}`));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ListView.tsx -- a data surface that ships all THREE UI states with design
|
|
2
|
+
// tokens, wired to the M1 contract hook. This is what "designed, not default"
|
|
3
|
+
// looks like: skeleton while loading, an inviting empty state with a CTA, a
|
|
4
|
+
// first-run "sample data" banner, and real rows in a tokenized card. It consumes
|
|
5
|
+
// the generated hook, so it physically cannot reference a non-contract endpoint.
|
|
6
|
+
//
|
|
7
|
+
// Generated by contract-scaffold M3 (general: __COLL__/__RESOURCE_CAP__ tokens,
|
|
8
|
+
// no product-specific content). Small touches that make users smile: friendly
|
|
9
|
+
// copy, an icon, a clear primary action, graceful loading + empty states.
|
|
10
|
+
import { useList__RESOURCE_CAP__, useCreate__RESOURCE_CAP__ } from "./generated/api";
|
|
11
|
+
|
|
12
|
+
export function __RESOURCE_CAP__ListView() {
|
|
13
|
+
const { data, isLoading, isError } = useList__RESOURCE_CAP__();
|
|
14
|
+
const create = useCreate__RESOURCE_CAP__();
|
|
15
|
+
// Orval's react-query hook returns the full response object; the array is at
|
|
16
|
+
// .data.data, typed straight from the OpenAPI contract (a real __RESOURCE_CAP__[]).
|
|
17
|
+
// Reading it wrong is a typecheck error -- the contract-first spine catching a
|
|
18
|
+
// drift in the UI itself.
|
|
19
|
+
const items = data?.data ?? [];
|
|
20
|
+
// A seeded backend (M2) means the first real screen shows sample data, not a void.
|
|
21
|
+
const isFirstRun = items.length > 0 && items.length <= 1;
|
|
22
|
+
|
|
23
|
+
// State 1: skeleton -- never a blank flash while loading.
|
|
24
|
+
if (isLoading) {
|
|
25
|
+
return (
|
|
26
|
+
<section className="card" aria-busy="true" aria-label="Loading __COLL__">
|
|
27
|
+
<div className="skeleton" style={{ width: "40%", marginBottom: 12 }} />
|
|
28
|
+
<div className="skeleton" style={{ width: "100%", marginBottom: 8 }} />
|
|
29
|
+
<div className="skeleton" style={{ width: "90%" }} />
|
|
30
|
+
</section>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isError) {
|
|
35
|
+
return (
|
|
36
|
+
<section className="empty" role="alert">
|
|
37
|
+
<p>We couldn't load your __COLL__. Please try again.</p>
|
|
38
|
+
</section>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// State 2: empty -- a dashed invitation with a clear next action, never a void.
|
|
43
|
+
if (items.length === 0) {
|
|
44
|
+
return (
|
|
45
|
+
<section className="empty">
|
|
46
|
+
<h3 style={{ margin: "0 0 4px" }}>No __COLL__ yet</h3>
|
|
47
|
+
<p style={{ margin: "0 0 16px" }}>Get started by creating your first one.</p>
|
|
48
|
+
<button className="btn-primary" onClick={() => create.mutate({ data: {} as never })}>
|
|
49
|
+
Create __RESOURCE_CAP__
|
|
50
|
+
</button>
|
|
51
|
+
</section>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// State 3: populated (with a friendly first-run banner when it's sample data).
|
|
56
|
+
return (
|
|
57
|
+
<section>
|
|
58
|
+
{isFirstRun ? (
|
|
59
|
+
<div className="first-run" role="status">
|
|
60
|
+
<span aria-hidden="true">*</span>
|
|
61
|
+
You're viewing sample data. Create a __RESOURCE_CAP__ to make it yours.
|
|
62
|
+
</div>
|
|
63
|
+
) : null}
|
|
64
|
+
<ul style={{ listStyle: "none", padding: 0, margin: "12px 0 0", display: "grid", gap: 12 }}>
|
|
65
|
+
{items.map((it) => (
|
|
66
|
+
<li key={String((it as { id?: unknown }).id)} className="card">
|
|
67
|
+
<pre style={{ margin: 0, fontFamily: "inherit", whiteSpace: "pre-wrap" }}>
|
|
68
|
+
{JSON.stringify(it, null, 2)}
|
|
69
|
+
</pre>
|
|
70
|
+
</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* theme.css -- design tokens (role-based, NEVER raw hex in components).
|
|
2
|
+
*
|
|
3
|
+
* Why this exists: generated apps look "AI-default" because they scatter raw
|
|
4
|
+
* colors and ship only a happy-path light theme. A real designed app uses ROLE
|
|
5
|
+
* tokens (bg, surface, text, primary, ...) referenced everywhere, ships a
|
|
6
|
+
* parallel dark theme from the same variable block, and honors the OS setting.
|
|
7
|
+
* Swapping one block re-themes the whole app. Modern, high-contrast, accessible.
|
|
8
|
+
*
|
|
9
|
+
* Generated by contract-scaffold M3 (general -- no product-specific colors;
|
|
10
|
+
* a strong neutral+indigo default any greenfield app can ship and re-brand).
|
|
11
|
+
*/
|
|
12
|
+
:root {
|
|
13
|
+
/* Surfaces */
|
|
14
|
+
--bg: #f7f8fa;
|
|
15
|
+
--surface: #ffffff;
|
|
16
|
+
--surface-2: #f0f2f5;
|
|
17
|
+
--border: #e3e6eb;
|
|
18
|
+
/* Text (AA contrast on --bg / --surface) */
|
|
19
|
+
--text: #1a1d24;
|
|
20
|
+
--text-muted: #5a6472;
|
|
21
|
+
/* Brand accent -- a confident modern indigo (re-brand by editing here only) */
|
|
22
|
+
--primary: #5b35e8;
|
|
23
|
+
--primary-contrast: #ffffff;
|
|
24
|
+
--primary-weak: #ece8fd;
|
|
25
|
+
/* Semantic */
|
|
26
|
+
--ok: #1f9d55;
|
|
27
|
+
--warn: #b7791f;
|
|
28
|
+
--danger: #c2333a;
|
|
29
|
+
/* Shape + depth (subtle, not shadow-heavy) */
|
|
30
|
+
--radius: 12px;
|
|
31
|
+
--radius-sm: 8px;
|
|
32
|
+
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.08);
|
|
33
|
+
--space: 16px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Dark theme -- same roles, honored automatically from the OS preference. */
|
|
37
|
+
@media (prefers-color-scheme: dark) {
|
|
38
|
+
:root {
|
|
39
|
+
--bg: #0f1117;
|
|
40
|
+
--surface: #171a21;
|
|
41
|
+
--surface-2: #1e222b;
|
|
42
|
+
--border: #2a2f3a;
|
|
43
|
+
--text: #eef1f6;
|
|
44
|
+
--text-muted: #9aa4b2;
|
|
45
|
+
--primary: #8b6dff;
|
|
46
|
+
--primary-contrast: #0f1117;
|
|
47
|
+
--primary-weak: #241f3d;
|
|
48
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/* Explicit override hook (a theme toggle sets data-theme on <html>). */
|
|
52
|
+
:root[data-theme="dark"] {
|
|
53
|
+
--bg: #0f1117; --surface: #171a21; --surface-2: #1e222b; --border: #2a2f3a;
|
|
54
|
+
--text: #eef1f6; --text-muted: #9aa4b2; --primary: #8b6dff;
|
|
55
|
+
--primary-contrast: #0f1117; --primary-weak: #241f3d;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
* { box-sizing: border-box; }
|
|
59
|
+
body {
|
|
60
|
+
margin: 0;
|
|
61
|
+
background: var(--bg);
|
|
62
|
+
color: var(--text);
|
|
63
|
+
font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
64
|
+
-webkit-font-smoothing: antialiased;
|
|
65
|
+
}
|
|
66
|
+
.card {
|
|
67
|
+
background: var(--surface);
|
|
68
|
+
border: 1px solid var(--border);
|
|
69
|
+
border-radius: var(--radius);
|
|
70
|
+
box-shadow: var(--shadow-sm);
|
|
71
|
+
padding: var(--space);
|
|
72
|
+
}
|
|
73
|
+
.btn-primary {
|
|
74
|
+
background: var(--primary); color: var(--primary-contrast);
|
|
75
|
+
border: none; border-radius: var(--radius-sm);
|
|
76
|
+
padding: 8px 16px; font-weight: 600; cursor: pointer;
|
|
77
|
+
}
|
|
78
|
+
.btn-primary:hover { filter: brightness(1.05); }
|
|
79
|
+
|
|
80
|
+
/* --- the three UI states every data surface ships --- */
|
|
81
|
+
/* 1. skeleton (loading) -- a shimmer, never a blank flash */
|
|
82
|
+
.skeleton {
|
|
83
|
+
background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 37%, var(--surface-2) 63%);
|
|
84
|
+
background-size: 400% 100%;
|
|
85
|
+
animation: shimmer 1.4s ease infinite;
|
|
86
|
+
border-radius: var(--radius-sm); height: 16px;
|
|
87
|
+
}
|
|
88
|
+
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
|
|
89
|
+
/* 2. empty state -- a dashed invitation with a CTA, never a void */
|
|
90
|
+
.empty {
|
|
91
|
+
border: 1px dashed var(--border); border-radius: var(--radius);
|
|
92
|
+
padding: calc(var(--space) * 2); text-align: center; color: var(--text-muted);
|
|
93
|
+
}
|
|
94
|
+
/* 3. first-run banner -- "you're viewing sample data" so screen 1 is alive */
|
|
95
|
+
.first-run {
|
|
96
|
+
background: var(--primary-weak); border: 1px solid var(--border);
|
|
97
|
+
border-radius: var(--radius); padding: 12px 16px; color: var(--text);
|
|
98
|
+
display: flex; align-items: center; gap: 8px; font-size: 14px;
|
|
99
|
+
}
|