@zigrivers/scaffold 3.25.1 → 3.27.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/README.md +139 -7
- package/content/knowledge/web3/web3-access-control.md +189 -0
- package/content/knowledge/web3/web3-architecture.md +162 -0
- package/content/knowledge/web3/web3-audit-workflow.md +151 -0
- package/content/knowledge/web3/web3-common-vulnerabilities.md +171 -0
- package/content/knowledge/web3/web3-conventions.md +162 -0
- package/content/knowledge/web3/web3-deployment-and-verification.md +216 -0
- package/content/knowledge/web3/web3-dev-environment.md +150 -0
- package/content/knowledge/web3/web3-gas-optimization.md +165 -0
- package/content/knowledge/web3/web3-oracles-and-external-data.md +155 -0
- package/content/knowledge/web3/web3-project-structure.md +212 -0
- package/content/knowledge/web3/web3-requirements.md +152 -0
- package/content/knowledge/web3/web3-security.md +163 -0
- package/content/knowledge/web3/web3-testing.md +180 -0
- package/content/knowledge/web3/web3-upgradeability.md +189 -0
- package/content/methodology/web3-overlay.yml +40 -0
- package/dist/cli/commands/complete.d.ts.map +1 -1
- package/dist/cli/commands/complete.js +6 -13
- package/dist/cli/commands/complete.js.map +1 -1
- package/dist/cli/commands/complete.test.js +18 -0
- package/dist/cli/commands/complete.test.js.map +1 -1
- package/dist/cli/commands/knowledge.test.js +4 -4
- package/dist/cli/commands/knowledge.test.js.map +1 -1
- package/dist/cli/commands/observe.d.ts +58 -0
- package/dist/cli/commands/observe.d.ts.map +1 -0
- package/dist/cli/commands/observe.js +430 -0
- package/dist/cli/commands/observe.js.map +1 -0
- package/dist/cli/commands/observe.test.d.ts +2 -0
- package/dist/cli/commands/observe.test.d.ts.map +1 -0
- package/dist/cli/commands/observe.test.js +452 -0
- package/dist/cli/commands/observe.test.js.map +1 -0
- package/dist/cli/commands/run.js +3 -3
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/run.test.js +1 -1
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config/schema.d.ts +672 -126
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +8 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +2 -2
- package/dist/config/schema.test.js.map +1 -1
- package/dist/config/validators/index.d.ts.map +1 -1
- package/dist/config/validators/index.js +2 -0
- package/dist/config/validators/index.js.map +1 -1
- package/dist/config/validators/web3.d.ts +4 -0
- package/dist/config/validators/web3.d.ts.map +1 -0
- package/dist/config/validators/web3.js +15 -0
- package/dist/config/validators/web3.js.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +76 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/observability/adapters/audit-history.d.ts +17 -0
- package/dist/observability/adapters/audit-history.d.ts.map +1 -0
- package/dist/observability/adapters/audit-history.js +113 -0
- package/dist/observability/adapters/audit-history.js.map +1 -0
- package/dist/observability/adapters/audit-history.test.d.ts +2 -0
- package/dist/observability/adapters/audit-history.test.d.ts.map +1 -0
- package/dist/observability/adapters/audit-history.test.js +137 -0
- package/dist/observability/adapters/audit-history.test.js.map +1 -0
- package/dist/observability/adapters/beads.d.ts +9 -0
- package/dist/observability/adapters/beads.d.ts.map +1 -0
- package/dist/observability/adapters/beads.js +40 -0
- package/dist/observability/adapters/beads.js.map +1 -0
- package/dist/observability/adapters/beads.test.d.ts +2 -0
- package/dist/observability/adapters/beads.test.d.ts.map +1 -0
- package/dist/observability/adapters/beads.test.js +25 -0
- package/dist/observability/adapters/beads.test.js.map +1 -0
- package/dist/observability/adapters/gh.d.ts +27 -0
- package/dist/observability/adapters/gh.d.ts.map +1 -0
- package/dist/observability/adapters/gh.js +118 -0
- package/dist/observability/adapters/gh.js.map +1 -0
- package/dist/observability/adapters/gh.test.d.ts +2 -0
- package/dist/observability/adapters/gh.test.d.ts.map +1 -0
- package/dist/observability/adapters/gh.test.js +79 -0
- package/dist/observability/adapters/gh.test.js.map +1 -0
- package/dist/observability/adapters/git.d.ts +24 -0
- package/dist/observability/adapters/git.d.ts.map +1 -0
- package/dist/observability/adapters/git.js +110 -0
- package/dist/observability/adapters/git.js.map +1 -0
- package/dist/observability/adapters/git.test.d.ts +2 -0
- package/dist/observability/adapters/git.test.d.ts.map +1 -0
- package/dist/observability/adapters/git.test.js +66 -0
- package/dist/observability/adapters/git.test.js.map +1 -0
- package/dist/observability/adapters/mmr.d.ts +15 -0
- package/dist/observability/adapters/mmr.d.ts.map +1 -0
- package/dist/observability/adapters/mmr.js +85 -0
- package/dist/observability/adapters/mmr.js.map +1 -0
- package/dist/observability/adapters/mmr.test.d.ts +2 -0
- package/dist/observability/adapters/mmr.test.d.ts.map +1 -0
- package/dist/observability/adapters/mmr.test.js +55 -0
- package/dist/observability/adapters/mmr.test.js.map +1 -0
- package/dist/observability/adapters/pipeline-docs.d.ts +8 -0
- package/dist/observability/adapters/pipeline-docs.d.ts.map +1 -0
- package/dist/observability/adapters/pipeline-docs.js +68 -0
- package/dist/observability/adapters/pipeline-docs.js.map +1 -0
- package/dist/observability/adapters/pipeline-docs.test.d.ts +2 -0
- package/dist/observability/adapters/pipeline-docs.test.d.ts.map +1 -0
- package/dist/observability/adapters/pipeline-docs.test.js +58 -0
- package/dist/observability/adapters/pipeline-docs.test.js.map +1 -0
- package/dist/observability/adapters/state.d.ts +21 -0
- package/dist/observability/adapters/state.d.ts.map +1 -0
- package/dist/observability/adapters/state.js +87 -0
- package/dist/observability/adapters/state.js.map +1 -0
- package/dist/observability/adapters/state.test.d.ts +2 -0
- package/dist/observability/adapters/state.test.d.ts.map +1 -0
- package/dist/observability/adapters/state.test.js +92 -0
- package/dist/observability/adapters/state.test.js.map +1 -0
- package/dist/observability/adapters/tests.d.ts +20 -0
- package/dist/observability/adapters/tests.d.ts.map +1 -0
- package/dist/observability/adapters/tests.js +52 -0
- package/dist/observability/adapters/tests.js.map +1 -0
- package/dist/observability/adapters/tests.test.d.ts +2 -0
- package/dist/observability/adapters/tests.test.d.ts.map +1 -0
- package/dist/observability/adapters/tests.test.js +66 -0
- package/dist/observability/adapters/tests.test.js.map +1 -0
- package/dist/observability/adapters/types.d.ts +7 -0
- package/dist/observability/adapters/types.d.ts.map +1 -0
- package/dist/observability/adapters/types.js +2 -0
- package/dist/observability/adapters/types.js.map +1 -0
- package/dist/observability/checks/lens-a-tdd.d.ts +3 -0
- package/dist/observability/checks/lens-a-tdd.d.ts.map +1 -0
- package/dist/observability/checks/lens-a-tdd.js +34 -0
- package/dist/observability/checks/lens-a-tdd.js.map +1 -0
- package/dist/observability/checks/lens-a-tdd.test.d.ts +2 -0
- package/dist/observability/checks/lens-a-tdd.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-a-tdd.test.js +62 -0
- package/dist/observability/checks/lens-a-tdd.test.js.map +1 -0
- package/dist/observability/checks/lens-b-ac-coverage.d.ts +3 -0
- package/dist/observability/checks/lens-b-ac-coverage.d.ts.map +1 -0
- package/dist/observability/checks/lens-b-ac-coverage.js +63 -0
- package/dist/observability/checks/lens-b-ac-coverage.js.map +1 -0
- package/dist/observability/checks/lens-b-ac-coverage.test.d.ts +2 -0
- package/dist/observability/checks/lens-b-ac-coverage.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-b-ac-coverage.test.js +59 -0
- package/dist/observability/checks/lens-b-ac-coverage.test.js.map +1 -0
- package/dist/observability/checks/lens-c-standards.d.ts +3 -0
- package/dist/observability/checks/lens-c-standards.d.ts.map +1 -0
- package/dist/observability/checks/lens-c-standards.js +104 -0
- package/dist/observability/checks/lens-c-standards.js.map +1 -0
- package/dist/observability/checks/lens-c-standards.test.d.ts +2 -0
- package/dist/observability/checks/lens-c-standards.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-c-standards.test.js +79 -0
- package/dist/observability/checks/lens-c-standards.test.js.map +1 -0
- package/dist/observability/checks/lens-d-stack.d.ts +3 -0
- package/dist/observability/checks/lens-d-stack.d.ts.map +1 -0
- package/dist/observability/checks/lens-d-stack.js +108 -0
- package/dist/observability/checks/lens-d-stack.js.map +1 -0
- package/dist/observability/checks/lens-d-stack.test.d.ts +2 -0
- package/dist/observability/checks/lens-d-stack.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-d-stack.test.js +60 -0
- package/dist/observability/checks/lens-d-stack.test.js.map +1 -0
- package/dist/observability/checks/lens-e-design.d.ts +3 -0
- package/dist/observability/checks/lens-e-design.d.ts.map +1 -0
- package/dist/observability/checks/lens-e-design.js +76 -0
- package/dist/observability/checks/lens-e-design.js.map +1 -0
- package/dist/observability/checks/lens-e-design.test.d.ts +2 -0
- package/dist/observability/checks/lens-e-design.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-e-design.test.js +85 -0
- package/dist/observability/checks/lens-e-design.test.js.map +1 -0
- package/dist/observability/checks/lens-f-scope.d.ts +3 -0
- package/dist/observability/checks/lens-f-scope.d.ts.map +1 -0
- package/dist/observability/checks/lens-f-scope.js +92 -0
- package/dist/observability/checks/lens-f-scope.js.map +1 -0
- package/dist/observability/checks/lens-f-scope.test.d.ts +2 -0
- package/dist/observability/checks/lens-f-scope.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-f-scope.test.js +63 -0
- package/dist/observability/checks/lens-f-scope.test.js.map +1 -0
- package/dist/observability/checks/lens-g-decisions.d.ts +3 -0
- package/dist/observability/checks/lens-g-decisions.d.ts.map +1 -0
- package/dist/observability/checks/lens-g-decisions.js +139 -0
- package/dist/observability/checks/lens-g-decisions.js.map +1 -0
- package/dist/observability/checks/lens-g-decisions.test.d.ts +2 -0
- package/dist/observability/checks/lens-g-decisions.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-g-decisions.test.js +118 -0
- package/dist/observability/checks/lens-g-decisions.test.js.map +1 -0
- package/dist/observability/checks/lens-h-cross-doc.d.ts +3 -0
- package/dist/observability/checks/lens-h-cross-doc.d.ts.map +1 -0
- package/dist/observability/checks/lens-h-cross-doc.js +322 -0
- package/dist/observability/checks/lens-h-cross-doc.js.map +1 -0
- package/dist/observability/checks/lens-h-cross-doc.test.d.ts +2 -0
- package/dist/observability/checks/lens-h-cross-doc.test.d.ts.map +1 -0
- package/dist/observability/checks/lens-h-cross-doc.test.js +174 -0
- package/dist/observability/checks/lens-h-cross-doc.test.js.map +1 -0
- package/dist/observability/engine/abort-snapshot.d.ts +10 -0
- package/dist/observability/engine/abort-snapshot.d.ts.map +1 -0
- package/dist/observability/engine/abort-snapshot.js +36 -0
- package/dist/observability/engine/abort-snapshot.js.map +1 -0
- package/dist/observability/engine/abort-snapshot.test.d.ts +2 -0
- package/dist/observability/engine/abort-snapshot.test.d.ts.map +1 -0
- package/dist/observability/engine/abort-snapshot.test.js +66 -0
- package/dist/observability/engine/abort-snapshot.test.js.map +1 -0
- package/dist/observability/engine/api.d.ts +24 -0
- package/dist/observability/engine/api.d.ts.map +1 -0
- package/dist/observability/engine/api.js +203 -0
- package/dist/observability/engine/api.js.map +1 -0
- package/dist/observability/engine/api.test.d.ts +2 -0
- package/dist/observability/engine/api.test.d.ts.map +1 -0
- package/dist/observability/engine/api.test.js +174 -0
- package/dist/observability/engine/api.test.js.map +1 -0
- package/dist/observability/engine/checks/findings-aggregator.d.ts +6 -0
- package/dist/observability/engine/checks/findings-aggregator.d.ts.map +1 -0
- package/dist/observability/engine/checks/findings-aggregator.js +56 -0
- package/dist/observability/engine/checks/findings-aggregator.js.map +1 -0
- package/dist/observability/engine/checks/findings-aggregator.test.d.ts +2 -0
- package/dist/observability/engine/checks/findings-aggregator.test.d.ts.map +1 -0
- package/dist/observability/engine/checks/findings-aggregator.test.js +63 -0
- package/dist/observability/engine/checks/findings-aggregator.test.js.map +1 -0
- package/dist/observability/engine/checks/fix-threshold.d.ts +3 -0
- package/dist/observability/engine/checks/fix-threshold.d.ts.map +1 -0
- package/dist/observability/engine/checks/fix-threshold.js +24 -0
- package/dist/observability/engine/checks/fix-threshold.js.map +1 -0
- package/dist/observability/engine/checks/fix-threshold.test.d.ts +2 -0
- package/dist/observability/engine/checks/fix-threshold.test.d.ts.map +1 -0
- package/dist/observability/engine/checks/fix-threshold.test.js +29 -0
- package/dist/observability/engine/checks/fix-threshold.test.js.map +1 -0
- package/dist/observability/engine/checks/observability-config.d.ts +64 -0
- package/dist/observability/engine/checks/observability-config.d.ts.map +1 -0
- package/dist/observability/engine/checks/observability-config.js +56 -0
- package/dist/observability/engine/checks/observability-config.js.map +1 -0
- package/dist/observability/engine/checks/observability-config.test.d.ts +2 -0
- package/dist/observability/engine/checks/observability-config.test.d.ts.map +1 -0
- package/dist/observability/engine/checks/observability-config.test.js +39 -0
- package/dist/observability/engine/checks/observability-config.test.js.map +1 -0
- package/dist/observability/engine/checks/registry.d.ts +19 -0
- package/dist/observability/engine/checks/registry.d.ts.map +1 -0
- package/dist/observability/engine/checks/registry.js +44 -0
- package/dist/observability/engine/checks/registry.js.map +1 -0
- package/dist/observability/engine/checks/registry.test.d.ts +2 -0
- package/dist/observability/engine/checks/registry.test.d.ts.map +1 -0
- package/dist/observability/engine/checks/registry.test.js +23 -0
- package/dist/observability/engine/checks/registry.test.js.map +1 -0
- package/dist/observability/engine/checks/runner.d.ts +23 -0
- package/dist/observability/engine/checks/runner.d.ts.map +1 -0
- package/dist/observability/engine/checks/runner.js +66 -0
- package/dist/observability/engine/checks/runner.js.map +1 -0
- package/dist/observability/engine/checks/runner.test.d.ts +2 -0
- package/dist/observability/engine/checks/runner.test.d.ts.map +1 -0
- package/dist/observability/engine/checks/runner.test.js +95 -0
- package/dist/observability/engine/checks/runner.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/component-parser.d.ts +3 -0
- package/dist/observability/engine/doc-graph/component-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/component-parser.js +42 -0
- package/dist/observability/engine/doc-graph/component-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/component-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/component-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/component-parser.test.js +40 -0
- package/dist/observability/engine/doc-graph/component-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/component-use-detector.d.ts +8 -0
- package/dist/observability/engine/doc-graph/component-use-detector.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/component-use-detector.js +62 -0
- package/dist/observability/engine/doc-graph/component-use-detector.js.map +1 -0
- package/dist/observability/engine/doc-graph/component-use-detector.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/component-use-detector.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/component-use-detector.test.js +38 -0
- package/dist/observability/engine/doc-graph/component-use-detector.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/decision-parser.d.ts +3 -0
- package/dist/observability/engine/doc-graph/decision-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/decision-parser.js +60 -0
- package/dist/observability/engine/doc-graph/decision-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/decision-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/decision-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/decision-parser.test.js +65 -0
- package/dist/observability/engine/doc-graph/decision-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/design-props.d.ts +9 -0
- package/dist/observability/engine/doc-graph/design-props.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/design-props.js +50 -0
- package/dist/observability/engine/doc-graph/design-props.js.map +1 -0
- package/dist/observability/engine/doc-graph/edge-builder.d.ts +28 -0
- package/dist/observability/engine/doc-graph/edge-builder.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/edge-builder.js +75 -0
- package/dist/observability/engine/doc-graph/edge-builder.js.map +1 -0
- package/dist/observability/engine/doc-graph/edge-builder.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/edge-builder.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/edge-builder.test.js +124 -0
- package/dist/observability/engine/doc-graph/edge-builder.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/feature-parser.d.ts +3 -0
- package/dist/observability/engine/doc-graph/feature-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/feature-parser.js +78 -0
- package/dist/observability/engine/doc-graph/feature-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/feature-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/feature-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/feature-parser.test.js +51 -0
- package/dist/observability/engine/doc-graph/feature-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/index.d.ts +3 -0
- package/dist/observability/engine/doc-graph/index.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/index.js +138 -0
- package/dist/observability/engine/doc-graph/index.js.map +1 -0
- package/dist/observability/engine/doc-graph/index.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/index.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/index.test.js +82 -0
- package/dist/observability/engine/doc-graph/index.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/parse-markdown.d.ts +15 -0
- package/dist/observability/engine/doc-graph/parse-markdown.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/parse-markdown.js +79 -0
- package/dist/observability/engine/doc-graph/parse-markdown.js.map +1 -0
- package/dist/observability/engine/doc-graph/parse-markdown.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/parse-markdown.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/parse-markdown.test.js +50 -0
- package/dist/observability/engine/doc-graph/parse-markdown.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.d.ts +3 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.js +38 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.test.js +38 -0
- package/dist/observability/engine/doc-graph/plan-task-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.d.ts +3 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.js +40 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.test.js +31 -0
- package/dist/observability/engine/doc-graph/playbook-task-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/rule-parser.d.ts +3 -0
- package/dist/observability/engine/doc-graph/rule-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/rule-parser.js +65 -0
- package/dist/observability/engine/doc-graph/rule-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/rule-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/rule-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/rule-parser.test.js +44 -0
- package/dist/observability/engine/doc-graph/rule-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/story-parser.d.ts +8 -0
- package/dist/observability/engine/doc-graph/story-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/story-parser.js +109 -0
- package/dist/observability/engine/doc-graph/story-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/story-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/story-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/story-parser.test.js +66 -0
- package/dist/observability/engine/doc-graph/story-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/test-discovery.d.ts +3 -0
- package/dist/observability/engine/doc-graph/test-discovery.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/test-discovery.js +122 -0
- package/dist/observability/engine/doc-graph/test-discovery.js.map +1 -0
- package/dist/observability/engine/doc-graph/test-discovery.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/test-discovery.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/test-discovery.test.js +39 -0
- package/dist/observability/engine/doc-graph/test-discovery.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/token-parser.d.ts +3 -0
- package/dist/observability/engine/doc-graph/token-parser.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/token-parser.js +67 -0
- package/dist/observability/engine/doc-graph/token-parser.js.map +1 -0
- package/dist/observability/engine/doc-graph/token-parser.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/token-parser.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/token-parser.test.js +39 -0
- package/dist/observability/engine/doc-graph/token-parser.test.js.map +1 -0
- package/dist/observability/engine/doc-graph/token-use-detector.d.ts +10 -0
- package/dist/observability/engine/doc-graph/token-use-detector.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/token-use-detector.js +105 -0
- package/dist/observability/engine/doc-graph/token-use-detector.js.map +1 -0
- package/dist/observability/engine/doc-graph/token-use-detector.test.d.ts +2 -0
- package/dist/observability/engine/doc-graph/token-use-detector.test.d.ts.map +1 -0
- package/dist/observability/engine/doc-graph/token-use-detector.test.js +60 -0
- package/dist/observability/engine/doc-graph/token-use-detector.test.js.map +1 -0
- package/dist/observability/engine/event-schemas.d.ts +12 -0
- package/dist/observability/engine/event-schemas.d.ts.map +1 -0
- package/dist/observability/engine/event-schemas.js +182 -0
- package/dist/observability/engine/event-schemas.js.map +1 -0
- package/dist/observability/engine/event-schemas.test.d.ts +2 -0
- package/dist/observability/engine/event-schemas.test.d.ts.map +1 -0
- package/dist/observability/engine/event-schemas.test.js +200 -0
- package/dist/observability/engine/event-schemas.test.js.map +1 -0
- package/dist/observability/engine/fix-agent-dispatcher.d.ts +19 -0
- package/dist/observability/engine/fix-agent-dispatcher.d.ts.map +1 -0
- package/dist/observability/engine/fix-agent-dispatcher.js +88 -0
- package/dist/observability/engine/fix-agent-dispatcher.js.map +1 -0
- package/dist/observability/engine/fix-agent-dispatcher.test.d.ts +2 -0
- package/dist/observability/engine/fix-agent-dispatcher.test.d.ts.map +1 -0
- package/dist/observability/engine/fix-agent-dispatcher.test.js +54 -0
- package/dist/observability/engine/fix-agent-dispatcher.test.js.map +1 -0
- package/dist/observability/engine/fix-flow.d.ts +30 -0
- package/dist/observability/engine/fix-flow.d.ts.map +1 -0
- package/dist/observability/engine/fix-flow.js +105 -0
- package/dist/observability/engine/fix-flow.js.map +1 -0
- package/dist/observability/engine/fix-flow.test.d.ts +2 -0
- package/dist/observability/engine/fix-flow.test.d.ts.map +1 -0
- package/dist/observability/engine/fix-flow.test.js +127 -0
- package/dist/observability/engine/fix-flow.test.js.map +1 -0
- package/dist/observability/engine/fix-plan.d.ts +3 -0
- package/dist/observability/engine/fix-plan.d.ts.map +1 -0
- package/dist/observability/engine/fix-plan.js +13 -0
- package/dist/observability/engine/fix-plan.js.map +1 -0
- package/dist/observability/engine/fix-plan.test.d.ts +2 -0
- package/dist/observability/engine/fix-plan.test.d.ts.map +1 -0
- package/dist/observability/engine/fix-plan.test.js +44 -0
- package/dist/observability/engine/fix-plan.test.js.map +1 -0
- package/dist/observability/engine/harvester.d.ts +16 -0
- package/dist/observability/engine/harvester.d.ts.map +1 -0
- package/dist/observability/engine/harvester.js +106 -0
- package/dist/observability/engine/harvester.js.map +1 -0
- package/dist/observability/engine/harvester.test.d.ts +2 -0
- package/dist/observability/engine/harvester.test.d.ts.map +1 -0
- package/dist/observability/engine/harvester.test.js +99 -0
- package/dist/observability/engine/harvester.test.js.map +1 -0
- package/dist/observability/engine/identity.d.ts +6 -0
- package/dist/observability/engine/identity.d.ts.map +1 -0
- package/dist/observability/engine/identity.js +50 -0
- package/dist/observability/engine/identity.js.map +1 -0
- package/dist/observability/engine/identity.test.d.ts +2 -0
- package/dist/observability/engine/identity.test.d.ts.map +1 -0
- package/dist/observability/engine/identity.test.js +29 -0
- package/dist/observability/engine/identity.test.js.map +1 -0
- package/dist/observability/engine/ledger-writer.d.ts +10 -0
- package/dist/observability/engine/ledger-writer.d.ts.map +1 -0
- package/dist/observability/engine/ledger-writer.js +50 -0
- package/dist/observability/engine/ledger-writer.js.map +1 -0
- package/dist/observability/engine/ledger-writer.test.d.ts +2 -0
- package/dist/observability/engine/ledger-writer.test.d.ts.map +1 -0
- package/dist/observability/engine/ledger-writer.test.js +72 -0
- package/dist/observability/engine/ledger-writer.test.js.map +1 -0
- package/dist/observability/engine/llm-dispatcher.d.ts +16 -0
- package/dist/observability/engine/llm-dispatcher.d.ts.map +1 -0
- package/dist/observability/engine/llm-dispatcher.js +183 -0
- package/dist/observability/engine/llm-dispatcher.js.map +1 -0
- package/dist/observability/engine/llm-dispatcher.test.d.ts +2 -0
- package/dist/observability/engine/llm-dispatcher.test.d.ts.map +1 -0
- package/dist/observability/engine/llm-dispatcher.test.js +109 -0
- package/dist/observability/engine/llm-dispatcher.test.js.map +1 -0
- package/dist/observability/engine/phase-audit.d.ts +22 -0
- package/dist/observability/engine/phase-audit.d.ts.map +1 -0
- package/dist/observability/engine/phase-audit.js +98 -0
- package/dist/observability/engine/phase-audit.js.map +1 -0
- package/dist/observability/engine/phase-audit.test.d.ts +2 -0
- package/dist/observability/engine/phase-audit.test.d.ts.map +1 -0
- package/dist/observability/engine/phase-audit.test.js +82 -0
- package/dist/observability/engine/phase-audit.test.js.map +1 -0
- package/dist/observability/engine/phase-subsets.d.ts +5 -0
- package/dist/observability/engine/phase-subsets.d.ts.map +1 -0
- package/dist/observability/engine/phase-subsets.js +23 -0
- package/dist/observability/engine/phase-subsets.js.map +1 -0
- package/dist/observability/engine/phase-subsets.test.d.ts +2 -0
- package/dist/observability/engine/phase-subsets.test.d.ts.map +1 -0
- package/dist/observability/engine/phase-subsets.test.js +24 -0
- package/dist/observability/engine/phase-subsets.test.js.map +1 -0
- package/dist/observability/engine/redact.d.ts +9 -0
- package/dist/observability/engine/redact.d.ts.map +1 -0
- package/dist/observability/engine/redact.js +134 -0
- package/dist/observability/engine/redact.js.map +1 -0
- package/dist/observability/engine/redact.test.d.ts +2 -0
- package/dist/observability/engine/redact.test.d.ts.map +1 -0
- package/dist/observability/engine/redact.test.js +244 -0
- package/dist/observability/engine/redact.test.js.map +1 -0
- package/dist/observability/engine/stall.d.ts +13 -0
- package/dist/observability/engine/stall.d.ts.map +1 -0
- package/dist/observability/engine/stall.js +167 -0
- package/dist/observability/engine/stall.js.map +1 -0
- package/dist/observability/engine/stall.test.d.ts +2 -0
- package/dist/observability/engine/stall.test.d.ts.map +1 -0
- package/dist/observability/engine/stall.test.js +148 -0
- package/dist/observability/engine/stall.test.js.map +1 -0
- package/dist/observability/engine/synthesizer.d.ts +35 -0
- package/dist/observability/engine/synthesizer.d.ts.map +1 -0
- package/dist/observability/engine/synthesizer.js +298 -0
- package/dist/observability/engine/synthesizer.js.map +1 -0
- package/dist/observability/engine/synthesizer.test.d.ts +2 -0
- package/dist/observability/engine/synthesizer.test.d.ts.map +1 -0
- package/dist/observability/engine/synthesizer.test.js +183 -0
- package/dist/observability/engine/synthesizer.test.js.map +1 -0
- package/dist/observability/engine/types.d.ts +422 -0
- package/dist/observability/engine/types.d.ts.map +1 -0
- package/dist/observability/engine/types.js +3 -0
- package/dist/observability/engine/types.js.map +1 -0
- package/dist/observability/engine/types.test.d.ts +2 -0
- package/dist/observability/engine/types.test.d.ts.map +1 -0
- package/dist/observability/engine/types.test.js +16 -0
- package/dist/observability/engine/types.test.js.map +1 -0
- package/dist/observability/renderers/_lib.d.ts +7 -0
- package/dist/observability/renderers/_lib.d.ts.map +1 -0
- package/dist/observability/renderers/_lib.js +28 -0
- package/dist/observability/renderers/_lib.js.map +1 -0
- package/dist/observability/renderers/dashboard.d.ts +5 -0
- package/dist/observability/renderers/dashboard.d.ts.map +1 -0
- package/dist/observability/renderers/dashboard.js +100 -0
- package/dist/observability/renderers/dashboard.js.map +1 -0
- package/dist/observability/renderers/dashboard.test.d.ts +2 -0
- package/dist/observability/renderers/dashboard.test.d.ts.map +1 -0
- package/dist/observability/renderers/dashboard.test.js +142 -0
- package/dist/observability/renderers/dashboard.test.js.map +1 -0
- package/dist/observability/renderers/markdown.d.ts +4 -0
- package/dist/observability/renderers/markdown.d.ts.map +1 -0
- package/dist/observability/renderers/markdown.js +225 -0
- package/dist/observability/renderers/markdown.js.map +1 -0
- package/dist/observability/renderers/markdown.test.d.ts +2 -0
- package/dist/observability/renderers/markdown.test.d.ts.map +1 -0
- package/dist/observability/renderers/markdown.test.js +169 -0
- package/dist/observability/renderers/markdown.test.js.map +1 -0
- package/dist/observability/renderers/mmr-findings.d.ts +10 -0
- package/dist/observability/renderers/mmr-findings.d.ts.map +1 -0
- package/dist/observability/renderers/mmr-findings.js +21 -0
- package/dist/observability/renderers/mmr-findings.js.map +1 -0
- package/dist/observability/renderers/mmr-findings.test.d.ts +2 -0
- package/dist/observability/renderers/mmr-findings.test.d.ts.map +1 -0
- package/dist/observability/renderers/mmr-findings.test.js +86 -0
- package/dist/observability/renderers/mmr-findings.test.js.map +1 -0
- package/dist/observability/renderers/sidecar.d.ts +5 -0
- package/dist/observability/renderers/sidecar.d.ts.map +1 -0
- package/dist/observability/renderers/sidecar.js +51 -0
- package/dist/observability/renderers/sidecar.js.map +1 -0
- package/dist/observability/renderers/sidecar.test.d.ts +2 -0
- package/dist/observability/renderers/sidecar.test.d.ts.map +1 -0
- package/dist/observability/renderers/sidecar.test.js +77 -0
- package/dist/observability/renderers/sidecar.test.js.map +1 -0
- package/dist/observability/renderers/terminal.d.ts +7 -0
- package/dist/observability/renderers/terminal.d.ts.map +1 -0
- package/dist/observability/renderers/terminal.js +96 -0
- package/dist/observability/renderers/terminal.js.map +1 -0
- package/dist/observability/renderers/terminal.test.d.ts +2 -0
- package/dist/observability/renderers/terminal.test.d.ts.map +1 -0
- package/dist/observability/renderers/terminal.test.js +163 -0
- package/dist/observability/renderers/terminal.test.js.map +1 -0
- package/dist/project/adopt.d.ts.map +1 -1
- package/dist/project/adopt.js +3 -1
- package/dist/project/adopt.js.map +1 -1
- package/dist/project/detectors/coverage.test.js +3 -2
- package/dist/project/detectors/coverage.test.js.map +1 -1
- package/dist/project/detectors/disambiguate.js +1 -1
- package/dist/project/detectors/disambiguate.js.map +1 -1
- package/dist/project/detectors/index.d.ts.map +1 -1
- package/dist/project/detectors/index.js +2 -0
- package/dist/project/detectors/index.js.map +1 -1
- package/dist/project/detectors/resolve-detection.test.js +57 -0
- package/dist/project/detectors/resolve-detection.test.js.map +1 -1
- package/dist/project/detectors/types.d.ts +6 -2
- package/dist/project/detectors/types.d.ts.map +1 -1
- package/dist/project/detectors/types.js.map +1 -1
- package/dist/project/detectors/web3.d.ts +4 -0
- package/dist/project/detectors/web3.d.ts.map +1 -0
- package/dist/project/detectors/web3.js +37 -0
- package/dist/project/detectors/web3.js.map +1 -0
- package/dist/project/detectors/web3.test.d.ts +2 -0
- package/dist/project/detectors/web3.test.d.ts.map +1 -0
- package/dist/project/detectors/web3.test.js +75 -0
- package/dist/project/detectors/web3.test.js.map +1 -0
- package/dist/state/state-manager.d.ts +7 -2
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +31 -2
- package/dist/state/state-manager.js.map +1 -1
- package/dist/state/state-manager.test.js +88 -3
- package/dist/state/state-manager.test.js.map +1 -1
- package/dist/types/config.d.ts +8 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/state.d.ts +2 -0
- package/dist/types/state.d.ts.map +1 -1
- package/dist/wizard/copy/core.d.ts.map +1 -1
- package/dist/wizard/copy/core.js +4 -0
- package/dist/wizard/copy/core.js.map +1 -1
- package/dist/wizard/copy/index.d.ts.map +1 -1
- package/dist/wizard/copy/index.js +2 -0
- package/dist/wizard/copy/index.js.map +1 -1
- package/dist/wizard/copy/types.d.ts +5 -1
- package/dist/wizard/copy/types.d.ts.map +1 -1
- package/dist/wizard/copy/types.test-d.js +7 -0
- package/dist/wizard/copy/types.test-d.js.map +1 -1
- package/dist/wizard/copy/web3.d.ts +3 -0
- package/dist/wizard/copy/web3.d.ts.map +1 -0
- package/dist/wizard/copy/web3.js +15 -0
- package/dist/wizard/copy/web3.js.map +1 -0
- package/dist/wizard/questions.d.ts +2 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +8 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +14 -0
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +1 -0
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Either way, Scaffold constructs the prompt and the target AI tool does the work.
|
|
|
29
29
|
|
|
30
30
|
**Assembly engine** — At execution time, Scaffold builds a 7-section prompt from: system metadata, the meta-prompt, knowledge base entries, project context (artifacts from prior steps), methodology settings, layered instructions, and depth-specific execution guidance.
|
|
31
31
|
|
|
32
|
-
**Knowledge base** —
|
|
32
|
+
**Knowledge base** — 267 domain expertise entries in `content/knowledge/` organized in nineteen categories (core, product, review, validation, finalization, execution, tools, game, web-app, backend, cli, library, mobile-app, data-pipeline, ml, browser-extension, research, data-science, web3) covering testing strategy, domain modeling, API design, security best practices, eval craft, TDD execution, task claiming, worktree management, release management, rendering strategies, data stores, CLI patterns, game engines, library bundling, mobile deployment, batch and streaming pipelines, model training and serving, browser extension manifests and service workers, data-science reproducibility and notebook discipline, smart-contract security and audit workflow, and more. These get injected into prompts based on each step's `knowledge-base` frontmatter field. Knowledge files with a `## Deep Guidance` section are optimized for CLI assembly — only the deep guidance content is loaded, avoiding redundancy with the prompt text. Teams can add project-local overrides in `.scaffold/knowledge/` that layer on top of the global entries.
|
|
33
33
|
|
|
34
34
|
**Methodology presets** — Three built-in presets control which steps run and how deep the analysis goes:
|
|
35
35
|
- **deep** (depth 5) — all steps enabled, exhaustive analysis
|
|
@@ -368,7 +368,7 @@ Every `scaffold init` wizard question can be answered via CLI flags, making scaf
|
|
|
368
368
|
| `--depth` | 1-5 | Custom methodology depth (requires `--methodology custom`) |
|
|
369
369
|
| `--adapters` | comma-sep | AI adapters: claude-code, codex, gemini |
|
|
370
370
|
| `--traits` | comma-sep | Project traits: web, mobile |
|
|
371
|
-
| `--project-type` | string | web-app, mobile-app, backend, cli, library, game, data-pipeline, ml, browser-extension, research, data-science |
|
|
371
|
+
| `--project-type` | string | web-app, mobile-app, backend, cli, library, game, data-pipeline, ml, browser-extension, research, data-science, web3 |
|
|
372
372
|
| `--auto` | boolean | Non-interactive mode (uses Zod defaults for unset flags) |
|
|
373
373
|
|
|
374
374
|
#### Web-App Config Flags (require `--project-type web-app` or auto-set it)
|
|
@@ -465,6 +465,14 @@ Data science has one forward-compatible config field in the schema, defaulted au
|
|
|
465
465
|
|------|------|--------|
|
|
466
466
|
| `dataScienceConfig.audience` | `solo` | Default (applied by the wizard and `--auto`). Covers the DS-1 audience (solo / small-team, local-first, prototyping). A future DS-2 release will extend the enum with `'platform'` (platform-engineered / larger-team DS) additively, without breaking existing configs. |
|
|
467
467
|
|
|
468
|
+
#### Web3 Config (`--project-type web3`)
|
|
469
|
+
|
|
470
|
+
Web3 has one forward-compatible config field in the schema, defaulted automatically — no CLI flags are needed in v1:
|
|
471
|
+
|
|
472
|
+
| Config field | Values | Notes |
|
|
473
|
+
|------|------|--------|
|
|
474
|
+
| `web3Config.scope` | `contracts` | Default (applied by the wizard and `--auto`). Covers the W3-1 audience (smart-contract / protocol projects on EVM chains — Foundry / Hardhat). A future W3-2 release will extend the enum with `'dapp'` (web3 application / dApp) additively, without breaking existing configs. |
|
|
475
|
+
|
|
468
476
|
#### Game Config Flags (require `--project-type game` or auto-set it)
|
|
469
477
|
|
|
470
478
|
| Flag | Type | Values |
|
|
@@ -514,7 +522,7 @@ during assembly.
|
|
|
514
522
|
|
|
515
523
|
- **Flag > auto > interactive**: Flags always take highest precedence. `--auto --engine unreal` uses defaults for everything except engine.
|
|
516
524
|
- **Partial flags + interactive**: Provide some flags and the wizard asks only the remaining questions. `scaffold init --project-type game --engine unreal` prompts interactively for multiplayer, platforms, etc.
|
|
517
|
-
- **Type-specific flags auto-set project type**: `--engine unity` automatically sets `--project-type game`, `--web-rendering ssr` sets `--project-type web-app`, `--backend-api-style rest` sets `--project-type backend`, `--cli-interactivity hybrid` sets `--project-type cli`, `--lib-visibility public` sets `--project-type library`, `--mobile-platform ios` sets `--project-type mobile-app`, `--pipeline-processing batch` sets `--project-type data-pipeline`, `--ml-phase training` sets `--project-type ml`, `--ext-manifest 3` sets `--project-type browser-extension`, `--research-driver code-driven` sets `--project-type research`. Error if conflicting type. (Data science currently
|
|
525
|
+
- **Type-specific flags auto-set project type**: `--engine unity` automatically sets `--project-type game`, `--web-rendering ssr` sets `--project-type web-app`, `--backend-api-style rest` sets `--project-type backend`, `--cli-interactivity hybrid` sets `--project-type cli`, `--lib-visibility public` sets `--project-type library`, `--mobile-platform ios` sets `--project-type mobile-app`, `--pipeline-processing batch` sets `--project-type data-pipeline`, `--ml-phase training` sets `--project-type ml`, `--ext-manifest 3` sets `--project-type browser-extension`, `--research-driver code-driven` sets `--project-type research`. Error if conflicting type. (Data science and web3 currently have no dedicated CLI flags — pass `--project-type data-science` or `--project-type web3` directly.)
|
|
518
526
|
- **Cannot mix flag families**: `--web-rendering ssr --backend-api-style rest` is an error. Each flag family (`--web-*`, `--backend-*`, `--cli-*`, `--lib-*`, `--mobile-*`, `--pipeline-*`, `--ml-*`, `--research-*`, `--ext-*`, game) is exclusive.
|
|
519
527
|
- **Validation**: `--depth` requires `--methodology custom`. `--online-services` requires `--multiplayer online` or `hybrid`. SSR/hybrid rendering is incompatible with static deploy target. Session auth requires server state (not static). ML inference projects must specify a serving pattern. Browser extensions must declare at least one capability (UI surface, content script, or background worker). Notebook-driven research cannot be fully autonomous.
|
|
520
528
|
|
|
@@ -610,6 +618,9 @@ scaffold init --auto --methodology deep --project-type research \
|
|
|
610
618
|
# Solo / small-team data science project (reproducibility-first)
|
|
611
619
|
scaffold init --auto --methodology deep --project-type data-science
|
|
612
620
|
|
|
621
|
+
# Web3 smart-contract / protocol project (Foundry / Hardhat on EVM)
|
|
622
|
+
scaffold init --auto --methodology deep --project-type web3
|
|
623
|
+
|
|
613
624
|
# Multiplayer mobile game with Unity
|
|
614
625
|
scaffold init --project-type game --methodology deep --auto \
|
|
615
626
|
--engine unity --multiplayer online --target-platforms ios,android \
|
|
@@ -636,7 +647,7 @@ Scaffold supports **project-type overlays** — domain-specific knowledge and pi
|
|
|
636
647
|
|
|
637
648
|
- **Injects domain knowledge** into existing pipeline steps (e.g., SSR caching strategies into `tech-stack`, API pagination patterns into `coding-standards`)
|
|
638
649
|
|
|
639
|
-
The game overlay additionally adjusts step enablement, remaps artifact references, and adds dependency overrides (because game development has fundamentally different artifacts). The web-app, backend, CLI, library, mobile-app, data-pipeline, ML, browser-extension, research,
|
|
650
|
+
The game overlay additionally adjusts step enablement, remaps artifact references, and adds dependency overrides (because game development has fundamentally different artifacts). The web-app, backend, CLI, library, mobile-app, data-pipeline, ML, browser-extension, research, data-science, and web3 overlays are **knowledge-only** — they inject domain expertise into existing steps without changing which steps run or how they depend on each other. The research type additionally supports **domain sub-overlays** (quant-finance, ml-research, simulation) that layer domain-specific knowledge on top of the core research overlay, and the backend type supports a `fintech` sub-overlay. Both research and backend accept `domain` as either a single string or an array (e.g. `domain: ['quant-finance', 'simulation']`) for stacking multiple sub-overlays; the wizard and CLI flags remain single-select in v1, so multi-domain stacking requires hand-editing `.scaffold/config.yml`.
|
|
640
651
|
|
|
641
652
|
Overlays are composable with methodology presets. An MVP web-app gets fewer steps at lower depth; a deep backend project gets exhaustive analysis of every architectural decision.
|
|
642
653
|
|
|
@@ -652,6 +663,7 @@ Overlays are composable with methodology presets. An MVP web-app gets fewer step
|
|
|
652
663
|
| `browser-extension` | `browser-extension-overlay.yml` | 12 entries (architecture, manifest configuration, service workers, content scripts, cross-browser, store submission, testing, security) | Manifest version, UI surfaces, content script, background worker |
|
|
653
664
|
| `research` | `research-overlay.yml` + domain sub-overlays | 25 entries (experiment loop, tracking, overfitting prevention, backtesting, risk metrics, architecture search, simulation) | Experiment driver, interaction mode, domain, experiment tracking |
|
|
654
665
|
| `data-science` | `data-science-overlay.yml` | 13 entries (reproducibility, experiment tracking, notebook discipline, model evaluation, data versioning, dev environment, observability, project structure, conventions, requirements, security, testing, architecture) | Audience (`solo` default; `platform` reserved for DS-2) |
|
|
666
|
+
| `web3` | `web3-overlay.yml` | 14 entries (Foundry tooling, smart-contract security, upgradeability, gas optimization, oracles, audit workflow, deployment, testing patterns, EVM fundamentals, ABI/interface design, event/log indexing, supply-chain) | Scope (`contracts` default; `dapp` reserved for W3-2) |
|
|
655
667
|
| `game` | `game-overlay.yml` | 24 entries (engines, networking, audio, VR/AR, economy, save systems, certification) | Engine, multiplayer, platforms, economy, narrative, and 6 more |
|
|
656
668
|
|
|
657
669
|
### Game Development
|
|
@@ -737,7 +749,7 @@ These answers control which conditional steps activate. A single-player puzzle g
|
|
|
737
749
|
|
|
738
750
|
#### Multi-type Detection
|
|
739
751
|
|
|
740
|
-
`scaffold adopt` detects
|
|
752
|
+
`scaffold adopt` detects 12 project types from manifest files and directory layouts:
|
|
741
753
|
|
|
742
754
|
| Type | Key Signals |
|
|
743
755
|
|------|-------------|
|
|
@@ -752,6 +764,7 @@ These answers control which conditional steps activate. A single-player puzzle g
|
|
|
752
764
|
| `browser-extension` | `manifest.json` with `manifest_version` field |
|
|
753
765
|
| `research` | `program.md` + `results.tsv`, backtest/strategy files with trading deps, optimization deps + experiment dirs, simulation framework deps |
|
|
754
766
|
| `data-science` | Marimo signals required (`marimo` dep or `.marimo.toml`); DVC (`dvc.yaml`, `.dvc/config`, `dvc` py dep) is supplementary evidence only. Low-tier; defers to `ml` / `research` / `data-pipeline` when those match at medium/high tier |
|
|
767
|
+
| `web3` | `foundry.toml` or `hardhat.config.{ts,js,cjs,mjs}` (medium-tier); `remappings.txt`, `lib/forge-std` are supplementary low-tier signals. EVM-only scope. Library-collision boundary pinned by tiebreak (high-tier `library` wins over medium-tier `web3` for published-library Hardhat projects) |
|
|
755
768
|
|
|
756
769
|
Each detector returns a confidence tier (high/medium/low) with evidence trails. Override detection with `--project-type <type>`.
|
|
757
770
|
|
|
@@ -1311,6 +1324,124 @@ At depth 1-3, reviews are Claude-only — still thorough with multiple passes, b
|
|
|
1311
1324
|
- **At least one additional CLI** — Codex, Gemini, and/or Claude CLI. All three dispatched independently as MMR channels when available. Missing Codex or Gemini channels fall back to compensating Claude passes (labeled `[compensating: Codex-equivalent]` / `[compensating: Gemini-equivalent]`, single-source confidence); if Claude itself is unavailable, the review proceeds with the remaining channels — MMR does not compensate for a missing Claude channel.
|
|
1312
1325
|
- **Valid authentication** — Scaffold checks before every dispatch (run `mmr config test` to pre-flight all three at once) and tells you if credentials need refreshing
|
|
1313
1326
|
|
|
1327
|
+
## Build Observability
|
|
1328
|
+
|
|
1329
|
+
Build observability gives you a live view of what is happening during a multi-agent implementation run — who claimed what task, which decisions were made, where work is stalled, and whether the codebase conforms to your standards. All data lives under `.scaffold/` and never interferes with the normal pipeline.
|
|
1330
|
+
|
|
1331
|
+
### Ledger events
|
|
1332
|
+
|
|
1333
|
+
```bash
|
|
1334
|
+
scaffold observe event task_claimed --task-id T-42 --task-title "Add auth flow"
|
|
1335
|
+
scaffold observe event decision_made --task-id T-42 --decision "Use JWT" --rationale "Stateless auth for microservices"
|
|
1336
|
+
scaffold observe event task_completed --task-id T-42
|
|
1337
|
+
scaffold observe event pr_opened --pr-number 17
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
Events are written to `.scaffold/activity.jsonl` (append-only, lockfile-safe). Each event is stamped with a worktree ID, actor label, branch, and ISO-8601 timestamp.
|
|
1341
|
+
|
|
1342
|
+
### Progress snapshot
|
|
1343
|
+
|
|
1344
|
+
```bash
|
|
1345
|
+
scaffold observe progress # terminal snapshot
|
|
1346
|
+
scaffold observe progress --replay # full timeline with git/PR/test events fused in
|
|
1347
|
+
scaffold observe progress --no-stall-check # suppress the Needs Attention surface
|
|
1348
|
+
scaffold observe progress --output=docs/status.md # write to a custom path
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
`--replay` fuses the ledger with six adapter event streams (git, GitHub, MMR, pipeline state, test results, pipeline docs) into a chronological timeline. The "Needs Attention" surface fires when work appears stalled — unclaimed tasks, unreviewed PRs, unresolved blockers, or repeated lens skips.
|
|
1352
|
+
|
|
1353
|
+
### Audit
|
|
1354
|
+
|
|
1355
|
+
```bash
|
|
1356
|
+
scaffold observe audit # run all eight lenses (A–H)
|
|
1357
|
+
scaffold observe audit --scope=code # lenses A–G (code conformance only)
|
|
1358
|
+
scaffold observe audit --scope=docs # lens H (cross-document consistency only)
|
|
1359
|
+
scaffold observe audit --profile=full # include LLM-graded Lens H sub-checks
|
|
1360
|
+
scaffold observe audit --lens G-decisions # run a single lens
|
|
1361
|
+
scaffold observe audit --output=docs/audit.md # write to a custom path
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
Eight lenses cover TDD evidence (A), acceptance-criteria coverage (B), coding-standards compliance (C), stack conformance (D), design-token usage (E), scope coverage (F), decision-log completeness (G), and cross-document consistency (H). Findings get stable IDs for cross-run tracking:
|
|
1365
|
+
|
|
1366
|
+
```bash
|
|
1367
|
+
scaffold observe ack abc123 --note "accepted, tracked in INFRA-77"
|
|
1368
|
+
scaffold observe ack abc123 --status open # reopen a previously acknowledged finding
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
`scaffold observe audit` exits 1 when the verdict is `blocked` (findings at or above `fix_threshold`). This makes it composable as a CI gate.
|
|
1372
|
+
|
|
1373
|
+
### Automated fix flow
|
|
1374
|
+
|
|
1375
|
+
```bash
|
|
1376
|
+
scaffold observe audit --fix
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
After the initial audit, Scaffold dispatches an AI agent for each blocking finding, verifies the fix, and writes a post-fix report:
|
|
1380
|
+
|
|
1381
|
+
1. **Plan** — filter and sort blocking findings (P0 first, then by lens).
|
|
1382
|
+
2. **Dispatch** — spawn the configured fix agent (default: `claude -p`) with the finding prompt on stdin. Output is live-streamed so you see what the agent is doing.
|
|
1383
|
+
3. **Verify** — re-run the finding's lens to confirm the issue is resolved (up to `per_finding_max_attempts` retries).
|
|
1384
|
+
4. **Report** — run a full post-fix audit and write `docs/audits/<id>-postfix.{md,json}`.
|
|
1385
|
+
|
|
1386
|
+
Press Ctrl-C at any time — Scaffold reverts any staged changes the fix agents made and re-applies your original WIP, leaving the working tree unchanged.
|
|
1387
|
+
|
|
1388
|
+
Configure via `.scaffold/observability.yaml`:
|
|
1389
|
+
```yaml
|
|
1390
|
+
fix:
|
|
1391
|
+
dispatcher_command: "claude -p" # change to any CLI that reads stdin
|
|
1392
|
+
timeout_s: 300
|
|
1393
|
+
per_finding_max_attempts: 3
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
### Worktree harvest and teardown
|
|
1397
|
+
|
|
1398
|
+
When running parallel agents in separate worktrees, collect their activity before removing the worktree:
|
|
1399
|
+
|
|
1400
|
+
```bash
|
|
1401
|
+
# Harvest a live worktree's ledger into the central archive
|
|
1402
|
+
scaffold observe harvest --worktree=../my-feature-worktree
|
|
1403
|
+
|
|
1404
|
+
# Rotate stale archive entries (worktrees that no longer exist)
|
|
1405
|
+
scaffold observe harvest --recover
|
|
1406
|
+
|
|
1407
|
+
# Full teardown: harvest + remove worktree + delete branch
|
|
1408
|
+
scripts/teardown-agent-worktree.sh ../my-feature-worktree
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
Harvested events accumulate in `.scaffold/activity-archive/`. `--recover` scans for entries whose worktree is gone and rotates them into monthly `YYYY-MM.jsonl` archives.
|
|
1412
|
+
|
|
1413
|
+
### Dashboard
|
|
1414
|
+
|
|
1415
|
+
```bash
|
|
1416
|
+
scripts/generate-dashboard.sh
|
|
1417
|
+
```
|
|
1418
|
+
|
|
1419
|
+
The generated dashboard includes "Build Progress" and "Audit" panels that refresh from the sidecar JSON files. Each panel shows stall signals, a timeline, findings by severity, and lens skip counts.
|
|
1420
|
+
|
|
1421
|
+
### Phase-boundary audits
|
|
1422
|
+
|
|
1423
|
+
`scaffold complete <step>` automatically runs a cross-document audit (Lens H) after completing any phase-boundary step — `user-stories`, `tech-stack`, `coding-standards`, `design-system`, `implementation-plan`, or `implementation-playbook`. The audit result is printed inline (`[audit] N findings (verdict=…) — see docs/audits/…`) but never blocks the state transition.
|
|
1424
|
+
|
|
1425
|
+
Configure via `.scaffold/observability.yaml`:
|
|
1426
|
+
```yaml
|
|
1427
|
+
phase_audit:
|
|
1428
|
+
enabled: true # set false to disable
|
|
1429
|
+
timeout_s: 60
|
|
1430
|
+
detached: false # true = fire-and-forget (non-blocking always)
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
### MMR `doc-conformance` channel
|
|
1434
|
+
|
|
1435
|
+
```bash
|
|
1436
|
+
mmr review --channels=doc-conformance
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
Runs `scaffold observe audit` as a built-in MMR channel, mapping findings to MMR's Finding shape with stable composite location IDs (`<source_doc>::<lens_id>::<short_id>`). Disabled by default — enable per-project in `.mmr.yaml`:
|
|
1440
|
+
```yaml
|
|
1441
|
+
channels_enabled:
|
|
1442
|
+
- doc-conformance
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1314
1445
|
## Methodology Presets
|
|
1315
1446
|
|
|
1316
1447
|
Not every project needs all 60 steps. Choose a methodology when you run `scaffold init`:
|
|
@@ -1394,7 +1525,7 @@ scaffold dashboard
|
|
|
1394
1525
|
|
|
1395
1526
|
## Knowledge System
|
|
1396
1527
|
|
|
1397
|
-
Scaffold ships with
|
|
1528
|
+
Scaffold ships with 267 domain expertise entries organized in nineteen categories:
|
|
1398
1529
|
|
|
1399
1530
|
- **core/** (26 entries) — eval craft, testing strategy, domain modeling, API design, database design, system architecture, ADR craft, security best practices, operations, task decomposition, user stories, UX specification, design system tokens, user story innovation, AI memory management, coding conventions, tech stack selection, project structure patterns, task tracking, CLAUDE.md patterns, multi-model review dispatch, review step template, dev environment, git workflow patterns, automated review tooling, vision craft
|
|
1400
1531
|
- **product/** (5 entries) — PRD craft, PRD innovation, gap analysis, vision craft, vision innovation
|
|
@@ -1414,6 +1545,7 @@ Scaffold ships with 235 domain expertise entries organized in eighteen categorie
|
|
|
1414
1545
|
- **browser-extension/** (12 entries) — Manifest V3, content scripts, service workers, cross-browser compatibility, extension security, store submission
|
|
1415
1546
|
- **research/** (25 entries) — experiment loop architecture, parameter optimization, overfitting prevention, experiment tracking, security/sandboxing; domain knowledge for quant-finance (backtesting, risk metrics, market data, strategy patterns), ML-research (architecture search, ablation studies, evaluation), and simulation (engine integration, parameter spaces, compute management)
|
|
1416
1547
|
- **data-science/** (13 entries) — reproducibility, experiment tracking, notebook discipline, model evaluation, data versioning, dev environment (Marimo/Jupyter/Hex), observability, project structure, conventions, requirements, security, testing, architecture
|
|
1548
|
+
- **web3/** (14 entries) — Foundry tooling and dev environment, smart-contract security and common vulnerabilities, upgradeability patterns, gas optimization, oracles and external data, audit workflow, deployment and verification, testing patterns, access control, EVM/contract architecture, conventions, project structure, requirements
|
|
1417
1549
|
|
|
1418
1550
|
Each pipeline step declares which knowledge entries it needs in its frontmatter. The assembly engine injects them automatically. Knowledge files with a `## Deep Guidance` section are optimized for the CLI — only the deep guidance content is loaded into the assembled prompt, skipping the summary to avoid redundancy with the prompt text.
|
|
1419
1551
|
|
|
@@ -1622,7 +1754,7 @@ All build inputs live under `content/`:
|
|
|
1622
1754
|
content/
|
|
1623
1755
|
├── pipeline/ # 60 meta-prompts organized by 16 phases (phases 0-15, including build)
|
|
1624
1756
|
├── tools/ # 10 tool meta-prompts (stateless, category: tool)
|
|
1625
|
-
├── knowledge/ #
|
|
1757
|
+
├── knowledge/ # 267 domain expertise entries (core, product, review, validation, finalization, execution, tools, game, web-app, backend, cli, library, mobile-app, data-pipeline, ml, browser-extension, research, data-science, web3)
|
|
1626
1758
|
├── methodology/ # 3 YAML presets (deep, mvp, custom)
|
|
1627
1759
|
└── skills/ # Skill templates with {{markers}} for multi-platform resolution (includes mmr)
|
|
1628
1760
|
```
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web3-access-control
|
|
3
|
+
description: Role-based access control for smart contracts — Ownable2Step, OpenZeppelin AccessControl, Safe multisig as admin, TimelockController on dangerous ops, role separation, and decentralization via renouncing
|
|
4
|
+
topics: [web3, access-control, openzeppelin, multisig, timelock]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Most contract exploits are not novel cryptography — they are either missing access-control checks ("anyone can call `setFeeRecipient`") or a single admin key getting drained, phished, or coerced. Access control is the answer to three questions: who can change what, when can they change it, and whose consent is required to authorize the change. A protocol whose answer is "the deployer EOA, immediately, alone" is a protocol one phishing email away from a post-mortem. Production-grade access control replaces each of those answers with a deliberate engineering choice: granular roles, time-locked execution windows, and multi-party signing.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Default to OpenZeppelin's `AccessControl` over `Ownable` for anything heading to mainnet — multiple narrow roles beat one omnipotent owner, and revocation lets you respond to a key compromise without a redeploy. When you do need single-admin semantics in an early prototype, use `Ownable2Step` not raw `Ownable`, so a fat-fingered transfer to a wrong address cannot brick your protocol. The admin role itself should live on a Safe multisig — 2-of-3 minimum, 3-of-5 typical for serious TVL — with signers on different hardware in different physical locations. Wrap every irreversible operation (upgrades, treasury drains, fee-cap changes, oracle swaps) in a `TimelockController` with a 24–72 hour delay so users can exit if a malicious or compromised proposal lands. Separate `MINTER_ROLE`, `PAUSER_ROLE`, `UPGRADER_ROLE`, and `TREASURER_ROLE` to distinct multisigs where the budget allows, and renounce roles you no longer need once decentralization milestones are reached.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### Ownable vs AccessControl
|
|
16
|
+
|
|
17
|
+
`Ownable` gives you exactly one privileged address — the `owner` — and one modifier, `onlyOwner`. That is fine for a hackathon contract or a private prototype. It is wrong for anything production-bound, because every privileged operation in your protocol now collapses to the same key: pausing, upgrading, withdrawing treasury, rotating oracle, minting. Compromise that key and the attacker gets everything in one transaction.
|
|
18
|
+
|
|
19
|
+
The post-mortem pattern is depressingly consistent: protocol launches with `Ownable` and a single key on a laptop, team plans to "migrate to a multisig later," the migration keeps slipping because nothing is on fire, the laptop is compromised, attacker calls every `onlyOwner` function in a single transaction, protocol is drained. The fix is not "be more careful with the key" — it is to never have a contract whose privileged surface is one key in the first place.
|
|
20
|
+
|
|
21
|
+
`AccessControl` from OpenZeppelin gives you `bytes32`-identified roles, each independently grantable and revocable, gated by an `onlyRole(ROLE)` modifier. The `DEFAULT_ADMIN_ROLE` controls grants and revocations; everything else is whatever you define. Default to `AccessControl` for production:
|
|
22
|
+
|
|
23
|
+
```solidity
|
|
24
|
+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
|
|
25
|
+
|
|
26
|
+
contract Protocol is AccessControl {
|
|
27
|
+
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
|
28
|
+
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
|
|
29
|
+
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
|
|
30
|
+
|
|
31
|
+
constructor(address admin) {
|
|
32
|
+
_grantRole(DEFAULT_ADMIN_ROLE, admin); // admin can grant/revoke others
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
|
|
36
|
+
// ...
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`AccessControl` also gives you `hasRole(role, account)` for off-chain queries (your indexer, your dashboard, your audit checklist), `getRoleAdmin(role)` for understanding the grant hierarchy, and the standard `RoleGranted` / `RoleRevoked` / `RoleAdminChanged` events for the kind of monitoring an EOA owner cannot offer. The auditability of the system is itself a security property — when every grant lands as an indexed event, missing or unexpected role grants are observable, not invisible.
|
|
42
|
+
|
|
43
|
+
`Ownable` makes sense in exactly one narrow case: a brand-new contract where you genuinely have only one privileged action and you will migrate to `AccessControl` before mainnet. Anything else, reach for `AccessControl` from the first commit.
|
|
44
|
+
|
|
45
|
+
The two are not mutually exclusive at the protocol level — you can have an `Ownable2Step` factory that deploys `AccessControl`-based clones — but pick one as the canonical access pattern for any given contract. Mixing `onlyOwner` and `onlyRole(DEFAULT_ADMIN_ROLE)` in the same contract is a footgun: reviewers have to mentally re-derive which check actually gates a given function, and a future maintainer will inevitably gate one function with the wrong modifier.
|
|
46
|
+
|
|
47
|
+
### Two-step ownership transfer (Ownable2Step)
|
|
48
|
+
|
|
49
|
+
If you must use `Ownable`, use `Ownable2Step`. Raw `Ownable.transferOwnership(newOwner)` is a single-call transfer: type the wrong address (zero-knowledge-proof copy-paste hash, address with one character flipped, an address that is actually a contract that cannot accept ownership) and your protocol is now owned by a black hole. There is no recovery.
|
|
50
|
+
|
|
51
|
+
`Ownable2Step` requires the recipient to actively `acceptOwnership` in a second transaction:
|
|
52
|
+
|
|
53
|
+
```solidity
|
|
54
|
+
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
|
|
55
|
+
|
|
56
|
+
contract Treasury is Ownable2Step {
|
|
57
|
+
constructor(address initialOwner) Ownable(initialOwner) {}
|
|
58
|
+
// existing owner calls transferOwnership(newOwner); pendingOwner = newOwner
|
|
59
|
+
// newOwner calls acceptOwnership(); owner = newOwner, pendingOwner = 0
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The pending-owner pattern means a wrong address simply never accepts, the transfer expires harmlessly, and you try again. Never deploy raw `Ownable` to mainnet — the fat-fingering risk is real and the cost of `Ownable2Step` is one extra transaction at handoff time. The same two-step pattern applies inside `AccessControl` for admin-role transfers: grant the new admin first, verify they hold the role, then revoke the old one. Atomic role swaps with no overlap are the same hazard as one-step ownership transfers.
|
|
64
|
+
|
|
65
|
+
Real-world incidents have happened where a team intended to transfer ownership to a multisig but pasted an address that was actually a contract with no payable fallback, or an address from the wrong network's deployment — in both cases the new owner could neither act on the contract nor recover from the mistake. Two-step transfer makes both classes of error visible during the pending window: the recipient calls `acceptOwnership` from the intended address or the handoff fails safely.
|
|
66
|
+
|
|
67
|
+
A quick decision tree for new contracts:
|
|
68
|
+
|
|
69
|
+
- One privileged action, prototype scope, throwaway deploy → `Ownable` is fine.
|
|
70
|
+
- One privileged action, mainnet deploy, single admin → `Ownable2Step` only.
|
|
71
|
+
- Two or more privileged actions, or any mainnet deploy with meaningful TVL → `AccessControl`.
|
|
72
|
+
- Any contract that may be upgraded later → `AccessControl` from day one, because retrofitting roles into a deployed `Ownable` contract via upgrade is painful and error-prone.
|
|
73
|
+
|
|
74
|
+
### Roles via OpenZeppelin AccessControl
|
|
75
|
+
|
|
76
|
+
Roles are `bytes32` identifiers, by convention `keccak256("ROLE_NAME")`. Define each role as a `public constant` so off-chain tooling can read it from the ABI. Use `_grantRole` and `_revokeRole` inside the constructor or admin functions; user-facing grants go through `grantRole(role, account)` which is gated by the role's admin (defaults to `DEFAULT_ADMIN_ROLE`). Never hand-compute the bytes32 literal in code reviews — paste the keccak256 call instead, because reviewers compare role definitions across files by their string name, not by their hash.
|
|
77
|
+
|
|
78
|
+
```solidity
|
|
79
|
+
bytes32 public constant TREASURER_ROLE = keccak256("TREASURER_ROLE");
|
|
80
|
+
|
|
81
|
+
constructor(address adminMultisig, address treasurerMultisig) {
|
|
82
|
+
_grantRole(DEFAULT_ADMIN_ROLE, adminMultisig);
|
|
83
|
+
_grantRole(TREASURER_ROLE, treasurerMultisig);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function withdrawFees(address to, uint256 amount) external onlyRole(TREASURER_ROLE) {
|
|
87
|
+
// ...
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A subtle hazard with `_grantRole` in constructors: if you grant `DEFAULT_ADMIN_ROLE` to two addresses (say, an EOA deployer and the eventual multisig), you have just doubled your attack surface for the entire lifetime of the contract. Grant exactly one admin from the constructor, then have that admin perform any further grants in a subsequent transaction so the on-chain history records each privilege handoff as a distinct event.
|
|
92
|
+
|
|
93
|
+
Set distinct admin roles per role with `_setRoleAdmin(MINTER_ROLE, MINTER_ADMIN_ROLE)` when you want minter grants to require a different multisig than the protocol-wide admin. That extra layer is worth it for roles that are commonly granted (a permissioned launchpad granting minter rights to dozens of partners) so the global admin key is not constantly touched.
|
|
94
|
+
|
|
95
|
+
Emit and index the standard `RoleGranted` and `RoleRevoked` events that `AccessControl` provides for free — your off-chain monitoring (Tenderly, OpenZeppelin Defender, custom indexer) should page on any role-membership change so an unexpected grant cannot land silently. Off-chain visibility is half the value of on-chain access control: a granted role nobody noticed is the same as an unconfigured permission. Pair this with a published `roles.md` listing the canonical holder of each role so the community can independently verify that on-chain reality matches the documented governance.
|
|
96
|
+
|
|
97
|
+
A common mistake worth calling out explicitly: granting `DEFAULT_ADMIN_ROLE` to the `msg.sender` of the constructor when deploying from a CI script. The deployer EOA now permanently holds the protocol's most powerful role, and unless step two of your deploy script is a `grantRole(DEFAULT_ADMIN_ROLE, multisig)` followed by `renounceRole(DEFAULT_ADMIN_ROLE, msg.sender)` in the same forge script, you have a window where the deployer key gates everything. Bake the multisig address into the constructor or deploy script parameters and grant the admin role to the multisig directly from the constructor; never use the deployer EOA as a permanent admin.
|
|
98
|
+
|
|
99
|
+
### Multisig (Safe)
|
|
100
|
+
|
|
101
|
+
Every privileged role on mainnet goes behind a Safe — `safe.global`, formerly Gnosis Safe — never an EOA. A Safe is itself a smart contract that requires `M-of-N` signatures from configured signer addresses before executing any transaction. The trade-off is simple: an EOA owner is a single phishing email or compromised laptop away from a drained protocol; a 3-of-5 Safe survives any two signers being compromised or unavailable.
|
|
102
|
+
|
|
103
|
+
Defaults worth memorizing:
|
|
104
|
+
|
|
105
|
+
- **2-of-3** — minimum acceptable for any mainnet deployment with non-trivial value. Three signers, two required. Cheap to operate, survives one compromise.
|
|
106
|
+
- **3-of-5** — typical for serious TVL. Five signers (ideally with operational diversity — different team members, different geographies, different hardware vendors), three required. Survives two compromises and tolerates one signer being on vacation.
|
|
107
|
+
- **4-of-7 or 5-of-9** — for protocols with eight-or-nine-figure TVL or those acting as governance for a DAO treasury.
|
|
108
|
+
|
|
109
|
+
Signer hygiene matters as much as the threshold. Every signer holds their key on a hardware wallet (Ledger, GridPlus, Keystone) — never a hot wallet, never a browser extension as the sole layer. Rotate signers when team members leave: `addOwnerWithThreshold` and `removeOwner` are governance operations, not afterthoughts. Publish the current signer set and threshold in your docs so users can verify the Safe matches your stated security model. The Safe should hold `DEFAULT_ADMIN_ROLE` and any sensitive operational role; passing `safe.address` as the admin in the constructor is the standard pattern.
|
|
110
|
+
|
|
111
|
+
Two operational practices worth wiring up at deployment time:
|
|
112
|
+
|
|
113
|
+
- **Rehearse a signing ceremony before mainnet.** Have every signer connect their hardware, review the transaction in Safe's UI, simulate via Tenderly, and sign — for a no-op transaction like a self-pause-and-unpause. The first time someone signs should not be the night of an incident, and unfamiliar tooling is how `_setImplementation` gets signed by accident.
|
|
114
|
+
- **Threshold-aware time budget.** A 3-of-5 across three time zones means the realistic time-to-execute is hours, not minutes. Size your `Pausable` kill-switch on a tighter 2-of-3 ops Safe so emergency pauses are fast, and keep the slow `DEFAULT_ADMIN_ROLE` on the wider Safe.
|
|
115
|
+
|
|
116
|
+
### Timelock (TimelockController)
|
|
117
|
+
|
|
118
|
+
A multisig stops one compromised key. A timelock stops a compromised multisig — or, more commonly, a coerced one. `TimelockController` from OpenZeppelin sits between the multisig and the protocol: dangerous operations are first `schedule`d (recording the call hash and a delay), then `execute`d after the delay elapses. During the delay, the call is publicly visible on-chain; if it is malicious, users have time to exit.
|
|
119
|
+
|
|
120
|
+
Coercion is the often-overlooked threat model. A 3-of-5 Safe defeats one or two compromised keys but does not defeat a court order, a physical threat to multiple signers in the same jurisdiction, or social-engineering of a quorum through a coordinated phishing campaign. The timelock buys 48 hours during which the broader community can observe the proposed action, raise the alarm, and exit positions even if every signer wanted the operation to land. It also creates a real audit trail for any future post-mortem about whether the operation was legitimate.
|
|
121
|
+
|
|
122
|
+
```solidity
|
|
123
|
+
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
|
|
124
|
+
|
|
125
|
+
// proposers: addresses that can schedule (typically the Safe multisig)
|
|
126
|
+
// executors: addresses that can execute after delay (often address(0) for "anyone")
|
|
127
|
+
// minDelay: seconds — 48 hours is typical for protocol-altering ops
|
|
128
|
+
address[] memory proposers = new address[](1);
|
|
129
|
+
proposers[0] = safeMultisig;
|
|
130
|
+
address[] memory executors = new address[](1);
|
|
131
|
+
executors[0] = address(0); // anyone can execute after the delay
|
|
132
|
+
TimelockController timelock = new TimelockController(
|
|
133
|
+
48 hours, // delay
|
|
134
|
+
proposers,
|
|
135
|
+
executors,
|
|
136
|
+
address(0) // admin — set to 0 to lock the timelock's own params
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Note the `address(0)` admin: passing zero means the timelock cannot have its own parameters (delay, proposer set, executor set) changed after deployment except through the timelock itself. That bootstraps a system where any change to the timelock's own configuration has to go through the timelock's delay — including a change to the delay itself. Leaving the admin set to an EOA or even the Safe means the multisig can shorten the delay with no warning, defeating the whole point.
|
|
141
|
+
|
|
142
|
+
Grant the protocol's `UPGRADER_ROLE`, `DEFAULT_ADMIN_ROLE`, or treasury-drain role to the `timelock` address, not to the Safe directly. The Safe proposes via `timelock.schedule(...)`; after 48 hours, anyone calls `timelock.execute(...)`. The pause kill-switch stays on the Safe directly (you cannot time-lock an emergency); the dangerous knobs are slow. Publish a monitoring feed of all scheduled operations — Tenderly Alerts, OpenZeppelin Defender, or a custom indexer — so users do not have to watch the mempool themselves.
|
|
143
|
+
|
|
144
|
+
Calibrate the delay to your protocol's exit time. A lending market where users can withdraw collateral in one transaction can run a 24h timelock; a liquid-staking protocol with a 7-day unbonding queue needs a 7-day-plus timelock or users have no real exit window. The asymmetry to avoid: a 24h timelock on an upgrade whose effect users cannot exit from in 24h is security theater. Pair the timelock with an `OperationCancelled` event so the multisig can publicly back out of a proposal mid-window if it discovers a mistake, and so users can verify cancellations rather than guess.
|
|
145
|
+
|
|
146
|
+
One pitfall worth noting: the timelock's `execute` permission. If `executors` is set to a specific address rather than `address(0)`, only that address can execute scheduled operations. Setting it to the multisig means a hostile multisig can simply refuse to execute a beneficial proposal it scheduled; setting it to `address(0)` means anyone can execute after the delay, which is almost always what you want — the multisig commits to the operation by scheduling it, and the community guarantees execution by being able to send the final transaction themselves.
|
|
147
|
+
|
|
148
|
+
### Role separation
|
|
149
|
+
|
|
150
|
+
Granular roles only matter if they are held by genuinely different parties. A `MINTER_ROLE`, `PAUSER_ROLE`, `UPGRADER_ROLE`, and `TREASURER_ROLE` all held by the same Safe collapse back to a single point of compromise — the ABI looks like role separation, but the on-chain reality is one key gating everything. Reviewers will spot this immediately; users may not, which is precisely the kind of asymmetry that destroys trust after an incident. Where the operational budget supports it, each role goes on a different multisig:
|
|
151
|
+
|
|
152
|
+
- **`MINTER_ROLE`** — held by the issuance multisig, ideally with a `MAX_MINT_PER_PERIOD` cap enforced in-contract. Compromise mints tokens; it does not drain the treasury.
|
|
153
|
+
- **`PAUSER_ROLE`** — held by an ops multisig with lower threshold (2-of-3) for faster incident response. Compromise pauses the protocol; it does not steal funds. Often paired with a `GUARDIAN_ROLE` that can pause but not unpause, so a noisy alert can trigger a halt without exposing the unpause power to the same hot key.
|
|
154
|
+
- **`UPGRADER_ROLE`** — held by a governance multisig behind the timelock. Compromise of the multisig still requires waiting out the timelock with users watching. See `web3-upgradeability.md` for the proxy-admin coupling.
|
|
155
|
+
- **`TREASURER_ROLE`** — held by a treasury multisig with the highest threshold and most conservative signer set. Withdrawals above a configured threshold should additionally require the timelock; small operational disbursements can bypass the delay.
|
|
156
|
+
- **`FEE_RECIPIENT`** — often stored as an address rather than a role; restrict who can change it with `DEFAULT_ADMIN_ROLE` behind the timelock. The fee recipient is a common phishing target because a single-line config change can redirect every protocol fee — keep it slow.
|
|
157
|
+
|
|
158
|
+
The principle: any single multisig compromise should bound the blast radius to one role's worth of damage. A pauser key getting phished should not drain the treasury.
|
|
159
|
+
|
|
160
|
+
Smaller protocols routinely cannot afford four distinct multisigs with non-overlapping signers, and that is fine — collapse to two (an "ops" Safe for pause and an "admin" Safe for everything else) rather than one. Document the consolidation honestly in your security model: "we run a single 3-of-5 Safe for all privileged roles" is a defensible position with a clear risk profile; "we have four roles" while all four point at the same address is misleading documentation and an audit finding waiting to happen.
|
|
161
|
+
|
|
162
|
+
In-contract caps are the complement to role separation: a `MINTER_ROLE` with a `MAX_MINT_PER_DAY` ceiling is materially safer than the same role without one, because a compromised minter cannot exceed the cap in a single block. Caps on the most blast-prone roles (mint rate, fee ceiling, oracle drift tolerance, withdrawal-from-treasury rate) are cheap to add at deploy time and very hard to bolt on after an incident.
|
|
163
|
+
|
|
164
|
+
### Renouncing roles
|
|
165
|
+
|
|
166
|
+
`renounceRole(role, msg.sender)` permanently removes the calling account from a role with no recovery. Use it deliberately at decentralization milestones — once the protocol parameters are frozen and the community is governing through a DAO contract, renounce `DEFAULT_ADMIN_ROLE` from the original deployer multisig so the role becomes uncallable. The on-chain renouncement is auditable proof that the team can no longer unilaterally change the protocol.
|
|
167
|
+
|
|
168
|
+
Critically, `renounceRole` only removes the role from `msg.sender` — the caller. There is no way to renounce on behalf of someone else, which is deliberate: a stolen admin key cannot be "renounced out" by the rest of the team, only revoked through normal `revokeRole` machinery. Users sometimes confuse "the team renounced ownership" (a single account dropping its own role) with "the protocol is unowned" (no account holds the admin role). The two coincide only when the team is the sole admin and explicitly renounces. Verify on-chain via `hasRole(DEFAULT_ADMIN_ROLE, account)` for every previously-privileged address, not by reading a blog post.
|
|
169
|
+
|
|
170
|
+
```solidity
|
|
171
|
+
// after a governance vote moves admin to a DAO timelock:
|
|
172
|
+
protocol.renounceRole(protocol.DEFAULT_ADMIN_ROLE(), originalDeployerMultisig);
|
|
173
|
+
// future grants/revokes must go through the DAO
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Two cautions. First, renouncing is irreversible: if you renounce the only admin and then discover you needed it for an upgrade, you have bricked the upgrade path. Stage decentralization — grant the new admin first, exercise it once on a no-op operation to prove it works, then renounce the old. Second, renounce specific roles, not blanket admin: an "immutable" stage usually still wants a pauser for emergencies. Audit each role independently and decide which ones are safe to surrender.
|
|
177
|
+
|
|
178
|
+
A pragmatic decentralization ladder, in order of typical adoption:
|
|
179
|
+
|
|
180
|
+
1. **Deploy with EOA as admin** for the launch week, when bugs are likeliest and fast iteration matters. Tolerable only because TVL is still capped by deposit limits.
|
|
181
|
+
2. **Migrate admin to a small team Safe** (2-of-3) once the contract is stable enough that the team is no longer hot-fixing daily.
|
|
182
|
+
3. **Add a `TimelockController` between the Safe and the protocol** for upgrade and treasury operations. Pause stays on the Safe directly.
|
|
183
|
+
4. **Expand to a larger, more diverse Safe** (3-of-5 or 4-of-7) as TVL grows.
|
|
184
|
+
5. **Migrate admin to a DAO governance contract** that itself proposes through the timelock, once the community is real and voting power is sufficiently distributed.
|
|
185
|
+
6. **Renounce role-specific admin powers** as parameters are locked permanently (e.g., supply cap, fee ceiling) — turn dials into constants.
|
|
186
|
+
|
|
187
|
+
Each step is a deliberate governance event with its own announcement, monitoring window, and rollback plan. Skipping straight from step 1 to step 6 is the "rugpull-by-incompetence" pattern that bricks more protocols than malicious admins do.
|
|
188
|
+
|
|
189
|
+
See `web3-upgradeability.md` for how the proxy-admin role interacts with this access-control model, `web3-audit-workflow.md` for the operational checks an audit will run against your role wiring, and `web3-security.md` for the wider security layer this access-control model lives inside.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web3-architecture
|
|
3
|
+
description: EVM smart-contract architecture decisions — modular vs monolithic decomposition, OpenZeppelin baseline, state minimization, library vs inheritance, diamond pattern caveats, and external-call discipline
|
|
4
|
+
topics: [web3, architecture, solidity, evm, openzeppelin]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This overlay targets **EVM** chains — Ethereum mainnet, the major L2s (Arbitrum, Optimism, Base, zkSync, Polygon zkEVM), and other EVM-compatible execution layers running Solidity 0.8.x bytecode. Non-EVM ecosystems (Solana / Anchor, Aptos and Sui / Move, Cosmos / CosmWasm) have fundamentally different storage, account, and execution models and are deliberately out of scope here; a future W3-2 overlay may cover dApp / frontend work and non-EVM runtimes. Architecture in this doc means the contract-side decisions a protocol lead makes before the first `forge build`: how to decompose, what to inherit, how state is laid out, and where the trust boundaries fall.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Decompose by responsibility, not by cleverness: a protocol over ~500 LOC almost always wants multiple contracts (each under 500 LOC of behavior), a small single-purpose contract is fine as one file. Inherit from `OpenZeppelin` for every standard primitive — ERC20, ERC721, `AccessControl`, `Pausable`, `ReentrancyGuard` — never re-implement them. Pack storage so each `struct` fits the fewest 32-byte slots possible and mark deploy-time constants `immutable` (or `constant` for compile-time literals) — every saved SLOAD is real money for users. Reach for libraries when logic is pure utility callable from many contracts, and for inheritance when you want shared state and roles. Avoid the `diamond pattern` (EIP-2535) unless you genuinely hit the 24 KB code-size limit or need pluggable facets — it adds storage-layout and audit complexity most protocols never need.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### EVM-only scope
|
|
16
|
+
|
|
17
|
+
This doc assumes Solidity ≥0.8.20 targeting EVM bytecode. L2-specific quirks (precompile differences, calldata pricing on rollups, account abstraction on zkSync) are noted only where they change a decomposition decision. If your protocol must run on Solana or Move-based chains, the entire decomposition vocabulary here — contracts, libraries, inheritance, slots — does not translate, and you should not generalize from this overlay. The reverse direction is roughly safe: most of what works on Ethereum mainnet works on any EVM L2, with gas constants and finality assumptions adjusted; consult chain-specific docs before deploying anything that depends on `block.timestamp` granularity, opcode pricing, or `msg.sender == tx.origin`.
|
|
18
|
+
|
|
19
|
+
### Modular vs monolithic decomposition
|
|
20
|
+
|
|
21
|
+
The default rule is brutal and useful: **if the contract is over ~500 lines of behavior, split it.** Above that line, auditors stop being able to hold the whole thing in their head, your test surface explodes, and you start brushing against the 24 KB deployed-code limit. Below it, one contract is almost always the right answer — cross-contract calls cost ~2,600 gas of `CALL` overhead plus calldata, and every external surface is a new trust boundary you must reason about. Split along responsibility lines: a vault, an oracle adapter, and a fee router are three contracts; a single ERC20 with a mint guard is one. Per-contract LOC budget keeps each component reviewable, but more importantly it forces you to name the boundary — what does this contract own, what does it merely call?
|
|
22
|
+
|
|
23
|
+
The tradeoffs to weigh consciously:
|
|
24
|
+
|
|
25
|
+
| Concern | Monolithic | Modular |
|
|
26
|
+
|---------------------|---------------------------------------------|-----------------------------------------------|
|
|
27
|
+
| Gas per user action | Cheapest — internal calls are JUMP | +2,600 gas per `CALL` plus calldata |
|
|
28
|
+
| Upgradeability | All-or-nothing redeploy | Swap one module without touching others |
|
|
29
|
+
| Audit surface | One file, one storage layout | Multiple interfaces, every boundary is review |
|
|
30
|
+
| Code-size headroom | Tight against 24 KB EIP-170 limit | Each contract has its own 24 KB budget |
|
|
31
|
+
|
|
32
|
+
A useful heuristic: if two responsibilities never share storage and could be developed by different people without merge conflicts, they should be different contracts.
|
|
33
|
+
|
|
34
|
+
Concrete worked example — a yield vault that supports multiple collateral types and a single fee recipient. Three contracts beat one:
|
|
35
|
+
|
|
36
|
+
- `Vault.sol` — accounting for shares, deposits, withdrawals, share-price math. ERC4626-shaped.
|
|
37
|
+
- `StrategyRegistry.sol` — maps each collateral asset to an approved strategy adapter; governance can rotate strategies without touching the vault.
|
|
38
|
+
- `FeeRouter.sol` — collects performance fees, splits between protocol treasury and stakers; can be swapped to change fee policy.
|
|
39
|
+
|
|
40
|
+
If you wrote that as one contract you would have ~800 lines, three independent governance flows tangled together, and every audit finding in one area would force re-review of the whole file. Three contracts gives you a 300 / 250 / 150 LOC split where each piece has one job and one storage layout. The gas cost of the extra `CALL`s — call it ~5,000 gas per deposit — is real but small relative to the SLOAD-heavy share accounting that dominates the transaction.
|
|
41
|
+
|
|
42
|
+
### OpenZeppelin as baseline
|
|
43
|
+
|
|
44
|
+
Never re-implement standard primitives. `OpenZeppelin` contracts have been audited dozens of times, formally verified in pieces, and are the de-facto reference implementations auditors expect to see. Inherit, don't fork:
|
|
45
|
+
|
|
46
|
+
```solidity
|
|
47
|
+
// src/Vault.sol
|
|
48
|
+
pragma solidity 0.8.24;
|
|
49
|
+
|
|
50
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
51
|
+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
|
|
52
|
+
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
|
|
53
|
+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
54
|
+
|
|
55
|
+
contract Vault is ERC20, AccessControl, Pausable, ReentrancyGuard {
|
|
56
|
+
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
|
|
57
|
+
// ...
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The mental model: OZ is your standard library. If you find yourself writing `_transfer` or a `nonReentrant` modifier from scratch, stop. See `web3-access-control.md` for how role partitioning composes with `AccessControl`.
|
|
62
|
+
|
|
63
|
+
Pin OZ to an exact minor version in `foundry.toml` (or `package.json` for Hardhat) and bump it deliberately — OZ ships breaking changes between majors (v4 → v5 reworked `AccessControl` initialization, removed `Ownable` defaults, and changed several event signatures). Track their security advisories; an audited OZ release is one of the few dependencies whose patch notes you should actually read on update. If you must override OZ internals, do so by overriding `_update`, `_mint`, or other documented hooks rather than copy-pasting the contract — every fork loses the next round of fixes.
|
|
64
|
+
|
|
65
|
+
### State minimization
|
|
66
|
+
|
|
67
|
+
Every storage slot is 20,000 gas to write the first time and 2,900 gas to update — at $50 ETH gas this is real money per user action. Two disciplines pay for themselves immediately. **First, pack structs to fit slots.** The EVM lays out storage in 32-byte (256-bit) slots; smaller types share a slot when declared contiguously:
|
|
68
|
+
|
|
69
|
+
```solidity
|
|
70
|
+
// 1 slot total — uint64 + uint64 + uint128 = 256 bits
|
|
71
|
+
struct Position {
|
|
72
|
+
uint64 openedAt; // timestamp fits in uint64 until year 584942417355
|
|
73
|
+
uint64 lastUpdatedAt;
|
|
74
|
+
uint128 amount; // up to ~3.4e38 — plenty for most token amounts
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// vs. the naive 3-slot version
|
|
78
|
+
struct PositionBad {
|
|
79
|
+
uint256 openedAt;
|
|
80
|
+
uint256 lastUpdatedAt;
|
|
81
|
+
uint256 amount;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Second, use `immutable` for values fixed at deploy and `constant` for compile-time literals.** Both live in bytecode, not storage, so reads cost ~3 gas instead of ~2,100:
|
|
86
|
+
|
|
87
|
+
```solidity
|
|
88
|
+
contract Vault {
|
|
89
|
+
address public immutable asset; // set once in constructor
|
|
90
|
+
uint256 public constant MAX_FEE_BPS = 500; // compile-time
|
|
91
|
+
|
|
92
|
+
constructor(address _asset) {
|
|
93
|
+
asset = _asset;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A third habit worth naming: **prefer events over storage for log-shaped data**. If a value is consumed off-chain (UI, indexer, subgraph) and never read by another contract, emit an event instead of writing storage. Events are roughly an order of magnitude cheaper than `SSTORE` and don't bloat the state your node operators carry forever.
|
|
99
|
+
|
|
100
|
+
One subtlety to watch: struct packing only works when fields are declared in order from smallest to largest within a 32-byte slot, and only for **value types** (uints, addresses, bools, bytes32). Dynamic types (`string`, `bytes`, arrays, mappings) always occupy their own slot regardless of position, so don't try to "pack" them. Use `forge inspect <Contract> storage-layout` (Foundry) or `hardhat-storage-layout` to dump the actual layout and verify your packing assumptions before you ship — guessing is how you end up with a 4-slot struct you thought was 1.
|
|
101
|
+
|
|
102
|
+
### Libraries vs inheritance
|
|
103
|
+
|
|
104
|
+
Solidity offers two reuse mechanisms and they answer different questions. A `library` is pure logic — no state, no inheritance hierarchy, called via `DELEGATECALL` (for `external` library functions) or inlined at compile (for `internal`). Use libraries when the logic is stateless and called from many contracts: math helpers, byte manipulation, signature recovery. The `using X for Y` syntax attaches library functions to a type for ergonomic call sites:
|
|
105
|
+
|
|
106
|
+
```solidity
|
|
107
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
108
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
109
|
+
|
|
110
|
+
contract Vault {
|
|
111
|
+
using SafeERC20 for IERC20;
|
|
112
|
+
|
|
113
|
+
function withdraw(IERC20 token, address to, uint256 amount) external {
|
|
114
|
+
token.safeTransfer(to, amount); // resolves to SafeERC20.safeTransfer(token, to, amount)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use **inheritance** when you want shared state, roles, modifiers, or a partial implementation — `ERC20`, `AccessControl`, and `Pausable` all carry storage and require inheritance. Rule of thumb: stateless and reusable → library; stateful and specializable → base contract. Beware deep inheritance chains (more than three levels): C3 linearization, function-override resolution, and storage-layout ordering all become harder to reason about, and auditors will flag it. If your hierarchy looks like a Java class tree, refactor toward composition with internal helpers instead.
|
|
120
|
+
|
|
121
|
+
Two further library-design notes worth remembering. **`internal` library functions are inlined** at the call site, so they cost nothing extra in deployed bytecode beyond the inlined ops — they are essentially free abstraction. **`external` library functions** are deployed as a separate contract and invoked via `DELEGATECALL`, which means they share the caller's storage layout and execution context but live at their own address; you must link the library at deployment time. For most utility code (math helpers, conversion, packing), `internal` is what you want — keep external libraries for genuinely large logic blocks where the deployment-size savings justify the linking step.
|
|
122
|
+
|
|
123
|
+
### Diamond pattern (EIP-2535)
|
|
124
|
+
|
|
125
|
+
The diamond pattern routes calls through a proxy to one of many "facet" contracts via a function-selector table, letting a single address expose effectively unlimited code. It is the answer when you genuinely need pluggable facets — large protocols like Aave v3 use related patterns — or when you are wedged against the **24 KB EVM contract-size limit** (EIP-170). For everything else it is over-engineering with real cost: storage layout is now governed by namespaced "diamond storage" patterns that auditors must reason about, upgrade flows multiply, and tooling support is uneven. Default to plain proxies or no upgradeability at all (see `web3-upgradeability.md`), and only adopt the `diamond pattern` after you have written down the specific facets you need and confirmed a simpler decomposition won't fit. Premature diamonds have shipped more bugs than they have prevented.
|
|
126
|
+
|
|
127
|
+
A simple checklist before reaching for a diamond:
|
|
128
|
+
|
|
129
|
+
1. Have you tried splitting into 2–3 plain contracts that share an interface? Most "we need a diamond" intuitions dissolve here.
|
|
130
|
+
2. Is the 24 KB code-size limit a *current* problem, or a hypothetical future one? Don't pre-pay for a complexity tax you may never owe.
|
|
131
|
+
3. Do you actually need to add new facets after deployment, or do you just want clean module boundaries? The latter is better served by separate contracts with explicit interfaces.
|
|
132
|
+
4. Does your audit firm have diamond expertise on the team you'd hire? If not, you are also paying for their learning curve.
|
|
133
|
+
|
|
134
|
+
### External call discipline
|
|
135
|
+
|
|
136
|
+
Every `CALL` to another contract is a trust boundary — execution leaves your codebase and re-enters at the callee's whim. Two disciplines: type your external interactions through **interfaces**, not concrete types, and treat every external call as a reentrancy and revert risk (see `web3-security.md` for Checks-Effects-Interactions). Interfaces decouple deployment order, let you point at a mock in tests, and document the API surface in one place:
|
|
137
|
+
|
|
138
|
+
```solidity
|
|
139
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
140
|
+
|
|
141
|
+
interface IUniswapV2Pair {
|
|
142
|
+
function getReserves() external view returns (uint112, uint112, uint32);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
contract Adapter {
|
|
146
|
+
IERC20 public immutable token;
|
|
147
|
+
IUniswapV2Pair public immutable pair;
|
|
148
|
+
// ...
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Public/external functions are your API; everything else stays `internal` for gas (cheaper than `public`/`external` jumps) and composability. Document the external surface with NatSpec — `@notice`, `@param`, `@return` — because that is what auditors and integrators read first. For oracles and any data source you don't control, see `web3-oracles-and-external-data.md` for staleness, deviation, and fallback discipline.
|
|
153
|
+
|
|
154
|
+
A final architectural habit: write the **interface file first**. Before implementing `Vault`, write `IVault.sol` with the function signatures, NatSpec, and custom errors you intend to expose. That file becomes the contract between your code and every integrator, and reviewing it before implementation forces you to commit to an API surface you can defend in audit. The implementation may evolve; the interface should not, except by versioning a new one alongside it.
|
|
155
|
+
|
|
156
|
+
Three concrete external-call rules worth codifying as review checks:
|
|
157
|
+
|
|
158
|
+
1. **Never `.call{value: x}("")` without checking the return value.** Solidity 0.8 will not auto-revert on a failed low-level call; you must `require(success, "transfer failed")` or use a custom error. Better still, use OZ's `Address.sendValue` which does it for you.
|
|
159
|
+
2. **Wrap any function that hands execution to an external contract with `nonReentrant`**, even if you "know" the callee is trusted. Trust assumptions rot — an audited token today becomes a callback-injecting fork tomorrow.
|
|
160
|
+
3. **Pull, don't push.** When sending value or tokens to a user-supplied address, prefer the pull-payment pattern (record an entitlement, let them withdraw) over pushing in the same transaction. One failing recipient cannot then brick a batch operation for everyone else. `web3-security.md` covers this in depth.
|
|
161
|
+
|
|
162
|
+
Taken together, the architecture decisions in this doc compose: a modular layout makes role partitioning (`web3-access-control.md`) tractable, immutable interfaces make upgrades (`web3-upgradeability.md`) safer, and disciplined external-call boundaries make oracle integration (`web3-oracles-and-external-data.md`) something you can actually reason about. The point is not to memorize patterns — it is to make the constraints of the EVM (gas, code size, immutability, public mempool) visible in the shape of the code itself, so they show up at design time rather than during an audit two weeks before mainnet.
|