@tekyzinc/gsd-t 5.2.11 → 5.4.10

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.
@@ -519,16 +519,16 @@ Use these when user asks for help on a specific command:
519
519
  - **Contract**: `.gsd-t/contracts/plan-hardening-contract.md` v1.0.0 STABLE.
520
520
 
521
521
  ### model-tier-policy (M85)
522
- - **Summary**: SINGLE source of truth for GSD-T model-tier assignments. Publishes the authoritative tier set (haiku/sonnet/opus/fable), the 7 designated stage→tier mappings, and the `requiresThinkingOmitted(model)` predicate (encoding Fable's HTTP-400 breaking change ONCE). M85 slots the Fable tier into the 5 highest-leverage stages (solution-space probe, partition probe, competition judge, pre-mortem, Red Team) where one call's judgment gates the most downstream spend; competition producers STAY opus (M82 blindness invariant); debug cycle-1→opus, cycle-2→fable. A M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy — a drifted literal FAILS the lint (mandatory negative test).
522
+ - **Summary**: SINGLE source of truth for GSD-T model-tier assignments. Publishes the authoritative tier set (haiku/sonnet/opus — **Fable removed 2026-07-24**; `opus` = claude-opus-5) and the 7 designated stage→tier mappings (all opus). Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so every stage formerly on Fable OR Opus 4.8 now runs Opus 5. The M82 competition judge-blindness invariant is RELAXED to "fresh independent context" producers AND judge both run opus. A M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy — a drifted literal FAILS the lint (mandatory negative test).
523
523
  - **Files**: `bin/gsd-t-model-tier-policy.cjs` (zero external deps — installer invariant).
524
524
  - **Use when**: Any phase that needs to resolve a concrete model id from a stage key at invoke time (M69 pattern). Workflows NEVER `require` this module (sandbox ban) — they use hard-coded tier alias literals the lint proves match the policy.
525
525
  - **CLI**: `gsd-t model-tier-policy resolve <stageKey> [--json]`. Emits `{ok, stageKey, tier, model, requiresThinkingOmitted}`. Exit 0 resolved · 1 unknown stage key.
526
526
  - **Contract**: `.gsd-t/contracts/model-tier-policy-contract.md` v1.1.0 STABLE.
527
527
 
528
528
  ### model-profile (M86)
529
- - **Summary**: Per-project model-tier profile switcher — a SECOND dimension over the M85 stage-tier policy. Manages `.gsd-t/model-profile.json` and resolves which concrete model id each workflow stage runs on under the active profile. Three named profiles: `standard` (zero Fablepre-M85 posture), `pro` (Fable on red-team + pre-mortem + debug-cycle-2), `premium` (all 6 M85 designated Fable stages — global default). Switching profiles injects the overrides into workflow args at invoke time (M69 pattern — NO tracked-file rewriting). Competition producers are HELD at opus in ALL profiles (M82 blindness invariant). The active profile is surfaced in the session banner (`[GSD-T PROFILE]`), the statusline, and `gsd-t status` — always named, never an implicit fallback (SC(f)).
529
+ - **Summary**: Per-project model-tier profile switcher — a SECOND dimension over the stage-tier policy. Manages `.gsd-t/model-profile.json` and resolves which concrete model id each workflow stage runs on under the active profile. Three named profiles (Fable removed 2026-07-24 — they now dial OPUS-vs-SONNET spend): `standard` (probes opus, high-stakes stages sonnet cost-leanest), `pro` (probes + pre-mortem + red-team + debug-cycle-2 opus, judge sonnet), `premium` (all 6 designated stages opus — global default). Switching profiles injects the overrides into workflow args at invoke time (M69 pattern — NO tracked-file rewriting). Competition producers are held at opus in ALL profiles. The active profile is surfaced in the session banner (`[GSD-T PROFILE]`), the statusline, and `gsd-t status` — always named, never an implicit fallback (SC(f)).
530
530
  - **Files**: `bin/gsd-t-model-profile.cjs` (zero external deps — installer invariant). Config: `.gsd-t/model-profile.json` (`{ "profile": "pro", "stageOverrides": { ... } }`).
531
- - **Use when**: A project needs a different spend posture than the global default (e.g., run `standard` on a CI cost budget, or `pro` for a production release with targeted Fable quality gates). Per-stage overrides allow fine-grained control beyond the named profiles.
531
+ - **Use when**: A project needs a different spend posture than the global default (e.g., run `standard` on a CI cost budget, or `pro` for a production release with targeted opus quality gates). Per-stage overrides allow fine-grained control beyond the named profiles.
532
532
  - **CLI**:
533
533
  - `gsd-t model-profile show [--json]` — display active profile + per-stage resolution
534
534
  - `gsd-t model-profile set <standard|pro|premium>` — switch the project profile
@@ -540,7 +540,7 @@ Use these when user asks for help on a specific command:
540
540
  - **Contract**: `.gsd-t/contracts/model-profile-config-contract.md` v1.0.0 STABLE.
541
541
 
542
542
  ### research-gate (M89)
543
- - **Summary**: Deterministic CLASSIFY step of the M89 auto-research pipeline. Takes a load-bearing claim the agent tagged `[GUESSED:*]` in its Stated Claims section. The classifier is a MECHANICAL STRING-FACT FILTER (NOT a semantic oracle) returning one of THREE classes: `internal` (grep/Read, route `grep`), `external` (web-research, route `web`), or `ambiguous` (route `judge`). No LLM call in the classifier itself — semantic judgment is deferred to the wiring's LLM judge. An `ambiguous` claim is routed to a small LLM `classify-judge` stage (model: "fable") that decides internal/external/uncertain; **uncertain → research** (never guess-internal). The research `agent()` stage (model: "fable") runs for external guesses. An ENFORCE marker (`<!-- auto-research-claim: ... status=uncited -->`) is written into the artifact (a deterministic `.gsd-t/research/...` fallback if the stage reports no artifact path — fail-closed); the verify gate FAILs if it stays uncited.
543
+ - **Summary**: Deterministic CLASSIFY step of the M89 auto-research pipeline. Takes a load-bearing claim the agent tagged `[GUESSED:*]` in its Stated Claims section. The classifier is a MECHANICAL STRING-FACT FILTER (NOT a semantic oracle) returning one of THREE classes: `internal` (grep/Read, route `grep`), `external` (web-research, route `web`), or `ambiguous` (route `judge`). No LLM call in the classifier itself — semantic judgment is deferred to the wiring's LLM judge. An `ambiguous` claim is routed to a small LLM `classify-judge` stage (model: "opus") that decides internal/external/uncertain; **uncertain → research** (never guess-internal). The research `agent()` stage (model: "opus") runs for external guesses. An ENFORCE marker (`<!-- auto-research-claim: ... status=uncited -->`) is written into the artifact (a deterministic `.gsd-t/research/...` fallback if the stage reports no artifact path — fail-closed); the verify gate FAILs if it stays uncited.
544
544
  - **Files**: `bin/gsd-t-research-gate.cjs` (D1). Contract: `.gsd-t/contracts/auto-research-contract.md` v1.3.2 STABLE. Stage prompt: `templates/prompts/research-subagent.md`. DETECT snippet: `templates/prompts/stated-claims-snippet.md`. Cite-format test: `test/m89-research-stage-cite-format.test.js`.
545
545
  - **Use when**: A workflow stage needs to classify a guessed claim before deciding whether to grep, run a web-research sub-agent, or route to the LLM judge. Called by D3 (upper phases: plan, pre-mortem, partition, discuss, milestone) and D4 (worker phases: execute, debug, quick). NOT called manually — the wiring invokes it per `[GUESSED:*]` entry in the `## Stated Claims` section.
546
546
  - **CLI**: `gsd-t research-gate classify "<guessed claim text>"`. Emits `{ok, gap, class, route, reason}` where `class` ∈ {internal, external, ambiguous} and `route` ∈ {grep, web, judge}. Exit 0 on classify · 1 on bad input.
@@ -549,7 +549,7 @@ Use these when user asks for help on a specific command:
549
549
 
550
550
  ### architectural-trigger (M90)
551
551
  - **Summary**: Deterministic architectural-assumption trigger (M90 §2). Two fire paths: **R-ARCH-1** (divergence-sampling — N fresh-context producer answers → divergence score; competition-arm only, EXPERIMENTAL+MEASURED) and **R-ARCH-2** (protocol-class — any task whose `**Touches**` lists an EXISTING file triggers unconditionally; everywhere feed). Fires produce an instrumentation record in `.gsd-t/metrics/arch-trigger-events.jsonl`; a `provenByAdversaryOnly=true` flag from either path surfaces at the §4 fail-closed verify gate (R-FAIL-2). The module NEVER asserts it works — it emits measurement data only.
552
- - **Files**: `bin/gsd-t-architectural-trigger.cjs`. Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2. Blind-adversary prompt: `templates/prompts/blind-adversary-subagent.md`. Model: `fable` (M85 RULE-ARCH-TIER).
552
+ - **Files**: `bin/gsd-t-architectural-trigger.cjs`. Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2. Blind-adversary prompt: `templates/prompts/blind-adversary-subagent.md`. Model: `opus` (RULE-ARCH-TIER; Fable removed 2026-07-24).
553
553
  - **Use when**: Workflows detect that a task extends existing code (R-ARCH-2 everywhere feed) or that competition producers diverged (R-ARCH-1 competition-arm only). Called automatically by `gsd-t-execute`, `gsd-t-quick`, and `gsd-t-phase` competition arm via inline runCli helper (M81 runtime-native).
554
554
  - **CLI**: `gsd-t architectural-trigger trigger '<JSON>'`. JSON shapes: `{"type":"extend-existing-code","context":"...","basis":"..."}` or `{"type":"divergence-sampling","answers":[...],"basis":"..."}`. Emits `{ok, firePath, fired, basis, reason, ...}`. Exit 0 on success · 1 on bad input.
555
555
  - **Contract**: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2 v1.0.0 STABLE.
@@ -58,7 +58,7 @@ binary), do NOT silently proceed on the premium fallback. Either:
58
58
  (configured profile unknown; stale global binary may lack model-profile subcommand)
