@xhulz/nina 0.84.0 → 0.85.0

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.
Files changed (137) hide show
  1. package/package.json +1 -1
  2. package/releases/0.85.0/core/GAPS.md +125 -0
  3. package/releases/0.85.0/core/answered.json +137 -0
  4. package/releases/0.85.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  5. package/releases/0.85.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  6. package/releases/0.85.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  7. package/releases/0.85.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  8. package/releases/0.85.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  9. package/releases/0.85.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  10. package/releases/0.85.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  11. package/releases/0.85.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  12. package/releases/0.85.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  13. package/releases/0.85.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  14. package/releases/0.85.0/core/tiers.json +5 -0
  15. package/releases/0.85.0/core/tree/.claude/agents/architect.md +140 -0
  16. package/releases/0.85.0/core/tree/.claude/agents/dba.md +123 -0
  17. package/releases/0.85.0/core/tree/.claude/agents/design-reviewer.md +82 -0
  18. package/releases/0.85.0/core/tree/.claude/agents/devops.md +122 -0
  19. package/releases/0.85.0/core/tree/.claude/agents/implementer.md +208 -0
  20. package/releases/0.85.0/core/tree/.claude/agents/integration-tester.md +174 -0
  21. package/releases/0.85.0/core/tree/.claude/agents/planner.md +132 -0
  22. package/releases/0.85.0/core/tree/.claude/agents/qa.md +154 -0
  23. package/releases/0.85.0/core/tree/.claude/agents/reviewer.md +191 -0
  24. package/releases/0.85.0/core/tree/.claude/agents/secops.md +124 -0
  25. package/releases/0.85.0/core/tree/.claude/agents/solidity-auditor.md +130 -0
  26. package/releases/0.85.0/core/tree/.claude/agents/solidity-dev.md +127 -0
  27. package/releases/0.85.0/core/tree/.claude/agents-overview.md +65 -0
  28. package/releases/0.85.0/core/tree/.claude/frontend.md +10 -0
  29. package/releases/0.85.0/core/tree/.claude/graph.md +79 -0
  30. package/releases/0.85.0/core/tree/.claude/patterns.md +256 -0
  31. package/releases/0.85.0/core/tree/.claude/pills/README.md +150 -0
  32. package/releases/0.85.0/core/tree/.claude/pipeline.md +101 -0
  33. package/releases/0.85.0/core/tree/.claude/retrieval.md +103 -0
  34. package/releases/0.85.0/core/tree/.claude/router.md +224 -0
  35. package/releases/0.85.0/core/tree/.claude/templates/integration.md +48 -0
  36. package/releases/0.85.0/core/tree/CLAUDE.md +167 -0
  37. package/releases/0.85.0/core/tree/scripts/cost-watch.mjs +27 -0
  38. package/releases/0.85.0/core/tree/scripts/edit-guard.mjs +24 -0
  39. package/releases/0.85.0/core/tree/scripts/harness-check.mjs +84 -0
  40. package/releases/0.85.0/core/tree/scripts/loop-gate.mjs +19 -0
  41. package/releases/0.85.0/core/vocabulary.json +18 -0
  42. package/releases/0.85.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  43. package/releases/0.85.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  44. package/releases/0.85.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  45. package/releases/0.85.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  46. package/releases/0.85.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  47. package/releases/0.85.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  48. package/releases/0.85.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  49. package/releases/0.85.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  50. package/releases/0.85.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  51. package/releases/0.85.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  52. package/releases/0.85.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  53. package/releases/0.85.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.85.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  55. package/releases/0.85.0/surfaces/d1/tree/CLAUDE.md +2 -0
  56. package/releases/0.85.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  57. package/releases/0.85.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  58. package/releases/0.85.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  59. package/releases/0.85.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  60. package/releases/0.85.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  61. package/releases/0.85.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  62. package/releases/0.85.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  63. package/releases/0.85.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  64. package/releases/0.85.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  65. package/releases/0.85.0/surfaces/db/tree/.claude/graph.md +8 -0
  66. package/releases/0.85.0/surfaces/db/tree/.claude/patterns.md +22 -0
  67. package/releases/0.85.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  68. package/releases/0.85.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  69. package/releases/0.85.0/surfaces/db/tree/.claude/router.md +4 -0
  70. package/releases/0.85.0/surfaces/db/tree/CLAUDE.md +17 -0
  71. package/releases/0.85.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  72. package/releases/0.85.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  73. package/releases/0.85.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  74. package/releases/0.85.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  75. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  76. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  77. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  78. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  79. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  80. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  81. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  82. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  83. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  84. package/releases/0.85.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  85. package/releases/0.85.0/surfaces/edge-cf/tree/CLAUDE.md +29 -0
  86. package/releases/0.85.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  87. package/releases/0.85.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  88. package/releases/0.85.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  89. package/releases/0.85.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  90. package/releases/0.85.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  91. package/releases/0.85.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  92. package/releases/0.85.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  93. package/releases/0.85.0/surfaces/frontend/tree/.claude/router.md +14 -0
  94. package/releases/0.85.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  95. package/releases/0.85.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  96. package/releases/0.85.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  97. package/releases/0.85.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  98. package/releases/0.85.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  99. package/releases/0.85.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  100. package/releases/0.85.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  101. package/releases/0.85.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  102. package/releases/0.85.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  103. package/releases/0.85.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  104. package/releases/0.85.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  105. package/releases/0.85.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  106. package/releases/0.85.0/surfaces/integrations/tree/.claude/router.md +6 -0
  107. package/releases/0.85.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  108. package/releases/0.85.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  109. package/releases/0.85.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  110. package/releases/0.85.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  111. package/releases/0.85.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  112. package/releases/0.85.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  113. package/releases/0.85.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  114. package/releases/0.85.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  115. package/releases/0.85.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  116. package/releases/0.85.0/surfaces/money/tree/.claude/graph.md +2 -0
  117. package/releases/0.85.0/surfaces/money/tree/.claude/patterns.md +49 -0
  118. package/releases/0.85.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  119. package/releases/0.85.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  120. package/releases/0.85.0/surfaces/money/tree/CLAUDE.md +11 -0
  121. package/releases/0.85.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  122. package/releases/0.85.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  123. package/releases/0.85.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  124. package/releases/0.85.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  125. package/releases/0.85.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  126. package/releases/0.85.0/surfaces/pii/tree/CLAUDE.md +5 -0
  127. package/releases/0.85.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  128. package/releases/0.85.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  129. package/releases/0.85.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  130. package/releases/0.85.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  131. package/releases/0.85.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  132. package/releases/0.85.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  133. package/releases/0.85.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  134. package/releases/0.85.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  135. package/src/dashboard.mjs +6 -8
  136. package/src/demo.mjs +22 -5
  137. package/src/office.mjs +29 -9
