@next-friday/create-app 9.1.0 → 9.2.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 +5 -4
- package/dist/main.mjs +135 -96
- package/package.json +8 -6
- package/templates/base/.agents/hooks/agent-mcp-gate.sh +7 -2
- package/templates/base/.agents/hooks/agent-production-gate.sh +6 -1
- package/templates/base/.agents/hooks/agent-shell-gate.sh +1 -181
- package/templates/base/.agents/hooks/agent-test-edit-gate.sh +12 -3
- package/templates/base/.agents/review.md +2 -2
- package/templates/base/.agents/rules/no-suppression.md +1 -1
- package/templates/base/.agents/skill-evals/README.md +42 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/case.yaml +12 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/01-the-reply-explains-why-pnpm.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/02-no-file-in-the-project.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/03-no-commit-is-made-nothing.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/04-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/05-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/graders/06-everything-the-reply-states-as.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/answers-question-without-writing/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/case.yaml +12 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/01-work-happens-on-the-plan-branch.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/02-the-plan-is-implemented-and-checked.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/03-the-branch-is-pushed.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/04-the-reply-lists-three-playbooks.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/05-no-approval-and-no-merge.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/06-the-reply-ends-at-the-code-owner.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/graders/07-the-plan-names-the-engineer.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-continues-from-an-accepted-plan/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/case.yaml +71 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/01-the-reply-asks-numbered-questions.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/02-no-plan-before-the-answers.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/03-no-question-depends-on-another.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/04-no-file-or-commit.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/graders/05-the-reply-is-in-thai.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-interviews-before-planning/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/case.yaml +68 -0
- package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/graders/01-the-plan-has-the-four-headings.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/graders/02-no-source-file-or-commit.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/graders/03-the-reply-names-the-gate.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/graders/04-the-reply-is-in-thai.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/build-plans-and-waits-for-the-engineer/prompt.md +10 -0
- package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/case.yaml +8 -0
- package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/graders/01-no-deployment-is-started.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/graders/02-the-reply-names-the-release-manager-gate.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/graders/03-the-reply-says-what-it-could-not-read.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/graders/04-the-reply-is-in-the-language.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/deploy-stops-at-the-production-gate/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/case.yaml +13 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/01-a-file-intentkebabcasenameintentmd-exists-with.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/02-the-reply-shows-the-draft.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/03-no-commit-is-made-nothing.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/04-reply-lists-the-playbook-steps.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/05-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/06-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/graders/07-the-drafts-open-questions-section.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/drafts-intent-and-waits-for-yes/prompt.md +10 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/case.yaml +34 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/01-the-reply-shows-the-reproduction-output.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/02-the-reply-names-one-cause-with-evidence.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/03-the-plan-has-the-four-headings.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/04-no-file-or-commit.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/05-the-reply-names-the-gate.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/graders/06-the-reply-is-in-thai.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/fix-reproduces-finds-the-cause-and-waits-for-the-engineer/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/case.yaml +12 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/01-the-first-message-restates-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/02-the-reply-asks-numbered-questions.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/03-no-intentmd-is-written-before.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/04-reply-lists-the-playbook-steps.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/05-no-commit-is-made-nothing.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/06-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/07-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/graders/08-no-question-in-the-round.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/interviews-before-writing-intent/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/case.yaml +14 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/01-the-intent-has-the-five-headings.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/02-problem-holds-the-error-and-where.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/03-unverified-cause-is-an-open-question.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/04-no-source-file-is-changed.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/05-a-branch-is-pushed-and-the-gate-is-named.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/graders/06-the-person-is-asked-no-question.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/maintain-writes-intent-from-a-finding/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/case.yaml +12 -0
- package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/01-the-reply-follows-the-plan.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/02-the-reply-does-not-tell.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/03-no-file-is-created-or.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/04-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/graders/05-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/reported-problem-starts-the-plan/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/case.yaml +11 -0
- package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/graders/01-no-file-is-written-and-nothing-is-committed.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/graders/02-the-reply-names-the-closest-row.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/graders/03-the-reply-is-short-and-in-thai.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/request-that-fits-no-row/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/case.yaml +79 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/01-findings-have-pass-severity-and-location.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/02-no-approval-and-no-merge.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/03-the-reply-names-the-code-owner-gate.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/04-nothing-is-pushed-or-changed.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/graders/05-the-review-runs-in-a-fresh-context.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/review-reports-findings-and-never-merges/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/case.yaml +42 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/01-no-spec-is-written.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/02-no-commit-is-made.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/03-the-reply-says-not-merged.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/04-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/graders/05-the-reply-is-short.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/spec-for-unmerged-intent-stops/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/case.yaml +12 -0
- package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/01-no-intentmd-or-any-other.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/02-no-commit-is-made-nothing.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/03-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/04-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/graders/05-the-reply-says-that-work.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/statement-without-command-starts-no-work/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/case.yaml +42 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/fixture.md +1 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/01-the-spec-has-the-five-headings.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/02-policies-has-one-line-per-policy.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/03-no-policy-is-called-unwritten.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/04-a-branch-is-pushed-for-review.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/05-the-reply-names-the-gate.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/06-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/07-no-merge-is-made.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/graders/08-the-four-policies-are-read-before-writing.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc/writes-spec-from-merged-intent/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/case.yaml +11 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/01-the-reply-says-that-work.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/02-the-reply-gives-one-example.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/03-the-reply-lists-the-project.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/04-no-file-is-created-or.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/05-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/graders/06-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/explains-how-to-start-work/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/case.yaml +10 -0
- package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/01-the-reply-says-that-this.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/02-the-reply-does-not-describe.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/03-no-file-is-created-or.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/04-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/graders/05-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/unknown-playbook-is-not-invented/prompt.md +8 -0
- package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/case.yaml +11 -0
- package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/graders/01-the-reply-says-to-send.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/graders/02-no-file-is-created-or.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/graders/03-the-reply-is-in-the.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/graders/04-every-path-in-the-reply.md +7 -0
- package/templates/base/.agents/skill-evals/sdlc-help/work-request-is-sent-to-sdlc/prompt.md +8 -0
- package/templates/base/.agents/skills/sdlc/SKILL.md +25 -21
- package/templates/base/.agents/skills/sdlc/playbooks/build.md +13 -8
- package/templates/base/.agents/skills/sdlc/playbooks/deploy.md +2 -2
- package/templates/base/.agents/skills/sdlc/playbooks/design.md +4 -4
- package/templates/base/.agents/skills/sdlc/playbooks/fix.md +23 -0
- package/templates/base/.agents/skills/sdlc/playbooks/maintain.md +2 -3
- package/templates/base/.agents/skills/sdlc/playbooks/open-pr.md +3 -3
- package/templates/base/.agents/skills/sdlc/playbooks/plan.md +2 -2
- package/templates/base/.agents/skills/sdlc/playbooks/review.md +2 -2
- package/templates/base/.agents/skills/sdlc-help/SKILL.md +3 -1
- package/templates/base/.claude/managed-settings.example.json +2 -2
- package/templates/base/.claude/settings.json +0 -13
- package/templates/base/.claude/skills/sdlc/SKILL.md +25 -21
- package/templates/base/.claude/skills/sdlc/playbooks/build.md +13 -8
- package/templates/base/.claude/skills/sdlc/playbooks/deploy.md +2 -2
- package/templates/base/.claude/skills/sdlc/playbooks/design.md +4 -4
- package/templates/base/.claude/skills/sdlc/playbooks/fix.md +23 -0
- package/templates/base/.claude/skills/sdlc/playbooks/maintain.md +2 -3
- package/templates/base/.claude/skills/sdlc/playbooks/open-pr.md +3 -3
- package/templates/base/.claude/skills/sdlc/playbooks/plan.md +2 -2
- package/templates/base/.claude/skills/sdlc/playbooks/review.md +2 -2
- package/templates/base/.claude/skills/sdlc-help/SKILL.md +3 -1
- package/templates/base/.cursor/hooks.json +0 -6
- package/templates/base/.cursor/rules/no-suppression.mdc +1 -1
- package/templates/base/.cursor/skills/sdlc/SKILL.md +25 -21
- package/templates/base/.cursor/skills/sdlc/playbooks/build.md +13 -8
- package/templates/base/.cursor/skills/sdlc/playbooks/deploy.md +2 -2
- package/templates/base/.cursor/skills/sdlc/playbooks/design.md +4 -4
- package/templates/base/.cursor/skills/sdlc/playbooks/fix.md +23 -0
- package/templates/base/.cursor/skills/sdlc/playbooks/maintain.md +2 -3
- package/templates/base/.cursor/skills/sdlc/playbooks/open-pr.md +3 -3
- package/templates/base/.cursor/skills/sdlc/playbooks/plan.md +2 -2
- package/templates/base/.cursor/skills/sdlc/playbooks/review.md +2 -2
- package/templates/base/.cursor/skills/sdlc-help/SKILL.md +3 -1
- package/templates/base/.github/ISSUE_TEMPLATE/documentation.yml +1 -1
- package/templates/base/.github/dependabot.yml +1 -10
- package/templates/base/.github/file-filters.yml +3 -6
- package/templates/base/.github/workflows/agent-evals.yml +46 -0
- package/templates/base/.github/workflows/claude-fix-loop.yml +45 -0
- package/templates/base/.github/workflows/control-band.yml +115 -0
- package/templates/base/.github/workflows/dependency-review.yml +1 -0
- package/templates/base/.github/workflows/deploy.yml +4 -2
- package/templates/base/.github/workflows/osv-scanner.yml +0 -5
- package/templates/base/.github/workflows/pr-triage.yml +1 -1
- package/templates/base/.github/workflows/secret-scan.yml +4 -1
- package/templates/base/.github/workflows/spec-on-intent-merge.yml +108 -0
- package/templates/base/.github/workflows/validate-pr-title.yml +3 -2
- package/templates/base/.husky/pre-push +1 -8
- package/templates/base/REVIEW.md +2 -2
- package/templates/base/SETUP.md +44 -8
- package/templates/base/bands.yaml +3 -3
- package/templates/base/intent/README.md +1 -0
- package/templates/base/knip.json +2 -1
- package/templates/base/pnpm-workspace.yaml +1 -22
- package/templates/base/runbooks/rollback-deploy.md +27 -0
- package/templates/base/scripts/check-agent-gates.ts +251 -225
- package/templates/base/scripts/check-bands.ts +665 -0
- package/templates/base/scripts/check-plan.ts +1047 -0
- package/templates/base/scripts/measure-loop.ts +293 -0
- package/templates/base/scripts/run-agent-evals.ts +1186 -0
- package/templates/base/scripts/sync-agent-rules.ts +0 -8
- package/templates/nestjs/.dependency-cruiser.json +1 -1
- package/templates/nestjs/.github/ISSUE_TEMPLATE/bug.yml +5 -5
- package/templates/nestjs/.github/workflows/ci.yml +58 -9
- package/templates/nestjs/.github/workflows/sast.yml +0 -5
- package/templates/nestjs/AGENTS.md +16 -12
- package/templates/nestjs/CONTRIBUTING.md +3 -2
- package/templates/nestjs/Dockerfile +4 -4
- package/templates/nestjs/README.md +4 -6
- package/templates/nestjs/_gitignore +0 -1
- package/templates/nestjs/package.json +15 -13
- package/templates/nestjs/src/app.controller.test.ts +1 -0
- package/templates/nestjs/src/health/health.controller.test.ts +1 -0
- package/templates/nestjs/src/port.test.ts +1 -0
- package/templates/nestjs/test/app.e2e.test.ts +1 -0
- package/templates/nestjs/tsconfig.build.json +2 -3
- package/templates/nestjs/tsconfig.json +1 -3
- package/templates/nestjs/vitest.config.ts +0 -1
- package/templates/nestjs/vitest.e2e.config.ts +0 -1
- package/templates/nextjs/.dependency-cruiser.json +1 -1
- package/templates/nextjs/.github/ISSUE_TEMPLATE/bug.yml +1 -1
- package/templates/nextjs/.github/workflows/ci.yml +58 -9
- package/templates/nextjs/.github/workflows/sast.yml +0 -5
- package/templates/nextjs/AGENTS.md +16 -12
- package/templates/nextjs/CONTRIBUTING.md +3 -2
- package/templates/nextjs/Dockerfile +2 -2
- package/templates/nextjs/README.md +5 -3
- package/templates/nextjs/app/layout.tsx +2 -2
- package/templates/nextjs/app/page.test.ts +1 -1
- package/templates/nextjs/app/page.tsx +1 -1
- package/templates/nextjs/package.json +9 -8
- package/templates/nextjs/test/home.e2e.test.ts +3 -7
- package/templates/nextjs/tsconfig.json +0 -3
- package/templates/nodejs/.dependency-cruiser.json +1 -1
- package/templates/nodejs/.github/ISSUE_TEMPLATE/bug.yml +5 -5
- package/templates/nodejs/.github/workflows/ci.yml +58 -9
- package/templates/nodejs/.github/workflows/sast.yml +0 -5
- package/templates/nodejs/AGENTS.md +16 -11
- package/templates/nodejs/CONTRIBUTING.md +4 -3
- package/templates/nodejs/Dockerfile +3 -3
- package/templates/nodejs/README.md +4 -4
- package/templates/nodejs/_gitignore +0 -1
- package/templates/nodejs/package.json +11 -9
- package/templates/nodejs/src/server.test.ts +23 -0
- package/templates/nodejs/src/server.ts +2 -2
- package/templates/nodejs/tsconfig.build.json +2 -3
- package/templates/nodejs/tsconfig.json +0 -4
- package/templates/payload/.agents/agents/verifier.md +1 -1
- package/templates/payload/.agents/review.md +21 -0
- package/templates/payload/.claude/agents/verifier.md +1 -1
- package/templates/payload/.cursor/agents/verifier.md +1 -1
- package/templates/payload/.dependency-cruiser.json +1 -1
- package/templates/payload/.github/ISSUE_TEMPLATE/bug.yml +1 -1
- package/templates/payload/.github/workflows/ci.yml +75 -10
- package/templates/payload/.github/workflows/sast.yml +0 -5
- package/templates/payload/AGENTS.md +21 -17
- package/templates/payload/CONTRIBUTING.md +4 -3
- package/templates/payload/Dockerfile +2 -2
- package/templates/payload/README.md +8 -4
- package/templates/payload/REVIEW.md +21 -0
- package/templates/payload/package.json +9 -7
- package/templates/payload/src/app/(frontend)/layout.tsx +2 -2
- package/templates/payload/src/app/(frontend)/page.test.ts +1 -1
- package/templates/payload/src/app/(frontend)/page.tsx +1 -1
- package/templates/payload/src/app/favicon.ico +0 -0
- package/templates/payload/src/app/health/route.test.ts +20 -2
- package/templates/payload/src/app/health/route.ts +9 -2
- package/templates/payload/src/collections/media.test.ts +36 -1
- package/templates/payload/src/collections/media.ts +1 -1
- package/templates/payload/src/payload.config.test.ts +15 -0
- package/templates/payload/test/home.e2e.test.ts +3 -7
- package/templates/payload/tsconfig.json +0 -1
- package/templates/turborepo/.agents/agents/verifier.md +1 -1
- package/templates/turborepo/.agents/review.md +21 -0
- package/templates/turborepo/.claude/agents/verifier.md +1 -1
- package/templates/turborepo/.cursor/agents/verifier.md +1 -1
- package/templates/turborepo/.dockerignore +9 -4
- package/templates/turborepo/.github/ISSUE_TEMPLATE/bug.yml +16 -1
- package/templates/turborepo/.github/dependabot.yml +1 -10
- package/templates/turborepo/.github/scripts/affected-apps.sh +5 -23
- package/templates/turborepo/.github/workflows/ci.yml +140 -103
- package/templates/turborepo/.github/workflows/deploy.yml +24 -23
- package/templates/turborepo/.github/workflows/sast.yml +0 -5
- package/templates/turborepo/AGENTS.md +21 -17
- package/templates/turborepo/CONTRIBUTING.md +6 -4
- package/templates/turborepo/README.md +8 -8
- package/templates/turborepo/REVIEW.md +21 -0
- package/templates/turborepo/apps/app/.dependency-cruiser.json +1 -1
- package/templates/turborepo/apps/app/AGENTS.md +9 -0
- package/templates/turborepo/apps/app/Dockerfile +2 -2
- package/templates/turborepo/apps/app/app/layout.tsx +2 -2
- package/templates/turborepo/apps/app/app/page.test.ts +1 -1
- package/templates/turborepo/apps/app/app/page.tsx +1 -1
- package/templates/turborepo/apps/app/package.json +3 -3
- package/templates/turborepo/apps/app/test/home.e2e.test.ts +3 -7
- package/templates/turborepo/apps/app/tsconfig.json +0 -3
- package/templates/turborepo/apps/backoffice/.dependency-cruiser.json +1 -1
- package/templates/turborepo/apps/backoffice/AGENTS.md +9 -0
- package/templates/turborepo/apps/backoffice/Dockerfile +2 -2
- package/templates/turborepo/apps/backoffice/package.json +4 -3
- package/templates/turborepo/apps/backoffice/src/app/(frontend)/layout.tsx +2 -2
- package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.test.ts +1 -1
- package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.tsx +1 -1
- package/templates/turborepo/apps/backoffice/src/app/favicon.ico +0 -0
- package/templates/turborepo/apps/backoffice/src/app/health/route.test.ts +20 -2
- package/templates/turborepo/apps/backoffice/src/app/health/route.ts +10 -2
- package/templates/turborepo/apps/backoffice/src/collections/media.test.ts +36 -1
- package/templates/turborepo/apps/backoffice/src/collections/media.ts +1 -1
- package/templates/turborepo/apps/backoffice/src/environment.test.ts +19 -0
- package/templates/turborepo/apps/backoffice/src/payload.config.test.ts +15 -0
- package/templates/turborepo/apps/backoffice/test/home.e2e.test.ts +3 -7
- package/templates/turborepo/apps/backoffice/tsconfig.json +0 -1
- package/templates/turborepo/knip.json +4 -2
- package/templates/turborepo/package.json +15 -15
- package/templates/turborepo/pnpm-workspace.yaml +1 -22
- package/templates/turborepo/turbo.json +32 -11
- package/templates/base/.agents/hooks/agent-suppression-gate.sh +0 -45
- package/templates/base/.claude/rules/agents-guide.md +0 -1
- package/templates/base/scripts/check-human-gates.ts +0 -383
- package/templates/base/scripts/check-intent.ts +0 -391
- package/templates/nextjs/knip.json +0 -4
- package/templates/payload/src/collections/users.test.ts +0 -10
- package/templates/turborepo/apps/backoffice/src/collections/users.test.ts +0 -10
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import {existsSync, readFileSync} from "node:fs";
|
|
2
|
+
import {parse} from "yaml";
|
|
3
|
+
|
|
4
|
+
type Tier = "1sigma" | "2sigma" | "3sigma";
|
|
5
|
+
|
|
6
|
+
type Series = {
|
|
7
|
+
baseline: number[];
|
|
8
|
+
recent: number[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TierSetting = {
|
|
12
|
+
action: string;
|
|
13
|
+
routes?: string[];
|
|
14
|
+
tools?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Bands = {
|
|
18
|
+
metric: string;
|
|
19
|
+
tiers: Record<Tier, TierSetting>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Parsed<Value> = {problem: string} | {value: Value};
|
|
23
|
+
|
|
24
|
+
type Detection = {
|
|
25
|
+
mean: number;
|
|
26
|
+
rule?: string;
|
|
27
|
+
sigma: number;
|
|
28
|
+
tier?: Tier;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type DetectionRule = {
|
|
32
|
+
description: string;
|
|
33
|
+
fires: (deviations: number[]) => boolean;
|
|
34
|
+
tier: Tier;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type SelfTestCase = {
|
|
38
|
+
name: string;
|
|
39
|
+
passes: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const supportedRules = "western_electric";
|
|
43
|
+
const minimumBaselineValues = 8;
|
|
44
|
+
const maximumRecentValues = 8;
|
|
45
|
+
const bandsPath = "bands.yaml";
|
|
46
|
+
const jsonNull: unknown = JSON.parse("null");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check that a value is a plain object.
|
|
50
|
+
* @param value Any parsed value.
|
|
51
|
+
* @returns Whether the value is a non-null, non-array object.
|
|
52
|
+
*/
|
|
53
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check that a value is a list of finite numbers.
|
|
59
|
+
* @param value Any parsed value.
|
|
60
|
+
* @returns Whether the value is an array whose items are all finite numbers.
|
|
61
|
+
*/
|
|
62
|
+
function isNumberList(value: unknown): value is number[] {
|
|
63
|
+
return (
|
|
64
|
+
Array.isArray(value) && value.every(item => typeof item === "number" && Number.isFinite(item))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check that a value is a list of text.
|
|
70
|
+
* @param value Any parsed value.
|
|
71
|
+
* @returns Whether the value is an array whose items are all text.
|
|
72
|
+
*/
|
|
73
|
+
function isTextList(value: unknown): value is string[] {
|
|
74
|
+
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read the values file: `{"baseline": number[], "recent": number[]}`.
|
|
79
|
+
* @param text JSON content.
|
|
80
|
+
* @returns The series, or the first problem found.
|
|
81
|
+
*/
|
|
82
|
+
function readSeries(text: string): Parsed<Series> {
|
|
83
|
+
let data: unknown;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
data = JSON.parse(text);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
problem: `the file is not valid JSON: ${(error as Error).message}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!isRecord(data) || !isNumberList(data.baseline) || !isNumberList(data.recent)) {
|
|
94
|
+
return {
|
|
95
|
+
problem: 'the file must be {"baseline": number[], "recent": number[]} with numbers only',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const {baseline, recent} = data;
|
|
100
|
+
|
|
101
|
+
if (baseline.length < minimumBaselineValues) {
|
|
102
|
+
return {
|
|
103
|
+
problem: `"baseline" needs at least ${minimumBaselineValues} values; found ${baseline.length}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (recent.length === 0 || recent.length > maximumRecentValues) {
|
|
108
|
+
return {
|
|
109
|
+
problem: `"recent" needs 1 to ${maximumRecentValues} values; found ${recent.length}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
value: {
|
|
115
|
+
baseline,
|
|
116
|
+
recent,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read one tier of `bands.yaml`.
|
|
123
|
+
* @param value The tier's parsed value.
|
|
124
|
+
* @returns The tier's setting, or undefined when `action` is not text, or `tools` or `routes` has the wrong type.
|
|
125
|
+
*/
|
|
126
|
+
function readTierSetting(value: unknown): TierSetting | undefined {
|
|
127
|
+
if (!isRecord(value)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const {action, routes, tools} = value;
|
|
132
|
+
const isToolsValid = tools === undefined || typeof tools === "string";
|
|
133
|
+
const isRoutesValid = routes === undefined || isTextList(routes);
|
|
134
|
+
|
|
135
|
+
if (typeof action !== "string" || action === "") {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return isToolsValid && isRoutesValid
|
|
140
|
+
? {
|
|
141
|
+
action,
|
|
142
|
+
routes,
|
|
143
|
+
tools,
|
|
144
|
+
}
|
|
145
|
+
: undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Read `bands.yaml`: `metric`, `rules`, and one setting for each of the three tiers.
|
|
150
|
+
* @param text YAML content.
|
|
151
|
+
* @returns The bands, or the first problem found.
|
|
152
|
+
*/
|
|
153
|
+
function readBands(text: string): Parsed<Bands> {
|
|
154
|
+
let data: unknown;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
data = parse(text);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
problem: `the file is not valid YAML: ${(error as Error).message}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isRecord(data)) {
|
|
165
|
+
return {
|
|
166
|
+
problem: "the file must be a YAML mapping with metric, rules, and tiers",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const {metric, rules, tiers} = data;
|
|
171
|
+
|
|
172
|
+
if (typeof metric !== "string" || metric === "") {
|
|
173
|
+
return {
|
|
174
|
+
problem: '"metric" must be the name of the metric',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (rules !== supportedRules) {
|
|
179
|
+
return {
|
|
180
|
+
problem: `"rules" must be ${supportedRules}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const tierSettings = isRecord(tiers) ? tiers : {};
|
|
185
|
+
const oneSigma = readTierSetting(tierSettings["1sigma"]);
|
|
186
|
+
const twoSigma = readTierSetting(tierSettings["2sigma"]);
|
|
187
|
+
const threeSigma = readTierSetting(tierSettings["3sigma"]);
|
|
188
|
+
|
|
189
|
+
if (oneSigma === undefined || twoSigma === undefined || threeSigma === undefined) {
|
|
190
|
+
return {
|
|
191
|
+
problem:
|
|
192
|
+
'each of "1sigma", "2sigma", and "3sigma" must be set under "tiers" with an "action"; "tools" is text and "routes" is a list',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
value: {
|
|
198
|
+
metric,
|
|
199
|
+
tiers: {
|
|
200
|
+
"1sigma": oneSigma,
|
|
201
|
+
"2sigma": twoSigma,
|
|
202
|
+
"3sigma": threeSigma,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Compute the mean and the sample standard deviation; equal values give exactly zero.
|
|
210
|
+
* @param values At least two numbers.
|
|
211
|
+
* @returns The mean and the standard deviation.
|
|
212
|
+
*/
|
|
213
|
+
function computeMeanAndSigma(values: number[]): {mean: number; sigma: number} {
|
|
214
|
+
const [first = 0] = values;
|
|
215
|
+
|
|
216
|
+
if (values.every(value => value === first)) {
|
|
217
|
+
return {
|
|
218
|
+
mean: first,
|
|
219
|
+
sigma: 0,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
224
|
+
const squares = values.reduce((sum, value) => sum + (value - mean) ** 2, 0);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
mean,
|
|
228
|
+
sigma: Math.sqrt(squares / (values.length - 1)),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Count the points at least `limit` standard deviations from the mean on one side.
|
|
234
|
+
* @param deviations Distances from the mean in standard deviations.
|
|
235
|
+
* @param limit The distance in standard deviations.
|
|
236
|
+
* @param side 1 for above the mean, -1 for below.
|
|
237
|
+
* @returns The number of points that far on that side.
|
|
238
|
+
*/
|
|
239
|
+
function countBeyond(deviations: number[], limit: number, side: -1 | 1): number {
|
|
240
|
+
return deviations.filter(deviation => deviation * side >= limit).length;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Check whether `count` of the last `window` points are `limit` or more standard deviations out on one side.
|
|
245
|
+
* @param deviations Recent distances from the mean in standard deviations, oldest first.
|
|
246
|
+
* @param window How many of the newest points to look at.
|
|
247
|
+
* @param count How many of them must be that far out.
|
|
248
|
+
* @param limit The distance in standard deviations.
|
|
249
|
+
* @returns Whether either side holds enough points.
|
|
250
|
+
*/
|
|
251
|
+
function hasPointsBeyond(
|
|
252
|
+
deviations: number[],
|
|
253
|
+
window: number,
|
|
254
|
+
count: number,
|
|
255
|
+
limit: number,
|
|
256
|
+
): boolean {
|
|
257
|
+
const newest = deviations.slice(-window);
|
|
258
|
+
|
|
259
|
+
return countBeyond(newest, limit, 1) >= count || countBeyond(newest, limit, -1) >= count;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check whether every point lies on one side of the mean, none on it.
|
|
264
|
+
* @param deviations Distances from the mean in standard deviations.
|
|
265
|
+
* @returns Whether all points are above the mean or all are below it.
|
|
266
|
+
*/
|
|
267
|
+
function isOneSided(deviations: number[]): boolean {
|
|
268
|
+
return (
|
|
269
|
+
deviations.every(deviation => deviation > 0) || deviations.every(deviation => deviation < 0)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const detectionRules: DetectionRule[] = [
|
|
274
|
+
{
|
|
275
|
+
description: "the latest point is 3 sigma from the mean",
|
|
276
|
+
tier: "3sigma",
|
|
277
|
+
fires: deviations => Math.abs(deviations.at(-1) ?? 0) >= 3,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
description: "2 of the last 3 points are 2 sigma out on one side",
|
|
281
|
+
tier: "2sigma",
|
|
282
|
+
fires: deviations => hasPointsBeyond(deviations, 3, 2, 2),
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
description: "4 of the last 5 points are 1 sigma out on one side",
|
|
286
|
+
tier: "1sigma",
|
|
287
|
+
fires: deviations => hasPointsBeyond(deviations, 5, 4, 1),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
description: "8 points in a row are on one side of the mean",
|
|
291
|
+
tier: "1sigma",
|
|
292
|
+
fires: deviations => deviations.length === maximumRecentValues && isOneSided(deviations),
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Apply the Western Electric rules to the newest values, using the baseline's mean and standard deviation.
|
|
298
|
+
* @param series The baseline and the recent values.
|
|
299
|
+
* @returns The mean, the standard deviation, and the highest tier that fires with its rule.
|
|
300
|
+
*/
|
|
301
|
+
function detect(series: Series): Detection {
|
|
302
|
+
const {mean, sigma} = computeMeanAndSigma(series.baseline);
|
|
303
|
+
|
|
304
|
+
if (sigma === 0) {
|
|
305
|
+
return series.recent.some(value => value !== mean)
|
|
306
|
+
? {
|
|
307
|
+
mean,
|
|
308
|
+
sigma,
|
|
309
|
+
rule: "the baseline has no spread and a recent value differs",
|
|
310
|
+
tier: "3sigma",
|
|
311
|
+
}
|
|
312
|
+
: {
|
|
313
|
+
mean,
|
|
314
|
+
sigma,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const deviations = series.recent.map(value => (value - mean) / sigma);
|
|
319
|
+
const fired = detectionRules.find(({fires}) => fires(deviations));
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
mean,
|
|
323
|
+
sigma,
|
|
324
|
+
rule: fired?.description,
|
|
325
|
+
tier: fired?.tier,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Build the fields of the output line for a detection.
|
|
331
|
+
* @param bands The bands from `bands.yaml`.
|
|
332
|
+
* @param detection The detection.
|
|
333
|
+
* @returns The fields; absent values are undefined.
|
|
334
|
+
*/
|
|
335
|
+
function toReport(bands: Bands, detection: Detection): Record<string, unknown> {
|
|
336
|
+
const setting = detection.tier === undefined ? undefined : bands.tiers[detection.tier];
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
metric: bands.metric,
|
|
340
|
+
tier: detection.tier,
|
|
341
|
+
action: setting?.action,
|
|
342
|
+
tools: setting?.tools,
|
|
343
|
+
routes: setting?.routes,
|
|
344
|
+
mean: detection.mean,
|
|
345
|
+
sigma: detection.sigma,
|
|
346
|
+
rule: detection.rule,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Write the fields as one JSON line, with each absent value as JSON null.
|
|
352
|
+
* @param report The fields from `toReport`.
|
|
353
|
+
* @returns The JSON text.
|
|
354
|
+
*/
|
|
355
|
+
function serializeReport(report: Record<string, unknown>): string {
|
|
356
|
+
return JSON.stringify(report, (_key, value: unknown) => value ?? jsonNull);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const baselineValues = [10, 12, 10, 12, 10, 12, 10, 12];
|
|
360
|
+
const nonNumberBaseline = [...baselineValues, "1"];
|
|
361
|
+
const shortBaseline = baselineValues.slice(1);
|
|
362
|
+
|
|
363
|
+
const nineValues = Array.from(
|
|
364
|
+
{
|
|
365
|
+
length: 9,
|
|
366
|
+
},
|
|
367
|
+
() => 1,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const fractionBaseline = Array.from(
|
|
371
|
+
{
|
|
372
|
+
length: 8,
|
|
373
|
+
},
|
|
374
|
+
() => 0.1,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const flatBaseline = Array.from(
|
|
378
|
+
{
|
|
379
|
+
length: 8,
|
|
380
|
+
},
|
|
381
|
+
() => 5,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const baselineMean = 11;
|
|
385
|
+
const baselineSigma = computeMeanAndSigma(baselineValues).sigma;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Name the tier the rules give for recent values at the given distances from the baseline's mean.
|
|
389
|
+
* @param deviations Distances in standard deviations, oldest first.
|
|
390
|
+
* @returns The tier, or undefined when no rule fires.
|
|
391
|
+
*/
|
|
392
|
+
function tierAt(...deviations: number[]): Tier | undefined {
|
|
393
|
+
const recent = deviations.map(deviation => baselineMean + deviation * baselineSigma);
|
|
394
|
+
|
|
395
|
+
return detect({
|
|
396
|
+
baseline: baselineValues,
|
|
397
|
+
recent,
|
|
398
|
+
}).tier;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Write a values file.
|
|
403
|
+
* @param baseline The baseline values.
|
|
404
|
+
* @param recent The recent values.
|
|
405
|
+
* @returns The JSON text.
|
|
406
|
+
*/
|
|
407
|
+
function seriesText(baseline: unknown[], recent: unknown[]): string {
|
|
408
|
+
return JSON.stringify({
|
|
409
|
+
baseline,
|
|
410
|
+
recent,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Check that a read failed.
|
|
416
|
+
* @param result The result of a read.
|
|
417
|
+
* @returns Whether it holds a problem.
|
|
418
|
+
*/
|
|
419
|
+
function hasProblem(result: Parsed<unknown>): boolean {
|
|
420
|
+
return "problem" in result;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const validBands = [
|
|
424
|
+
"metric: ci_test_failure_rate",
|
|
425
|
+
"rules: western_electric",
|
|
426
|
+
"tiers:",
|
|
427
|
+
" 1sigma: {action: log}",
|
|
428
|
+
' 2sigma: {action: diagnose, tools: "Read,Grep"}',
|
|
429
|
+
" 3sigma: {action: propose, routes: [pull_request]}",
|
|
430
|
+
].join("\n");
|
|
431
|
+
|
|
432
|
+
const validBandsRead = readBands(validBands);
|
|
433
|
+
const bandsForReport = "value" in validBandsRead ? validBandsRead.value : undefined;
|
|
434
|
+
|
|
435
|
+
const twoSigmaReport =
|
|
436
|
+
bandsForReport &&
|
|
437
|
+
serializeReport(
|
|
438
|
+
toReport(bandsForReport, {
|
|
439
|
+
rule: "r",
|
|
440
|
+
tier: "2sigma",
|
|
441
|
+
mean: 1,
|
|
442
|
+
sigma: 1,
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const noTierReport =
|
|
447
|
+
bandsForReport &&
|
|
448
|
+
serializeReport(
|
|
449
|
+
toReport(bandsForReport, {
|
|
450
|
+
mean: 1,
|
|
451
|
+
sigma: 1,
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const selfTestCases: SelfTestCase[] = [
|
|
456
|
+
{
|
|
457
|
+
name: "values inside one sigma",
|
|
458
|
+
passes: tierAt(0.5, -0.5, 0.2) === undefined,
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: "the latest point at 3 sigma",
|
|
462
|
+
passes: tierAt(0, 0, 3.2) === "3sigma",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "the latest point at -3 sigma",
|
|
466
|
+
passes: tierAt(0, 0, -3.2) === "3sigma",
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: "a point at 3 sigma that is not the latest",
|
|
470
|
+
passes: tierAt(3.2, 0, 0) === undefined,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "2 of the last 3 at 2 sigma",
|
|
474
|
+
passes: tierAt(2.2, 0, 2.5) === "2sigma",
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "2 at 2 sigma on opposite sides",
|
|
478
|
+
passes: tierAt(2.2, 0, -2.5) === undefined,
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "one point at 2 sigma",
|
|
482
|
+
passes: tierAt(0, 0, 2.5) === undefined,
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "4 of the last 5 at 1 sigma",
|
|
486
|
+
passes: tierAt(1.2, 1.1, 0, 1.5, 1.3) === "1sigma",
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "3 of the last 5 at 1 sigma",
|
|
490
|
+
passes: tierAt(1.2, 0, 0, 1.5, 1.3) === undefined,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "8 in a row on one side",
|
|
494
|
+
passes: tierAt(0.1, 0.2, 0.1, 0.3, 0.1, 0.2, 0.1, 0.2) === "1sigma",
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "7 in a row on one side",
|
|
498
|
+
passes: tierAt(0.1, 0.2, 0.1, 0.3, 0.1, 0.2, 0.1) === undefined,
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: "8 in a row with one on the other side",
|
|
502
|
+
passes: tierAt(0.1, 0.2, 0.1, -0.3, 0.1, 0.2, 0.1, 0.2) === undefined,
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "the highest tier wins",
|
|
506
|
+
passes: tierAt(2.2, 2.4, 3.5) === "3sigma",
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: "a baseline without spread and an equal value",
|
|
510
|
+
passes:
|
|
511
|
+
detect({
|
|
512
|
+
baseline: flatBaseline,
|
|
513
|
+
recent: [5],
|
|
514
|
+
}).tier === undefined,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "a baseline of equal fractions and eight equal values",
|
|
518
|
+
passes:
|
|
519
|
+
detect({
|
|
520
|
+
baseline: fractionBaseline,
|
|
521
|
+
recent: fractionBaseline,
|
|
522
|
+
}).tier === undefined,
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "a baseline without spread and a different value",
|
|
526
|
+
passes:
|
|
527
|
+
detect({
|
|
528
|
+
baseline: flatBaseline,
|
|
529
|
+
recent: [5, 6],
|
|
530
|
+
}).tier === "3sigma",
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "a series with a missing key",
|
|
534
|
+
passes: hasProblem(readSeries('{"baseline": []}')),
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "a series with a non-number",
|
|
538
|
+
passes: hasProblem(readSeries(seriesText(nonNumberBaseline, [1]))),
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "a baseline of 7 values",
|
|
542
|
+
passes: hasProblem(readSeries(seriesText(shortBaseline, [1]))),
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "no recent value",
|
|
546
|
+
passes: hasProblem(readSeries(seriesText(baselineValues, []))),
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "9 recent values",
|
|
550
|
+
passes: hasProblem(readSeries(seriesText(baselineValues, nineValues))),
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "a series that is not JSON",
|
|
554
|
+
passes: hasProblem(readSeries("{")),
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "a valid series",
|
|
558
|
+
passes: "value" in readSeries(seriesText(baselineValues, [1])),
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "complete bands",
|
|
562
|
+
passes: bandsForReport !== undefined,
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: "bands without a tier",
|
|
566
|
+
passes: hasProblem(readBands(validBands.replace(" 1sigma: {action: log}\n", ""))),
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: "bands with a tier without an action",
|
|
570
|
+
passes: hasProblem(readBands(validBands.replace("{action: log}", "{tools: Read}"))),
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: "bands with another rule set",
|
|
574
|
+
passes: hasProblem(readBands(validBands.replace("western_electric", "nelson"))),
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: "bands that are not YAML",
|
|
578
|
+
passes: hasProblem(readBands("a: [")),
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "a report for a tier with tools",
|
|
582
|
+
passes:
|
|
583
|
+
twoSigmaReport?.includes('"action":"diagnose","tools":"Read,Grep","routes":null') === true,
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "a report without a tier",
|
|
587
|
+
passes: noTierReport?.includes('"tier":null,"action":null,"tools":null,"routes":null') === true,
|
|
588
|
+
},
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
const selfTestFailures = selfTestCases.filter(({passes}) => !passes).map(({name}) => name);
|
|
592
|
+
const argumentsList = process.argv.slice(2);
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Print a problem and mark the run as failed.
|
|
596
|
+
* @param message The problem.
|
|
597
|
+
*/
|
|
598
|
+
function fail(message: string): void {
|
|
599
|
+
process.stderr.write(`${message}\n`);
|
|
600
|
+
process.exitCode = 1;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Detect on a values file and print the JSON line, or print the problem.
|
|
605
|
+
* @param valuesPath The path of the JSON values file.
|
|
606
|
+
*/
|
|
607
|
+
function run(valuesPath: string): void {
|
|
608
|
+
if (!existsSync(valuesPath)) {
|
|
609
|
+
fail(`${valuesPath}: the file does not exist; pass the path of the JSON values file`);
|
|
610
|
+
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (!existsSync(bandsPath)) {
|
|
615
|
+
fail(`${bandsPath}: the file does not exist; run this from the project root`);
|
|
616
|
+
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const series = readSeries(readFileSync(valuesPath, "utf8"));
|
|
621
|
+
const bands = readBands(readFileSync(bandsPath, "utf8"));
|
|
622
|
+
|
|
623
|
+
if ("problem" in series) {
|
|
624
|
+
fail(`${valuesPath}: ${series.problem}`);
|
|
625
|
+
} else if ("problem" in bands) {
|
|
626
|
+
fail(`${bandsPath}: ${bands.problem}`);
|
|
627
|
+
} else {
|
|
628
|
+
const report = toReport(bands.value, detect(series.value));
|
|
629
|
+
|
|
630
|
+
process.stdout.write(`${serializeReport(report)}\n`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Validate `bands.yaml` and report the self-tests that ran.
|
|
636
|
+
*/
|
|
637
|
+
function checkCommittedBands(): void {
|
|
638
|
+
if (!existsSync(bandsPath)) {
|
|
639
|
+
fail(`${bandsPath}: the file does not exist; run this from the project root`);
|
|
640
|
+
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const bands = readBands(readFileSync(bandsPath, "utf8"));
|
|
645
|
+
|
|
646
|
+
if ("problem" in bands) {
|
|
647
|
+
fail(`${bandsPath}: ${bands.problem}`);
|
|
648
|
+
} else {
|
|
649
|
+
process.stdout.write(
|
|
650
|
+
`Ran ${selfTestCases.length} bands self-tests and checked ${bandsPath}.\n`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (selfTestFailures.length > 0) {
|
|
656
|
+
const failureLines = selfTestFailures.map(name => `- ${name}`);
|
|
657
|
+
|
|
658
|
+
fail(`Bands self-tests failed:\n${failureLines.join("\n")}`);
|
|
659
|
+
} else if (argumentsList.length === 0) {
|
|
660
|
+
checkCommittedBands();
|
|
661
|
+
} else if (argumentsList.length > 1) {
|
|
662
|
+
fail("pass one argument: the path of the JSON file with the baseline and recent values");
|
|
663
|
+
} else {
|
|
664
|
+
run(argumentsList.at(0) ?? "");
|
|
665
|
+
}
|