59
59
  ```
60
60
 
61
- A configured-standard project silently billing premium fable post-promo is the exact inverse of
61
+ A configured-standard project silently billing premium opus post-promo is the exact inverse of
62
62
  the spend-switch goal. Never silently fall through.
63
63
 
64
64
  Also surface a SUCCESSFUL resolve that carries a `configError` field (the resolver returns a
@@ -81,10 +81,10 @@ The **Model Profile** line MUST always name the active profile — never blank,
81
81
  - If the file is absent, display the global default by name with the `(default)` marker: `Model Profile: premium (default)`.
82
82
  - If the file is present but malformed or contains an unknown profile, display: `Model Profile: premium (default, config-error)` — never silently promote to the most expensive posture.
83
83
 
84
- Profiles control which workflow stages run on Fable vs. Opus/Sonnet:
85
- - `standard` — zero Fable stages (pre-M85 posture)
86
- - `pro` — Fable on red-team + pre-mortem + debug-cycle-2
87
- - `premium` — all 6 M85 designated Fable stages (full posture, global default)
84
+ Profiles control which workflow stages run on Opus vs. Sonnet (Fable removed 2026-07-24 — `opus` = claude-opus-5):
85
+ - `standard` — probes opus; high-stakes stages (judge/pre-mortem/red-team/debug-cycle-2) sonnet (cost-leanest)
86
+ - `pro` — probes + pre-mortem + red-team + debug-cycle-2 opus; judge sonnet
87
+ - `premium` — all 6 designated stages opus (full posture, global default)
88
88
 
89
89
  ### Backlog Section
90
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.2.11",
3
+ "version": "5.4.10",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
@@ -251,7 +251,7 @@ Every GSD-T project gets TWO logging streams scaffolded by default at `gsd-t-ini
251
251
  Every code-producing phase ends with `gsd-t-verify.workflow.js`, which runs three orthogonal validators as `parallel()` `agent()` stages with schema-validated output. Per `.gsd-t/contracts/orthogonal-validation-contract.md` v1.0.0 STABLE, they are declared orthogonal objective functions — no collapse, no substitution, no transitive trust.
