sanook-cli 0.4.0 → 0.5.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.
- package/.env.example +19 -0
- package/CHANGELOG.md +144 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +394 -51
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +2 -2
- package/dist/providers/keys.js +3 -2
- package/dist/providers/registry.js +133 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +218 -27
- package/dist/ui/banner.js +4 -9
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/setup.js +6 -5
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- package/skills/visual-regression-testing/SKILL.md +120 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ship-mobile-app-store-release
|
|
3
|
+
description: Prepares and ships iOS App Store and Google Play releases — code signing (certs/provisioning, upload vs app-signing keystores), marketing-version/build-number bumps, Fastlane/EAS/Gradle build lanes, TestFlight/Play-track uploads, phased rollout, store metadata, and review-rejection remediation.
|
|
4
|
+
when_to_use: Cutting a mobile release — fixing signing or keystores, automating builds with Fastlane/EAS/Gradle, uploading to TestFlight or Play tracks, staged/phased rollout, store listing/metadata, or fixing an App Store/Play review rejection. Distinct from deploy-release (server/web deploys), release-notes (changelog prose), and cicd-pipeline-author (the CI workflow that calls these lanes).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when the artifact is a **store binary** (`.ipa`/`.aab`) headed for Apple or Google review, not a server rollout:
|
|
10
|
+
|
|
11
|
+
- "Set up code signing / fix `No profiles for 'com.x' were found` / rotate an expired cert"
|
|
12
|
+
- "Generate or recover an Android keystore; enroll in Play App Signing; upload key vs app-signing key confusion"
|
|
13
|
+
- "Bump the version + build number and ship to TestFlight / Play internal track"
|
|
14
|
+
- "Wire up Fastlane (`gym`/`pilot`/`supply`) or EAS (`eas build`/`eas submit`) in CI"
|
|
15
|
+
- "Do a phased / staged rollout at 1% → 100%, then halt it"
|
|
16
|
+
- "Upload screenshots, description, what's-new, age rating"
|
|
17
|
+
- "Our build got rejected for Guideline 4.3 / 5.1.1 / IAP — fix and resubmit"
|
|
18
|
+
|
|
19
|
+
NOT this skill:
|
|
20
|
+
- Deploying a server, web app, or container to prod (blue-green, canary pods) → deploy-release
|
|
21
|
+
- Writing the human-facing changelog / what's-new *prose* → release-notes (this skill only *places* the text)
|
|
22
|
+
- Authoring the CI YAML that runs these lanes (job graph, caching, secrets injection) → cicd-pipeline-author
|
|
23
|
+
- Storing the keystore password / App Store Connect API key safely → secrets-management
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **Set versioning first — two independent numbers, never reused.** Marketing version (user-visible) vs build number (uniqueness). Bump the build number on *every* upload even within the same marketing version; stores reject duplicates.
|
|
28
|
+
|
|
29
|
+
| Field | iOS (`Info.plist`) | Android (`build.gradle`) | Rule |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| Marketing version | `CFBundleShortVersionString` | `versionName` | SemVer `1.4.0`, free-form |
|
|
32
|
+
| Build number | `CFBundleVersion` | `versionCode` | **Must strictly increase per upload.** iOS: any string with increasing numerics. Android: a single **integer**, monotonic, max 2.1B |
|
|
33
|
+
|
|
34
|
+
Default: derive build number from CI run number or commit count (`git rev-list --count HEAD`) so it auto-increments and is reproducible. Use Fastlane `increment_build_number` / `increment_version_code` — never hand-edit and forget.
|
|
35
|
+
|
|
36
|
+
2. **iOS signing — prefer Fastlane `match`, not Xcode "Automatically manage signing", for CI.** You need a **Distribution certificate** (`.p12`) + an **App Store provisioning profile** bound to the explicit App ID and that cert.
|
|
37
|
+
|
|
38
|
+
- `match` stores certs/profiles encrypted in a private git repo (or S3/GCS), so every machine/CI agent shares one cert instead of each minting a new one (Apple caps you at 2–3 distribution certs).
|
|
39
|
+
- Authenticate CI with an **App Store Connect API key** (`.p8` + key id + issuer id) via `app_store_connect_api_key` — not your Apple ID password / 2FA, which breaks unattended.
|
|
40
|
+
- Match entitlements to enabled capabilities: Push → `aps-environment: production` in the **release** entitlements; App Groups, Sign in with Apple, Associated Domains must each be toggled on the App ID *and* present in the profile, or the build is signed but the feature silently fails.
|
|
41
|
+
```bash
|
|
42
|
+
# one-time, populates the encrypted repo
|
|
43
|
+
bundle exec fastlane match appstore
|
|
44
|
+
# CI: fetch read-only, never regenerate on agents
|
|
45
|
+
bundle exec fastlane match appstore --readonly
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. **Android signing — separate the upload key from the app-signing key.** Enroll in **Play App Signing**: Google holds the *app-signing key* and re-signs your `.aab`; you sign uploads with an *upload key* you control. Losing the upload key is recoverable (reset via support); losing the app-signing key without Play App Signing is fatal — you can never update the app.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
keytool -genkeypair -v -keystore upload.keystore -alias upload \
|
|
52
|
+
-keyalg RSA -keysize 2048 -validity 9125 -storetype PKCS12 # 25-yr validity
|
|
53
|
+
```
|
|
54
|
+
- Ship **`.aab`** (App Bundle), not `.apk` — Play requires it for new apps and generates per-device APKs.
|
|
55
|
+
- Keystore password, key password, alias → secrets store / env, never committed (see secrets-management). Gradle reads them from `~/.gradle/gradle.properties` or env, not `build.gradle`.
|
|
56
|
+
|
|
57
|
+
4. **Automate the build + upload with one lane per store.** Decide the toolchain up front:
|
|
58
|
+
|
|
59
|
+
| Stack | Build | Upload | Use when |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| Native iOS | `gym` (`build_app`) → `.ipa` | `pilot` (`upload_to_testflight`) / `deliver` (`upload_to_app_store`) | Bare Xcode project, full control |
|
|
62
|
+
| Native Android | `gradle bundleRelease` → `.aab` | `supply` (`upload_to_play_store`) | Bare Gradle project |
|
|
63
|
+
| Expo / RN managed | `eas build -p ios\|android` | `eas submit -p ios\|android` | Expo-managed; EAS handles signing |
|
|
64
|
+
|
|
65
|
+
Default to **Fastlane** for bare native and **EAS** for Expo-managed. Minimal Fastfile lanes:
|
|
66
|
+
```ruby
|
|
67
|
+
lane :beta do # iOS → TestFlight
|
|
68
|
+
match(type: "appstore", readonly: true)
|
|
69
|
+
increment_build_number(xcodeproj: "App.xcodeproj")
|
|
70
|
+
build_app(scheme: "App", export_method: "app-store")
|
|
71
|
+
upload_to_testflight(skip_waiting_for_build_processing: true)
|
|
72
|
+
end
|
|
73
|
+
lane :play_internal do # Android → internal track
|
|
74
|
+
gradle(task: "bundle", build_type: "Release")
|
|
75
|
+
upload_to_play_store(track: "internal", aab: "app/build/outputs/bundle/release/app-release.aab")
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
For `supply` you need a **Play Developer API service-account JSON** (`json_key`) with Release Manager permission. For EAS submit, store the same Apple/Google creds in EAS secrets.
|
|
79
|
+
|
|
80
|
+
5. **Distribute through the right track, then promote — don't ship straight to production.**
|
|
81
|
+
|
|
82
|
+
| Store | Tracks (narrow → wide) | Notes |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| TestFlight | Internal (≤100, no review) → External (review, up to 10k via groups/public link) | Internal testers see builds in minutes; external needs Beta App Review |
|
|
85
|
+
| Play | `internal` → `closed` (alpha/beta) → `open` (beta) → `production` | Promote the *same* build between tracks; don't rebuild |
|
|
86
|
+
|
|
87
|
+
Default flow: upload to TestFlight Internal / Play `internal` first, smoke-test, then promote. Gate external/production behind a manual approval.
|
|
88
|
+
|
|
89
|
+
6. **Roll out in stages with a percentage, never 100% on day one.**
|
|
90
|
+
- **Play:** set `rollout: 0.01` (1%) on the `production` track via `upload_to_play_store(rollout: "0.01")`, then bump `0.01 → 0.05 → 0.2 → 0.5 → 1.0` over days, watching crash-free rate between steps.
|
|
91
|
+
- **iOS:** App Store "Phased Release for Automatic Updates" ramps over 7 days automatically (≈1/2/5/10/20/50/100%). Enable it in App Store Connect or via `deliver`'s phased-release flag; it only covers auto-updaters, manual updaters get it immediately.
|
|
92
|
+
|
|
93
|
+
7. **Fill store metadata so review doesn't bounce on format.** Required: localized title/description, **what's-new** for this version, keywords (iOS), category, **age/content rating** questionnaire, privacy nutrition label (iOS `App Privacy`) / Play **Data safety** form, and **screenshots at every required device size** (e.g. iOS 6.7" + 6.5"; missing a required size blocks submission). Automate text/screenshot upload with `deliver`/`supply` (`metadata/`, `screenshots/` dirs) so it's version-controlled, not hand-pasted.
|
|
94
|
+
|
|
95
|
+
8. **Pre-flight against the top rejection reasons before you submit.** Address these in the binary/listing, not after a 1-week review round-trip:
|
|
96
|
+
|
|
97
|
+
| Reason | Apple guideline | Fix |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| Crash / broken on review device | 2.1 | Test on a clean device + the OS in review; provide working demo creds |
|
|
100
|
+
| Hidden/incomplete features ("placeholder") | 2.1 | No dead buttons, no "coming soon"; ship only finished flows |
|
|
101
|
+
| Spam / thin / web-wrapper | 4.3 / 4.2 | Native value beyond a website wrapper |
|
|
102
|
+
| Login wall with no demo account | 5.1.1 / 2.1 | Put **demo username+password** in Review Notes |
|
|
103
|
+
| Sign in with Apple missing | 4.8 | Required if you offer 3rd-party social login |
|
|
104
|
+
| Buying digital goods outside IAP | 3.1.1 | Digital content must use StoreKit IAP, not external payment |
|
|
105
|
+
| Privacy label mismatch | 5.1 | Declared data collection must match actual SDK behavior |
|
|
106
|
+
|
|
107
|
+
Play parallels: target the **current required API level** (`targetSdkVersion`), complete Data safety honestly, declare sensitive permissions, no deceptive metadata. Put credentials and any special-access steps in the review notes field.
|
|
108
|
+
|
|
109
|
+
9. **After release, watch crash-free rate and keep the halt path one command away.** Monitor crash-free *sessions* (target **≥ 99.5%**) and ANR rate in Crashlytics / Play vitals during ramp. If it regresses below threshold: **Play** → halt rollout in Console or `upload_to_play_store(rollout:)` won't un-ship, so use **"Halt rollout"** (stops further % but doesn't pull installed users) then ship a fixed build. **iOS** → "Pause Phased Release" in App Store Connect; to actually pull a broken build you must **expedite a new build through review** (Apple has no instant rollback). Plan a hotfix lane, not a rollback button.
|
|
110
|
+
|
|
111
|
+
## Common Errors
|
|
112
|
+
|
|
113
|
+
- **Reusing a build number.** `ERROR ITMS-90186 / "Version already exists"` (iOS) or `Version code N has already been used` (Play). Always increment per upload, even for the same marketing version.
|
|
114
|
+
- **Each CI agent minting its own distribution cert.** Hits Apple's 2–3 cert cap, then nothing can sign. Use `match --readonly` so agents fetch one shared cert.
|
|
115
|
+
- **Apple ID + password (not API key) in CI.** Breaks on 2FA prompts. Use an App Store Connect API key (`.p8`).
|
|
116
|
+
- **Committing the keystore or its passwords.** Anyone with the repo can sign as you. Keystore + passwords go to a secrets store; `.gradle` properties out of VCS.
|
|
117
|
+
- **Treating the upload key as the app-signing key.** With Play App Signing, Google re-signs; the key in your keystore is only the *upload* key. Don't panic-rotate the wrong one — resetting the upload key is a support flow, not a config change.
|
|
118
|
+
- **Shipping `.apk` to a new Play app.** Rejected — new apps require `.aab`. Use `bundleRelease`, not `assembleRelease`.
|
|
119
|
+
- **Entitlement enabled in Xcode but not on the App ID / profile.** Signs fine, feature dead at runtime (push silently drops, App Group reads empty). Toggle the capability on the App ID and regenerate the profile.
|
|
120
|
+
- **`aps-environment: development` in a store build.** Push notifications work in debug, fail in production. Release entitlements must use `production`.
|
|
121
|
+
- **Missing a required screenshot size.** Submission blocked with no obvious cause. Supply every required device dimension for the platform.
|
|
122
|
+
- **100% rollout on day one.** A crash hits every user at once with no staged escape. Start at 1% and ramp.
|
|
123
|
+
- **Expecting an instant rollback.** Neither store has one. iOS needs an expedited re-review; Play halt only stops *new* installs. Always keep a hotfix lane ready.
|
|
124
|
+
- **Login-walled app with no demo credentials.** Auto-reject under 5.1.1/2.1. Reviewer can't get past the login. Put working creds in Review Notes.
|
|
125
|
+
|
|
126
|
+
## Verify
|
|
127
|
+
|
|
128
|
+
1. **Versioning:** the build number is strictly greater than the last accepted upload (check App Store Connect / Play Console history); marketing version is correct.
|
|
129
|
+
2. **iOS signing:** `codesign -dvv <App>.app` shows the **Distribution** cert and the explicit App ID; the embedded profile is the App Store profile and not expired (`security cms -D -i embedded.mobileprovision`).
|
|
130
|
+
3. **Android signing:** `jarsigner -verify -verbose app-release.aab` (or `apksigner verify` on a built APK) passes with the **upload** key; the app is enrolled in Play App Signing.
|
|
131
|
+
4. **Entitlements:** `codesign -d --entitlements - <App>.app` lists every capability the app uses, with `aps-environment: production` for a store build.
|
|
132
|
+
5. **Upload landed:** the build appears in TestFlight Internal / the target Play track and finishes processing without an email rejection (ITMS-* / Play pre-launch report clean).
|
|
133
|
+
6. **Metadata complete:** what's-new, all required screenshot sizes, age/content rating, and the privacy/Data-safety form are filled — the submit button is enabled with no blocking warnings.
|
|
134
|
+
7. **Rollout staged:** production is at the intended start percentage (e.g. 1%), not 100%, and the phased/staged toggle is on.
|
|
135
|
+
8. **Post-release watch:** crash-free sessions and ANR are visible on a dashboard, and you've confirmed the halt/pause control works (locate it, don't trigger it).
|
|
136
|
+
|
|
137
|
+
Done = a binary with a unique, increasing build number is correctly signed (distribution cert + production entitlements / upload key + Play App Signing), uploaded to the intended track with complete metadata, rolling out at a staged percentage, and the crash-free monitor + halt path are both confirmed available.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: structured-output-llm
|
|
3
|
+
description: Gets machine-parseable JSON out of an LLM reliably — prefer provider-enforced grammar (OpenAI `response_format:{type:"json_schema",strict:true}`, Anthropic tool-calling / `tool_choice:{type:"tool"}`, Gemini `responseSchema`) over a "respond in JSON" prompt, define the contract once as a Pydantic/Zod model and validate every response (never trust the string), use constrained decoding / grammars for open weights (Outlines, llguidance, llama.cpp GBNF, vLLM `guided_json`), keep schemas SHALLOW with required fields + enums + `additionalProperties:false`, and build a repair-and-retry loop that handles refusals, `length`/`max_tokens` truncation, and streaming partial JSON (`partial-json-parser`). Schema-guided generation eliminates parse-error retries; validate-and-repair is the backstop, not the primary mechanism.
|
|
4
|
+
when_to_use: You need a model to emit JSON/objects a program consumes — extraction, classification into fixed labels, function arguments, form-filling, or feeding output into another service — and string-parsing or regex on free text keeps breaking. Distinct from prompt-engineering (crafts the instruction/few-shot prose; this skill enforces the output SHAPE via grammar+validation) and agent-tool-mcp-builder (defines the tools/MCP an agent decides to call; this skill is the one-shot "make THIS call return a typed object" half, even when implemented via a single forced tool).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when a program — not a human — consumes the model's output and the shape must be guaranteed:
|
|
10
|
+
|
|
11
|
+
- "Extract these 8 fields from this invoice/email as JSON I can `JSON.parse`"
|
|
12
|
+
- "Classify into one of {refund, dispute, question} — give me just the label"
|
|
13
|
+
- "The model returns ```json fences / prose / trailing commas and my parse breaks ~3% of the time"
|
|
14
|
+
- "Function arguments come back malformed or with hallucinated keys"
|
|
15
|
+
- "Fill this form schema" / "map this text to my DB row"
|
|
16
|
+
- "Open-weights model (Llama/Mistral/Qwen) won't reliably produce valid JSON"
|
|
17
|
+
- "Long extraction gets cut off mid-object" or "streaming JSON is unparseable until complete"
|
|
18
|
+
|
|
19
|
+
NOT this skill:
|
|
20
|
+
- Writing the *instruction*, few-shot examples, role/system prompt, or chain-of-thought prose → prompt-engineering (it shapes WHAT to say; this skill enforces the machine SHAPE of the reply)
|
|
21
|
+
- Defining tools/an MCP server an agent autonomously selects among across many turns → agent-tool-mcp-builder (this skill is one forced, schema-constrained call returning a typed object)
|
|
22
|
+
- Reliability of the whole LLM app — timeouts, fallbacks, circuit-breakers, idempotency around the call → harden-llm-app-reliability (this skill owns only the output-shape contract)
|
|
23
|
+
- Token/latency/cost tuning, caching, model selection → optimize-llm-cost-latency (note: strict schemas cost a cache miss on first use — that skill weighs it)
|
|
24
|
+
- Stopping a malicious payload in retrieved/user text from hijacking the model → defend-llm-prompt-injection (orthogonal; a valid-schema output can still be adversarial content)
|
|
25
|
+
- Retrieval/chunking/grounding that feeds the prompt → rag-pipeline (this skill structures rag's *output* step)
|
|
26
|
+
- Scoring whether the extracted values are *correct* across a dataset → llm-eval-harness (this skill guarantees parseable; that one measures accuracy)
|
|
27
|
+
- Validating ordinary user-submitted form data (no LLM) → build-form-validation
|
|
28
|
+
- The wire/serialization contract between your own services → rest-graphql-contract
|
|
29
|
+
- Scraping the HTML/DOM the text came from → scrape-structured-web-data
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
1. **Prefer provider-enforced schema over a "respond in JSON" prompt — it's a different mechanism, not a stronger hint.** A prompt is a request the model may ignore (fences, preamble, extra keys); enforced schema masks the decoder's token logits so only schema-valid tokens are emittable — invalid JSON becomes *impossible*, not just unlikely. Use the strongest enforcement the provider offers:
|
|
34
|
+
|
|
35
|
+
| Provider | Enforcement | How |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| **OpenAI** (gpt-4o+, gpt-4.1, o-series) | Structured Outputs, 100% guaranteed | `response_format:{type:"json_schema",json_schema:{name,schema,strict:true}}` — or `strict:true` on a function tool |
|
|
38
|
+
| **Anthropic Claude** | Tool-calling (no native json_schema) | define one tool with `input_schema`, set `tool_choice:{type:"tool",name:"..."}` to force it; read `tool_use.input` |
|
|
39
|
+
| **Google Gemini** | Controlled generation | `generationConfig:{responseMimeType:"application/json",responseSchema:{...}}` (also supports `responseSchema` enums) |
|
|
40
|
+
| **Azure OpenAI** | same as OpenAI | `response_format` json_schema on `2024-08-01-preview`+ |
|
|
41
|
+
| **Open weights** (vLLM/TGI/llama.cpp/Ollama) | Constrained decoding (step 5) | `guided_json` / GBNF grammar / Outlines |
|
|
42
|
+
|
|
43
|
+
`json_object` mode (the older `{type:"json_object"}`) only guarantees *syntactically* valid JSON, NOT your schema — treat it as a weak fallback, never the goal.
|
|
44
|
+
|
|
45
|
+
2. **Define the contract ONCE as a typed model; let the SDK emit the schema and parse the result.** Don't hand-write JSON Schema and a parser separately — they drift. Use the model class as the single source of truth:
|
|
46
|
+
- **Python** → Pydantic v2. OpenAI: `client.beta.chat.completions.parse(..., response_format=MyModel)` → `.choices[0].message.parsed` is a typed instance (or `.refusal`). Or `MyModel.model_validate_json(raw)`.
|
|
47
|
+
- **TS/JS** → Zod + the OpenAI helper `zodResponseFormat(MySchema, "name")`, then `completion.choices[0].message.parsed`; or `MySchema.parse(JSON.parse(raw))`. (Instructor / `instructor-js` wrap this with retries.)
|
|
48
|
+
- **Anthropic** → derive `input_schema` from Pydantic via `Model.model_json_schema()` and pass as the tool's input_schema; validate `tool_use.input` back through the model.
|
|
49
|
+
The rule: **validate every response through the typed model even when the provider guarantees the schema** — guarantees cover JSON-schema-expressible constraints, not your business invariants (date ranges, cross-field rules, enum-of-enums), and self-hosted/fallback paths have no guarantee at all.
|
|
50
|
+
|
|
51
|
+
3. **Keep the schema SHALLOW and tight — depth and looseness are where models fail and where strict mode rejects you.** Constraints that materially improve reliability:
|
|
52
|
+
- **`required` everything + `additionalProperties:false`.** OpenAI strict mode *requires* every property be in `required` and forbids additional props — model "optional" as `required` + nullable union (`{"type":["string","null"]}`). This also blocks hallucinated keys.
|
|
53
|
+
- **Enums over free strings for categories** (`"type":"string","enum":["refund","dispute","question"]`) — turns classification into a closed set the decoder can't escape.
|
|
54
|
+
- **Flatten.** Prefer a flat object or a top-level `{"items":[...]}` array wrapper over 4-level nesting. Deep/recursive schemas raise latency, hit provider depth/property caps (OpenAI: ≤5000 props, ≤5 nesting for strict), and degrade accuracy.
|
|
55
|
+
- **Avoid where unsupported:** OpenAI strict mode disallows `minLength`/`maximum`/`pattern`/`format` and many keywords — enforce those in step 2's validator, not the wire schema. (Anthropic/Gemini tolerate more but don't *guarantee* them.)
|
|
56
|
+
- **Order fields so reasoning precedes the answer:** put a `reasoning`/`evidence` string field *before* the `answer`/`label` field — the model generates them in order, so it "thinks" before committing (cheap, schema-native CoT).
|
|
57
|
+
|
|
58
|
+
4. **Don't ask for JSON and reasoning in the same free-text turn.** If you need chain-of-thought, either (a) put it inside the schema as a leading field (step 3), or (b) do a two-call split: one reasoning call (free text), one extraction call (strict schema) over the reasoning. Mixing "think step by step, then output JSON" in one un-enforced response is the classic source of prose-before-the-brace parse failures.
|
|
59
|
+
|
|
60
|
+
5. **For open-weights / self-hosted, use constrained decoding (grammar-level), not prompting.** These enforce validity at the token sampler:
|
|
61
|
+
|
|
62
|
+
| Tool | Use | Invoke |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **vLLM** | production serving | `guided_json=<schema>` / `guided_choice=[...]` / `guided_regex` in `extra_body` (backed by xgrammar/outlines) |
|
|
65
|
+
| **Outlines** | library, any HF model | `outlines.generate.json(model, PydanticModel)` → returns typed object |
|
|
66
|
+
| **llguidance / xgrammar** | fast grammar engines | embedded in vLLM/TGI; sub-ms per-token masking |
|
|
67
|
+
| **llama.cpp / Ollama** | local GGUF | GBNF grammar file, or `format:"json"`+schema (Ollama) |
|
|
68
|
+
| **TGI** | HF Inference | `grammar:{type:"json",value:<schema>}` |
|
|
69
|
+
|
|
70
|
+
Constrained decoding makes invalid JSON *structurally impossible*. Caveat: an over-tight grammar can force the model down an unnatural path and *lower content* quality (it'll fill a required field with junk rather than refuse) — keep the grammar permissive on values, strict on structure, and still validate semantics in step 2.
|
|
71
|
+
|
|
72
|
+
6. **Build repair-and-retry as the BACKSTOP for paths with no hard guarantee.** Even guaranteed providers can return refusals or truncation; self-hosted/`json_object` paths can emit broken JSON. Ladder:
|
|
73
|
+
1. **Salvage first (no LLM call):** strip ```` ```json ```` fences and any pre/post prose; extract the outermost balanced `{...}`/`[...]`; libraries: `json-repair` (py), `jsonrepair` (js) fix trailing commas/single quotes/unescaped newlines. Cheap, deterministic — try before re-prompting.
|
|
74
|
+
2. **Re-prompt with the error:** on `ValidationError`, send the broken output + the exact validator message back ("Your previous reply failed validation: `<pydantic error>`. Return ONLY valid JSON matching the schema.") — Instructor's `max_retries` does exactly this. Cap at **2–3 attempts**; a 4th rarely converges.
|
|
75
|
+
3. **Feed the schema again** in the repair turn; models drift from it across long contexts.
|
|
76
|
+
|
|
77
|
+
7. **Handle refusals, truncation, and length explicitly — they are NOT parse errors and must not be "repaired" into garbage.**
|
|
78
|
+
- **Refusal:** OpenAI returns `message.refusal` (non-null) instead of `parsed`; Anthropic may emit a text block, not the forced tool. Branch on it — surface to the user/safety path, don't retry-loop into the rate limit.
|
|
79
|
+
- **Truncation:** check `finish_reason == "length"` (OpenAI) / `stop_reason == "max_tokens"` (Anthropic) → the JSON is cut mid-object and is *un*-repairable by salvage. Fix the cause: raise `max_tokens`, shrink the schema/array, or paginate the extraction — don't json-repair a truncated object (it'll silently drop fields).
|
|
80
|
+
- **Empty/whitespace:** treat as failure, retry once, then fail loud.
|
|
81
|
+
|
|
82
|
+
8. **Streaming structured output — buffer or parse incrementally, never `JSON.parse` mid-stream.** A partial stream `{"name":"Al` is invalid JSON. Two correct patterns:
|
|
83
|
+
- **Simplest:** accumulate all deltas, parse once on `finish`. Use when you don't need live UI.
|
|
84
|
+
- **Incremental:** `partial-json-parser` (js) / `pydantic` partial / OpenAI's streamed-parse helpers / `jsonriver` to coerce the buffer into a valid partial object on each delta for live rendering. For tool-calls, concatenate `tool_call.function.arguments` deltas across chunks (they arrive fragmented) and parse only when the tool-call completes.
|
|
85
|
+
|
|
86
|
+
9. **Verify.** (a) Round-trip: 100+ live calls on representative inputs → 100% deserialize into the typed model with zero `JSON.parse`/`ValidationError`; assert in CI against a recorded/replayed set. (b) Schema-escape probe: feed adversarial inputs that tempt the model to add commentary or a key — confirm `additionalProperties:false` + strict mode still yields clean objects. (c) Edge branches: force a refusal (policy-tripping input) and a truncation (tiny `max_tokens`) and assert each hits its dedicated branch, not the JSON repairer. (d) Enum closure: classification only ever returns a member of the enum. (e) Open-weights: same round-trip on the self-hosted path with constrained decoding ON, then OFF — confirm the constraint is what's carrying validity. Done = the program never sees a string it can't parse into the typed model, categories are closed enums, refusals/truncation route to their own branches, repair is rare (a metric you watch, not the load-bearing path), and the typed model — not the wire schema — is the single source of the contract.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supply-chain-sbom-provenance
|
|
3
|
+
description: Hardens the software supply chain by generating/validating an SBOM (CycloneDX/SPDX via syft/cdxgen), signing artifacts keylessly (cosign + OIDC), emitting SLSA/in-toto build provenance, pinning deps and base images to digests, and enforcing signature/attestation policy at consumption.
|
|
4
|
+
when_to_use: User must prove what's in an artifact and that it was built from trusted source — producing/consuming an SBOM, signing containers/releases, adding provenance, raising SLSA level, or hardening CI against poisoned deps (EO 14028, EU CRA). Distinct from dependency-upgrade (bumping versions), secrets-management (handling credentials), and security-review (auditing source code).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when the request is about **proving artifact integrity and origin**, not about the code's behavior:
|
|
10
|
+
|
|
11
|
+
- "Generate/attach an SBOM to our releases" or "a customer wants a CycloneDX/SPDX SBOM"
|
|
12
|
+
- "Sign our container images / release binaries" (cosign, keyless OIDC)
|
|
13
|
+
- "Add build provenance / attestations" or "get us to SLSA Level 3"
|
|
14
|
+
- "Pin base images to digests, not `:latest`" / "we got hit by a typosquat / dependency-confusion package"
|
|
15
|
+
- "Reject unsigned or unattested images at deploy/admission time"
|
|
16
|
+
- "Keep scanning what we already shipped for new CVEs against the SBOM"
|
|
17
|
+
|
|
18
|
+
NOT this skill:
|
|
19
|
+
- Bumping a dependency to a newer version / resolving a lockfile → dependency-upgrade
|
|
20
|
+
- Storing/rotating the signing key or registry token itself → secrets-management (this skill prefers **keyless**, so there's no key to store)
|
|
21
|
+
- Auditing the source for vulnerabilities/logic bugs → security-review (this proves *where the artifact came from*, not whether the code is safe)
|
|
22
|
+
- Optimizing the Dockerfile layers/size → dockerfile-optimize
|
|
23
|
+
- Writing the deploy/CI YAML in general → cicd-pipeline-author / gitops-deploy-workflow / deploy-release
|
|
24
|
+
- Enforcing the policy specifically inside Kubernetes manifests → k8s-manifest-review
|
|
25
|
+
|
|
26
|
+
## Steps
|
|
27
|
+
|
|
28
|
+
1. **Inventory inputs, then pick format + tool by ecosystem — do not hand-write an SBOM.** An SBOM must cover **direct + transitive** deps, base image layers, and build tools. Choose:
|
|
29
|
+
|
|
30
|
+
| Need | Format | Generator | Why |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| Container/filesystem, broadest ecosystem coverage | **CycloneDX** (`--output cyclonedx-json`) | **syft** | Best default; reads installed packages from the built image, not just manifests |
|
|
33
|
+
| Same, when consumer mandates SPDX (US federal / NTIA) | **SPDX** (`--output spdx-json`) | **syft** | One flag swap; emit both if asked |
|
|
34
|
+
| Source-tree / app deps with rich pURLs + license + VEX | CycloneDX | **cdxgen** | Deeper per-language resolution (npm/maven/gradle/go/pip) |
|
|
35
|
+
|
|
36
|
+
Default to **CycloneDX JSON from syft, run against the final built image** (`syft <image>@sha256:... -o cyclonedx-json=sbom.cdx.json`). Generating from source misses what the base image actually ships.
|
|
37
|
+
|
|
38
|
+
2. **Generate the SBOM in CI as a build step, attach to the release, and validate completeness.** Pin the digest you scanned. A valid SBOM has `bom-ref`/pURL for every component, declared versions, and a license field. Validate, don't trust:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
syft "$IMAGE@$DIGEST" -o cyclonedx-json=sbom.cdx.json
|
|
42
|
+
cyclonedx-cli validate --input-file sbom.cdx.json --fail-on-errors # schema valid
|
|
43
|
+
jq -e '[.components[] | select(.version==null or .version=="")] | length == 0' sbom.cdx.json # no unversioned comps
|
|
44
|
+
```
|
|
45
|
+
Reject the build if validation fails. An SBOM with missing versions/hashes is worse than none — it lies.
|
|
46
|
+
|
|
47
|
+
3. **Pin and verify every input to a hash, never a moving tag.** This is the actual tamper defense; the SBOM only *describes* it.
|
|
48
|
+
- **Base images:** `FROM node:20.11-bookworm@sha256:<digest>` — never `:latest`, never a bare tag. A tag can be repointed under you.
|
|
49
|
+
- **Deps:** require a lockfile with **integrity hashes** and install in frozen mode: `npm ci` (uses `package-lock.json` `integrity`), `pip install --require-hashes -r requirements.txt`, `go mod verify` + committed `go.sum`, `cargo --locked`. A lockfile without hashes (or `npm install`, which mutates it) is not pinning.
|
|
50
|
+
- **Defend dependency-confusion / typosquatting:** set a **scoped private registry** (`@yourscope:registry=...`) so internal names never resolve to public; **reserve your namespace** on the public registry; pin `registry`/`@scope` in `.npmrc`/`pip.conf`; maintain an **allowlist** and fail CI on any new top-level dep not on it. Confusion attacks beat any signature because you signed the wrong package.
|
|
51
|
+
|
|
52
|
+
4. **Sign artifacts and the SBOM keylessly with cosign via the CI OIDC identity.** No long-lived key to leak or rotate — the signature is bound to the workflow identity and logged in the Rekor transparency log.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# cosign 2.x: keyless is the default — no COSIGN_EXPERIMENTAL flag needed
|
|
56
|
+
cosign sign --yes "$IMAGE@$DIGEST" # keyless, OIDC → Fulcio cert → Rekor
|
|
57
|
+
cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "$IMAGE@$DIGEST" # SBOM as an attestation
|
|
58
|
+
```
|
|
59
|
+
Sign the **digest**, not the tag. Pushing a tag after signing leaves the signature pointing at a digest a re-tag can bypass.
|
|
60
|
+
|
|
61
|
+
5. **Emit SLSA build provenance (in-toto attestation) linking artifact → source commit → builder, then harden to raise the level.** Provenance answers "which commit, which builder, what inputs." On GitHub the cheapest path is the official generator/`actions/attest-build-provenance`; verify the chain with cosign:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cosign verify-attestation --type slsaprovenance \
|
|
65
|
+
--certificate-identity-regexp '^https://github.com/<org>/.+/.github/workflows/.+@refs/' \
|
|
66
|
+
--certificate-oidc-issuer https://token.actions.githubusercontent.com "$IMAGE@$DIGEST"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| SLSA Build level | Requirement | How to reach it |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| L1 | Provenance exists | Generate + attach any provenance |
|
|
72
|
+
| L2 | Signed provenance, hosted build | Keyless cosign + CI-hosted runner (above) |
|
|
73
|
+
| **L3** | Non-falsifiable, isolated build | Use a trusted builder that isolates the run from the steps it builds (reusable trusted workflow); no secrets exposed to user build steps |
|
|
74
|
+
|
|
75
|
+
Target **L3** for anything customer-facing; L2 is the floor.
|
|
76
|
+
|
|
77
|
+
6. **Enforce on consumption — reject unsigned / unattested artifacts at the gate.** Signing nothing-checks is theater. Put a verifying admission/policy controller (e.g. a Sigstore policy controller or `cosign verify` gate in the deploy job) that **denies** images lacking a valid signature *and* required attestations from the *expected* identity:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# policy intent: only images signed by OUR workflow, with an SBOM attestation, may deploy
|
|
81
|
+
require:
|
|
82
|
+
signature:
|
|
83
|
+
issuer: https://token.actions.githubusercontent.com
|
|
84
|
+
subjectRegExp: ^https://github.com/<org>/<repo>/.github/workflows/release.yml@refs/tags/.+$
|
|
85
|
+
attestations: [cyclonedx, slsaprovenance]
|
|
86
|
+
```
|
|
87
|
+
Pin the **identity**, not just "is it signed" — anyone can sign with keyless. Fail closed.
|
|
88
|
+
|
|
89
|
+
7. **Continuously scan the shipped SBOM for new CVEs.** Vulns are disclosed after you ship. Re-scan the stored SBOM on a schedule (not just at build) so a component clean yesterday flags today:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
osv-scanner scan --sbom sbom.cdx.json --fail-on-vuln # or: grype sbom:sbom.cdx.json --fail-on high
|
|
93
|
+
```
|
|
94
|
+
Use a VEX document to suppress not-exploitable findings deliberately — never by lowering the threshold.
|
|
95
|
+
|
|
96
|
+
## Common Errors
|
|
97
|
+
|
|
98
|
+
- **Pinning to a tag, not a digest.** `FROM python:3.12` / `cosign sign $IMAGE:latest` — a tag is mutable and can be repointed after you scan/sign. Always `@sha256:<digest>`.
|
|
99
|
+
- **SBOM generated from source, attached to a binary built elsewhere.** It won't list the base-image OS packages that actually ship in the artifact. Scan the **built image at its digest**.
|
|
100
|
+
- **`npm install` / unfrozen install in CI.** Mutates the lockfile and can pull an unpinned version, voiding the hashes. Use `npm ci` / `--require-hashes` / `--locked` / `--frozen-lockfile`.
|
|
101
|
+
- **Signing without verifying identity on the consumer side.** `cosign verify` with no `--certificate-identity*` accepts a signature from *anyone* keyless. Always pin issuer + subject regexp.
|
|
102
|
+
- **Treating the SBOM as the tamper control.** The SBOM only *describes*; integrity comes from **digest pins + hashes + signatures**. An accurate SBOM of a poisoned artifact is still poisoned.
|
|
103
|
+
- **Lockfile without integrity hashes.** `go.sum` missing, `requirements.txt` without `--hash=`, a `package-lock.json` from `lockfileVersion:1`. Versions alone don't detect content swaps; require hashes.
|
|
104
|
+
- **Internal package name resolvable on the public registry.** Classic dependency confusion — the public one wins by higher version. Scope it, reserve the name, and allowlist.
|
|
105
|
+
- **Provenance that doesn't bind to a commit/builder.** Provenance with no `materials`/`buildDefinition` source ref proves nothing. It must name the exact commit SHA and builder.
|
|
106
|
+
- **Policy in "audit"/warn mode forever.** A controller that logs violations but admits anyway is not enforcement. Flip to **deny / fail-closed** once green.
|
|
107
|
+
- **Suppressing scanner findings by raising the severity threshold.** Hides real CVEs. Suppress specific not-exploitable CVEs via VEX with a reason, leave the threshold strict.
|
|
108
|
+
- **Storing a long-lived cosign private key in CI secrets.** Defeats the point and creates a rotation burden. Use keyless OIDC; if a key is truly required, that's a secrets-management problem.
|
|
109
|
+
|
|
110
|
+
## Verify
|
|
111
|
+
|
|
112
|
+
1. **SBOM completeness:** `cyclonedx-cli validate --fail-on-errors` passes, and `jq` confirms zero components with null/empty `version`. Spot-check that a known base-image OS package (e.g. `glibc`) appears — proves it scanned the image, not just the manifest.
|
|
113
|
+
2. **Digest pinning:** `grep -rE 'FROM .+:[^@]+$' Dockerfile*` returns nothing (every `FROM` ends in `@sha256:`); the lockfile carries integrity hashes; frozen-install command is the one used in CI.
|
|
114
|
+
3. **Signature + attestation present:** `cosign verify --certificate-identity-regexp ... --certificate-oidc-issuer ...` exits `0` against the **digest**, and `cosign verify-attestation --type cyclonedx ...` returns the SBOM predicate. Tampering with one byte of the image flips both to non-zero.
|
|
115
|
+
4. **Provenance binds source+builder:** the SLSA predicate's `buildDefinition`/`materials` names the exact commit SHA and the builder identity; `cosign verify-attestation --type slsaprovenance` exits `0`.
|
|
116
|
+
5. **Enforcement is fail-closed:** deploy an image that is unsigned (or signed by a *different* identity) → the gate/admission controller **denies** it; the legitimately-signed image admits. A wrong-identity signature must be rejected, not just an unsigned one.
|
|
117
|
+
6. **Confusion defense:** attempt to install an internal package name from the public registry → resolution fails or is blocked by the scoped registry/allowlist; a new unlisted top-level dep fails CI.
|
|
118
|
+
7. **Continuous scan wired:** `osv-scanner --sbom ... --fail-on-vuln` (or `grype --fail-on high`) runs on a schedule against the stored SBOM and breaks the job on a new CVE; any suppression is a VEX entry with a reason, not a lowered threshold.
|
|
119
|
+
|
|
120
|
+
Done = SBOM (CycloneDX/SPDX) validates and is attested to the artifact; every base image and dependency is digest/hash-pinned with confusion defenses; the artifact and SBOM are keyless-signed with SLSA L2+ provenance binding source commit and builder; the consumption gate fails closed against unsigned and wrong-identity artifacts; and a scheduled scanner re-checks the SBOM for new CVEs.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-data-factories
|
|
3
|
+
description: Generates realistic, maintainable test data with factories instead of brittle shared fixtures — factory libraries (Ruby factory_bot, Python factory_boy/model-bakery, PHP Foundry/Faker, JS Fishery/@mswjs/data/Fabbrica, Java instancio/Java-faker, Go fake) that build valid objects with sane defaults, Faker for realistic values, traits/transient params/variants for state, build vs create (in-memory vs persisted), sequences for unique fields, nested associations and object graphs without combinatorial fixtures, deterministic seeding (Faker.seed/locale pinning) for reproducible CI, idempotent upsert-based DB seeders for dev/E2E that re-run cleanly, and anonymized prod-like data via masking/synthesis — so every test declares only the fields it cares about and stays valid as the schema evolves.
|
|
4
|
+
when_to_use: Building test data — replacing shared YAML/SQL fixtures, generating valid model instances for unit/integration/E2E tests, seeding a dev or E2E database idempotently, creating object graphs with associations, or producing anonymized prod-like datasets. Distinct from write-tests (structures the assertions and suite; this generates the inputs they assert on) and validate-data-quality (checks real datasets for nulls/dupes/outliers; this manufactures synthetic data on purpose).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when you need to manufacture valid, realistic test data and the pain is fixtures that rot or tests coupled to a giant shared dataset:
|
|
10
|
+
|
|
11
|
+
- "Replace our `fixtures/*.yml` — every schema change breaks 200 tests"
|
|
12
|
+
- "Give me a valid User/Order with just the 2 fields this test cares about"
|
|
13
|
+
- "Build an order with 3 line items, a customer, and an address" (object graph)
|
|
14
|
+
- "Seed the dev/E2E database so `db:seed` is safe to re-run"
|
|
15
|
+
- "Tests pass locally, flake in CI" (non-deterministic random data)
|
|
16
|
+
- "Generate realistic-but-fake names/emails/addresses" (Faker)
|
|
17
|
+
- "Make a prod-like dataset for staging without leaking PII" (anonymize)
|
|
18
|
+
|
|
19
|
+
NOT this skill:
|
|
20
|
+
- Structuring the assertions, arrange/act/assert, mocking, coverage, test naming → write-tests (it organizes the suite that *consumes* the data this skill builds)
|
|
21
|
+
- Checking a *real* dataset for nulls, dupes, outliers, schema drift → validate-data-quality (it inspects data you didn't generate; this fabricates data on purpose)
|
|
22
|
+
- Profiling/exploring an unfamiliar dataset's distributions → profile-dataset
|
|
23
|
+
- Generating *inputs to find bugs* by shrinking counterexamples → property-based-testing (it searches the input space; this hands you fixed, named, realistic instances)
|
|
24
|
+
- Driving a browser through the app to set up E2E state via the UI/API → write-playwright-e2e (it may *call* this skill's seeder to plant rows directly)
|
|
25
|
+
- Stabilizing a flaky test whose data was already non-deterministic → debug-flaky-tests (seed pinning here is one of its fixes)
|
|
26
|
+
- Safe schema changes / running the migration the seeder targets → db-migration-safety
|
|
27
|
+
- Designing the schema/associations themselves → design-relational-schema
|
|
28
|
+
|
|
29
|
+
## Steps
|
|
30
|
+
|
|
31
|
+
1. **Prefer factories over shared fixtures — fixtures are the anti-pattern you're replacing.** A global `users.yml`/seed SQL becomes load-bearing: tests depend on `user(:admin)` having exactly these fields, so any edit ripples across the suite and tests silently couple to unrelated data ("mystery guest"). Factories invert this: a `build(:user)` is valid by default, and each test **overrides only the attributes it asserts on**. Pick the idiomatic library:
|
|
32
|
+
|
|
33
|
+
| Stack | Library | Build vs persist |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Ruby / Rails | **factory_bot** | `build(:user)` (RAM) · `create(:user)` (DB) · `build_stubbed` (no DB, fake id) · `attributes_for` (hash) |
|
|
36
|
+
| Python / Django | **factory_boy** (`DjangoModelFactory`) or **model-bakery** (`baker.make`/`baker.prepare`) | `.build()` vs `.create()` / `prepare` vs `make` |
|
|
37
|
+
| Python (plain) | **factory_boy** `Factory` + `faker` | `.build()` only |
|
|
38
|
+
| JS / TS | **Fishery** (`.build()`), **@mswjs/data**, **Fabbrica** (Prisma), `@faker-js/faker` | build returns object; persist via your ORM |
|
|
39
|
+
| PHP / Laravel | **Foundry** or Eloquent factories + **fakerphp/faker** | `Model::factory()->make()` vs `->create()` |
|
|
40
|
+
| Java / Kotlin | **instancio**, **easy-random**, **datafaker** (Java-faker successor) | POJO in memory |
|
|
41
|
+
| Go | `go-faker`/`gofakeit` + hand-rolled builder funcs | struct in memory |
|
|
42
|
+
|
|
43
|
+
2. **Make the default object minimally valid; override per test.** The factory's defaults must pass all model validations on their own so `create(:user)` never fails for an unrelated reason. Then a test passes only what it cares about:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# factory_bot
|
|
47
|
+
factory :user do
|
|
48
|
+
name { Faker::Name.name }
|
|
49
|
+
email { Faker::Internet.unique.email } # unique → no collisions
|
|
50
|
+
role { "member" }
|
|
51
|
+
end
|
|
52
|
+
# test asserts on role only:
|
|
53
|
+
admin = create(:user, role: "admin") # name/email auto-filled, valid
|
|
54
|
+
```
|
|
55
|
+
```ts
|
|
56
|
+
// Fishery
|
|
57
|
+
const userFactory = Factory.define<User>(({ sequence }) => ({
|
|
58
|
+
id: sequence, // 1,2,3… unique per build
|
|
59
|
+
email: `user${sequence}@example.test`,
|
|
60
|
+
role: 'member',
|
|
61
|
+
}));
|
|
62
|
+
userFactory.build({ role: 'admin' }); // override one field
|
|
63
|
+
```
|
|
64
|
+
Use the reserved **`example.test`/`example.com`** domains and the `+tag` trick for emails so generated data never hits a real inbox.
|
|
65
|
+
|
|
66
|
+
3. **Use Faker for realism, but never for fields you assert on.** Faker gives plausible names/emails/addresses/companies so data looks real and surfaces formatting bugs. The discipline: **if a test checks a value, set it explicitly; let Faker fill the rest.** Asserting against a Faker-generated value is a guaranteed flake. Pin locale (`Faker::Config.locale = :en` / `faker.setLocale`) so address/phone formats are stable across machines, and use `unique` generators (`Faker::Internet.unique.email`, `faker.helpers.unique` — note `@faker-js/faker` removed `unique`; use a sequence or `faker.string.uuid` instead) for columns under a UNIQUE constraint.
|
|
67
|
+
|
|
68
|
+
4. **Sequences for unique/monotonic fields; transient params for build-time knobs that aren't attributes.** Sequences (`sequence(:email) { |n| "user#{n}@example.test" }`) guarantee uniqueness without a global mutable counter. **Transient/transient-params** are inputs that shape the build but aren't columns:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
factory :order do
|
|
72
|
+
transient { line_item_count { 3 } } # not a column
|
|
73
|
+
after(:create) do |order, ev|
|
|
74
|
+
create_list(:line_item, ev.line_item_count, order: order)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
create(:order, line_item_count: 5)
|
|
78
|
+
```
|
|
79
|
+
factory_boy calls these `Params`/`class Params: ...` with `factory.Trait`; Foundry uses `->with()`/states. Reach for them whenever a test wants "an order with N items" without N being an order column.
|
|
80
|
+
|
|
81
|
+
5. **Model variants with traits, not a forest of sub-factories.** A trait is a named bundle of attribute overrides; compose several in one call. This beats `factory :admin_user`, `factory :suspended_admin_user`, … which explodes combinatorially:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
factory :user do
|
|
85
|
+
trait(:admin) { role { "admin" } }
|
|
86
|
+
trait(:suspended) { suspended_at { Time.current } }
|
|
87
|
+
factory :premium { plan { "premium" } } # nested for true subtype
|
|
88
|
+
end
|
|
89
|
+
create(:user, :admin, :suspended) # compose traits
|
|
90
|
+
```
|
|
91
|
+
factory_boy → `class Params:` + `factory.Trait`; Fishery → `.params()` + transient `transientParams`, or named factory variants; model-bakery → recipes. **One base factory + traits** is the maintainable shape; deep factory inheritance is not.
|
|
92
|
+
|
|
93
|
+
6. **Build object graphs through associations — don't hand-wire foreign keys.** Declare relations so the factory creates the whole graph and back-references resolve automatically:
|
|
94
|
+
|
|
95
|
+
| Lib | Association syntax |
|
|
96
|
+
|---|---|
|
|
97
|
+
| factory_bot | `association :customer` · `customer { create(:customer) }` · `create_list(:item, 3, order:)` |
|
|
98
|
+
| factory_boy | `customer = factory.SubFactory(CustomerFactory)` · `factory.RelatedFactory` (reverse) · `factory.List` |
|
|
99
|
+
| Fishery | `customer: customerFactory.build()` inside the generator, or `associations` param |
|
|
100
|
+
| Foundry | `CustomerFactory::new()` as an attribute; `->many(3)` for collections |
|
|
101
|
+
|
|
102
|
+
**Build the minimal graph the test needs** — pulling in 4 levels of associations slows every test and recreates the fixture problem. Use `build`/`build_stubbed` (no DB) for pure-logic tests; reserve `create` for tests that actually query the DB. Guard against accidental N× object creation in `before` hooks.
|
|
103
|
+
|
|
104
|
+
7. **Seed dev/E2E databases idempotently — upsert, never blind insert.** A seed script that `INSERT`s breaks on the second run (unique-constraint violation) and corrupts state. Make it re-runnable:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Rails: find_or_create_by / upsert on a natural key
|
|
108
|
+
User.find_or_create_by!(email: "demo@example.test") { |u| u.name = "Demo" }
|
|
109
|
+
```
|
|
110
|
+
```sql
|
|
111
|
+
INSERT INTO plans (code, name) VALUES ('pro','Pro')
|
|
112
|
+
ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name; -- idempotent
|
|
113
|
+
```
|
|
114
|
+
Rules: key every seed row on a **stable natural key** (slug/code/email), not an auto-id; wrap the whole seed in a transaction; make `db:seed` / `prisma db seed` / `php artisan db:seed` safe to run N times with identical end state. Separate **dev seed** (rich demo data, may be large) from **E2E/test seed** (minimal, deterministic, reset between runs via truncate or transactional rollback). For E2E, prefer planting rows via the factory/seeder directly over driving the UI — orders of magnitude faster and less flaky.
|
|
115
|
+
|
|
116
|
+
8. **Make data deterministic in CI — pin the RNG seed and locale.** Random factory data that flakes is worse than fixtures. Pin a seed so a failing CI run is reproducible:
|
|
117
|
+
|
|
118
|
+
| Tool | Seed control |
|
|
119
|
+
|---|---|
|
|
120
|
+
| Faker (Ruby) | `Faker::Config.random = Random.new(RSEED)` |
|
|
121
|
+
| @faker-js/faker | `faker.seed(12345)` (per-suite, in a `beforeEach`/global setup) |
|
|
122
|
+
| faker (Python) | `Faker.seed(0)` / `fake.seed_instance(0)` |
|
|
123
|
+
| factory_boy | `factory.random.reseed_random('seed')` |
|
|
124
|
+
| RSpec / Jest | `--seed`/`config.seed`; Jest `--testSequencer` + faker seed |
|
|
125
|
+
|
|
126
|
+
Print the seed on every run and let CI re-run with a fixed seed to reproduce a failure. **Pin the locale too** — default-locale drift changes address/phone formats and breaks format-sensitive assertions. Don't share mutable factory state across parallel test workers; each worker reseeds.
|
|
127
|
+
|
|
128
|
+
9. **For prod-like datasets, anonymize — synthesize or mask, never copy raw PII.** Staging/perf data that mirrors prod shape without leaking real people: (a) **synthesize** from factories at volume (`create_list(:user, 100_000)` / a Faker loop) when you only need realistic shape; (b) **mask/anonymize** a prod snapshot when you need real distributions — replace names/emails/SSNs with Faker values, **deterministically** (hash the original → same fake every time, so foreign keys stay consistent), null or tokenize free-text, and shift dates by a constant offset. Tools: pg `anon` extension, `pganonymize`, Snaplet/`@snaplet/seed`, `faker` + a mapping table. Never load an un-anonymized prod dump into a lower environment — that's a PII breach. Keep the anonymization mapping out of the lower environment.
|
|
129
|
+
|
|
130
|
+
10. **Keep factories close to the code and validated.** Co-locate factories with the test suite (`spec/factories`, `test/factories`, `src/test-utils/factories`), auto-load them, and add a CI lint that **`build`s every factory and asserts it's valid** (factory_bot's `FactoryBot.lint`) so a schema/validation change that breaks a factory fails fast instead of in 50 unrelated tests. When a column is added with a NOT NULL/validation, fix it in the **one** factory, not across the suite.
|
|
131
|
+
|
|
132
|
+
## Common Errors
|
|
133
|
+
|
|
134
|
+
- **Shared fixtures as source of truth.** One `users.yml` every test secretly depends on → mystery-guest coupling, schema edits break the world. Fix: factories with per-test overrides; delete the global fixture.
|
|
135
|
+
- **Asserting against a Faker-generated value.** `expect(user.name).to eq(faker_name)` flakes the moment the seed changes. Fix: set asserted fields explicitly; Faker only fills don't-care fields.
|
|
136
|
+
- **Non-deterministic data in CI with no seed.** Intermittent failures no one can reproduce. Fix: pin `faker.seed`/`Faker.seed` and the locale; print and replay the seed.
|
|
137
|
+
- **`create` everywhere when `build` would do.** Hitting the DB (and its associations) for pure-logic tests makes the suite slow and order-dependent. Fix: `build`/`build_stubbed`/`prepare` for in-memory; `create` only when you query.
|
|
138
|
+
- **Non-idempotent seed script.** Blind `INSERT` → second run violates UNIQUE / duplicates rows. Fix: `find_or_create_by` / `ON CONFLICT DO UPDATE` on a natural key; wrap in a transaction.
|
|
139
|
+
- **Sub-factory explosion.** `admin_user`, `suspended_admin_user`, `premium_suspended_admin_user`… Fix: one base factory + composable traits.
|
|
140
|
+
- **Over-deep association graphs.** Every `create` drags in 4 levels of records → slow tests and re-coupled data. Fix: build the minimal graph; stub the rest.
|
|
141
|
+
- **Duplicate-key collisions from static defaults.** A hardcoded `email: "a@b.com"` default fails the second `create`. Fix: sequences or `Faker::Internet.unique` (or `faker.string.uuid` in @faker-js/faker, which dropped `unique`).
|
|
142
|
+
- **Loading raw prod data into staging.** Real PII in a lower environment = breach. Fix: deterministic anonymization/masking or synthesize; keep the mapping out of staging.
|
|
143
|
+
- **Locale drift.** Default Faker locale differs by machine/CI → address/phone format assertions break. Fix: pin the locale explicitly.
|
|
144
|
+
- **Factories that drift from the schema.** A new NOT NULL column makes every `create` fail cryptically. Fix: `FactoryBot.lint`-style CI check that builds every factory.
|
|
145
|
+
|
|
146
|
+
## Verify
|
|
147
|
+
|
|
148
|
+
1. **Factories are valid standalone:** run the lint (`FactoryBot.lint` / build-every-factory test) — every factory and trait `build`s and passes validations with zero overrides.
|
|
149
|
+
2. **No fixture coupling:** grep the suite for the old shared fixture references; a test reads only the fields it sets/asserts, and editing an unrelated factory attribute breaks nothing.
|
|
150
|
+
3. **Determinism:** run the suite twice with the same pinned seed → identical data and pass/fail; run with two different seeds → still green (no test asserts a Faker value).
|
|
151
|
+
4. **Uniqueness holds:** create N rows from a factory with a UNIQUE column in a loop → no constraint violation (sequence/`unique`/uuid working).
|
|
152
|
+
5. **Seed is idempotent:** run `db:seed` twice → identical row count and end state, no UNIQUE error; the second run is a no-op or clean upsert.
|
|
153
|
+
6. **build vs create honored:** `build`/`build_stubbed` issues zero SQL INSERTs (assert via query log/`assert_no_queries`); `create` persists exactly the intended graph.
|
|
154
|
+
7. **Traits compose:** `create(:user, :admin, :suspended)` yields both states; no combinatorial sub-factory needed.
|
|
155
|
+
8. **Object graph is minimal and correct:** an order factory creates exactly its declared associations (customer + N items), foreign keys resolve, and no surprise extra records appear.
|
|
156
|
+
9. **Anonymized data is safe:** spot-check the prod-like dataset — no real PII, the masking is deterministic (same input → same fake, FKs consistent), and date/format distributions are realistic.
|
|
157
|
+
|
|
158
|
+
Done = brittle shared fixtures are gone, each test declares only the fields it cares about against a valid-by-default factory, Faker fills the rest with a pinned seed+locale so CI is reproducible, object graphs and traits compose without sub-factory explosion, the dev/E2E seed is idempotent on a natural key, and any prod-like data is deterministically anonymized — all proven by the factory lint, the twice-with-same-seed run, and the double-seed idempotence check.
|