loki-mode 7.121.3 → 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/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
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""functional-verify.py -- FV-1: does the built app actually DO what the spec asked?
|
|
3
|
+
|
|
4
|
+
The completion verifier proves code was written + tests/build passed. It does NOT
|
|
5
|
+
prove the spec's BEHAVIORS work: a "waitlist" spec can be built as a static page
|
|
6
|
+
that captures nothing, yet read VERIFIED (measured: ~half of backend-implying
|
|
7
|
+
specs). This harness closes that gap by EXERCISING the running app against
|
|
8
|
+
assertions DERIVED FROM THE SPEC, and reporting an honest functional signal.
|
|
9
|
+
|
|
10
|
+
SCOPE (FV-1, deliberately narrow + honest):
|
|
11
|
+
- It reports a DESCRIPTIVE signal only. It does NOT feed the VERIFIED headline
|
|
12
|
+
(that is FV-2: a founder-gated trust-semantics decision, council-reviewed).
|
|
13
|
+
- It derives HTTP endpoint assertions from the spec (GET/POST/DELETE <path>) and
|
|
14
|
+
runs the CRUD lifecycle they imply against the LIVE app. It never fabricates:
|
|
15
|
+
an endpoint it cannot reach is `inconclusive`, a behavior that failed is
|
|
16
|
+
`failed`, a behavior proven is `passed`. Same inconclusive-never-false moat as
|
|
17
|
+
the rest of the trust layer.
|
|
18
|
+
- It requires the app to already be running (URL passed in) -- app-runner (>=
|
|
19
|
+
v7.121.3, static sites included) starts it; this harness only probes.
|
|
20
|
+
|
|
21
|
+
Output: JSON to stdout -- {spec_behaviors: [...], summary: {passed, failed,
|
|
22
|
+
inconclusive}, functional_status: verified|partial|failed|inconclusive}.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.request
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- spec -> behavioral assertions ------------------------------------------
|
|
34
|
+
|
|
35
|
+
# Endpoint declarations in a spec/PRD: "GET /api/tasks", "POST /api/tasks",
|
|
36
|
+
# "DELETE /api/tasks/:id". Method + path; path may carry a :param placeholder.
|
|
37
|
+
_ENDPOINT_RE = re.compile(
|
|
38
|
+
r'\b(GET|POST|PUT|PATCH|DELETE)\s+(/[A-Za-z0-9_./:{}-]*)',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _clean_path(path: str) -> str:
|
|
43
|
+
"""Strip trailing sentence punctuation a spec sentence leaves on a path
|
|
44
|
+
(e.g. "DELETE /api/tasks/:id." -> "/api/tasks/:id"). Without this, a probe
|
|
45
|
+
hits a wrong URL and fabricates a failure on a working app -- a fake-RED in
|
|
46
|
+
the FV layer itself, exactly what this harness exists to prevent."""
|
|
47
|
+
return path.rstrip(".,;:)]}") or path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def derive_endpoint_assertions(spec_text: str) -> list[dict]:
|
|
51
|
+
"""Extract {method, path} endpoint assertions the spec explicitly names.
|
|
52
|
+
|
|
53
|
+
Deduped, order-preserved. A path with a :param / {param} is a resource route
|
|
54
|
+
(used for the DELETE-a-created-resource lifecycle below)."""
|
|
55
|
+
seen = set()
|
|
56
|
+
out = []
|
|
57
|
+
for m in _ENDPOINT_RE.finditer(spec_text or ""):
|
|
58
|
+
method = m.group(1).upper()
|
|
59
|
+
path = _clean_path(m.group(2))
|
|
60
|
+
key = (method, path)
|
|
61
|
+
if key in seen:
|
|
62
|
+
continue
|
|
63
|
+
seen.add(key)
|
|
64
|
+
out.append({"method": method, "path": path,
|
|
65
|
+
"is_resource": bool(re.search(r'[:{]', path))})
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --- run assertions against the live app ------------------------------------
|
|
70
|
+
|
|
71
|
+
def _req(base_url: str, method: str, path: str, body=None, timeout=8):
|
|
72
|
+
"""One HTTP call. Returns (status_code, json_or_text) or (None, error_str)."""
|
|
73
|
+
url = base_url.rstrip("/") + path
|
|
74
|
+
data = None
|
|
75
|
+
headers = {}
|
|
76
|
+
if body is not None:
|
|
77
|
+
data = json.dumps(body).encode()
|
|
78
|
+
headers["Content-Type"] = "application/json"
|
|
79
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
80
|
+
try:
|
|
81
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
82
|
+
raw = resp.read().decode("utf-8", "replace")
|
|
83
|
+
try:
|
|
84
|
+
return resp.status, json.loads(raw) if raw else None
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
return resp.status, raw
|
|
87
|
+
except urllib.error.HTTPError as e:
|
|
88
|
+
return e.code, None
|
|
89
|
+
except (urllib.error.URLError, OSError) as e:
|
|
90
|
+
return None, str(e)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def verify_crud_lifecycle(base_url: str, assertions: list[dict]) -> list[dict]:
|
|
94
|
+
"""The core functional check: for a REST resource the spec names, prove the
|
|
95
|
+
CRUD LIFECYCLE actually works end to end (not just that a route responds):
|
|
96
|
+
POST creates -> the created item appears on GET -> DELETE removes it ->
|
|
97
|
+
it is gone from a subsequent GET.
|
|
98
|
+
This is what distinguishes a working backend from a static shell: a static
|
|
99
|
+
page returns 200 on GET but a POST does not persist. Each behavior is
|
|
100
|
+
passed / failed / inconclusive, never fabricated."""
|
|
101
|
+
results = []
|
|
102
|
+
|
|
103
|
+
# Collection GET/POST endpoints (non-resource) and their resource DELETE.
|
|
104
|
+
gets = [a for a in assertions if a["method"] == "GET" and not a["is_resource"]]
|
|
105
|
+
posts = [a for a in assertions if a["method"] == "POST" and not a["is_resource"]]
|
|
106
|
+
deletes = [a for a in assertions if a["method"] == "DELETE" and a["is_resource"]]
|
|
107
|
+
|
|
108
|
+
# 1. Every named collection GET must actually respond 2xx.
|
|
109
|
+
for a in gets:
|
|
110
|
+
code, payload = _req(base_url, "GET", a["path"])
|
|
111
|
+
if code is None:
|
|
112
|
+
results.append({"behavior": f"GET {a['path']} responds",
|
|
113
|
+
"status": "inconclusive", "detail": f"unreachable: {payload}"})
|
|
114
|
+
elif 200 <= code < 300:
|
|
115
|
+
results.append({"behavior": f"GET {a['path']} responds",
|
|
116
|
+
"status": "passed", "detail": f"HTTP {code}"})
|
|
117
|
+
else:
|
|
118
|
+
results.append({"behavior": f"GET {a['path']} responds",
|
|
119
|
+
"status": "failed", "detail": f"HTTP {code}"})
|
|
120
|
+
|
|
121
|
+
# 2. POST-creates-and-persists lifecycle: for each collection POST that has a
|
|
122
|
+
# matching collection GET, POST an item and assert the collection grew.
|
|
123
|
+
for p in posts:
|
|
124
|
+
coll = p["path"]
|
|
125
|
+
matching_get = next((g for g in gets if g["path"] == coll), None)
|
|
126
|
+
if matching_get is None:
|
|
127
|
+
results.append({"behavior": f"POST {coll} persists (verifiable)",
|
|
128
|
+
"status": "inconclusive",
|
|
129
|
+
"detail": "no matching collection GET to confirm persistence"})
|
|
130
|
+
continue
|
|
131
|
+
before_code, before = _req(base_url, "GET", coll)
|
|
132
|
+
before_n = len(before) if isinstance(before, list) else None
|
|
133
|
+
# A minimal, generic payload. Real apps validate; we send a common shape.
|
|
134
|
+
post_code, created = _req(base_url, "POST", coll,
|
|
135
|
+
body={"title": "fv-probe", "name": "fv-probe",
|
|
136
|
+
"text": "fv-probe"})
|
|
137
|
+
after_code, after = _req(base_url, "GET", coll)
|
|
138
|
+
after_n = len(after) if isinstance(after, list) else None
|
|
139
|
+
if post_code is None or after_code is None:
|
|
140
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
141
|
+
"status": "inconclusive",
|
|
142
|
+
"detail": "app unreachable during lifecycle"})
|
|
143
|
+
elif post_code and 200 <= post_code < 300 and before_n is not None \
|
|
144
|
+
and after_n is not None and after_n > before_n:
|
|
145
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
146
|
+
"status": "passed",
|
|
147
|
+
"detail": f"collection grew {before_n} -> {after_n} after POST"})
|
|
148
|
+
elif post_code and 200 <= post_code < 300 and (before_n is None or after_n is None):
|
|
149
|
+
# POST accepted but the collection is not a JSON array we can count:
|
|
150
|
+
# we cannot PROVE persistence -> honest inconclusive, never a pass.
|
|
151
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
152
|
+
"status": "inconclusive",
|
|
153
|
+
"detail": f"POST HTTP {post_code} but GET is not a countable list"})
|
|
154
|
+
elif post_code in (400, 422):
|
|
155
|
+
# The endpoint EXISTS and rejected OUR probe payload (validation).
|
|
156
|
+
# That is a working backend with a schema our generic {title,name,text}
|
|
157
|
+
# did not satisfy -- NOT proof the behavior is broken. Marking it failed
|
|
158
|
+
# would be a fake-RED (a strict-but-working app read as non-working).
|
|
159
|
+
# Honest inconclusive; FV-2 must never wire a failed verdict off this.
|
|
160
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
161
|
+
"status": "inconclusive",
|
|
162
|
+
"detail": f"POST HTTP {post_code}: endpoint exists but rejected the "
|
|
163
|
+
"probe payload (schema mismatch, not proof of no-persistence)"})
|
|
164
|
+
else:
|
|
165
|
+
# POST 404 (endpoint absent), 501 (not implemented), or 2xx-but-the-
|
|
166
|
+
# collection did NOT grow -> the behavior the spec implies does NOT
|
|
167
|
+
# work (a static shell or a non-persisting backend). Genuine failed.
|
|
168
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
169
|
+
"status": "failed",
|
|
170
|
+
"detail": f"POST HTTP {post_code}; collection {before_n} -> {after_n} "
|
|
171
|
+
"(no persistence -- a static shell or non-working backend)"})
|
|
172
|
+
|
|
173
|
+
# 3. DELETE lifecycle: if the spec names a resource DELETE and we created
|
|
174
|
+
# an item with an id, delete it and assert it's gone.
|
|
175
|
+
created_id = None
|
|
176
|
+
if isinstance(created, dict):
|
|
177
|
+
created_id = created.get("id") or created.get("_id")
|
|
178
|
+
if deletes and created_id is not None:
|
|
179
|
+
dpath_tmpl = deletes[0]["path"]
|
|
180
|
+
dpath = re.sub(r'[:{][A-Za-z0-9_]+[}]?', str(created_id), dpath_tmpl)
|
|
181
|
+
del_code, _ = _req(base_url, "DELETE", dpath)
|
|
182
|
+
_, after_del = _req(base_url, "GET", coll)
|
|
183
|
+
still_there = isinstance(after_del, list) and any(
|
|
184
|
+
isinstance(x, dict) and (x.get("id") == created_id or x.get("_id") == created_id)
|
|
185
|
+
for x in after_del)
|
|
186
|
+
if del_code is None:
|
|
187
|
+
results.append({"behavior": f"DELETE {dpath_tmpl} removes",
|
|
188
|
+
"status": "inconclusive", "detail": "unreachable"})
|
|
189
|
+
elif 200 <= del_code < 300 and not still_there:
|
|
190
|
+
results.append({"behavior": f"DELETE {dpath_tmpl} removes",
|
|
191
|
+
"status": "passed",
|
|
192
|
+
"detail": f"item {created_id} gone after DELETE (HTTP {del_code})"})
|
|
193
|
+
else:
|
|
194
|
+
results.append({"behavior": f"DELETE {dpath_tmpl} removes",
|
|
195
|
+
"status": "failed",
|
|
196
|
+
"detail": f"DELETE HTTP {del_code}; item still present={still_there}"})
|
|
197
|
+
return results
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def summarize(results: list[dict]) -> dict:
|
|
201
|
+
passed = sum(1 for r in results if r["status"] == "passed")
|
|
202
|
+
failed = sum(1 for r in results if r["status"] == "failed")
|
|
203
|
+
inconclusive = sum(1 for r in results if r["status"] == "inconclusive")
|
|
204
|
+
# functional_status: verified only if >=1 behavior passed and NONE failed;
|
|
205
|
+
# failed if any behavior failed; inconclusive if nothing could be exercised.
|
|
206
|
+
if failed > 0:
|
|
207
|
+
status = "failed"
|
|
208
|
+
elif passed > 0 and inconclusive == 0:
|
|
209
|
+
status = "verified"
|
|
210
|
+
elif passed > 0:
|
|
211
|
+
status = "partial"
|
|
212
|
+
else:
|
|
213
|
+
status = "inconclusive"
|
|
214
|
+
return {"passed": passed, "failed": failed, "inconclusive": inconclusive,
|
|
215
|
+
"functional_status": status}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def run(spec_text: str, base_url: str) -> dict:
|
|
219
|
+
assertions = derive_endpoint_assertions(spec_text)
|
|
220
|
+
if not assertions:
|
|
221
|
+
return {"spec_behaviors": [], "summary": {"passed": 0, "failed": 0,
|
|
222
|
+
"inconclusive": 0, "functional_status": "inconclusive"},
|
|
223
|
+
"note": "no HTTP endpoint behaviors derivable from the spec"}
|
|
224
|
+
results = verify_crud_lifecycle(base_url, assertions)
|
|
225
|
+
summ = summarize(results)
|
|
226
|
+
return {"assertions_derived": assertions, "spec_behaviors": results,
|
|
227
|
+
"summary": summ, "functional_status": summ["functional_status"]}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main(argv):
|
|
231
|
+
if len(argv) < 3:
|
|
232
|
+
print("usage: functional-verify.py <spec_file> <base_url>", file=sys.stderr)
|
|
233
|
+
return 2
|
|
234
|
+
spec_file, base_url = argv[1], argv[2]
|
|
235
|
+
try:
|
|
236
|
+
with open(spec_file, errors="replace") as f:
|
|
237
|
+
spec_text = f.read()
|
|
238
|
+
except OSError as e:
|
|
239
|
+
print(json.dumps({"error": f"cannot read spec: {e}"}))
|
|
240
|
+
return 1
|
|
241
|
+
print(json.dumps(run(spec_text, base_url), indent=2))
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
sys.exit(main(sys.argv))
|
|
@@ -287,9 +287,22 @@ def _collect_quality_gates(loki_dir):
|
|
|
287
287
|
elif os.path.exists(result_json):
|
|
288
288
|
rj = _read_json(result_json, default=None)
|
|
289
289
|
if isinstance(rj, dict):
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
# Read the marker's outcome key. enforce_static_analysis writes
|
|
291
|
+
# `"pass"` (a bool); other markers may write `"passed"` or
|
|
292
|
+
# `"status"`. Try all three so a real result is NEVER misread as
|
|
293
|
+
# not_run: a failing static-analysis marker ({"pass":false,
|
|
294
|
+
# "findings":11}) was collapsing to not_run (the reader looked
|
|
295
|
+
# only for "passed"/"status"), understating a real gate FAILURE
|
|
296
|
+
# as "did not run" -- the receipt read "gaps" where it should
|
|
297
|
+
# read a failed gate. _norm_gate_status maps a bool correctly
|
|
298
|
+
# (True->passed, False->failed). A key that is genuinely absent
|
|
299
|
+
# still defaults to not_run (honest -- never fabricated passed).
|
|
300
|
+
if "pass" in rj:
|
|
301
|
+
status = _norm_gate_status(rj.get("pass"))
|
|
302
|
+
else:
|
|
303
|
+
status = _norm_gate_status(
|
|
304
|
+
rj.get("passed", rj.get("status", "not_run"))
|
|
305
|
+
)
|
|
293
306
|
if status is not None:
|
|
294
307
|
gates.append({"name": gate_name, "status": status})
|
|
295
308
|
seen.add(gate_name)
|
|
@@ -431,6 +444,64 @@ def _norm_tests_status(raw):
|
|
|
431
444
|
return s
|
|
432
445
|
|
|
433
446
|
|
|
447
|
+
def _collect_functional(loki_dir):
|
|
448
|
+
"""Read .loki/quality/functional-results.json (the FV-1 functional harness).
|
|
449
|
+
|
|
450
|
+
Deterministic FACT: did the built app actually DO what the spec asked (run the
|
|
451
|
+
app + exercise spec-derived behaviors -- POST persists, GET reflects, ...), as
|
|
452
|
+
opposed to just compiling and passing unit tests. Tolerates an absent file ->
|
|
453
|
+
status not_run. Shape mirrors the FV-1 harness output:
|
|
454
|
+
{ran, functional_status, passed, failed, inconclusive}.
|
|
455
|
+
|
|
456
|
+
DESCRIPTIVE ONLY (FV-2, record half): this fact is RECORDED on the receipt but
|
|
457
|
+
is deliberately NOT read by _compute_headline / _compute_degraded, so it does
|
|
458
|
+
NOT change what "Verified" means. Making functional-satisfaction gate the green
|
|
459
|
+
headline is a trust-semantics product decision (council + founder), the second
|
|
460
|
+
half of FV-2. Recording it first lets the signal be seen and validated safely.
|
|
461
|
+
"""
|
|
462
|
+
out = {"ran": False, "functional_status": "not_run",
|
|
463
|
+
"passed": 0, "failed": 0, "inconclusive": 0}
|
|
464
|
+
raw = _read_json(
|
|
465
|
+
os.path.join(loki_dir, "quality", "functional-results.json"), default=None
|
|
466
|
+
)
|
|
467
|
+
if not isinstance(raw, dict):
|
|
468
|
+
return out
|
|
469
|
+
out["ran"] = True
|
|
470
|
+
out["functional_status"] = str(raw.get("functional_status") or "inconclusive")
|
|
471
|
+
summary = raw.get("summary") if isinstance(raw.get("summary"), dict) else raw
|
|
472
|
+
for k in ("passed", "failed", "inconclusive"):
|
|
473
|
+
v = summary.get(k)
|
|
474
|
+
if isinstance(v, int):
|
|
475
|
+
out[k] = v
|
|
476
|
+
return out
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _collect_healthcheck(loki_dir):
|
|
480
|
+
"""Read .loki/app-runner/health.json (the app-runner liveness probe).
|
|
481
|
+
|
|
482
|
+
Deterministic FACT: did the built app actually come up and respond (HTTP/PID
|
|
483
|
+
health), as written by app-runner. Absent -> not_run. Shape:
|
|
484
|
+
{ran, ok, status, checked_at}. status: not_run (never checked) | healthy
|
|
485
|
+
(ran, ok:true) | unhealthy (ran, ok:false).
|
|
486
|
+
|
|
487
|
+
DESCRIPTIVE ONLY (Evidence Receipt record half): recorded for transparency,
|
|
488
|
+
NOT read by _compute_headline / _compute_degraded, so it does not change what
|
|
489
|
+
"Verified" means. Gating on it is the founder-gated trust decision (mirrors the
|
|
490
|
+
FV-2 opt-in gate). ponytail: reuses the health.json app-runner already writes.
|
|
491
|
+
"""
|
|
492
|
+
out = {"ran": False, "ok": False, "status": "not_run", "checked_at": ""}
|
|
493
|
+
raw = _read_json(
|
|
494
|
+
os.path.join(loki_dir, "app-runner", "health.json"), default=None
|
|
495
|
+
)
|
|
496
|
+
if not isinstance(raw, dict):
|
|
497
|
+
return out
|
|
498
|
+
out["ran"] = True
|
|
499
|
+
out["ok"] = bool(raw.get("ok"))
|
|
500
|
+
out["checked_at"] = str(raw.get("checked_at") or "")
|
|
501
|
+
out["status"] = "healthy" if out["ok"] else "unhealthy"
|
|
502
|
+
return out
|
|
503
|
+
|
|
504
|
+
|
|
434
505
|
def _collect_tests(loki_dir):
|
|
435
506
|
"""Read .loki/quality/test-results.json.
|
|
436
507
|
|
|
@@ -763,6 +834,8 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
763
834
|
build = _collect_build(loki_dir)
|
|
764
835
|
tests = _collect_tests(loki_dir)
|
|
765
836
|
security = _collect_security(loki_dir)
|
|
837
|
+
functional = _collect_functional(loki_dir) # FV-2 record-half: descriptive only
|
|
838
|
+
healthcheck = _collect_healthcheck(loki_dir) # Evidence Receipt record-half
|
|
766
839
|
evidence_gate = _collect_evidence_gate(loki_dir)
|
|
767
840
|
|
|
768
841
|
deployed_url = os.environ.get("LOKI_DEPLOYED_URL") or None
|
|
@@ -800,6 +873,14 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
800
873
|
for g in (quality_gates.get("gates") or [])
|
|
801
874
|
],
|
|
802
875
|
"security": security,
|
|
876
|
+
# FV-2 (record half): did the app actually DO what the spec asked? Present
|
|
877
|
+
# for transparency; DELIBERATELY NOT read by _compute_headline /
|
|
878
|
+
# _compute_degraded, so it does not (yet) change the verdict. Wiring it into
|
|
879
|
+
# the green headline is the founder-gated trust-semantics decision.
|
|
880
|
+
"functional": functional,
|
|
881
|
+
# Evidence Receipt (record half): did the built app come up + respond?
|
|
882
|
+
# Descriptive; NOT read by _compute_headline (gating is founder-gated).
|
|
883
|
+
"healthcheck": healthcheck,
|
|
803
884
|
"cost": cost,
|
|
804
885
|
"meta": {
|
|
805
886
|
"run_id": run_id,
|
|
@@ -964,12 +1045,25 @@ def _compute_headline(facts, degraded):
|
|
|
964
1045
|
# intent). This keeps the receipt honest about security, not just tests.
|
|
965
1046
|
sec = facts.get("security") or {}
|
|
966
1047
|
sec_high = bool(sec.get("ran") and (sec.get("high_active") or 0) > 0)
|
|
1048
|
+
# FV-2 gate (opt-in via LOKI_FV_GATE=1, default OFF -> headline unchanged). When
|
|
1049
|
+
# enabled, a functional check that RAN and FAILED (the built app does not do what
|
|
1050
|
+
# the spec asked -- a static shell for a backend spec) is a hard failure, same
|
|
1051
|
+
# class as a failed test. Default-off keeps every existing build's verdict
|
|
1052
|
+
# byte-identical; the founder flips it on after reviewing the reclassification.
|
|
1053
|
+
# ponytail: reads the already-recorded functional fact; no new plumbing.
|
|
1054
|
+
fn = facts.get("functional") or {}
|
|
1055
|
+
fn_failed = bool(
|
|
1056
|
+
os.environ.get("LOKI_FV_GATE") == "1"
|
|
1057
|
+
and fn.get("ran")
|
|
1058
|
+
and fn.get("functional_status") == "failed"
|
|
1059
|
+
)
|
|
967
1060
|
any_failed = (
|
|
968
1061
|
tests.get("status") == "failed"
|
|
969
1062
|
or build.get("status") == "failed"
|
|
970
1063
|
or any(g.get("status") == "failed"
|
|
971
1064
|
for g in (facts.get("quality_gates") or []))
|
|
972
1065
|
or sec_high
|
|
1066
|
+
or fn_failed
|
|
973
1067
|
)
|
|
974
1068
|
if any_failed:
|
|
975
1069
|
return "NOT VERIFIED"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scaffold-hook.sh -- opt-in seam that wires the contract-first scaffold (M1-M3)
|
|
3
|
+
# into the build lane. Mirrors the LOKI_SENTRUX_GATE pattern: sourced ONLY when
|
|
4
|
+
# the gate is enabled, and no-ops safely otherwise.
|
|
5
|
+
#
|
|
6
|
+
# ENABLED by LOKI_SCAFFOLD_CONTRACT_FIRST=1 (default OFF -> the default build path
|
|
7
|
+
# is byte-identical and completely unaffected). When ON, and ONLY for a GREENFIELD
|
|
8
|
+
# target (an effectively-empty project dir) whose PRD implies a web/API backend,
|
|
9
|
+
# it lays down the contract-first + real-backend + design-system skeleton BEFORE
|
|
10
|
+
# the SDLC loop, so the model builds ON a real wired substrate instead of
|
|
11
|
+
# hand-writing disconnected files. It NEVER touches a non-empty/brownfield repo.
|
|
12
|
+
#
|
|
13
|
+
# It is deliberately conservative: on any doubt it does nothing (returns 0) and
|
|
14
|
+
# lets the normal build proceed -- it can only ADD a starting skeleton, never
|
|
15
|
+
# remove or overwrite existing code.
|
|
16
|
+
#
|
|
17
|
+
# PARITY: this is a bash-only ORCHESTRATION seam in run_autonomous, NOT part of
|
|
18
|
+
# the byte-mirrored build_prompt() contract (build_prompt.ts is unchanged). It
|
|
19
|
+
# mirrors the LOKI_SENTRUX_GATE precedent, which is likewise bash-orchestration
|
|
20
|
+
# only (not behavior-mirrored in the bun runner). Because it is DEFAULT-OFF, the
|
|
21
|
+
# bun default path is unaffected. When the bun runner (loki-ts autonomous.ts,
|
|
22
|
+
# currently a skeleton port) reaches run_autonomous behavior parity, it should
|
|
23
|
+
# mirror this opt-in gate.
|
|
24
|
+
|
|
25
|
+
# Is the target dir greenfield (no meaningful source yet)? Ignores .loki, .git,
|
|
26
|
+
# the PRD, and dotfiles. Echoes "yes"/"no".
|
|
27
|
+
_scaffold_is_greenfield() {
|
|
28
|
+
local dir="${1:-.}"
|
|
29
|
+
local n
|
|
30
|
+
n=$(find "$dir" -type f \
|
|
31
|
+
-not -path "*/.loki/*" -not -path "*/.git/*" \
|
|
32
|
+
-not -name "PRD.md" -not -name "prd.md" -not -name ".*" \
|
|
33
|
+
-not -path "*/node_modules/*" 2>/dev/null | head -5 | wc -l | tr -d ' ')
|
|
34
|
+
[ "${n:-0}" -eq 0 ] && echo "yes" || echo "no"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Derive a resource + fields from a PRD, conservatively. Only fires when the PRD
|
|
38
|
+
# clearly names a REST resource; otherwise echoes nothing (caller skips).
|
|
39
|
+
# Kept intentionally simple + general (no product-specific knowledge).
|
|
40
|
+
_scaffold_derive_resource() {
|
|
41
|
+
local prd="$1"
|
|
42
|
+
[ -f "$prd" ] || return 0
|
|
43
|
+
# Look for an explicit "GET/POST /api/<collection>" the way FV-1 does.
|
|
44
|
+
local coll
|
|
45
|
+
coll=$(grep -oiE '(GET|POST)[[:space:]]+/api/[a-z][a-z0-9_-]+' "$prd" 2>/dev/null \
|
|
46
|
+
| grep -oE '/api/[a-z][a-z0-9_-]+' | head -1 | sed 's#/api/##')
|
|
47
|
+
[ -z "$coll" ] && return 0
|
|
48
|
+
# singularize naively for the resource name
|
|
49
|
+
local res="${coll%s}"; [ -z "$res" ] && res="$coll"
|
|
50
|
+
echo "$res"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# The hook. Args: <target_dir> <prd_path>. Safe to call unconditionally; it
|
|
54
|
+
# self-gates on the flag + greenfield + a derivable resource.
|
|
55
|
+
run_contract_scaffold_hook() {
|
|
56
|
+
[ "${LOKI_SCAFFOLD_CONTRACT_FIRST:-0}" = "1" ] || return 0
|
|
57
|
+
local dir="${1:-.}" prd="${2:-}"
|
|
58
|
+
local script_dir
|
|
59
|
+
script_dir="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
60
|
+
|
|
61
|
+
[ "$(_scaffold_is_greenfield "$dir")" = "yes" ] || {
|
|
62
|
+
log_info "contract-scaffold: target is not greenfield -> skipping (never overwrites existing code)" 2>/dev/null || true
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
local res
|
|
66
|
+
res=$(_scaffold_derive_resource "$prd")
|
|
67
|
+
[ -z "$res" ] && {
|
|
68
|
+
log_info "contract-scaffold: no REST resource derivable from the PRD -> skipping" 2>/dev/null || true
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
local scaffold="${script_dir}/lib/contract-scaffold/scaffold.sh"
|
|
73
|
+
local generate="${script_dir}/lib/contract-scaffold/generate.mjs"
|
|
74
|
+
[ -f "$scaffold" ] && [ -f "$generate" ] || return 0
|
|
75
|
+
|
|
76
|
+
log_info "contract-scaffold: greenfield + resource '$res' -> laying down contract-first + backend + design skeleton" 2>/dev/null || true
|
|
77
|
+
bash "$scaffold" "$dir" "$res" 2>/dev/null || true
|
|
78
|
+
if command -v node >/dev/null 2>&1; then
|
|
79
|
+
node "$generate" "$dir" "$res" 2>/dev/null || true
|
|
80
|
+
fi
|
|
81
|
+
return 0
|
|
82
|
+
}
|
package/autonomy/run.sh
CHANGED
|
@@ -8511,6 +8511,30 @@ enforce_build_check() {
|
|
|
8511
8511
|
return 0
|
|
8512
8512
|
}
|
|
8513
8513
|
|
|
8514
|
+
# True when the workspace's package.json declares a non-empty `lint` script, so
|
|
8515
|
+
# the static-analysis gate can run the app's OWN linter (oxlint/eslint/biome/...)
|
|
8516
|
+
# via `npm run lint` rather than only recognizing eslint config files. Pure read
|
|
8517
|
+
# of package.json; returns non-zero on absence / no lint script / unreadable.
|
|
8518
|
+
has_npm_lint_script() {
|
|
8519
|
+
local dir="${1:-.}"
|
|
8520
|
+
local pkg="$dir/package.json"
|
|
8521
|
+
[ -f "$pkg" ] || return 1
|
|
8522
|
+
# Parse with python3 (already a hard dep of the engine) so a `"lint":` inside
|
|
8523
|
+
# a string value or comment cannot yield a false positive; require a real,
|
|
8524
|
+
# non-empty scripts.lint entry.
|
|
8525
|
+
python3 - "$pkg" <<'PYEOF' 2>/dev/null
|
|
8526
|
+
import json, sys
|
|
8527
|
+
try:
|
|
8528
|
+
with open(sys.argv[1]) as fh:
|
|
8529
|
+
data = json.load(fh)
|
|
8530
|
+
except Exception:
|
|
8531
|
+
sys.exit(1)
|
|
8532
|
+
scripts = data.get("scripts") or {}
|
|
8533
|
+
lint = scripts.get("lint")
|
|
8534
|
+
sys.exit(0 if isinstance(lint, str) and lint.strip() else 1)
|
|
8535
|
+
PYEOF
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8514
8538
|
enforce_static_analysis() {
|
|
8515
8539
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
8516
8540
|
local quality_dir="$loki_dir/quality"
|
|
@@ -8539,6 +8563,31 @@ enforce_static_analysis() {
|
|
|
8539
8563
|
done
|
|
8540
8564
|
if [ -n "$abs_files" ]; then
|
|
8541
8565
|
total_checked=$((total_checked + $(echo "$abs_files" | wc -w)))
|
|
8566
|
+
# v7.x (static-analysis honest coverage): ADDITIVELY run the app's OWN
|
|
8567
|
+
# declared `lint` script when present. Generated apps increasingly use
|
|
8568
|
+
# oxlint / biome (not eslint) -- e.g. package.json `"lint": "oxlint"`
|
|
8569
|
+
# with a `.oxlintrc.json` -- which the eslint-config probe below does
|
|
8570
|
+
# not recognize, so the app's real lint check was skipped. Running
|
|
8571
|
+
# `npm run lint` respects whatever linter the app declares. This is
|
|
8572
|
+
# ADDITIVE (not a replacement): we STILL run the eslint/tsc type checks
|
|
8573
|
+
# below, so a lint pass never silences the type-error check for TS apps
|
|
8574
|
+
# (oxlint is not type-aware). rc 127 = the declared linter is not
|
|
8575
|
+
# installed/resolvable -> HONEST skip, never counted as a violation
|
|
8576
|
+
# (rc-only signal; a real lint failure whose OUTPUT mentions "not
|
|
8577
|
+
# found" must still count, so we do NOT grep the message).
|
|
8578
|
+
if has_npm_lint_script "${TARGET_DIR:-.}"; then
|
|
8579
|
+
local lint_out lint_rc=0
|
|
8580
|
+
lint_out=$(cd "${TARGET_DIR:-.}" && npm run --silent lint 2>&1) || lint_rc=$?
|
|
8581
|
+
if [ "$lint_rc" -eq 127 ]; then
|
|
8582
|
+
log_info "Static analysis: app 'lint' script did not resolve a linter (not run, honest skip)"
|
|
8583
|
+
elif [ "$lint_rc" -ne 0 ]; then
|
|
8584
|
+
findings=$((findings + 1))
|
|
8585
|
+
details="${details}Lint (npm run lint): $(echo "$lint_out" | tail -3 | tr '\n' ' '). "
|
|
8586
|
+
fi
|
|
8587
|
+
fi
|
|
8588
|
+
# Type/syntax check path (unchanged contract): eslint when configured,
|
|
8589
|
+
# else tsc project-mode + per-file parse. Runs REGARDLESS of the lint
|
|
8590
|
+
# step above so TS type errors are always caught.
|
|
8542
8591
|
if [ -f "${TARGET_DIR:-.}/.eslintrc.js" ] || [ -f "${TARGET_DIR:-.}/.eslintrc.json" ] || \
|
|
8543
8592
|
[ -f "${TARGET_DIR:-.}/eslint.config.js" ] || [ -f "${TARGET_DIR:-.}/eslint.config.mjs" ]; then
|
|
8544
8593
|
local eslint_out
|
|
@@ -8559,7 +8608,7 @@ enforce_static_analysis() {
|
|
|
8559
8608
|
if [ -f "${TARGET_DIR:-.}/tsconfig.json" ] && command -v tsc &>/dev/null; then
|
|
8560
8609
|
local _has_ts=0
|
|
8561
8610
|
for f in $abs_files; do
|
|
8562
|
-
case "$f" in *.ts|*.tsx) _has_ts=1; break ;; esac
|
|
8611
|
+
case "$f" in *.ts|*.tsx|*.jsx) _has_ts=1; break ;; esac
|
|
8563
8612
|
done
|
|
8564
8613
|
if [ "$_has_ts" -eq 1 ]; then
|
|
8565
8614
|
_ts_project_mode=1
|
|
@@ -8569,7 +8618,7 @@ enforce_static_analysis() {
|
|
|
8569
8618
|
local _changed_ts_errors=""
|
|
8570
8619
|
for f in $js_files; do
|
|
8571
8620
|
case "$f" in
|
|
8572
|
-
*.ts|*.tsx)
|
|
8621
|
+
*.ts|*.tsx|*.jsx)
|
|
8573
8622
|
# tsc emits paths relative to project root with `(line,col):` suffix.
|
|
8574
8623
|
# v7.5.12 Dev11 (R1 MED): use grep -F (literal) so filenames
|
|
8575
8624
|
# containing regex metacharacters cannot cause false positives
|
|
@@ -8591,11 +8640,16 @@ enforce_static_analysis() {
|
|
|
8591
8640
|
fi
|
|
8592
8641
|
fi
|
|
8593
8642
|
for f in $abs_files; do
|
|
8594
|
-
# node --check cannot parse TypeScript / TSX files; it
|
|
8595
|
-
# crashes with ERR_UNKNOWN_FILE_EXTENSION
|
|
8596
|
-
#
|
|
8643
|
+
# node --check cannot parse TypeScript / TSX / JSX files; it
|
|
8644
|
+
# crashes with ERR_UNKNOWN_FILE_EXTENSION (JSX) or a parse error
|
|
8645
|
+
# (TS syntax). Route .ts/.tsx AND .jsx to the JSX-capable tsc
|
|
8646
|
+
# path; only plain .js/.mjs/.cjs go through node --check. Before
|
|
8647
|
+
# the .jsx addition here, every valid .jsx file was falsely
|
|
8648
|
+
# reported as a "Syntax error" (node --check ERR_UNKNOWN_FILE_
|
|
8649
|
+
# EXTENSION) -- a whole class of false static-analysis findings
|
|
8650
|
+
# on React apps.
|
|
8597
8651
|
case "$f" in
|
|
8598
|
-
*.ts|*.tsx)
|
|
8652
|
+
*.ts|*.tsx|*.jsx)
|
|
8599
8653
|
# When tsconfig project-mode handled it above, skip
|
|
8600
8654
|
# the per-file fallback to avoid duplicate / false errors.
|
|
8601
8655
|
if [ "$_ts_project_mode" -eq 1 ]; then
|
|
@@ -16254,6 +16308,21 @@ run_autonomous() {
|
|
|
16254
16308
|
esac
|
|
16255
16309
|
fi
|
|
16256
16310
|
|
|
16311
|
+
# Contract-first scaffold seam (opt-in via LOKI_SCAFFOLD_CONTRACT_FIRST=1,
|
|
16312
|
+
# default OFF -> default build path is byte-identical). Mirrors the
|
|
16313
|
+
# LOKI_SENTRUX_GATE pattern: source the helper only when the gate is enabled,
|
|
16314
|
+
# and it self-gates further on greenfield + a derivable REST resource. It can
|
|
16315
|
+
# ONLY add a starting skeleton (contract-first codegen + real backend + design
|
|
16316
|
+
# system, M1-M3); it never overwrites existing code or touches a brownfield
|
|
16317
|
+
# repo. On any doubt it no-ops and the normal build proceeds unchanged.
|
|
16318
|
+
if [ "${LOKI_SCAFFOLD_CONTRACT_FIRST:-0}" = "1" ]; then
|
|
16319
|
+
# shellcheck disable=SC1090,SC1091
|
|
16320
|
+
source "${SCRIPT_DIR}/lib/scaffold-hook.sh" 2>/dev/null || true
|
|
16321
|
+
if type run_contract_scaffold_hook >/dev/null 2>&1; then
|
|
16322
|
+
run_contract_scaffold_hook "${TARGET_DIR:-.}" "$prd_path" || true
|
|
16323
|
+
fi
|
|
16324
|
+
fi
|
|
16325
|
+
|
|
16257
16326
|
# Auto-detect PRD if not provided
|
|
16258
16327
|
if [ -z "$prd_path" ]; then
|
|
16259
16328
|
log_step "No PRD provided, searching for existing PRD files..."
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.121.
|
|
5
|
+
**Version:** v7.121.4
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
396
396
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
397
397
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
398
398
|
-v $(pwd):/workspace -w /workspace \
|
|
399
|
-
asklokesh/loki-mode:7.121.
|
|
399
|
+
asklokesh/loki-mode:7.121.4 start ./my-spec.md
|
|
400
400
|
```
|
|
401
401
|
|
|
402
402
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.121.
|
|
2
|
+
var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.121.4";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function R($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([c1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function r($){return b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -814,4 +814,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
814
814
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
815
815
|
`),process.stderr.write(K8),2}}e1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var SZ=await NZ(Bun.argv.slice(2));process.exit(SZ);
|
|
816
816
|
|
|
817
|
-
//# debugId=
|
|
817
|
+
//# debugId=8F986B79CB65C96964756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.121.
|
|
4
|
+
"version": "7.121.4",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.121.
|
|
5
|
+
"version": "7.121.4",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|