252
252
 
253
253
  - **`/code-review ultra`** — cooperative correctness + cleanup. Severity: `important` / `nit` / `pre-existing`. Skippable via `args.skipUltra=true` + `args.skipUltraReason`. `skipUltra=true` is INELIGIBLE for `VERIFIED`.
254
- - **Red Team** — adversarial / security / boundaries. Non-skippable. Protocol: `templates/prompts/red-team-subagent.md`. Verdict: `FAIL` (any CRITICAL or HIGH bug — blocks completion) or `GRUDGING-PASS` (exhaustive search, nothing found). CRITICAL/HIGH bugs get up to 2 fix cycles before deferral. Runs on `model: "fable"` (M85).
254
+ - **Red Team** — adversarial / security / boundaries. Non-skippable. Protocol: `templates/prompts/red-team-subagent.md`. Verdict: `FAIL` (any CRITICAL or HIGH bug — blocks completion) or `GRUDGING-PASS` (exhaustive search, nothing found). CRITICAL/HIGH bugs get up to 2 fix cycles before deferral. Runs on `model: "opus"` (= claude-opus-5; Fable removed 2026-07-24).
255
255
  - **QA** — test execution + shallow-test detection + contract compliance. Non-skippable. Protocol: `templates/prompts/qa-subagent.md`. Writes ZERO feature code. Any shallow E2E test blocks phase completion. Runs on `model: "sonnet"`.
256
256
 
257
257
  When `.gsd-t/contracts/design-contract.md` or `.gsd-t/contracts/design/` exists, a fourth stage runs Design Verification (protocol: `templates/prompts/design-verify-subagent.md`) — opens a browser, compares the build against the design, returns a structured element-by-element MATCH/DEVIATION schema. Deviations block completion.
@@ -260,13 +260,13 @@ Synthesis stage merges results without category collapse. Verdict: `VERIFIED` /
260
260
 
261
261
  ## Model Display (MANDATORY)
262
262
 
263
- **Each Workflow `agent()` call declares its model explicitly** via the `model:` option (`"haiku"` / `"sonnet"` / `"opus"` / `"fable"`). The Workflow runtime emits a `⚙ [{model}] {label}` line per stage in `/workflows`, giving the user real-time visibility into which model handles each operation.
263
+ **Each Workflow `agent()` call declares its model explicitly** via the `model:` option (`"haiku"` / `"sonnet"` / `"opus"` **Fable removed 2026-07-24**; `opus` = claude-opus-5). The Workflow runtime emits a `⚙ [{model}] {label}` line per stage in `/workflows`, giving the user real-time visibility into which model handles each operation.
264
264
 
265
265
  **Model assignments:**
266
266
  - `model: "haiku"` — strictly mechanical tasks: run test suites and report counts, check file existence, validate JSON structure, branch guard checks
267
267
  - `model: "sonnet"` — mid-tier reasoning: routine code changes, standard refactors, test writing, QA evaluation, straightforward synthesis
268
268
  - `model: "opus"` — high-stakes reasoning: architecture decisions, security analysis, complex debugging, cross-module refactors, quality judgment on critical paths
269
- - `model: "fable"`highest-stakes calls where one judgment gates the most downstream spend (M85): solution-space probe, partition probe, competition judge, pre-mortem, Red Team. Competition producers STAY `opus` (M82 blindness invariant judge must differ from producers). Debug cycle-1 `opus`, cycle-2 `fable` (escalation). **Single source of truth for tier assignments:** `bin/gsd-t-model-tier-policy.cjs` + `.gsd-t/contracts/model-tier-policy-contract.md` v1.0.0 STABLE. The M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy and a drifted literal FAILS the lint (mandatory negative test).
269
+ - **Fable removed 2026-07-24** `opus` (= claude-opus-5) is the top tier and the default for every high-stakes stage: solution-space probe, partition probe, competition judge, pre-mortem, Red Team, competition producers, debug both cycles. Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so every stage formerly on Fable OR Opus 4.8 now runs Opus 5. The M82 competition judge-blindness invariant is RELAXED to "fresh independent context" — producers AND judge both run `opus` (fresh contexts remove memory-bias; the modest residual taste/blind-spot bias is accepted for a stronger judge). **Single source of truth for tier assignments:** `bin/gsd-t-model-tier-policy.cjs` + `.gsd-t/contracts/model-tier-policy-contract.md`. The M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy and a drifted literal FAILS the lint (mandatory negative test).
270
270
 
271
271
  **Context budget:** Workflow scripts receive a `budget` global (`budget.total`, `budget.spent()`, `budget.remaining()`) tied to the user's per-turn token target. Use it for dynamic loops (`while (budget.total && budget.remaining() > 50_000) { ... }`) or to scale fleet size. Opus 4.7/4.8 ship 1M context windows; the legacy meter at `bin/token-budget.cjs` was retired in M61 — use native `/context` for live in-session usage.
272
272
 
