Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Tekitl pre-commit hook
# Blocks commits whose staged JS/TS changes fail ESLint. Honors Constitution
# (Principle VI Code Hygiene + Development Workflow lint gate).
#
# Activated automatically by the `prepare` npm script which sets
# `git config core.hooksPath .githooks` after dependency install.

set -euo pipefail

# Collect staged JS/TS files (Added, Copied, Modified, Renamed).
mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACMR \
-- '*.js' '*.jsx' '*.ts' '*.tsx' '*.cjs' '*.mjs')

if [ "${#staged[@]}" -eq 0 ]; then
exit 0
fi

# Reject partially staged JS/TS files: ESLint reads the working tree, so
# linting partially staged content would produce a false pass or fail and
# undermine the staged-content gate. Force the developer to stage the full
# file (or stash unstaged hunks) so the lint result matches what is committed.
if ! git diff --quiet -- "${staged[@]}"; then
echo "[pre-commit] ERROR: partially staged JS/TS files detected." >&2
echo "[pre-commit] Stage full files (or stash unstaged hunks) before commit." >&2
exit 1
fi

echo "[pre-commit] Running ESLint on ${#staged[@]} staged file(s)..."

# `--` terminates option parsing so a filename starting with `-` cannot be
# misread as an ESLint flag.
if command -v bun >/dev/null 2>&1; then
bun x eslint --no-warn-ignored -- "${staged[@]}"
elif command -v npx >/dev/null 2>&1; then
npx --no-install eslint --no-warn-ignored -- "${staged[@]}"
else
echo "[pre-commit] ERROR: neither 'bun' nor 'npx' found in PATH." >&2
echo "[pre-commit] Install Bun or Node.js and run 'bun install' (or 'npm install')." >&2
exit 1
fi
152 changes: 123 additions & 29 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
<!--
SYNC IMPACT REPORT
==================
Version change: (uninitialized template) → 1.0.0
Bump rationale: Initial ratification of the project constitution. MAJOR per
semver convention for the first stable governance document.
Version change: 1.1.0 → 1.2.0
Bump rationale: Reframed Principle II from "Test-First" to explicit
"Test-Driven Development (TDD) (NON-NEGOTIABLE)" with the Red-Green-
Refactor cycle and frontend Vitest coverage; added an automated lint gate
(pre-commit hook) to the Development Workflow. MINOR per semver convention
because the existing Test-First obligation already covered the new TDD
requirements — this amendment expands and clarifies rather than redefines.

Modified principles:
- [PRINCIPLE_1_NAME] → I. Community-First Design
- [PRINCIPLE_2_NAME] → II. Test-First (NON-NEGOTIABLE)
- [PRINCIPLE_3_NAME] → III. Laravel-Native, Convention Over Custom
- [PRINCIPLE_4_NAME] → IV. Typed End-to-End
- [PRINCIPLE_5_NAME] → V. Accessibility & Privacy by Default
- I. Community-First Design (unchanged)
- II. Test-First → II. Test-Driven Development (NON-NEGOTIABLE)
(renamed and expanded to mandate Red-Green-Refactor cycle and to cover
frontend Vitest tests in addition to Pest)
- III. Laravel-Native, Convention Over Custom (unchanged)
- IV. Typed End-to-End (unchanged)
- V. Accessibility & Privacy by Default (unchanged)
- VI. Code Hygiene: DRY, SOLID, KISS (unchanged)

Added sections:
- Technology Constraints (Section 2)
- Development Workflow (Section 3)
- Governance
- Development Workflow → "Automated Lint Gate" subsection mandating the
`.githooks/pre-commit` ESLint hook activated via `core.hooksPath`.

Removed sections: none

Templates requiring updates:
- ✅ .specify/templates/plan-template.md (Constitution Check section already
references constitution generically; no rewrite required)
- ✅ .specify/templates/plan-template.md (Constitution Check section
references constitution generically; will pick up the renamed
principle and lint gate on next plan run; no template rewrite required)
- ✅ .specify/templates/spec-template.md (no constitution-specific gates;
aligned)
- ✅ .specify/templates/tasks-template.md (no constitution-specific
categorization required by current principles)
- ✅ .specify/templates/tasks-template.md (no per-principle categorization
required)
- ⚠ pending: CLAUDE.md / docs — runtime guidance does not yet cite the
constitution explicitly; may be cross-linked in a future amendment

Expand All @@ -49,15 +56,34 @@ wellbeing wins. Rationale: Tekitl exists for the people and organizations it
serves, not for the platform itself; this principle is the lens for every
product decision.

### II. Test-First (NON-NEGOTIABLE)

