@njeybe/nativ 2.5.1 → 2.7.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 (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +100 -5
  3. package/dist/commands/design-check.d.ts +4 -0
  4. package/dist/commands/design-check.js +76 -0
  5. package/dist/commands/init.d.ts +1 -0
  6. package/dist/commands/init.js +55 -11
  7. package/dist/commands/notify.d.ts +2 -0
  8. package/dist/commands/notify.js +25 -0
  9. package/dist/commands/session.d.ts +22 -0
  10. package/dist/commands/session.js +106 -0
  11. package/dist/commands/task/task-add.js +1 -1
  12. package/dist/commands/task/task-common.js +6 -6
  13. package/dist/commands/task/task-escalate.js +8 -10
  14. package/dist/commands/task/task-lifecycle.d.ts +7 -1
  15. package/dist/commands/task/task-lifecycle.js +30 -5
  16. package/dist/commands/task/task-query.d.ts +2 -0
  17. package/dist/commands/task/task-query.js +27 -12
  18. package/dist/commands/task/task-reclaim.d.ts +20 -0
  19. package/dist/commands/task/task-reclaim.js +87 -0
  20. package/dist/commands/task.d.ts +2 -0
  21. package/dist/commands/task.js +1 -0
  22. package/dist/commands/triage.js +3 -3
  23. package/dist/commands/update.js +46 -0
  24. package/dist/commands/validate.js +1 -0
  25. package/dist/commands/verify.js +21 -5
  26. package/dist/core/design-rules.d.ts +23 -0
  27. package/dist/core/design-rules.js +115 -0
  28. package/dist/core/enforcement.js +18 -17
  29. package/dist/core/harness-integrity.js +26 -7
  30. package/dist/core/lock-manager.d.ts +6 -0
  31. package/dist/core/lock-manager.js +50 -41
  32. package/dist/core/notify.d.ts +38 -0
  33. package/dist/core/notify.js +133 -0
  34. package/dist/core/profiles.js +7 -16
  35. package/dist/core/session-memory.d.ts +42 -0
  36. package/dist/core/session-memory.js +121 -0
  37. package/dist/core/setup/pm-playbook.d.ts +5 -0
  38. package/dist/core/setup/pm-playbook.js +26 -0
  39. package/dist/core/setup/session-context.js +35 -2
  40. package/dist/core/setup/setup-planner.js +2 -2
  41. package/dist/core/setup/types.d.ts +2 -0
  42. package/dist/core/setup/types.js +5 -1
  43. package/dist/core/task-claims.d.ts +15 -0
  44. package/dist/core/task-claims.js +32 -0
  45. package/dist/core/tier1-liaison.d.ts +1 -0
  46. package/dist/core/tier1-liaison.js +25 -27
  47. package/dist/core/verifier/output-digest.d.ts +19 -0
  48. package/dist/core/verifier/output-digest.js +48 -0
  49. package/dist/db/data-engine.js +33 -32
  50. package/dist/db/introspector.js +61 -60
  51. package/dist/db/sqlite.d.ts +3 -0
  52. package/dist/db/sqlite.js +10 -0
  53. package/dist/governor/circuit-breaker.js +12 -0
  54. package/dist/governor/patch-applier.d.ts +1 -0
  55. package/dist/governor/patch-applier.js +3 -7
  56. package/dist/index.js +40 -6
  57. package/dist/mcp/server.d.ts +1 -1
  58. package/dist/mcp/server.js +53 -14
  59. package/dist/providers/claude-api.js +3 -0
  60. package/dist/providers/gemini.js +4 -1
  61. package/dist/providers/registry.d.ts +6 -2
  62. package/dist/providers/registry.js +28 -16
  63. package/dist/providers/types.d.ts +4 -1
  64. package/dist/runner/agent-supervisor.js +2 -2
  65. package/dist/runner/cli-config.js +1 -1
  66. package/dist/scanner/types.d.ts +5 -1
  67. package/dist/scanner/types.js +7 -0
  68. package/dist/server/routes/task-routes.d.ts +2 -0
  69. package/dist/server/routes/triage-routes.js +3 -18
  70. package/dist/server/studio-ui/pages/benchmarks/styles.js +8 -5
  71. package/dist/server/studio-ui/pages/database/browser.js +1 -1
  72. package/dist/server/studio-ui/pages/database/connection.js +1 -1
  73. package/dist/server/studio-ui/pages/database/data-shell.js +5 -5
  74. package/dist/server/studio-ui/pages/database/drift.js +1 -1
  75. package/dist/server/studio-ui/pages/database/explorer.js +2 -2
  76. package/dist/server/studio-ui/pages/database/styles.js +5 -5
  77. package/dist/server/studio-ui/pages/flow/styles.js +3 -3
  78. package/dist/server/studio-ui/pages/home/script.js +110 -91
  79. package/dist/server/studio-ui/pages/home/styles.js +33 -45
  80. package/dist/server/studio-ui/pages/tasks/script.js +150 -3
  81. package/dist/server/studio-ui/pages/tasks/styles.js +63 -6
  82. package/dist/server/studio-ui/pages/team/script.js +49 -52
  83. package/dist/server/studio-ui/pages/team/styles.js +21 -20
  84. package/dist/server/studio-ui/pages/worktrees/script.js +47 -35
  85. package/dist/server/studio-ui/pages/worktrees/styles.js +12 -10
  86. package/dist/server/studio-ui/shell/brand.d.ts +4 -0
  87. package/dist/server/studio-ui/shell/brand.js +16 -0
  88. package/dist/server/studio-ui/shell/console.js +61 -2
  89. package/dist/server/studio-ui/shell/core.js +8 -8
  90. package/dist/server/studio-ui/shell/detail-view.js +1 -1
  91. package/dist/server/studio-ui/shell/dispatch.js +18 -0
  92. package/dist/server/studio-ui/shell/events-tools.js +5 -1
  93. package/dist/server/studio-ui/shell/format.js +5 -6
  94. package/dist/server/studio-ui/shell/icons.js +23 -15
  95. package/dist/server/studio-ui/shell/markup-head.js +11 -6
  96. package/dist/server/studio-ui/shell/markup-tools.js +19 -10
  97. package/dist/server/studio-ui/shell/paint.js +3 -0
  98. package/dist/server/studio-ui/shell/pipeline-state.js +1 -1
  99. package/dist/server/studio-ui/shell/proposals.js +6 -2
  100. package/dist/server/studio-ui/shell/pulse.d.ts +1 -0
  101. package/dist/server/studio-ui/shell/pulse.js +67 -0
  102. package/dist/server/studio-ui/shell/router.js +25 -0
  103. package/dist/server/studio-ui/shell/sidebar-view.js +6 -3
  104. package/dist/server/studio-ui/shell/toasts-format.js +4 -4
  105. package/dist/server/studio-ui/styles/console.js +33 -13
  106. package/dist/server/studio-ui/styles/dialogs.js +4 -4
  107. package/dist/server/studio-ui/styles/responsive.js +8 -0
  108. package/dist/server/studio-ui/styles/shell.js +24 -5
  109. package/dist/server/studio-ui/styles/strategist.js +10 -10
  110. package/dist/server/studio-ui/styles/tokens-base.js +23 -23
  111. package/dist/server/studio-ui.js +2 -0
  112. package/package.json +2 -2
  113. package/plugin/.claude-plugin/plugin.json +1 -1
  114. package/plugin/agents/architect.md +11 -12
  115. package/plugin/agents/explorer.md +1 -1
  116. package/plugin/agents/verifier.md +2 -2
  117. package/plugin/agents/worker.md +5 -7
  118. package/plugin/skills/nativ/SKILL.md +36 -49
  119. package/templates/AGENTS.md +36 -49
  120. package/templates/CLAUDE.md +1 -41
  121. package/templates/GEMINI.md +3 -2
  122. package/templates/claude-agents/architect.md +11 -12
  123. package/templates/claude-agents/explorer.md +1 -1
  124. package/templates/claude-agents/verifier.md +2 -2
  125. package/templates/claude-agents/worker.md +5 -7
  126. package/templates/design/taste/LICENSE +21 -0
  127. package/templates/design/taste/SOURCE.md +28 -0
  128. package/templates/design/taste/v1/DIGEST.md +57 -0
  129. package/templates/design/taste/v1/SKILL.md +226 -0
  130. package/templates/design/taste/v2/DIGEST.md +62 -0
  131. package/templates/design/taste/v2/SKILL.md +1206 -0
  132. package/templates/dot-ai/api_contracts.json +66 -66
  133. package/templates/dot-ai/context.md +38 -38
  134. package/templates/dot-ai/db_schema.json +39 -39
  135. package/templates/dot-ai/subagents/architect.md +6 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +2 -11
  137. package/templates/dot-ai/subagents/frontend.md +47 -44
  138. package/templates/dot-ai/ui_specs.md +15 -36
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { writeJsonAtomicSync } from '../core/lock-manager.js';
3
4
  import { createClaudeApiProvider } from './claude-api.js';
4
5
  import { createClaudeCliProvider, DEFAULT_LIMIT_COOLDOWN_MS } from './claude-cli.js';
5
6
  import { createGeminiProvider } from './gemini.js';
@@ -42,11 +43,7 @@ function readCooldowns(root) {
42
43
  }
43
44
  function writeCooldowns(root, cooldownUntil) {
44
45
  try {
45
- const file = path.join(root, STATE_FILE);
46
- fs.mkdirSync(path.dirname(file), { recursive: true });
47
- const tmp = `${file}.${process.pid}.tmp`;
48
- fs.writeFileSync(tmp, JSON.stringify({ cooldownUntil }, null, 2), 'utf8');
49
- fs.renameSync(tmp, file);
46
+ writeJsonAtomicSync(path.join(root, STATE_FILE), { cooldownUntil });
50
47
  }
51
48
  catch {
52
49
  // Cooldown is an optimisation; failing to persist it must never fail the call.
@@ -93,10 +90,17 @@ export function resolveProvider(role, root, options = {}) {
93
90
  return resolveProviderChain(role, root, options)[0] ?? null;
94
91
  }
95
92
  /**
96
- * Runs the request against each provider in turn. A rate limit puts that provider on cooldown, and any
97
- * ProviderError falls through to the next one. Throws NO_CREDENTIALS when nothing is available and the
93
+ * Runs the request against each provider in turn. A TRANSIENT failure (network, 5xx) is retried on the same
94
+ * provider up to MAX_TRANSIENT_RETRIES times with jittered backoff. A rate limit puts that provider on cooldown, and
95
+ * any other ProviderError falls through to the next one. Throws NO_CREDENTIALS when nothing is available and the
98
96
  * last error when every provider failed, so the caller can drop to its deterministic path.
99
97
  */
98
+ export const MAX_TRANSIENT_RETRIES = 2;
99
+ /** 0.5s, then 1s, each plus up to 50% jitter so parallel agents do not retry in lockstep. */
100
+ export function transientBackoffMs(attempt, random = Math.random) {
101
+ const base = 500 * 2 ** attempt;
102
+ return Math.round(base + base * 0.5 * random());
103
+ }
100
104
  export async function completeWithChain(role, root, request, options = {}) {
101
105
  const chain = resolveProviderChain(role, root, options);
102
106
  if (!chain.length)
@@ -104,17 +108,25 @@ export async function completeWithChain(role, root, request, options = {}) {
104
108
  const models = (options.config ?? loadProviderConfig(root)).models ?? {};
105
109
  const now = options.deps?.now ?? Date.now;
106
110
  let lastError = null;
111
+ const sleep = options.deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
107
112
  for (const provider of chain) {
108
113
  const model = request.model ?? models[provider.id] ?? provider.defaultModel;
109
- try {
110
- const text = await provider.complete({ ...request, model }, root);
111
- return { provider: provider.id, model, text };
112
- }
113
- catch (err) {
114
- const failure = err instanceof ProviderError ? err : new ProviderError('BAD_RESPONSE', err instanceof Error ? err.message : String(err));
115
- if (failure.code === 'RATE_LIMITED')
116
- setProviderCooldown(root, provider.id, failure.retryAfterMs ?? DEFAULT_LIMIT_COOLDOWN_MS, now());
117
- lastError = failure;
114
+ for (let attempt = 0;; attempt++) {
115
+ try {
116
+ const text = await provider.complete({ ...request, model }, root);
117
+ return { provider: provider.id, model, text };
118
+ }
119
+ catch (err) {
120
+ const failure = err instanceof ProviderError ? err : new ProviderError('BAD_RESPONSE', err instanceof Error ? err.message : String(err));
121
+ lastError = failure;
122
+ if (failure.code === 'TRANSIENT' && attempt < MAX_TRANSIENT_RETRIES) {
123
+ await sleep(transientBackoffMs(attempt));
124
+ continue;
125
+ }
126
+ if (failure.code === 'RATE_LIMITED')
127
+ setProviderCooldown(root, provider.id, failure.retryAfterMs ?? DEFAULT_LIMIT_COOLDOWN_MS, now());
128
+ break;
129
+ }
118
130
  }
119
131
  }
120
132
  throw lastError ?? new ProviderError('BAD_RESPONSE', 'Every provider failed.');
@@ -7,7 +7,8 @@
7
7
  export type ProviderId = 'claude-cli' | 'claude-api' | 'gemini';
8
8
  /** Roles a provider can be assigned to in `.nativ/config.json`. */
9
9
  export type ProviderRole = 'architect' | 'pm' | 'worker' | 'verifier' | 'triage';
10
- export type ProviderErrorCode = 'NO_CREDENTIALS' | 'TIMEOUT' | 'RATE_LIMITED' | 'BAD_RESPONSE';
10
+ /** TRANSIENT: a network failure or a 5xx answer, worth retrying on the same provider after a short wait. */
11
+ export type ProviderErrorCode = 'NO_CREDENTIALS' | 'TIMEOUT' | 'RATE_LIMITED' | 'TRANSIENT' | 'BAD_RESPONSE';
11
12
  /** Typed failure so callers can fall through the provider chain without string matching. */
12
13
  export declare class ProviderError extends Error {
13
14
  readonly code: ProviderErrorCode;
@@ -57,6 +58,8 @@ export interface ProviderDeps {
57
58
  anthropicClient?: () => AnthropicLike;
58
59
  env?: NodeJS.ProcessEnv;
59
60
  now?: () => number;
61
+ /** Waits between retries; injectable so tests do not sleep. */
62
+ sleep?: (ms: number) => Promise<void>;
60
63
  }
61
64
  export interface AnthropicLike {
62
65
  messages: {
@@ -5,6 +5,7 @@ import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import Anthropic from '@anthropic-ai/sdk';
7
7
  import { loadMasterPlan } from '../core/verifier.js';
8
+ import { writeJsonAtomicSync } from '../core/lock-manager.js';
8
9
  import { cacheHitRate, computeActualCostUsd, emptyActualUsage, recordRunnerUsage, } from '../core/telemetry.js';
9
10
  import { resolveProjectRoot, linkWorktreeAiDirectory, safeUnlinkWorktreeAiDirectory, linkWorktreeNodeModules, safeUnlinkWorktreeNodeModules, } from '../core/root-resolver.js';
10
11
  import { resolveWorkerModel, toNativeModelId } from '../core/model-routing.js';
@@ -713,8 +714,7 @@ export class AgentSupervisor extends EventEmitter {
713
714
  }
714
715
  persist(record) {
715
716
  try {
716
- fs.mkdirSync(this.runsDir, { recursive: true });
717
- fs.writeFileSync(path.join(this.runsDir, `${record.taskId}.json`), `${JSON.stringify(record, null, 2)}\n`, 'utf8');
717
+ writeJsonAtomicSync(path.join(this.runsDir, `${record.taskId}.json`), record);
718
718
  }
719
719
  catch {
720
720
  // Local-first persistence is advisory; in-memory state stays authoritative.
@@ -77,7 +77,7 @@ export function buildRunnerPrompt(task) {
77
77
  task.description ? `Description: ${task.description}.` : '',
78
78
  task.verificationCommand ? `Verify your work using: ${task.verificationCommand}.` : '',
79
79
  `Start by running: nativ task start ${task.id}. When finished and verified, run: nativ task complete ${task.id}.`,
80
- `If blocked, follow the Human-Centric Communication Protocol in CLAUDE.md: explain the user experience symptom, root cause in plain English, and clear options without technical jargon.`,
80
+ `If blocked, follow "Talking to the human" in AGENTS.md: explain the user experience symptom, root cause in plain English, and clear options without technical jargon.`,
81
81
  ]
82
82
  .filter(Boolean)
83
83
  .join(' ');
@@ -42,6 +42,9 @@ export interface MasterPlanTask {
42
42
  complexity?: TaskComplexity;
43
43
  /** Plain-language "done when" lines. */
44
44
  acceptanceCriteria?: string[];
45
+ /** Set by `task start` while in_progress: who claimed it (NATIV_AGENT_ID or --agent) and when. */
46
+ claimedBy?: string;
47
+ claimedAt?: string;
45
48
  }
46
49
  export type TaskComplexity = 'simple' | 'standard' | 'complex';
47
50
  export declare const TASK_COMPLEXITIES: readonly TaskComplexity[];
@@ -64,7 +67,8 @@ export interface MasterPlan {
64
67
  activeMilestoneId: string;
65
68
  milestones: MasterPlanMilestone[];
66
69
  }
67
- export type EscalationType = 'contract_drift' | 'schema_flaw' | 'missing_credential' | 'dependency_conflict' | 'architectural_ambiguity';
70
+ export declare const ESCALATION_TYPES: readonly ["contract_drift", "schema_flaw", "missing_credential", "dependency_conflict", "architectural_ambiguity"];
71
+ export type EscalationType = (typeof ESCALATION_TYPES)[number];
68
72
  export interface EscalationRecord {
69
73
  id: string;
70
74
  taskId: string;
@@ -56,3 +56,10 @@ export function validateMasterPlanTask(task) {
56
56
  }
57
57
  return errors;
58
58
  }
59
+ export const ESCALATION_TYPES = [
60
+ 'contract_drift',
61
+ 'schema_flaw',
62
+ 'missing_credential',
63
+ 'dependency_conflict',
64
+ 'architectural_ambiguity',
65
+ ];
@@ -71,6 +71,8 @@ export declare function handlePipelineTasks(root: string): {
71
71
  specRefs?: string[];
72
72
  complexity?: import("../../scanner/types.js").TaskComplexity;
73
73
  acceptanceCriteria?: string[];
74
+ claimedBy?: string;
75
+ claimedAt?: string;
74
76
  }[];
75
77
  id: string;
76
78
  name: string;
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { withFileLock, withPlanLock } from '../../core/lock-manager.js';
3
+ import { withFileLock, withPlanLock, writeJsonAtomicSync } from '../../core/lock-manager.js';
4
4
  import { CircuitBreaker, applySelfHealingProposal, } from '../../governor/index.js';
5
5
  import { finalizeTriage, triageBlocker } from '../../commands/triage.js';
6
6
  import { HttpError } from '../http-utils.js';
@@ -19,7 +19,7 @@ export function readEscalationFile(root) {
19
19
  catch {
20
20
  return null;
21
21
  }
22
- // withFileLock creates the file empty when it takes the first lock.
22
+ // Older versions left an empty file behind when they first took its lock.
23
23
  if (!raw.trim())
24
24
  return null;
25
25
  try {
@@ -34,22 +34,7 @@ export function readEscalationFile(root) {
34
34
  }
35
35
  /** tmp-file + rename so the SSE watcher and concurrent readers never observe a half-written file. */
36
36
  export function writeJsonAtomic(file, data) {
37
- const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
38
- fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
39
- for (let attempt = 0;; attempt++) {
40
- try {
41
- fs.renameSync(tmp, file);
42
- return;
43
- }
44
- catch (err) {
45
- const code = err.code;
46
- if (attempt >= 10 || (code !== 'EPERM' && code !== 'EBUSY')) {
47
- fs.rmSync(tmp, { force: true });
48
- throw err;
49
- }
50
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
51
- }
52
- }
37
+ writeJsonAtomicSync(file, data);
53
38
  }
54
39
  /** Moves a blocked task back to pending so it can be dispatched again. False when it was not blocked. */
55
40
  export async function unblockTask(root, taskId, note) {
@@ -1,6 +1,6 @@
1
1
  // Benchmarks view styles.
2
2
  export const benchmarksCss = String.raw `/* Benchmarks */
3
- .hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 20px 32px; padding: 22px; margin-bottom: 16px; transition: opacity .2s; }
3
+ .hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 20px 32px; padding: 22px 0; margin-bottom: 0; background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; box-shadow: none; transition: opacity .2s; }
4
4
  .hero.is-running { opacity: .6; }
5
5
  .eyebrow { margin: 0; font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
6
6
  .hero-value { display: flex; align-items: center; gap: 10px; margin: 6px 0; font-size: 40px; font-weight: 650; letter-spacing: -0.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
@@ -9,14 +9,17 @@ export const benchmarksCss = String.raw `/* Benchmarks */
9
9
  .hero-stats { display: grid; grid-template-columns: repeat(4, minmax(96px, auto)); gap: 4px 28px; margin: 0; }
10
10
  .hero-stats dt { font-size: 12.5px; color: var(--muted); }
11
11
  .hero-stats dd { margin: 0; font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
12
- .scenario-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
13
- .scenario { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; }
14
- .scenario.is-idle { box-shadow: none; border-style: dashed; }
12
+ .scenario-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
13
+ .scenario { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 12px 0; background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; box-shadow: none; transition: background .15s; }
14
+ .scenario:hover { background: var(--surface-2); }
15
+ .scenario.is-idle { color: var(--fg-3); }
15
16
  .scenario-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
16
17
  .scenario-head h3 { margin: 0; font-size: 14.5px; font-weight: 600; }
17
18
  .scenario-sub { margin: -6px 0 0; font-size: 13px; color: var(--muted); }
18
19
  .scenario p { margin: 0; font-size: 13.5px; line-height: 1.5; }
19
- .metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
20
+ .metrics { display: inline-flex; font-variant-numeric: tabular-nums; }
21
+ .metrics > * { padding: 0 16px; border-left: 1px solid var(--line); }
22
+ .metrics > :first-child { padding-left: 0; border-left: 0; }
20
23
  .metric-v { display: block; font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
21
24
  .metric-l { display: block; font-size: 12px; color: var(--muted); }
22
25
  .details { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; margin: 0; padding-top: 10px; font-size: 12.5px; border-top: 1px solid var(--border); }
@@ -116,7 +116,7 @@ export const databaseBrowserScript = String.raw ` // ─── Database Studio:
116
116
  var start = res.totalCount === 0 ? 0 : (res.page - 1) * res.limit + 1;
117
117
  var end = Math.min(res.page * res.limit, res.totalCount);
118
118
  var pagination = '<div class="pagination-bar">' +
119
- '<div>Showing ' + start + '–' + end + ' of ' + res.totalCount + ' ' + (isColl ? 'documents' : 'records') + '</div>' +
119
+ '<div>Showing ' + start + '-' + end + ' of ' + res.totalCount + ' ' + (isColl ? 'documents' : 'records') + '</div>' +
120
120
  '<div class="pagination-ctrls">' +
121
121
  '<button class="btn small" type="button" id="btn-page-prev"' + (res.page <= 1 ? ' disabled' : '') + '>Previous</button>' +
122
122
  '<span>Page ' + res.page + ' of ' + Math.max(1, res.totalPages) + '</span>' +
@@ -148,7 +148,7 @@ export const databaseConnectionScript = String.raw ` // ─── Database Stud
148
148
  out.className = 'result err';
149
149
  out.textContent = 'Error: ' + (r.error || 'Connection failed');
150
150
  var hint = hintFor(r.error);
151
- if (hint) out.textContent += ' — ' + hint;
151
+ if (hint) out.textContent += ': ' + hint;
152
152
  return false;
153
153
  }, function (e) {
154
154
  out.className = 'result err'; out.textContent = 'Error: ' + e.message; return false;
@@ -1,5 +1,5 @@
1
1
  // Database Studio data loading and shell.
2
- export const databaseDataScript = String.raw ` // ─── View 5: Database Studio — data loading ────────────────────────────────
2
+ export const databaseDataScript = String.raw ` // ─── View 5: Database Studio - data loading ────────────────────────────────
3
3
  function load(fresh) {
4
4
  state.loading = true;
5
5
  renderDb();
@@ -63,7 +63,7 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
63
63
  ? ' <span class="badge badge-source" title="Detected in ' + esc(s.exampleFile || '.env.example') + '">via ' + esc(s.sourceKey) + '</span>'
64
64
  : (s.sourceKey ? ' <span class="badge badge-source">via ' + esc(s.sourceKey) + '</span>' : '');
65
65
  return '<span class="pill" title="' + esc(s.maskedUrl) + '"><span class="dot on"></span><b>' + label + ':</b> ' +
66
- esc(ENGINE_LABEL[s.engine] || s.engine) + (s.database ? ' · ' + esc(s.database) : '') + ' (' + Math.round(s.pingMs) + 'ms) – ' +
66
+ esc(ENGINE_LABEL[s.engine] || s.engine) + (s.database ? ' · ' + esc(s.database) : '') + ' (' + Math.round(s.pingMs) + 'ms): ' +
67
67
  entityCount(s) + ' ' + entityNoun(s, true) + sourceBadge + ' <span class="badge badge-online">ONLINE</span></span>';
68
68
  }
69
69
 
@@ -119,7 +119,7 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
119
119
  : ((state.schema[activeEnv + 'Tables'] || []).length);
120
120
  $('count-data').textContent = String(dataCount);
121
121
  var d = state.diff && state.diff.summary;
122
- $('count-drift').textContent = d ? String(d.addedTablesCount + d.alteredTablesCount + d.droppedTablesCount) : '–';
122
+ $('count-drift').textContent = d ? String(d.addedTablesCount + d.alteredTablesCount + d.droppedTablesCount) : '-';
123
123
  var script = state.diff ? generateScript(state.diff) : { text: '', lang: 'sql', count: 0 };
124
124
  state.sql = script.text;
125
125
  state.scriptLang = script.lang;
@@ -154,8 +154,8 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
154
154
  return '<div class="sr-only">Loading database schema…</div><div class="panes" aria-busy="true">' + col + col + '</div>';
155
155
  }
156
156
 
157
- var DB_ICON = '<svg class="icon empty-icon" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true">' +
158
- '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/></svg>';
157
+ var DB_ICON = '<svg class="icon empty-icon" width="56" height="56" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">' +
158
+ '<path d="M128,24C74.17,24,32,48.6,32,80v96c0,31.4,42.17,56,96,56s96-24.6,96-56V80C224,48.6,181.83,24,128,24Zm80,104c0,9.62-7.88,19.43-21.61,26.92C170.93,163.35,150.19,168,128,168s-42.93-4.65-58.39-13.08C55.88,147.43,48,137.62,48,128V111.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64ZM69.61,53.08C85.07,44.65,105.81,40,128,40s42.93,4.65,58.39,13.08C200.12,60.57,208,70.38,208,80s-7.88,19.43-21.61,26.92C170.93,115.35,150.19,120,128,120s-42.93-4.65-58.39-13.08C55.88,99.43,48,89.62,48,80S55.88,60.57,69.61,53.08ZM186.39,202.92C170.93,211.35,150.19,216,128,216s-42.93-4.65-58.39-13.08C55.88,195.43,48,185.62,48,176V159.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64V176C208,185.62,200.12,195.43,186.39,202.92Z"/></svg>';
159
159
 
160
160
  function emptyState(title, text) {
161
161
  return '<div class="empty card">' + DB_ICON + '<h3>' + esc(title) + '</h3><p>' + esc(text) + '</p>' +
@@ -15,7 +15,7 @@ export const databaseDriftScript = String.raw ` // ─── Database Studio: d
15
15
  }
16
16
 
17
17
  function colCell(c) {
18
- if (!c) return '<span class="muted">—</span>';
18
+ if (!c) return '<span class="muted">-</span>';
19
19
  return esc(c.type) + (c.nullable ? '' : ' NOT NULL') + (c.default != null ? ' <span class="muted">DEFAULT</span> ' + esc(c.default) : '') +
20
20
  (c.primaryKey ? ' <span class="badge badge-pk">PK</span>' : '') + (c.isMedia ? ' <span class="badge badge-media">IMAGE</span>' : '') +
21
21
  (c.isSubcollection ? ' <span class="badge badge-subcol">SUBCOLLECTION</span>' : '');
@@ -32,9 +32,9 @@ export const databaseExplorerScript = String.raw ` // ─── Database Studio
32
32
  mediaBadge(t, c);
33
33
  var typeCell = esc(c.type) + (c.observedTypes ? ' <span class="muted">(' + esc(c.observedTypes.join(' | ')) + ')</span>' : '');
34
34
  var lastCell = coll
35
- ? (c.presence != null ? Math.round(c.presence * 100) + '%' : '<span class="muted">–</span>')
35
+ ? (c.presence != null ? Math.round(c.presence * 100) + '%' : '<span class="muted">-</span>')
36
36
  : (c.default == null ? '<span class="muted">NULL</span>' : esc(c.default));
37
- return '<tr><td>' + (tags || '<span class="muted">–</span>') + '</td><td class="mono">' + esc(c.name) +
37
+ return '<tr><td>' + (tags || '<span class="muted">-</span>') + '</td><td class="mono">' + esc(c.name) +
38
38
  (fk ? ' <span class="muted">→ ' + esc(fk.referencedTable + '.' + fk.referencedColumn) + '</span>' : '') + '</td><td class="mono">' + typeCell +
39
39
  '</td><td>' + (c.nullable ? 'YES' : '<b>NO</b>') + '</td><td class="mono">' + lastCell + '</td></tr>';
40
40
  }).join('');
@@ -6,7 +6,7 @@ export const databaseCss = String.raw `/* Database Studio */
6
6
  .diagnostic-banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; margin: 12px 0 16px; background: var(--danger-bg); border: 1px solid var(--danger-line); border-radius: var(--r-control); }
7
7
  .diagnostic-content { display: flex; align-items: center; gap: 10px; font-size: 13.5px; }
8
8
  .diagnostic-tag { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--danger); background: var(--surface); border: 1px solid var(--danger-line); padding: 0 7px; border-radius: var(--r-control); }
9
- .diagnostic-msg code { background: var(--surface); padding: 1px 6px; border-radius: 4px; font-family: var(--mono); }
9
+ .diagnostic-msg code { background: var(--surface); padding: 1px 6px; border-radius: var(--r-control); font-family: var(--mono); }
10
10
  .diagnostic-actions { display: flex; gap: 8px; }
11
11
  .template-helper-box { background: var(--active-bg); border: 1px solid var(--active-line); border-radius: var(--r-control); padding: 12px 14px; margin-bottom: 16px; }
12
12
  .template-helper-label { font-size: 12.5px; color: var(--muted); margin-bottom: 8px; }
@@ -18,7 +18,7 @@ export const databaseCss = String.raw `/* Database Studio */
18
18
  .chip-dot.on { background: var(--success); }
19
19
  .chip-dot.off { background: var(--danger); }
20
20
 
21
- .thumb-pop { position: fixed; z-index: 60; width: 220px; padding: 8px; border-radius: var(--r-card); border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow-float); pointer-events: none; }
21
+ .thumb-pop { position: fixed; z-index: 60; width: 220px; padding: 8px; border-radius: var(--r-card); border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow); pointer-events: none; }
22
22
  .thumb-box { display: grid; place-items: center; min-height: 120px; max-height: 200px; overflow: hidden; border-radius: var(--r-control); background: repeating-conic-gradient(var(--chip) 0% 25%, var(--surface) 0% 50%) 50% / 16px 16px; }
23
23
  .thumb-box img { max-width: 100%; max-height: 200px; display: block; }
24
24
  .thumb-box .ph { color: var(--muted); font-size: 12px; text-align: center; padding: 12px; }
@@ -77,7 +77,7 @@ pre.sql { margin: 0; padding: 16px; overflow: auto; max-height: 70vh; background
77
77
  .sql .p { color: var(--active); } .sql .n { color: var(--active); font-weight: 600; } .sql .o { color: var(--primary); }
78
78
  .lang { font-family: var(--mono); font-size: 11px; padding: 0 7px; border-radius: var(--r-control); border: 1px solid var(--primary-line); background: var(--primary-tint); color: var(--primary); margin-right: 6px; }
79
79
  .copy-wrap { position: relative; display: inline-flex; }
80
- .tooltip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--success); color: #fff; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-control); pointer-events: none; opacity: 0; transition: opacity .15s; white-space: nowrap; }
80
+ .tooltip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--success); color: var(--accent-fg); font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-control); pointer-events: none; opacity: 0; transition: opacity .15s; white-space: nowrap; }
81
81
  .tooltip.show { opacity: 1; }
82
82
 
83
83
  .empty { text-align: center; padding: 48px 24px; }
@@ -87,7 +87,7 @@ pre.sql { margin: 0; padding: 16px; overflow: auto; max-height: 70vh; background
87
87
 
88
88
  .env-toggle-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin: 14px 0 6px; }
89
89
  .env-segmented { display: inline-flex; gap: 2px; padding: 3px; background: var(--chip); border: 1px solid var(--border); border-radius: var(--r-control); }
90
- .env-seg-btn { border: 1px solid transparent; background: transparent; padding: 4px 14px; border-radius: 5px; cursor: pointer; color: var(--muted); font-size: 13px; font-weight: 600; transition: all .15s ease; }
90
+ .env-seg-btn { border: 1px solid transparent; background: transparent; padding: 4px 14px; border-radius: var(--r-control); cursor: pointer; color: var(--muted); font-size: 13px; font-weight: 600; transition: all .15s ease; }
91
91
  .env-seg-btn:hover { color: var(--text); }
92
92
  .env-seg-btn[aria-pressed="true"] { background: var(--surface); color: var(--primary); border-color: var(--border); box-shadow: var(--shadow); }
93
93
  .env-seg-btn[data-env-mode="prod"][aria-pressed="true"] { color: var(--danger); background: var(--danger-bg); border-color: var(--danger-line); }
@@ -112,7 +112,7 @@ table.data-table tr:hover td { background: var(--bg); }
112
112
  .td-actions { display: flex; gap: 6px; align-items: center; white-space: nowrap; }
113
113
  .pagination-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 4px; font-size: 13px; color: var(--muted); }
114
114
  .pagination-ctrls { display: flex; align-items: center; gap: 8px; }
115
- .cell-preview-btn { padding: 1px 6px; font-size: 11px; border-radius: 4px; border: 1px solid var(--primary-line); color: var(--primary); background: var(--primary-tint); cursor: pointer; }
115
+ .cell-preview-btn { padding: 1px 6px; font-size: 11px; border-radius: var(--r-control); border: 1px solid var(--primary-line); color: var(--primary); background: var(--primary-tint); cursor: pointer; }
116
116
  .cell-preview-btn:hover { background: var(--surface); }
117
117
 
118
118
  `;
@@ -1,9 +1,9 @@
1
1
  // Flow page styles, all scoped under #panel-flow.
2
- export const flowCss = String.raw `#panel-flow .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--surface); }
2
+ export const flowCss = String.raw `#panel-flow .seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r-control); overflow: hidden; background: var(--surface); }
3
3
  #panel-flow .seg button { border: 0; background: none; padding: 0 12px; height: 30px; font-size: 13px; color: var(--fg-2); cursor: pointer; }
4
4
  #panel-flow .seg button + button { border-left: 1px solid var(--line); }
5
5
  #panel-flow .seg button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
6
- #panel-flow .fl-panel { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; min-width: 0; overflow: hidden; }
6
+ #panel-flow .fl-panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); min-width: 0; overflow: hidden; }
7
7
  #panel-flow .fl-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px; padding: 12px 16px; }
8
8
  #panel-flow .fl-bar .right { margin-left: auto; display: flex; gap: 14px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: var(--fg-3); }
9
9
  #panel-flow .flow-wrap { overflow: auto; max-height: 72vh; position: relative; border-top: 1px solid var(--line); }
@@ -18,7 +18,7 @@ export const flowCss = String.raw `#panel-flow .seg { display: inline-flex; bord
18
18
  #panel-flow .lane-label { position: sticky; left: 0; width: 128px; padding: 10px 12px; display: flex; flex-direction: column; gap: 2px; background: var(--surface); border-right: 1px solid var(--line); height: 100%; z-index: 2; }
19
19
  #panel-flow .lane-label .faint { font-size: 11.5px; }
20
20
  #panel-flow .round-head { position: absolute; top: 0; height: 30px; display: flex; align-items: center; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-3); }
21
- #panel-flow .node { position: absolute; display: flex; flex-direction: column; justify-content: center; gap: 3px; padding: 7px 10px; background: var(--surface); border: 1px solid var(--line); border-radius: 9px; text-align: left; cursor: pointer; transition: opacity .15s, border-color .15s, box-shadow .15s; z-index: 1; }
21
+ #panel-flow .node { position: absolute; display: flex; flex-direction: column; justify-content: center; gap: 3px; padding: 7px 10px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-control); text-align: left; cursor: pointer; transition: opacity .15s, border-color .15s, box-shadow .15s; z-index: 1; }
22
22
  #panel-flow .node:hover { border-color: var(--line-2); box-shadow: var(--shadow); }
23
23
  #panel-flow .node.in_progress { border-color: color-mix(in srgb, var(--run) 50%, var(--line)); background: color-mix(in srgb, var(--run-soft) 55%, var(--surface)); }
24
24
  #panel-flow .node.blocked { border-color: color-mix(in srgb, var(--bad) 50%, var(--line)); background: color-mix(in srgb, var(--bad-soft) 55%, var(--surface)); }