@@ -421,9 +421,9 @@ For every load-bearing claim, tag it:
421
421
  - **`[GUESSED:stale]`** — external/time-varying fact that may have changed.
422
422
 
423
423
  A `[GUESSED:*]` claim is then CLASSIFIED — mechanical string-fact filter, three classes:
424
- - **`class: external`** → research agent (`model: "fable"`) writes a `## Verified Facts (auto-research)` block (URL + fetch date); ENFORCE marker `<!-- auto-research-claim: class=external key=<key> status=uncited -->` is written; verify FAILs if it stays `status=uncited` (R-FAIL-1).
424
+ - **`class: external`** → research agent (`model: "opus"`) writes a `## Verified Facts (auto-research)` block (URL + fetch date); ENFORCE marker `<!-- auto-research-claim: class=external key=<key> status=uncited -->` is written; verify FAILs if it stays `status=uncited` (R-FAIL-1).
425
425
  - **`class: internal`** → grep/Read only; escalate to external if grep empty.
426
- - **`class: ambiguous`** → LLM judge (`model: "fable"`) decides; uncertain → research (never guess-internal).
426
+ - **`class: ambiguous`** → LLM judge (`model: "opus"`) decides; uncertain → research (never guess-internal).
427
427
 
428
428
  **§2 — Architectural Trigger (`gsd-t architectural-trigger trigger '<JSON>'`):**
429
429
  Two fire paths — both INSTRUMENTED (fire-rate emitted to `.gsd-t/metrics/arch-trigger-events.jsonl`):
@@ -569,6 +569,35 @@ Skip the hint if auto-advancing (Level 3 mid-wave) — only show when the user n
569
569
  - Functions under 30 lines — split if longer
570
570
  - Files under 200 lines — create new modules if needed
571
571
  - Enums for state management and fixed option sets
572
+ - **Fixed value sets live in ONE shared constant** — every comparison references it, never a retyped literal (a second literal is where casing/typo mismatches hide)
573
+
574
+ ## Comparisons — case-insensitive by default (MANDATORY)
575
+
576
+ **Comparing a domain string VALUE against a literal is CASE-INSENSITIVE unless the user specifically directs otherwise.** Domain values (status, filter, tab, mode, role, category, email, user-entered text) cross boundaries — database, API, URL, form — and each is free to change the casing. A literal comparison silently returns `false` and the feature quietly does nothing.
577
+
578
+ ```
579
+ CASE-INSENSITIVE: statuses, filters, tabs, modes, roles, categories, emails,
580
+ user-entered values, route/query params carrying any of these
581
+ CASE-SENSITIVE: passwords, tokens, API keys, hashes, signatures (case-insensitive
582
+ here is a SECURITY defect); base64/JWT/hex/uuid-as-string/git SHA
583
+ (case IS data); Linux file paths; object/property keys; DOM tag
584
+ and prop names; env-var NAMES; branch names
585
+ ```
586
+
587
+ Guard for absent values: `(filter || '').toLowerCase() !== 'invoiced'`. In SQL: `lower(col) = lower($1)` with a functional index on `lower(col)` when queried at volume.
588
+
589
+ Full rule + worked examples: `templates/stacks/_comparison.md` (universal — auto-injected into every project).
590
+
591
+ ## Database schema — every new table gets a self-incrementing integer ID (MANDATORY)
592
+
593
+ **Every NEW relational table has a self-incrementing integer primary key named `id`.** API-exposed tables ALSO get a separate `public_id` UUID (unique, indexed) — expose that externally, never the integer id.
594
+
595
+ - **Why the integer inside** — 8 bytes vs 16, sorts in insert order so the index stays compact, joins fast, readable in a log. A UUID primary key scatters index writes and bloats every foreign key pointing at it.
596
+ - **Why the UUID at the edge** — a sequential id in a URL leaks row counts and lets anyone walk `/invoice/1`, `/invoice/2` looking for other people's data.
597
+ - **NEW tables only.** Existing UUID-keyed tables are NOT retrofitted — their foreign keys already point at the UUID, so changing the PK is a data migration and a **Destructive Action Guard** item requiring explicit user approval.
598
+ - **Does NOT apply to Firestore or Neo4j** — deliberate, not an oversight. Firestore has no sequence to increment (forcing one means a counter document that serializes every write); Neo4j has no rows, and its internal `id()` is reused after deletion. Each stack file states this.
599
+
600
+ Enforced mechanically by `bin/gsd-t-schema-id-check.cjs` (verify-gate `schema-id`, FAIL-CLOSED; no-op PASS when a run adds no schema files). Full rules: `templates/stacks/postgresql.md` §2, `prisma.md` §1, `supabase.md` §6.5.
572
601
 
573
602
  ## Naming