Pest feature and unit tests MUST be written and demonstrated failing before
the implementation that satisfies them. Every change MUST be programmatically
tested via a new or updated test. `php artisan test --compact` MUST pass
locally and in CI before merge. Verification scripts and manual tinker checks
MUST NOT replace tests when tests are feasible. Rationale: prevents
regressions in a multi-tenant collaboration platform where data correctness
between organizations is critical.
### II. Test-Driven Development (NON-NEGOTIABLE)

All production code MUST be produced via the Red-Green-Refactor cycle:

1. **Red.** Write a failing Pest (backend) or Vitest (frontend) test that
captures the next slice of behavior. Run it and observe it fail with a
meaningful assertion error — not a syntax or import error. Failing-by-
construction MUST be demonstrated before any production code is written.
2. **Green.** Write the minimum production code required to make the new
test pass. No speculative branches, no extra features, no premature
abstraction (this also enforces Principles VI/KISS).
3. **Refactor.** With the suite green, improve names, structure, and
duplication; rerun tests after each refactor step. Refactors that change
behavior require a new Red step, not edits to the existing test.

Every change — backend or frontend — MUST be programmatically covered. For
backend work, `php artisan test --compact` MUST pass locally and in CI
before merge. For frontend work, `bun run test:js` (Vitest) MUST pass; UI
that crosses the page boundary additionally requires a Pest browser test.
Verification scripts and manual tinker checks MUST NOT replace tests when
tests are feasible. Pull requests that introduce production code without
the corresponding test commit history (or a test added in the same PR with
a credible Red-Green narrative) MUST be rejected.

Rationale: TDD prevents regressions in a multi-tenant collaboration
platform where data correctness between organizations is critical, keeps
the design pressure on small testable units (reinforcing Principle VI),
and produces an executable specification of behavior alongside the code.

### III. Laravel-Native, Convention Over Custom

Expand Down Expand Up @@ -93,6 +119,52 @@ Rationale: non-profits handle sensitive constituent and donor data; defaults
that protect users and organizations are non-negotiable for the trust the
platform requires.

### VI. Code Hygiene: DRY, SOLID, KISS

All production code MUST observe the following design discipline; reviewers
and `/speckit-plan` Constitution Check MUST flag deviations.

**DRY (Don't Repeat Yourself).** Domain knowledge MUST have a single
authoritative representation. Duplicated logic across controllers, services,
or React components MUST be extracted (Eloquent scopes, Form Requests,
Action classes, custom hooks, shared TS types from Wayfinder) once it
recurs a third time or once divergence would create a correctness risk.
Copy-paste across modules without an extraction plan is prohibited.
Configuration values MUST live in `config/` and be read via `config()`;
literal duplication of strings, magic numbers, or URLs across files is a
violation. Note: premature abstraction is itself a DRY violation —
extraction MUST follow real duplication, not anticipated duplication.

**SOLID.** New classes and modules MUST respect:
- *Single Responsibility*: a class/function does one thing; controllers
delegate to services, jobs, or actions rather than absorbing business
logic. Fat controllers and god models are prohibited.
- *Open/Closed*: extension via composition, policies, events, or strategy
classes is preferred over editing stable shared code paths.
- *Liskov Substitution*: subclasses and trait users MUST honor the parent
contract; no surprise behavior changes when substituted.
- *Interface Segregation*: prefer small, focused interfaces over wide ones;
consumers MUST NOT depend on methods they do not use.
- *Dependency Inversion*: depend on abstractions (interfaces, contracts,
the service container) rather than concrete classes; resolve through
Laravel's container, not via `new` for services with collaborators.

**KISS (Keep It Simple).** The simplest design that satisfies the spec wins.
Speculative configurability, plugin systems, and abstraction layers added
"for the future" are prohibited; build them when a second concrete
caller materializes. Cyclomatic complexity, deep inheritance, and clever
one-liners that obscure intent MUST be refactored in favor of straight-line
code with descriptive names. When two designs are equivalent, prefer fewer
files, fewer indirections, and framework-native primitives (per Principle
III).

Rationale: a small team maintaining a multi-tenant collaboration platform
cannot afford the maintenance tax of duplicated logic, tangled
responsibilities, or speculative complexity. These three principles are the
floor of professional code quality and the lens through which `/speckit-plan`
and reviewers MUST evaluate every change. Justified deviations MUST be
recorded in the plan's Complexity Tracking section.

## Technology Constraints

The supported stack is PHP 8.5, Laravel 12, Inertia.js v2, React 19,
Expand Down Expand Up @@ -120,10 +192,32 @@ the feature's `specs/<dir>/` directory.
record any deviations in the plan's Complexity Tracking section with explicit
justification. Unjustified violations block the plan.

Pull requests MUST: (a) include or update Pest tests for changed behavior,
(b) pass `php artisan test --compact`, (c) pass Pint, and (d) reference the
relevant spec directory. Reviews MUST verify constitutional compliance in
addition to correctness.
Pull requests MUST: (a) include or update Pest and/or Vitest tests for
changed behavior, (b) pass `php artisan test --compact`, (c) pass
`bun run test:js` when frontend code is changed, (d) pass Pint, (e) pass
ESLint with no errors, and (f) reference the relevant spec directory.
Reviews MUST verify constitutional compliance in addition to correctness.

### Automated Lint Gate

The repository ships a tracked `.githooks/pre-commit` hook that runs ESLint
against the staged JavaScript and TypeScript files and aborts the commit on
any error. The hook is activated automatically by the `prepare` npm script,
which sets `core.hooksPath` to `.githooks` after dependency install; it is
also enforceable manually via `git config --local core.hooksPath .githooks`.

- The hook MUST NOT be bypassed with `--no-verify` except for emergency
hotfixes that are followed by a corrective commit on the same branch.
- ESLint configuration changes MUST be reviewed under the same standard as
any other code change; weakening rules to silence violations is
prohibited unless the rule itself is justified-as-incorrect in the PR.
- Removing, disabling, or short-circuiting the hook is a constitutional
change and requires a constitution amendment, not a silent edit.

Rationale: putting the lint gate at the commit boundary catches violations
before they reach review or CI, keeps the trunk green, and reinforces
Principle VI (code hygiene) automatically rather than relying on reviewer
vigilance.

## Governance

Expand All @@ -142,4 +236,4 @@ is the primary enforcement point for new feature work; reviewers are the
enforcement point for ad-hoc changes. Complexity that violates a principle
MUST be justified in writing or removed.

**Version**: 1.0.0 | **Ratified**: 2026-05-01 | **Last Amended**: 2026-05-01
**Version**: 1.2.0 | **Ratified**: 2026-05-01 | **Last Amended**: 2026-05-05
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"format": "prettier --write resources/",
"format:check": "prettier --check resources/",
"lint": "eslint . --fix",
"prepare": "git config --local core.hooksPath .githooks || echo \"[prepare] WARNING: could not set core.hooksPath=.githooks; activate it manually with 'git config --local core.hooksPath .githooks'.\"",
"test:js": "vitest run",
"types": "tsc --noEmit"
},
Expand Down
7 changes: 5 additions & 2 deletions resources/js/components/ui/proyectos/CrowdfundingProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { type ReactNode } from 'react'
import { type ProjectRole } from '@/components/ui/proyectos/ProjectRoles'
import CoinIcon from '@/components/vector-graphics/CoinIcon'
import { formatCount } from '@/lib/utils'
import { type ProjectRole } from '@/components/ui/proyectos/ProjectRoles'

