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 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.2
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.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.121.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.121.2
1
+ 7.121.4
@@ -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,10 @@
1
+ {
2
+ "name": "contract-first-__COLL__-server",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": { "start": "node index.mjs" },
6
+ "dependencies": {
7
+ "express": "^5.0.0",
8
+ "better-sqlite3": "^12.0.0"
9
+ }
10
+ }
@@ -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
+ }