574
603
  ```
@@ -4,7 +4,7 @@
4
4
  **Report concisely:** verdict/answer first, no preamble. Gloss every code/jargon term (e.g. `M93-D2` = milestone 93, domain 2) in plain words on first use. Bullets over paragraphs. Expand only if asked.
5
5
  <!-- /reader-contract -->
6
6
 
7
- **Model:** `fable` (M85 tier policy — highest-leverage judgment; separate context from the proposing agent)
7
+ **Model:** `opus` (= claude-opus-5; Fable removed 2026-07-24 — highest-leverage judgment; separate context from the proposing agent)
8
8
 
9
9
  **Framing:** You are reviewing someone ELSE's architectural design — you did NOT propose it and have no attachment to it. Your goal is to find the **fatal flaw** in the premise being challenged, before a single line of code is committed to that premise. This framing (independent reviewer, not the author) is essential for escaping self-preference bias: the proposing agent's prior context makes it systematically less able to see its own premise's failures (source: https://arxiv.org/abs/2310.08118 — LLM self-evaluation is biased toward confirming prior outputs; https://arxiv.org/abs/2404.13076 — blind adversarial framing surfaces failures that self-critique misses).
10
10
 
@@ -32,7 +32,7 @@ You are NOT checking whether the code is correct. You are attacking the **archit
32
32
  3. **Has someone else solved this class of problem? How?** Grep the repo, check the brief, research prior art. Do NOT confirm the premise by default — search for evidence that it is WRONG or that a different approach is clearly superior.
33
33
  4. **What is the minimal falsifying test?** What single experiment would prove the premise is wrong? Propose it.
34
34
 
35
- ## Extends M83 Pre-Mortem + Red-Team-on-fable
35
+ ## Extends M83 Pre-Mortem + Red-Team-on-opus
36
36
 
37
37
  This prompt is the **architectural dual** of the M83 pre-mortem: the pre-mortem attacks a PLAN before execution; this prompt attacks an ARCHITECTURAL PREMISE before the plan is written. It also extends the Red Team (which attacks finished code at verify) by moving the adversarial review LEFT to the assumption stage.
38
38
 
@@ -6,7 +6,7 @@ ZERO feature code writes. You do NOT answer questions about what the claim PROBA
6
6
  it up and cite it.
7
7
 
8
8
  <!-- M89 — Workflow-stage invocation -->
9
- **Invocation context.** This protocol runs as a native Workflow `agent()` stage (bare `model: "fable"`,
9
+ **Invocation context.** This protocol runs as a native Workflow `agent()` stage (bare `model: "opus"`,
10
10
  Fable tier — the single highest-leverage web call per phase). Your **final emission MUST be a single
11
11
  StructuredOutput object** matching the schema declared by the calling Workflow. The Verified-Facts block
12
12
  is embedded in the artifact; the StructuredOutput envelope carries the block text + the gap-key.
@@ -0,0 +1,134 @@
1
+ # Comparison Standards (Universal)
2
+
3
+ These rules are MANDATORY. Violations fail the task. No exceptions.
4
+
5
+ ---
6
+
7
+ ## 1. Domain String Comparisons Are Case-Insensitive by Default
8
+
9
+ **Comparing a domain string VALUE against a literal is case-insensitive unless the user has
10
+ specifically directed otherwise.**
11
+
12
+ A domain value is a string that names something in the business world: a status, a filter, a tab, a
13
+ mode, a role, a category, a user-entered value, an email address. These cross system boundaries —
14
+ database, API, URL, form input, config — and each boundary is free to change the casing. A literal
15
+ comparison silently returns `false` and the feature just quietly does nothing.
16
+
17
+ ```
18
+ CASE-INSENSITIVE (domain value vs literal):
19
+ ├── Statuses, filters, tabs, modes, view names
20
+ ├── Roles, categories, types, enum-ish values
21
+ ├── Email addresses (the local part is technically case-sensitive per spec,
22
+ │ but no real mail provider treats it that way — compare lowercased)
23
+ ├── User-entered text being matched against known values
24
+ └── Query-string and route params carrying any of the above
25
+ ```
26
+
27
+ **GOOD**
28
+ ```ts
29
+ filter.toLowerCase() !== 'invoiced'
30
+ status.toLowerCase() === 'pending'
31
+ user.email.toLowerCase() === input.email.toLowerCase()
32
+ row.category?.toLowerCase() === selected.toLowerCase()
33
+ ```
34
+
35
+ **BAD** — a real bug, shipped:
36
+ ```ts
37
+ // TILE_FILTERS maps the Invoiced tile to lowercase 'invoiced'.
38
+ // This compares against 'Invoiced'. It never matched. The "+ Add Order" button
39
+ // appeared on a tab where hand-adding an order is impossible, and the widening
40
+ // branch below pulled in orders that made an unrelated button appear too —
41
+ // two visible defects, one casing mismatch.
42
+ onAddOrder={view === 'orders' && filter !== 'Invoiced' ? handleAdd : undefined}
43
+ ```
44
+
45
+ Guard against null/undefined when lowercasing a value that may be absent:
46
+ ```ts
47
+ (filter || '').toLowerCase() !== 'invoiced' // or filter?.toLowerCase()
48
+ ```
49
+
50
+ ---
51
+
52
+ ## 2. Identifiers, Secrets, and Encoded Values Stay Case-Sensitive
53
+
54
+ Case-insensitivity is a **default for domain values, not a blanket rule**. Applying it to the
55
+ following is a defect — and for the first group, a security defect:
56
+
57
+ ```
58
+ ALWAYS CASE-SENSITIVE:
59
+ ├── Passwords, tokens, API keys, session IDs, signatures, hashes
60
+ │ (case-insensitive matching here weakens the secret — a real vulnerability)
61
+ ├── Encoded values: base64, JWTs, hex digests, cuid/uuid-as-string, git SHAs
62
+ │ (case IS data — aB and Ab are different values)
63
+ ├── File paths and filenames on Linux (Makefile ≠ makefile — different files)
64
+ ├── Object and property keys (obj.userId ≠ obj.userid — different properties)
65
+ ├── DOM/JSX tag names, prop names, CSS custom properties
66
+ ├── Environment variable NAMES, git branch names, DNS record values
67
+ └── Anything compared for cryptographic equality
68
+ ```
69
+
70
+ **BAD** — a security defect:
71
+ ```ts
72
+ if (providedToken.toLowerCase() === storedToken.toLowerCase()) grantAccess();
73
+ ```
74
+
75
+ **GOOD**
76
+ ```ts
77
+ if (providedToken === storedToken) grantAccess(); // exact, case-sensitive
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 3. Database Comparisons
83
+
84
+ The same split applies in SQL. A domain-value comparison is case-insensitive; an identifier or
85
+ token comparison is exact.
86
+
87
+ ```sql
88
+ -- GOOD — domain value, case-insensitive
89
+ WHERE lower(status) = 'pending'
90
+ WHERE lower(email) = lower($1)
91
+
92
+ -- GOOD — citext column, or a functional index to keep it fast
93
+ CREATE INDEX idx_orders_status_lower ON orders (lower(status));
94
+
95
+ -- GOOD — identifier, exact
96
+ WHERE public_id = $1
97
+ WHERE api_key_hash = $1
98
+
99
+ -- BAD — domain value compared exactly; breaks when casing drifts
100
+ WHERE status = 'Pending'
101
+ ```
102
+
103
+ Note: `lower(col) = ...` cannot use a plain index on `col`. Add a functional index on
104
+ `lower(col)`, or use a case-insensitive column type, when the column is queried at volume.
105
+
106
+ ---
107
+
108
+ ## 4. Prefer a Shared Constant Over Any String Literal
109
+
110
+ Case-insensitivity is the safety net. The structural fix is that a fixed set of values exists in
111
+ **one** place and every comparison references it — then a mismatch is caught by the compiler
112
+ instead of by a user.
113
+
114
+ ```ts
115
+ // The bug above cannot occur when both sites reference the same constant:
116
+ export const ORDER_FILTER = { ORDERS: 'orders', INVOICED: 'invoiced' } as const;
117
+ ```
118
+
119
+ Use both mechanisms. The shared constant catches typos and casing at build time; case-insensitive
120
+ comparison catches the same class of bug at the boundaries a constant can't reach — data arriving
121
+ from a database, an API response, a URL, or a user.
122
+
123
+ See `typescript.md` §6 for the typed form, and `postgresql.md`/`prisma.md` for database enums.
124
+
125
+ ---
126
+
127
+ ## Comparison Verification Checklist
128
+
129
+ - [ ] Every domain-value comparison (status/filter/tab/role/category/email) is case-insensitive
130
+ - [ ] No `.toLowerCase()` applied to a token, hash, signature, key, or encoded value
131
+ - [ ] Lowercasing guards against null/undefined (`(x || '').toLowerCase()`)
132
+ - [ ] Fixed value sets are declared once and referenced — not retyped as literals
133
+ - [ ] SQL domain-value filters use `lower()` (with a functional index if queried at volume)
134
+ - [ ] File paths, object keys, and env-var names compared exactly
@@ -44,9 +44,40 @@ MANDATORY:
44
44
  ├── Embedded objects for small, bounded data that's always read together