@@ -0,0 +1,5 @@
1
+ <!-- nina:slot pii.1 -->
2
+ 8. **Sensitive personal data is classified, and the classification is enforced.** What counts as sensitive here — identity documents, financial identifiers, anything a person would not expect to be readable by whoever can read a log — is declared in `.claude/architecture.md` § *Privacy*, along with the regime that governs it ({{PRIVACY_REGIME}}). Encrypted at rest. **No sensitive value in a log, an error message, a URL, an analytics event, or an agent transcript** — the places nobody thinks about are exactly the ones that persist. That includes what a library puts in its own errors and prints itself: a database client's error can carry the query and every bound value, and a framework can log a whole error past the logger it was given. A database error is mapped at the data layer to a typed error from a closed list before anything logs or returns it, and a test proves that a bound value it writes reaches no thrown error. **A value absent from an error that never held it proves nothing:** each test that shows a mapper or a sanitizer keeps a value out drives a failure whose raw error carries it — the spec names that failure class and cites where it renders bound values, an error that prints the call or a violation that echoes the input, never one that names only its constraint — and asserts first that the raw, unmapped error of the same operation contains the value, a control kept in memory and never printed; the mutant that copies the raw error's `message`, `cause` or details across turns it red.<!-- nina:why --> In one project a spec proved its error mapper on a foreign-key failure, whose message and details name only the constraint, so no mapper that copied them could turn the test red.<!-- /nina:why --> **A test that proves a value stays out matches by containment** (`String(v).includes(value)`), never by equality: a leak arrives inside an id, a message or a key. It gives each log call in the source a run, counts the console's calls by method and asserts each method the path must not use at zero, and reads at least what the console prints of each entry and each error the runs produce, recursively: every own key, symbols and non-enumerable ones too (`Reflect.ownKeys`), and the entries of every `Map`, `Set`, `Headers` and `URLSearchParams`, unless the spec rules one out in writing. Its controls put their value where the real emitter puts its own, past a call's first argument and among several, and the spec names the window its spy covers and why nothing logs after it. Its forbidden values are the run's own that § *Privacy* keeps out — where it lists what may be logged, every other value the run carries — each with a control that embeds it in a longer string under an allowed key and must fail the check — the values one run adds as much as a shared list's, since a check that folds accents passes every control written in ASCII — and no fixture value inside another's.<!-- nina:why --> In one project an allowlist check that refused only equal values passed a mutant logging `${id}:${amount}`; the next read `Object.keys` and missed an amount on a non-enumerable property and an info log no row inspected; the one after left out a class and four log paths.<!-- /nina:why --> Where what it claims to keep out names a class — an attribute, an amount — it covers every member: a surface with open-ended slots is read whole, never through a list of them — every attribute of every element a rendered page carries, just as every property of an error is — and a sweep that read nothing fails; each forbidden value is looked for in every form the code can give it, formatted and raw; and each state that shows, hides or replaces a value is swept on its own, the one built to withhold it most of all. One mutant puts the value in a slot the check does not name — `data-x` on a page; on an object or an error, a non-enumerable key, a symbol key, a nested object, a `Map` entry, `cause`, the end of `message` or `stack` — and one per field puts that field's raw value in each such slot, on every error the code throws, not only one it builds: one left green is a slot or a form the check misses. The implementer runs them on its mirror, as one block it reuses, before `DIFF-READY`, whatever the step lists, and reports one left green as the row the spec lacks (Hard Rule #17), adding it only in a file the step authorizes.<!-- nina:why --> In one project a walk of own keys missed a `Map`, a symbol key and a nested object, and every control put its value in a call's first argument, where the runtime's loggers put theirs second; then three steps in two days went back from review for a slot no listed mutant tried — an error's `cause`, its `stack`, a hidden key.<!-- /nina:why --><!-- nina:why --> In one project a sweep of `data-testid`, `aria-label` and `title`, proven by mutants in those three, stayed green with the label in a `data-label`; the next swept every attribute for the formatted amounts and missed their raw centavos; the one after swept only the state that shows the figure, and the row built to show none carried it in a `title`.<!-- /nina:why --> Document bytes live only in the object store, referenced by key, never in the database in clear text.
3
+
4
+ <!-- nina:slot pii.2 -->
5
+ - **personal data**: where it is collected, stored, logged or sent
@@ -0,0 +1,2 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ | `prisma-client-api` | a new or changed Prisma query |
@@ -0,0 +1,42 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ | `prisma-cli` | **before ANY `prisma` CLI command** — `validate`, `format`, `migrate dev`, `migrate diff`, `migrate deploy`. Flags differ between versions, and one of them (`migrate diff --shadow-database-url` against a live database) destroys the database it points at |
3
+ | `prisma-client-api` | auditing a new or changed query — filters, operators, `$transaction` semantics. Through a driver adapter, cite the adapter's installed source for what a transaction does: the skill describes the default engine, and an adapter can roll back less |
4
+
5
+ <!-- nina:slot prisma.2 -->
6
+ Neither skill covers a **caching or edge layer sitting on top of the client** — cache strategy,
7
+ edge-client behaviour and the rule about never caching a read that authorizes a write belong to that
8
+ layer's own integration doc, declared like any other integration. A skill does not replace it.
9
+
10
+ <!-- nina:slot prisma.3 -->
11
+ - **With Prisma, both offline.** Run `pnpm --filter {{DB_PKG_NAME}} exec prisma validate`; it must pass. Then run `prisma migrate diff` in its **offline, file-only form** and read the generated SQL, not just the Prisma delta. Its flags changed in Prisma 7, so read the installed major (`prisma --version`) before writing the command:
12
+
13
+ ```
14
+ # Prisma 6. Compare the PREVIOUS schema against the working one — pure file-to-file, no database.
15
+ git show HEAD:{{DB_PKG}}/prisma/schema.prisma > /tmp/schema-head.prisma
16
+ pnpm --filter {{DB_PKG_NAME}} exec prisma migrate diff \
17
+ --from-schema-datamodel /tmp/schema-head.prisma \
18
+ --to-schema-datamodel prisma/schema.prisma \
19
+ --script
20
+ ```
21
+
22
+ From Prisma 7 the two flags are `--from-schema` and `--to-schema`, and the schema engine starts from the
23
+ datasource in `prisma.config.ts` even for this diff: with none there, it prints nothing and exits 0. For
24
+ SQLite a placeholder the diff never opens is enough, a `file:` URL under a gitignored directory; for any
25
+ other provider, find out what the diff opens before you give it one. Never a real database's URL. A first schema has nothing at `HEAD`: diff it `--from-empty`. Compare the output against the
26
+ hand-written `migration.sql` to confirm the declarative parts match.
27
+
28
+ ⚠️ **Do NOT reach for `--from-migrations`.** It looks like the natural choice and it is a trap:
29
+ Prisma refuses it without a shadow database (`--shadow-database-url`, or from Prisma 7 a
30
+ `shadowDatabaseUrl` in `prisma.config.ts`), which pushes you toward supplying a database URL — the exact move that destroys databases (below). The file-to-file form above answers the
31
+ same question with no database involved.
32
+
33
+ ⛔ **NEVER point `--shadow-database-url`, `--from-url`, `--to-url`, `--from-schema-datasource` — or, from Prisma 7, a `shadowDatabaseUrl` or the config datasource behind `--from-config-datasource` — at a real database, most of all not the one `{{SECRETS_LOCAL}}` names.** Prisma **executes** the entire migration history against whatever database those flags name, which **DESTROYS ALL DATA** in it. A shadow database must be a disposable, empty database and nothing else. It does not announce itself: the command completes with a reassuring "empty migration" result and no error, having already destroyed everything in the database it was pointed at. If the offline form above cannot answer your question, **STOP and report that to the orchestrator** instead of reaching for a database URL.
34
+
35
+ <!-- nina:slot prisma.4 -->
36
+ - **Where reads are cached through Prisma Accelerate,** every `findUnique` / `findMany` on a hot read path names its `cacheStrategy` (`ttl` / `swr`), and one that feeds a correctness-critical write names none.
37
+
38
+ <!-- nina:slot prisma.5 -->
39
+ - **On an edge runtime,** new Prisma code paths use the edge client of the installed major: `@prisma/client/edge` up to Prisma 6; from 7, the client the `prisma-client` generator writes with the platform's `runtime` set, imported from where it is generated — `@prisma/client/edge` fails there with `MODULE_NOT_FOUND`.
40
+
41
+ <!-- nina:slot prisma.6 -->
42
+ - Run a Prisma command that writes to a real database — `prisma migrate dev`, `migrate deploy`, `migrate reset`, `migrate resolve`, `db push`, `db execute`, `$executeRaw*` — or **any `prisma migrate diff` variant carrying a database URL**, which silently wipes the database it points at.
@@ -0,0 +1,11 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ - **`prisma-cli`** — before `migrate deploy` or any other Prisma CLI command against a real database.
3
+
4
+ <!-- nina:slot prisma.2 -->
5
+ - **Prisma migrations go out with `prisma migrate deploy`**, and nothing else changes a shared database's schema.
6
+
7
+ <!-- nina:slot prisma.3 -->
8
+ - Run `prisma migrate diff --shadow-database-url <url>` against any live database. It **executes** the migration history against that database, destroying everything in it, and reports success. Use a disposable shadow database or don't run it.
9
+
10
+ <!-- nina:slot prisma.4 -->
11
+ - Run `prisma migrate reset` against a shared database, ever.
@@ -0,0 +1,2 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ | `prisma-client-api` | a new or changed Prisma query |
@@ -0,0 +1,2 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ | `prisma-cli` | a Prisma CLI command |
@@ -0,0 +1,2 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ | `prisma-client-api` | a new or changed Prisma query |
@@ -0,0 +1,15 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ - **Database table/column names** (Prisma `@map` / `@@map`): `snake_case` in SQL, `camelCase` in TS.
3
+
4
+ <!-- nina:slot prisma.2 -->
5
+ - Where Prisma reads are cached through Accelerate, `cacheStrategy` lives in `{{DB_PKG}}`: services do not add, strip, or override cache policies.
6
+
7
+ <!-- nina:slot prisma.3 -->
8
+
9
+ ### Prisma
10
+
11
+ - Schema lives in `{{DB_PKG}}/prisma/schema.prisma`. Nowhere else.
12
+ - **Never write raw SQL** unless justified in a code comment with perf data.
13
+ - N+1 is solved with `include` / `select`, chosen deliberately.
14
+ - Where reads are cached through Accelerate, every `findUnique` / `findMany` on a hot path defines its `cacheStrategy`, and no direct connection string is embedded in code.
15
+ - Tests run a real Prisma client against a real database; the client is never mocked.
@@ -0,0 +1,5 @@
1
+ <!-- nina:slot prisma.1 -->
2
+ | Run any `prisma` CLI command (the database gate, devops, and the integration gate where there is one) | `prisma-cli` | flags differ between versions; `migrate diff --shadow-database-url` against a live DB wiped dev/staging once |
3
+
4
+ <!-- nina:slot prisma.2 -->
5
+ | Write or audit a Prisma query (architect, implementer, the database gate, reviewer) | `prisma-client-api` | filters, operators, `$transaction` semantics. a caching or edge layer on top of the client is **NOT covered** — its cache strategy belongs to that layer's own integration doc |
package/src/dashboard.mjs CHANGED
@@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url';
16
16
  import { readProfile } from './paths.mjs';
17
17
  import { projectRecords } from './store.mjs';
18
18
  import { officeRoutes } from './office.mjs';
19
+ import { DEMO_OFFICE } from './demo.mjs';
19
20
  import { listenHere, openInBrowser, refuseElsewhere, untilStopped } from './serve.mjs';
20
21
  import { layerRootFor } from './commands/compose.mjs';
21
22
  import { taskList } from './commands/runs.mjs';
@@ -30,9 +31,6 @@ const PAGE = join(dirname(fileURLToPath(import.meta.url)), 'dashboard.html');
30
31
  /** The port it asks for first, one past the office's. Taken, any free one. */
31
32
  export const DASHBOARD_PORT = 6463;
32
33
 
33
- /** How the demo's office plays (`nina dashboard --demo`): its one session, over and over, for a screen left on. */
34
- export const DEMO_OFFICE = { replay: true, loop: true };
35
-
36
34
  /** How many tasks the tasks tab lists, newest first: a long project's history would otherwise fill the page. */
37
35
  const TASKS_SHOWN = 60;
38
36
 
@@ -103,19 +101,19 @@ export function dashboardTabs(target, ctx) {
103
101
  /**
104
102
  * Serves the dashboard: the page at `/`, each tab's data at `/api/<tab>`, and the office at `/office/`.
105
103
  *
106
- * @param {{target: string, ctx: {root: string}, port?: number, tell?: (line: string) => void, replay?: boolean, loop?: boolean, rest?: number}} options -
107
- * `tell` is where a failure is said: the terminal by default. `replay`, `loop` and `rest` are the office's
108
- * (`officeRoutes`): the demo's plays its one session over and over.
104
+ * @param {{target: string, ctx: {root: string}, port?: number, tell?: (line: string) => void, replay?: boolean, loop?: boolean, rest?: number, scale?: number}} options -
105
+ * `tell` is where a failure is said: the terminal by default. `replay`, `loop`, `rest` and `scale` are the office's
106
+ * (`officeRoutes`): the demo's plays its one session over and over (`DEMO_OFFICE`).
109
107
  * @returns {Promise<{url: string, close: () => Promise<void>}>}
110
108
  */
111
- export async function serveDashboard({ target, ctx, port = DASHBOARD_PORT, tell = console.error, replay = false, loop = false, rest }) {
109
+ export async function serveDashboard({ target, ctx, port = DASHBOARD_PORT, tell = console.error, replay = false, loop = false, rest, scale }) {
112
110
  const tabs = dashboardTabs(target, ctx);
113
111
  const room = JSON.stringify({ project: basename(target) }).replace(/</g, '\\u003c');
114
112
  const page = readFileSync(PAGE, 'utf8').replace('/*NINA_DASHBOARD*/null', () => room);
115
113
  // The office polls its ledger while anyone watches it: made when its tab is first opened, not before.
116
114
  let office = null;
117
115
  const officeFor = () => {
118
- if (!office) office = officeRoutes({ target, replay, loop, ...(rest === undefined ? {} : { rest }) });
116
+ if (!office) office = officeRoutes({ target, replay, loop, scale, ...(rest === undefined ? {} : { rest }) });
119
117
  return office;
120
118
  };
121
119
  // What failed is said in the terminal that runs the dashboard, and never in the response: an error's message carries
package/src/demo.mjs CHANGED
@@ -32,6 +32,19 @@ const BESIDE_REVIEW = ['dba'];
32
32
  /** A minute. */
33
33
  const MIN = 60_000;
34
34
 
35
+ /**
36
+ * How many times faster than it happened the demo's office plays today's session. Squeezed as a replay of a real
37
+ * session is, each agent sat at its desk for three seconds at most, up again before anyone watching had seen it work;
38
+ * at this pace each of today's runs, lasting seven to thirteen minutes, sits there for 20 to 40.
39
+ */
40
+ export const DEMO_PACE = 20;
41
+
42
+ /** How long each of today's runs stays at its desk in the demo's office, in ms: at least, and at most. */
43
+ export const DEMO_AT_DESK = [20_000, 40_000];
44
+
45
+ /** How the demo's office plays (`nina dashboard --demo`): its one session, over and over, at `DEMO_PACE`. */
46
+ export const DEMO_OFFICE = { replay: true, loop: true, scale: 1 / DEMO_PACE };
47
+
35
48
  /** A day. */
36
49
  const DAY = 86_400_000;
37
50
 
@@ -185,10 +198,11 @@ function seeded(seed) {
185
198
  *
186
199
  * @returns {{runs: object[], end: number}}
187
200
  */
188
- function taskRuns(random, start, { name, noun, design, sendBack = null, between = 0.2 }) {
201
+ function taskRuns(random, start, { name, noun, design, sendBack = null, between = 0.2, works = null }) {
189
202
  const runs = [];
190
203
  const slug = noun.toLowerCase().replace(/\s+/g, '-');
191
204
  const span = (role) => {
205
+ if (works) return works();
192
206
  const [lo, hi] = WORKS[role];
193
207
  return Math.round((lo + random() * (hi - lo)) * MIN);
194
208
  };
@@ -260,11 +274,14 @@ export function demoSessions(now) {
260
274
  }
261
275
  sessions.push({ id: sessionId(random), prompt: 'Next feature, please.', at: morning.getTime() - MIN, runs });
262
276
  }
263
- // Today's: the archive, sent back once, and the list's dates, begun while the archive's tests run. Laid out from zero,
264
- // then moved so its last report came in three minutes ago: nothing in it happens after `now`.
265
- const archive = taskRuns(seeded(7), MIN, { name: 'Add archiving to notes', noun: 'archive', design: true, sendBack: 'reviewer' });
277
+ // Today's: the archive, sent back once, and the list's dates, begun while the archive's tests run. Each run lasts what
278
+ // sits it at its desk for 20 to 40 seconds of the office's replay. Laid out from zero, then moved so its last report
279
+ // came in three minutes ago: nothing in it happens after `now`.
280
+ const desk = seeded(13);
281
+ const works = () => Math.round((DEMO_AT_DESK[0] + desk() * (DEMO_AT_DESK[1] - DEMO_AT_DESK[0])) * DEMO_PACE);
282
+ const archive = taskRuns(seeded(7), MIN, { name: 'Add archiving to notes', noun: 'archive', design: true, sendBack: 'reviewer', works });
266
283
  const qa = archive.runs.at(-1);
267
- const dates = taskRuns(seeded(11), qa.start + 1.5 * MIN, { name: 'Fix the dates on the notes list', noun: 'list dates', design: false });
284
+ const dates = taskRuns(seeded(11), qa.start + 1.5 * MIN, { name: 'Fix the dates on the notes list', noun: 'list dates', design: false, works });
268
285
  const runs = [...archive.runs, ...dates.runs].sort((a, b) => a.start - b.start);
269
286
  const shift = now - 3 * MIN - Math.max(...runs.map((r) => r.end));
270
287
  for (const r of runs) Object.assign(r, { start: r.start + shift, end: r.end + shift });
package/src/office.mjs CHANGED
@@ -305,6 +305,28 @@ const REPLAY_MIN_GAP_MS = 500;
305
305
  /** How long a replay in a loop rests on its last beat before it plays again from the start: the party is seen out. */
306
306
  export const LOOP_REST_MS = 8000;
307
307
 
308
+ /** The longest a replay at a scale waits between two beats: a lunch break in the session is not a minute of an empty room. */
309
+ const REPLAY_SCALED_MAX_GAP_MS = 45_000;
310
+
311
+ /**
312
+ * How long a replay waits after each beat before the next. Each real gap is squeezed to `REPLAY_MIN_GAP_MS` to
313
+ * `REPLAY_MAX_GAP_MS`, so hours of a session play in minutes; at a `scale` it is the real gap times the scale instead,
314
+ * so a run stays at its desk in proportion to how long it worked — the demo's, which an audience watches, sit 20 to 40
315
+ * seconds where a squeezed replay had them up again in three. The owner speaking passes quickly either way: it moves
316
+ * nobody across the room, and an answer keeps its wait, or the question it answers would not be seen.
317
+ *
318
+ * @param {object[]} beats - The beats in the order they are played.
319
+ * @param {{scale?: number}} [options] - Replay time per real time.
320
+ * @returns {number[]} The wait after each beat, in ms.
321
+ */
322
+ export function replayDelays(beats, { scale } = {}) {
323
+ return beats.map((beat, k) => {
324
+ const following = beats[k + 1];
325
+ const gap = following && following.type !== 'owner' ? following.at - beat.at : 0;
326
+ return scale ? Math.min(REPLAY_SCALED_MAX_GAP_MS, Math.max(REPLAY_MIN_GAP_MS, gap * scale)) : Math.min(REPLAY_MAX_GAP_MS, Math.max(REPLAY_MIN_GAP_MS, gap));
327
+ });
328
+ }
329
+
308
330
  /**
309
331
  * Each run the ledger saw start, in order: when, at which stage, by which agent once known, whether it was a message
310
332
  * to a running agent, when it was refused, and when its verdict ended it. The beats and the timeline read the same.
@@ -629,11 +651,12 @@ const send = (res, event, data) => res.write(`event: ${event}\ndata: ${JSON.stri
629
651
  * anyone watches live. `serveOffice` mounts them at the root; the dashboard (`nina dashboard`) at `/office/`, where the
630
652
  * page's relative `events` reaches `/office/events`.
631
653
  *
632
- * @param {{target: string, wanted?: string, replay?: boolean, loop?: boolean, rest?: number}} options - `loop`: a replay
633
- * that ends plays again from the start, for an office left on a screen, after resting `rest` ms on its last beat.
654
+ * @param {{target: string, wanted?: string, replay?: boolean, loop?: boolean, rest?: number, scale?: number}} options - `loop`: a
655
+ * replay that ends plays again from the start, for an office left on a screen, after resting `rest` ms on its last
656
+ * beat. `scale`: a replay that keeps the session's proportions at that much replay time per real time (`replayDelays`).
634
657
  * @returns {{handle: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, path: string) => boolean, close: () => void}}
635
658
  */
636
- export function officeRoutes({ target, wanted, replay = false, loop = false, rest = LOOP_REST_MS }) {
659
+ export function officeRoutes({ target, wanted, replay = false, loop = false, rest = LOOP_REST_MS, scale }) {
637
660
  const room = officeRoom(target);
638
661
  const graph = parseGraph(readFileSync(join(target, '.claude', 'graph.md'), 'utf8'));
639
662
  // A function, so a `$&` or `$'` in a project's name or its owner's is text and not a replacement pattern.
@@ -718,6 +741,7 @@ export function officeRoutes({ target, wanted, replay = false, loop = false, res
718
741
  greet(client, state);
719
742
  const heard = clock(state.session);
720
743
  const beats = officeBeats(state.entries, graph, Number.POSITIVE_INFINITY, heard);
744
+ const delays = replayDelays(beats, { scale });
721
745
  // The timeline as it stood at each beat: cut from the ledger up to that moment, so a replay tells nothing early.
722
746
  const known = names(state.entries);
723
747
  const look = looks(state.session);
@@ -745,16 +769,12 @@ export function officeRoutes({ target, wanted, replay = false, loop = false, res
745
769
  const beat = beats[i];
746
770
  send(client.res, 'beat', beat);
747
771
  tell(client, tasksAt(beat.at));
748
- i += 1;
749
- // The owner speaking moves nobody across the room: it passes quickly. An answer keeps its wait, or the question
750
- // it answers would not be seen.
751
- const quick = i < beats.length && beats[i].type === 'owner';
752
- const gap = i < beats.length && !quick ? beats[i].at - beat.at : 0;
753
772
  const timer = setTimeout(() => {
754
773
  timers.delete(timer);
755
774
  next();
756
- }, Math.min(REPLAY_MAX_GAP_MS, Math.max(REPLAY_MIN_GAP_MS, gap)));
775
+ }, delays[i]);
757
776
  timers.add(timer);
777
+ i += 1;
758
778
  };
759
779
  next();
760
780
  };