interface CrowdfundingProgressProps {
coins: number
roles: ProjectRole[]
children?: ReactNode
}

export default function CrowdfundingProgress({ coins, roles }: CrowdfundingProgressProps) {
export default function CrowdfundingProgress({ coins, roles, children }: CrowdfundingProgressProps) {
const totalSlots = roles.reduce((sum, r) => sum + r.slots, 0)
const filledSlots = roles.reduce((sum, r) => sum + r.filledSlots, 0)
const percentage = totalSlots > 0 ? Math.min(Math.round((filledSlots / totalSlots) * 100), 100) : 0
Expand All @@ -34,6 +36,7 @@ export default function CrowdfundingProgress({ coins, roles }: CrowdfundingProgr
{totalSlots > 0 && (
<p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">{percentage}% completo</p>
)}
{children}
</div>
)
}
24 changes: 13 additions & 11 deletions resources/js/pages/proyectos/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ export default function ProyectoShow({ project, post, isOwner, currentUserApplic
}

useEffect(() => {
loadComments()
loadComments().catch(() => {
setCommentsLoading(false)
setCommentsList([])
})
}, [])

return (
Expand Down Expand Up @@ -140,20 +143,19 @@ loadComments()
stageLabel={project.stageLabel}
/>

{/* Stage transition actions (owner only) */}
{isOwner && project.allowedTransitions.length > 0 && (
<ProjectStageActions
projectId={project.id}
allowedTransitions={project.allowedTransitions}
/>
)}

{/* Image gallery */}
<ImageGallery images={project.images} isOwner={isOwner} projectId={project.id} />

{/* Progress */}
{/* Progress (with stage transition actions for owner) */}
<div className="mt-6">
<CrowdfundingProgress coins={post.coins} roles={project.roles} />
<CrowdfundingProgress coins={post.coins} roles={project.roles}>
{isOwner && project.allowedTransitions.length > 0 && (
<ProjectStageActions
projectId={project.id}
allowedTransitions={project.allowedTransitions}
/>
)}
</CrowdfundingProgress>
</div>

{/* Roles */}
Expand Down
Loading