45
45
  ├── Store document IDs as fields when needed for queries
46
46
  ├── Use server timestamps: serverTimestamp() — not client Date.now()
47
+ ├── Use Firestore's auto-generated document IDs — NEVER build a sequential counter (see below)
47
48
  └── NEVER create deeply nested subcollection hierarchies (max 2-3 levels)
48
49
  ```
49
50
 
51
+ **The self-incrementing-integer-ID rule does NOT apply to Firestore.** This is deliberate, not an
52
+ oversight — the GSD-T relational stacks (postgresql, prisma, supabase) mandate an integer identity
53
+ primary key on every table. Firestore cannot provide one:
54
+
55
+ - A self-incrementing ID requires every writer to agree on "what was the last number", which means
56
+ one machine must be the authority and every insert must wait its turn. Firestore is built to
57
+ spread writes across many machines precisely to avoid that.
58
+ - Forcing it means a single counter document that every write locks and updates — capping the
59
+ collection's write rate at roughly one per second and re-introducing the bottleneck the database
60
+ exists to avoid.
61
+
62
+ Firestore's auto-generated IDs need no coordination and cannot collide between writers. Use them.
63
+
64
+ **BAD** — the counter-document anti-pattern:
65
+ ```typescript
66
+ // NEVER — serializes every write in the collection behind one document
67
+ const counterRef = db.doc('counters/orders');
68
+ await db.runTransaction(async (tx) => {
69
+ const snap = await tx.get(counterRef);
70
+ const next = snap.data().value + 1;
71
+ tx.update(counterRef, { value: next });
72
+ tx.set(db.doc(`orders/${next}`), orderData);
73
+ });
74
+ ```
75
+
76
+ **GOOD** — let Firestore assign the ID:
77
+ ```typescript
78
+ await db.collection('orders').add(orderData); // auto-ID, no coordination, no contention
79
+ ```
80
+
50
81
  **GOOD**
51
82
  ```typescript
52
83
  // User document — embedded small data
@@ -15,9 +15,21 @@ MANDATORY:
15
15
  ├── Properties: camelCase (firstName, createdAt)
16
16
  ├── Store properties on the node or relationship that "owns" them
17
17
  ├── Relationships ALWAYS have a direction — even if queried bidirectionally
18
+ ├── Every node carries an application-assigned `id` property with a UNIQUE constraint (see §3)
19
+ ├── NEVER persist or expose Neo4j's internal id() — it is reused after deletion (see below)
18
20
  └── NEVER use relationships as nodes (reify only when the relationship needs its own relationships)
19
21
  ```
20
22
 
23
+ **The self-incrementing-integer-ID rule does NOT apply to Neo4j.** This is deliberate, not an
24
+ oversight — the GSD-T relational stacks (postgresql, prisma, supabase) mandate an integer identity
25
+ primary key on every table. Neo4j has no tables and no rows to number: it stores nodes and the
26
+ relationships between them, so there is nothing for a sequence to attach to.
27
+
28
+ Neo4j does expose an internal `id()` per node, but it is **not a stable identifier** — delete a node
29
+ and the number is reused by a different node later. Anything that stored the old value now points at
30
+ the wrong entity. Use an application-assigned `id` property with a uniqueness constraint instead
31
+ (§3 Indexing and Constraints).
32
+
21
33
  **GOOD**
22
34
  ```cypher
23
35
  (:User {id: "u-123", name: "Jane", email: "jane@example.com"})
@@ -10,7 +10,8 @@ These rules are MANDATORY. Violations fail the task. No exceptions.
10
10
  MANDATORY:
11
11
  ├── Tables: snake_case, plural (users, order_items)
12
12
  ├── Columns: snake_case, singular (first_name, created_at)
13
- ├── Primary keys: id (UUID preferred over serial for distributed systems)
13
+ ├── Primary keys: id ALWAYS a self-incrementing integer identity (see §2)
14
+ ├── Public identifiers: public_id UUID — only on API-exposed tables (see §2)
14
15
  ├── Foreign keys: {referenced_table_singular}_id (user_id, order_id)
15
16
  ├── Indexes: idx_{table}_{columns} (idx_users_email)
16
17
  ├── Constraints: {type}_{table}_{columns} (uq_users_email, fk_orders_user_id)
@@ -24,29 +25,64 @@ MANDATORY:
24
25
 
25
26
  ```
26
27
  MANDATORY:
27
- ├── Every table has: id (PK), created_at (timestamptz DEFAULT now()), updated_at
28
+ ├── EVERY table has a self-incrementing integer primary key named id — NO EXCEPTIONS
29
+ │ BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
30
+ │ NEVER a UUID primary key. NEVER a natural/composite key as the PK.
31
+ ├── API-exposed tables ALSO get public_id UUID NOT NULL DEFAULT gen_random_uuid()
32
+ │ with a UNIQUE constraint — expose public_id externally, NEVER the integer id
33
+ ├── Every table has: created_at (timestamptz DEFAULT now()), updated_at
28
34
  ├── Use timestamptz — NEVER timestamp without time zone
29
- ├── Use UUID for primary keys in distributed or API-exposed systems
30
35
  ├── Use appropriate types: text (not varchar), numeric (not float for money)
31
36
  ├── Add NOT NULL constraints by default — allow NULL only with explicit reason
32
37
  ├── Foreign keys with ON DELETE policy (CASCADE, SET NULL, or RESTRICT — choose deliberately)
33
38
  └── NEVER store JSON when a proper relational schema exists — use jsonb only for truly unstructured data
34
39
  ```
35
40
 
41
+ **Why an integer PK plus a separate public UUID** — the two identifiers do different jobs:
42
+
43
+ - **Integer `id` inside the database** — 8 bytes (vs 16 for UUID), sorts in insert order so the
44
+ index stays compact, joins fast, and is readable in a log (`order 4471`). A UUID primary key
45
+ scatters index writes and bloats every foreign key pointing at it.
46
+ - **UUID `public_id` at the edge** — a sequential ID in a URL leaks your row count and lets anyone
47
+ walk `/invoice/1`, `/invoice/2` looking for other people's data. The public column removes that
48
+ without giving up the storage and join win.
49
+
36
50
  **GOOD**
37
51
  ```sql
38
52
  CREATE TABLE users (
39
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
53
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
54
+ public_id UUID NOT NULL DEFAULT gen_random_uuid(),
40
55
  email TEXT NOT NULL,
41
56
  display_name TEXT NOT NULL,
42
57
  role user_role NOT NULL DEFAULT 'viewer',
43
58
  is_active BOOLEAN NOT NULL DEFAULT true,
44
59
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
45
60
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
61
+ CONSTRAINT uq_users_public_id UNIQUE (public_id),
46
62
  CONSTRAINT uq_users_email UNIQUE (email)
47
63
  );
48
64
  ```
49
65
 
66
+ **BAD**
67
+ ```sql
68
+ CREATE TABLE users (
69
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- UUID as PK: scattered index writes,
70
+ email TEXT NOT NULL -- 16-byte FKs, unreadable in logs
71
+ );
72
+
73
+ CREATE TABLE order_items (
74
+ order_id BIGINT NOT NULL, -- composite natural key as PK:
75
+ sku TEXT NOT NULL, -- no stable single-column identity
76
+ PRIMARY KEY (order_id, sku)
77
+ );
78
+ ```
79
+
80
+ **Scope** — this rule governs NEW tables. Existing tables with UUID primary keys are NOT
81
+ retrofitted: their foreign keys already point at the UUID, so adding an integer id without
82
+ repointing every referencing table leaves two competing identifier systems. Changing an existing
83
+ table's primary key is a data migration and a Destructive Action Guard item — it requires explicit
84
+ user approval, never an in-passing change.
85
+
50
86
  ---
51
87
 
52
88
  ## 3. Migrations
@@ -214,6 +250,8 @@ NEVER:
214
250
  ## PostgreSQL Verification Checklist
215
251
 
216
252
  - [ ] Naming follows conventions (snake_case, plural tables, singular columns)
253
+ - [ ] Every NEW table has a self-incrementing integer `id` primary key (identity/bigserial)
254
+ - [ ] API-exposed tables also have a UNIQUE `public_id UUID` — and expose that, not the integer id
217
255
  - [ ] Every table has id, created_at, updated_at
218
256
  - [ ] timestamptz used — not timestamp
219
257
  - [ ] All foreign keys indexed
@@ -10,7 +10,9 @@ Applies when `prisma` or `@prisma/client` is in `package.json` or `schema.prisma
10
10
  ```
11
11
  MANDATORY:
12
12
  ├── One schema.prisma file — NEVER split across multiple files (use comments to section)
13
- ├── Use @id with autoincrement() or uuid() NEVER application-generated IDs
13
+ ├── EVERY model has id Int @id @default(autoincrement()) NEVER a uuid()/cuid() primary key
14
+ ├── API-exposed models ALSO get publicId String @unique @default(uuid()) — expose publicId
15
+ │ externally, NEVER the integer id (a sequential id in a URL leaks row counts and is enumerable)
14
16
  ├── Every model has createdAt DateTime @default(now()) and updatedAt DateTime @updatedAt
15
17
  ├── Use @map and @@map for snake_case DB column/table names with camelCase Prisma models
16
18
  ├── Define explicit relation names when a model has multiple relations to the same table
@@ -21,7 +23,8 @@ MANDATORY:
21
23
  **GOOD**
22
24
  ```prisma
23
25
  model User {
24
- id String @id @default(uuid())
26
+ id Int @id @default(autoincrement())
27
+ publicId String @unique @default(uuid()) @map("public_id")
25
28
  email String @unique
26
29
  name String
27
30
  role UserRole @default(MEMBER)
@@ -45,12 +48,16 @@ enum UserRole {
45
48
  **BAD**
46
49
  ```prisma
47
50
  model User {
48
- id Int @id @default(autoincrement())
49
- role String // Should be enum
50
- // Missing createdAt, updatedAt, indexes
51
+ id String @id @default(uuid()) // UUID as PK — scattered index writes, 16-byte FKs
52
+ role String // Should be enum
53
+ // Missing publicId, createdAt, updatedAt, indexes
51
54
  }
52
55
  ```
53
56
 
57
+ **Scope** — governs NEW models. Existing models with uuid()/cuid() primary keys are NOT retrofitted:
58
+ their relations already point at the string id, so switching the PK is a data migration and a
59
+ Destructive Action Guard item requiring explicit user approval.
60
+
54
61
  ---
55
62
 
56
63
  ## 2. Client Setup
@@ -144,6 +144,43 @@ MANDATORY:
144
144
 
145
145
  ---
146
146
 
147
+ ## 6.5. Schema Design — Primary Keys
148
+
149
+ Supabase is PostgreSQL — the full PostgreSQL standards apply. The primary-key rule:
150
+
151
+ ```
152
+ MANDATORY:
153
+ ├── EVERY new table has a self-incrementing integer primary key named id
154
+ │ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
155
+ ├── API-exposed tables ALSO get public_id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE
156
+ │ PostgREST exposes every table over HTTP by default — treat tables as API-exposed
157
+ │ unless RLS blocks all client access. Return public_id to clients, never the integer id.
158
+ ├── A user_id column referencing auth.users STAYS UUID — auth.uid() returns a UUID and
159
+ │ RLS policies compare against it directly. This is NOT the table's primary key.
160
+ └── Existing tables are NOT retrofitted (see PostgreSQL standards §2 Scope)
161
+ ```
162
+
163
+ **GOOD**
164
+ ```sql
165
+ CREATE TABLE user_profiles (
166
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
167
+ public_id UUID NOT NULL DEFAULT gen_random_uuid(),
168
+ user_id UUID NOT NULL REFERENCES auth.users (id) ON DELETE CASCADE, -- UUID: RLS compares to auth.uid()
169
+ display_name TEXT NOT NULL,
170
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
171
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
172
+ CONSTRAINT uq_user_profiles_public_id UNIQUE (public_id)
173
+ );
174
+
175
+ ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
176
+
177
+ CREATE POLICY "Users read own profile"
178
+ ON user_profiles FOR SELECT
179
+ USING (auth.uid() = user_id);
180
+ ```
181
+
182
+ ---
183
+
147
184
  ## 7. Migrations
148
185
 
149
186
  ```
@@ -176,6 +213,9 @@ NEVER:
176
213
 
177
214
  ## Supabase Verification Checklist
178
215
 
216
+ - [ ] Every NEW table has a self-incrementing integer `id` primary key
217
+ - [ ] API-exposed tables have a UNIQUE `public_id UUID` — clients get public_id, not the integer id
218
+ - [ ] `user_id` columns referencing auth.users are UUID (RLS compares to auth.uid())
179
219
  - [ ] RLS enabled on every table with explicit policies
180
220
  - [ ] auth.uid() used in policies — no client-sent user IDs trusted
181
221
  - [ ] Service role key only used server-side
@@ -113,6 +113,31 @@ enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' }
113
113
  if (status === 'pndng') { /* ... */ }
114
114
  ```
115
115
 
116
+ **MANDATORY — a fixed set of values lives in ONE shared declaration, and every comparison
117
+ references it.** Declaring the type is not enough: if the literal `'invoiced'` is typed in two
118
+ files, nothing connects them, and a casing or spelling mismatch fails silently at runtime.
119
+
120
+ ```ts
121
+ // BAD — the same value written as a loose literal in two places.
122
+ // TILE_FILTERS maps the tile to 'invoiced'; this file compares against 'Invoiced'.
123
+ // Different strings. Never matches. No error, no warning — the button just never appears.
124
+ onAddOrder={view === 'orders' && filter !== 'Invoiced' ? handleAdd : undefined}
125
+
126
+ // GOOD — one source of truth, referenced everywhere.
127
+ export const ORDER_FILTER = {
128
+ ORDERS: 'orders',
129
+ INVOICED: 'invoiced',
130
+ } as const;
131
+ export type OrderFilter = typeof ORDER_FILTER[keyof typeof ORDER_FILTER];
132
+
133
+ onAddOrder={view === 'orders' && filter !== ORDER_FILTER.INVOICED ? handleAdd : undefined}
134
+ // A typo is now a compile error, not a silent mismatch a user finds later.
135
+ ```
136
+
137
+ This is the structural fix for the casing-mismatch bug class. Case-insensitive comparison
138
+ (`_comparison.md`) is the safety net that catches the same bug where a shared constant doesn't
139
+ exist or the value crosses a system boundary — use both.
140
+
116
141
  ---
117
142
 
118
143
  ## 7. Import Ordering