forgedock 1.1.1 → 1.1.3
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 +53 -8
- package/bin/batch-runner.mjs +339 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +24 -2
- package/bin/engine/state.mjs +8 -3
- package/bin/engine-cli.mjs +177 -5
- package/bin/forgedock.mjs +409 -26
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +72 -14
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +70 -20
- package/bin/settings-hook.mjs +235 -1
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +518 -361
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +58 -1
- package/commands/deploy-pr.md +485 -0
- package/commands/design-bench.md +5 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/fix-ci.md +577 -0
- package/commands/forge-stats.md +121 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +23 -2
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2045
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +248 -10
- package/commands/recover-orphans.md +576 -0
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +591 -52
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +26 -7
- package/commands/work-on/build.md +163 -24
- package/commands/work-on/close.md +130 -3
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +24 -4
- package/commands/work-on-monolithic.md +7 -2
- package/commands/work-on.md +202 -4
- package/docs/BRAND.md +14 -7
- package/docs/CI.md +16 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +5 -8
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +120 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +386 -111
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +43 -4
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +9 -1
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +3 -1
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +254 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/worktree-lifecycle.sh +3 -3
- package/templates/devdocs/agent/using-forgedock.md +14 -0
- package/docs/site/.vitepress/config.mjs +0 -85
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ForgeDock Demo — A Risk-Free First Pipeline Run
|
|
2
2
|
|
|
3
|
-
A tiny **Notes API** (~
|
|
3
|
+
A tiny **Notes API** (~400 lines, zero dependencies) with twenty pre-written
|
|
4
4
|
issues. It exists so you can watch ForgeDock investigate, build, review, and
|
|
5
5
|
merge a change **without touching your real codebase**.
|
|
6
6
|
|
|
@@ -14,9 +14,10 @@ Trying ForgeDock on your own project means trusting an AI pipeline with real
|
|
|
14
14
|
code on your very first run. This demo removes that risk: it's a self-contained
|
|
15
15
|
sandbox where every issue is small, fast, and safe to break.
|
|
16
16
|
|
|
17
|
-
The API has a
|
|
18
|
-
check,
|
|
19
|
-
findings to surface — that's the part
|
|
17
|
+
The API has a handful of **intentional, clearly-labelled flaws** (a missing auth
|
|
18
|
+
check, two injection-vulnerable query paths, a mass-assignment gap) so that
|
|
19
|
+
ForgeDock's `/review-pr` agents have real findings to surface — that's the part
|
|
20
|
+
that makes the review feel impressive.
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -27,11 +28,11 @@ forgedock-demo/
|
|
|
27
28
|
├── src/
|
|
28
29
|
│ ├── server.js # built-in http server + router (no Express)
|
|
29
30
|
│ ├── router.js # (added by Issue #3)
|
|
30
|
-
│ ├── routes/notes.js # CRUD handlers — has
|
|
31
|
-
│ ├── db.js # in-memory store
|
|
31
|
+
│ ├── routes/notes.js # CRUD + count/tags/patch handlers — has intentional flaws
|
|
32
|
+
│ ├── db.js # in-memory store with tags, createdAt, archived fields
|
|
32
33
|
│ └── auth.js # bearer-token check
|
|
33
34
|
├── scripts/smoke.js # dependency-free smoke test (npm run smoke)
|
|
34
|
-
├── issues/ #
|
|
35
|
+
├── issues/ # 20 graded issue specs
|
|
35
36
|
├── forge.yaml # minimal working ForgeDock config
|
|
36
37
|
├── labels.json # workflow + priority labels
|
|
37
38
|
└── bootstrap.sh # one command to stand up the live repo
|
|
@@ -52,20 +53,37 @@ npm run smoke # runs the smoke test
|
|
|
52
53
|
|
|
53
54
|
---
|
|
54
55
|
|
|
55
|
-
## The
|
|
56
|
-
|
|
57
|
-
Each issue targets a different pipeline strength and
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
56
|
+
## The twenty demo issues
|
|
57
|
+
|
|
58
|
+
Each issue targets a different pipeline strength and difficulty tier.
|
|
59
|
+
Issues 1–5 are the original quick-start set; issues 6–20 expand the corpus
|
|
60
|
+
for statistically meaningful one-shot benchmark runs.
|
|
61
|
+
|
|
62
|
+
| # | Difficulty | Type | Title |
|
|
63
|
+
|---|-----------|------|-------|
|
|
64
|
+
| 1 | Easy | Bug / security | DELETE is missing an auth check |
|
|
65
|
+
| 2 | Medium | Feature / security | Safe filtering for `GET /notes` |
|
|
66
|
+
| 3 | Easy | Refactor | Extract the router module |
|
|
67
|
+
| 4 | Medium | Performance | O(1) `findById` |
|
|
68
|
+
| 5 | Easy | Docs | Add an API reference |
|
|
69
|
+
| 6 | Medium | Bug | POST stores malformed tags without validation |
|
|
70
|
+
| 7 | Medium | Bug | Negative `?offset=` returns wrong notes instead of 400 |
|
|
71
|
+
| 8 | Hard | Bug / security | PATCH allows mass-assignment of owner and secret |
|
|
72
|
+
| 9 | Hard | Bug / security | GET /notes/count is vulnerable to expression injection |
|
|
73
|
+
| 10 | Easy | Feature | Add `?sort=` parameter to GET /notes |
|
|
74
|
+
| 11 | Medium | Feature | Allow PATCH to update tags |
|
|
75
|
+
| 12 | Hard | Feature | POST /notes/bulk for all-or-nothing batch creation |
|
|
76
|
+
| 13 | Hard | Feature | Archive/soft-delete with `?archived=` list filter |
|
|
77
|
+
| 14 | Easy | Refactor | Extract title validation into validate.js |
|
|
78
|
+
| 15 | Medium | Refactor | Centralize auth enforcement with a protected route flag |
|
|
79
|
+
| 16 | Hard | Refactor | Extract generic store machinery into store.js |
|
|
80
|
+
| 17 | Easy | Performance | Replace O(n²) tag de-dup in listTags with a Set |
|
|
81
|
+
| 18 | Medium | Performance | Remove unnecessary O(n²) sort before pagination slice |
|
|
82
|
+
| 19 | Easy | Docs | Complete API reference covering all endpoints |
|
|
83
|
+
| 20 | Easy | Docs | Document the intentional-flaw and graded-issue convention |
|
|
66
84
|
|
|
67
85
|
Full specs live in [`issues/`](./issues). When you run `bootstrap.sh`, these
|
|
68
|
-
become real GitHub issues #1–#
|
|
86
|
+
become real GitHub issues #1–#20 in your demo repo, in this order.
|
|
69
87
|
|
|
70
88
|
---
|
|
71
89
|
|
|
@@ -87,7 +105,7 @@ credentials is creating the actual repo. One command does it:
|
|
|
87
105
|
2. Push this codebase to `main`.
|
|
88
106
|
3. Create the workflow + priority labels (via `npx forgedock labels setup`,
|
|
89
107
|
falling back to `labels.json`).
|
|
90
|
-
4. File
|
|
108
|
+
4. File all twenty issues from [`issues/`](./issues).
|
|
91
109
|
|
|
92
110
|
Then:
|
|
93
111
|
|
|
@@ -106,8 +124,9 @@ gate, open a PR, review it, and merge — all on a repo you can safely throw awa
|
|
|
106
124
|
|
|
107
125
|
## After your first run
|
|
108
126
|
|
|
109
|
-
Try `/work-on 2` through `/work-on
|
|
110
|
-
`/orchestrate` to let ForgeDock work several issues in
|
|
127
|
+
Try `/work-on 2` through `/work-on 20` to see the full range of pipeline phases
|
|
128
|
+
and issue types, or run `/orchestrate` to let ForgeDock work several issues in
|
|
129
|
+
parallel.
|
|
111
130
|
|
|
112
131
|
When you're comfortable, point ForgeDock at your real project — you've already
|
|
113
132
|
seen exactly what it does.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): POST /notes stores malformed tags without validation"
|
|
3
|
+
labels: ["bug", "priority:P2"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`POST /notes` accepts a `tags` field from the request body but stores it without
|
|
10
|
+
checking that it is an array. When a caller passes `"tags": 42` (or any non-array
|
|
11
|
+
value), the malformed value is saved to the in-memory store. A subsequent
|
|
12
|
+
`GET /notes?tag=work` then throws an uncaught `TypeError: n.tags.includes is not
|
|
13
|
+
a function` from the tag-filter in `listNotes()`, returning a 500 response for
|
|
14
|
+
all future tag-filtered requests — until the server is restarted.
|
|
15
|
+
|
|
16
|
+
## Steps to Reproduce
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. Create a note with malformed tags
|
|
20
|
+
curl -s -X POST http://localhost:3000/notes \
|
|
21
|
+
-H "Authorization: Bearer demo-token" \
|
|
22
|
+
-H "Content-Type: application/json" \
|
|
23
|
+
-d '{"title":"bad","tags":99}' | jq .
|
|
24
|
+
|
|
25
|
+
# 2. Any tag-filtered list now 500s
|
|
26
|
+
curl -s "http://localhost:3000/notes?tag=work" | jq .
|
|
27
|
+
# => {"error":"n.tags.includes is not a function"}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Affected Files
|
|
31
|
+
|
|
32
|
+
1. `src/routes/notes.js` — `createNote()` must validate that `tags`, when
|
|
33
|
+
supplied, is an array. Return 400 if it is not.
|
|
34
|
+
|
|
35
|
+
## Acceptance Criteria
|
|
36
|
+
|
|
37
|
+
- [ ] `POST /notes` with `"tags": 42` returns `400 Bad Request`.
|
|
38
|
+
- [ ] `POST /notes` with `"tags": ["work"]` still succeeds (201).
|
|
39
|
+
- [ ] `GET /notes?tag=work` works correctly after a valid note with tags is created.
|
|
40
|
+
- [ ] `npm run smoke` still passes.
|
|
41
|
+
|
|
42
|
+
> This issue exercises the **validation gap** pattern — the pipeline should
|
|
43
|
+
> add a type check at the write boundary so the read path stays safe.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): negative ?offset= returns wrong notes instead of 400"
|
|
3
|
+
labels: ["bug", "priority:P2"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes?offset=-1` silently applies JavaScript's negative-index slice
|
|
10
|
+
semantics (`Array.prototype.slice(-1)` returns the last element) instead of
|
|
11
|
+
returning a client error. A caller expecting an empty or 400 response receives
|
|
12
|
+
the final note in the list, which is confusing and incorrect.
|
|
13
|
+
|
|
14
|
+
## Steps to Reproduce
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# List all notes first
|
|
18
|
+
curl -s http://localhost:3000/notes | jq '.notes | length'
|
|
19
|
+
# => 3
|
|
20
|
+
|
|
21
|
+
# Negative offset returns the last note — unexpected
|
|
22
|
+
curl -s "http://localhost:3000/notes?offset=-1" | jq '.notes'
|
|
23
|
+
# => [{...note 3...}]
|
|
24
|
+
|
|
25
|
+
# Expected: 400 Bad Request or normalized to 0
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Affected Files
|
|
29
|
+
|
|
30
|
+
1. `src/routes/notes.js` — `listNotes()` must clamp `offset` to a non-negative
|
|
31
|
+
integer (reject with 400, or normalize to 0) before passing it to
|
|
32
|
+
`Array.prototype.slice`.
|
|
33
|
+
|
|
34
|
+
## Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- [ ] `GET /notes?offset=-1` returns `400 Bad Request` with a clear error message.
|
|
37
|
+
- [ ] `GET /notes?offset=0` still returns all notes.
|
|
38
|
+
- [ ] `GET /notes?offset=1&limit=2` returns notes starting from index 1.
|
|
39
|
+
- [ ] `npm run smoke` still passes.
|
|
40
|
+
|
|
41
|
+
> This issue demonstrates that **numeric input validation** is a separate concern
|
|
42
|
+
> from type validation — the value is a number but falls outside the accepted range.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): PATCH /notes/:id allows mass-assignment of owner and secret"
|
|
3
|
+
labels: ["bug", "security", "priority:P1"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`PATCH /notes/:id` lets any authenticated caller overwrite the `owner` and
|
|
10
|
+
`secret` fields alongside `title` and `body`. This is a mass-assignment
|
|
11
|
+
vulnerability: a caller can flip `"secret": true` notes to `false`, making them
|
|
12
|
+
visible in unauthenticated `GET /notes` responses, or reassign `owner` to
|
|
13
|
+
impersonate another user.
|
|
14
|
+
|
|
15
|
+
Unlike issue #1 (which has no auth check at all), this endpoint does require a
|
|
16
|
+
token — the flaw is that it accepts a too-wide field set.
|
|
17
|
+
|
|
18
|
+
## Steps to Reproduce
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Flip a secret note to non-secret without knowing its content
|
|
22
|
+
curl -s -X PATCH http://localhost:3000/notes/2 \
|
|
23
|
+
-H "Authorization: Bearer demo-token" \
|
|
24
|
+
-H "Content-Type: application/json" \
|
|
25
|
+
-d '{"secret":false}' | jq .
|
|
26
|
+
# => note is now visible to unauthenticated callers
|
|
27
|
+
|
|
28
|
+
# Confirm it is now publicly listed
|
|
29
|
+
curl -s http://localhost:3000/notes | jq '[.notes[] | select(.id==2)]'
|
|
30
|
+
# => [{...secret note now visible...}]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Affected Files
|
|
34
|
+
|
|
35
|
+
1. `src/routes/notes.js` — `updateNote()` must restrict the allowed fields to
|
|
36
|
+
`title` and `body` only. Attempts to set `owner` or `secret` should return
|
|
37
|
+
`400 Bad Request` with a message listing the disallowed fields.
|
|
38
|
+
|
|
39
|
+
## Acceptance Criteria
|
|
40
|
+
|
|
41
|
+
- [ ] `PATCH /notes/:id` with `{"secret": false}` returns `400 Bad Request`.
|
|
42
|
+
- [ ] `PATCH /notes/:id` with `{"owner": "hacker"}` returns `400 Bad Request`.
|
|
43
|
+
- [ ] `PATCH /notes/:id` with `{"title": "new"}` still succeeds and updates the title.
|
|
44
|
+
- [ ] `PATCH /notes/:id` with both `{"title": "new", "secret": false}` returns `400`.
|
|
45
|
+
- [ ] `npm run smoke` still passes.
|
|
46
|
+
|
|
47
|
+
> This issue covers the **mass-assignment / over-permissive write** pattern — a
|
|
48
|
+
> common class of security bugs where an update endpoint accepts more fields than
|
|
49
|
+
> it should.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): GET /notes/count is vulnerable to expression injection"
|
|
3
|
+
labels: ["bug", "security", "priority:P1"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes/count` builds its `db.query()` where-clause by concatenating the raw
|
|
10
|
+
`?owner=` query parameter into a string: `'owner === "' + owner + '"'`. This is
|
|
11
|
+
the same injection class as the `?where=` flaw in issue #2, but in a new
|
|
12
|
+
endpoint that issue #2's fix does not cover.
|
|
13
|
+
|
|
14
|
+
A caller can inject arbitrary JavaScript: `?owner=" || true; //` would evaluate
|
|
15
|
+
to `owner === "" || true; //` — matching every note and leaking the real count
|
|
16
|
+
regardless of owner.
|
|
17
|
+
|
|
18
|
+
## Steps to Reproduce
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Normal count
|
|
22
|
+
curl -s "http://localhost:3000/notes/count?owner=alice" | jq .
|
|
23
|
+
# => {"count":2}
|
|
24
|
+
|
|
25
|
+
# Injection — count all notes regardless of owner
|
|
26
|
+
curl -s 'http://localhost:3000/notes/count?owner=" || true; //' | jq .
|
|
27
|
+
# => {"count":3} (all notes leaked)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Affected Files
|
|
31
|
+
|
|
32
|
+
1. `src/routes/notes.js` — `countNotes()` must not build a `db.query()` clause
|
|
33
|
+
via string concatenation. Replace the `db.query()` call with a direct
|
|
34
|
+
`db.all().filter()` using strict equality checks on typed values.
|
|
35
|
+
|
|
36
|
+
## Acceptance Criteria
|
|
37
|
+
|
|
38
|
+
- [ ] `GET /notes/count?owner=alice` returns `{"count": 2}`.
|
|
39
|
+
- [ ] `GET /notes/count?owner=" || true; //` returns `{"count": 0}` (no match,
|
|
40
|
+
not a leak).
|
|
41
|
+
- [ ] `GET /notes/count?secret=true` returns the correct count of secret notes.
|
|
42
|
+
- [ ] `GET /notes/count` (no filters) returns the total note count.
|
|
43
|
+
- [ ] `npm run smoke` still passes.
|
|
44
|
+
|
|
45
|
+
> This issue reinforces the pattern from issue #2 — injection through dynamic
|
|
46
|
+
> `db.query()` clause building — but in a different endpoint, demonstrating that
|
|
47
|
+
> fixing the first occurrence doesn't automatically close the class.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): add ?sort= parameter to GET /notes"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "easy"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes` always returns notes in insertion order. There is no way for a
|
|
10
|
+
caller to request notes sorted by `title` or `createdAt` without fetching all
|
|
11
|
+
notes and sorting client-side.
|
|
12
|
+
|
|
13
|
+
## Proposed Solution
|
|
14
|
+
|
|
15
|
+
Add a `?sort=` query parameter to `GET /notes` that accepts:
|
|
16
|
+
|
|
17
|
+
- `sort=id` — sort by numeric id ascending (current default behavior)
|
|
18
|
+
- `sort=title` — sort alphabetically by `title`
|
|
19
|
+
- `sort=createdAt` — sort by `createdAt` string (ISO 8601, lexicographic sort
|
|
20
|
+
is equivalent to chronological for the demo's fixed-format timestamps)
|
|
21
|
+
|
|
22
|
+
An unknown `sort` value should return `400 Bad Request`.
|
|
23
|
+
|
|
24
|
+
## Affected Files
|
|
25
|
+
|
|
26
|
+
1. `src/routes/notes.js` — extend `listNotes()` to read a `sort` query param
|
|
27
|
+
and apply the appropriate comparison before returning the results array.
|
|
28
|
+
|
|
29
|
+
## Acceptance Criteria
|
|
30
|
+
|
|
31
|
+
- [ ] `GET /notes?sort=title` returns notes sorted alphabetically by title.
|
|
32
|
+
- [ ] `GET /notes?sort=createdAt` returns notes sorted oldest-first.
|
|
33
|
+
- [ ] `GET /notes?sort=id` returns notes in id order (same as current default).
|
|
34
|
+
- [ ] `GET /notes?sort=unknown` returns `400 Bad Request`.
|
|
35
|
+
- [ ] `GET /notes` (no sort param) still returns notes in insertion order.
|
|
36
|
+
- [ ] `npm run smoke` still passes.
|
|
37
|
+
|
|
38
|
+
> A focused, additive feature — the pipeline should confirm no existing
|
|
39
|
+
> behavior changes and that the 400 guard is present.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): allow PATCH /notes/:id to update tags"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`PATCH /notes/:id` currently only updates `title` and `body` (after the fix from
|
|
10
|
+
issue #8). There is no way to add or replace tags on an existing note without
|
|
11
|
+
deleting and recreating it.
|
|
12
|
+
|
|
13
|
+
## Proposed Solution
|
|
14
|
+
|
|
15
|
+
Extend `PATCH /notes/:id` to accept an optional `tags` field. The value must be
|
|
16
|
+
an array of strings (validate at write time, same fix pattern as issue #6).
|
|
17
|
+
When supplied, it replaces the note's existing `tags` array entirely — a full
|
|
18
|
+
replacement (not a merge/append), keeping the semantics simple.
|
|
19
|
+
|
|
20
|
+
## Affected Files
|
|
21
|
+
|
|
22
|
+
1. `src/routes/notes.js` — `updateNote()` should accept `tags` in addition to
|
|
23
|
+
`title` and `body`. Validate that `tags` is an array when present; return
|
|
24
|
+
`400` if it is not.
|
|
25
|
+
|
|
26
|
+
## Acceptance Criteria
|
|
27
|
+
|
|
28
|
+
- [ ] `PATCH /notes/:id` with `{"tags": ["work", "urgent"]}` replaces the note's
|
|
29
|
+
tags and returns the updated note.
|
|
30
|
+
- [ ] `PATCH /notes/:id` with `{"tags": 99}` returns `400 Bad Request`.
|
|
31
|
+
- [ ] Omitting `tags` from the patch body leaves existing tags unchanged.
|
|
32
|
+
- [ ] `GET /notes?tag=urgent` returns the patched note after its tags are updated.
|
|
33
|
+
- [ ] `npm run smoke` still passes.
|
|
34
|
+
|
|
35
|
+
> This feature has a deliberate dependency on issue #8 being fixed first — tags
|
|
36
|
+
> must not be writable via mass-assignment before they are intentionally writable
|
|
37
|
+
> via a validated field. Run issue #8 before this one.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): add POST /notes/bulk for all-or-nothing batch creation"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
Creating multiple notes requires a separate `POST /notes` call per note.
|
|
10
|
+
For loading test fixtures or seeding data, callers want to create several notes
|
|
11
|
+
in one request, with the guarantee that either all are created or none are
|
|
12
|
+
(all-or-nothing semantics).
|
|
13
|
+
|
|
14
|
+
## Proposed Solution
|
|
15
|
+
|
|
16
|
+
Add `POST /notes/bulk` (protected) that accepts `{ "notes": [...] }` where each
|
|
17
|
+
item has the same shape as the single-note `POST /notes` body. The endpoint
|
|
18
|
+
must:
|
|
19
|
+
|
|
20
|
+
1. Validate that the top-level `notes` field is a non-empty array.
|
|
21
|
+
2. Validate that every item has a non-empty `title`.
|
|
22
|
+
3. Only insert any notes if all items are valid (all-or-nothing).
|
|
23
|
+
4. Return `{ "notes": [...created notes] }` with status 201 on success.
|
|
24
|
+
|
|
25
|
+
## Affected Files
|
|
26
|
+
|
|
27
|
+
1. `src/routes/notes.js` — add a new `bulkCreateNotes()` handler.
|
|
28
|
+
2. `src/server.js` — register `POST /notes/bulk` in the route table **before**
|
|
29
|
+
the existing `POST /notes` entry so the literal path match takes priority.
|
|
30
|
+
|
|
31
|
+
## Acceptance Criteria
|
|
32
|
+
|
|
33
|
+
- [ ] `POST /notes/bulk` with a valid array creates all notes and returns 201.
|
|
34
|
+
- [ ] If any note is missing `title`, returns `400` and no notes are created.
|
|
35
|
+
- [ ] `POST /notes/bulk` without a token returns `401`.
|
|
36
|
+
- [ ] `POST /notes/bulk` with `{"notes": []}` returns `400`.
|
|
37
|
+
- [ ] `GET /notes` after a successful bulk create shows the new notes.
|
|
38
|
+
- [ ] `npm run smoke` still passes.
|
|
39
|
+
|
|
40
|
+
> This feature requires careful thought about route ordering (the `/bulk`
|
|
41
|
+
> literal path must not be shadowed by `POST /notes$`) and about transaction
|
|
42
|
+
> semantics in the in-memory store.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): implement archive/soft-delete with ?archived= list filter"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`DELETE /notes/:id` permanently removes a note. There is no way to soft-delete
|
|
10
|
+
(archive) a note and later restore it or filter archived notes out of the default
|
|
11
|
+
listing. The `archived` field already exists in the data model but is not
|
|
12
|
+
writable or filterable.
|
|
13
|
+
|
|
14
|
+
## Proposed Solution
|
|
15
|
+
|
|
16
|
+
1. Add `POST /notes/:id/archive` (protected) — sets `archived: true` on the
|
|
17
|
+
note. Returns the updated note.
|
|
18
|
+
2. Add `POST /notes/:id/restore` (protected) — sets `archived: false`.
|
|
19
|
+
3. Extend `GET /notes` to support `?archived=true` (list only archived notes),
|
|
20
|
+
`?archived=false` (list only non-archived notes — new default behavior), and
|
|
21
|
+
no `archived` param (list all, preserving today's behavior).
|
|
22
|
+
|
|
23
|
+
By default (no `?archived=` param) `GET /notes` should still return all notes,
|
|
24
|
+
including archived ones, to avoid a breaking change for the existing smoke test.
|
|
25
|
+
|
|
26
|
+
## Affected Files
|
|
27
|
+
|
|
28
|
+
1. `src/routes/notes.js` — add `archiveNote()` and `restoreNote()` handlers;
|
|
29
|
+
extend `listNotes()` to apply an `archived` filter when the param is present.
|
|
30
|
+
2. `src/server.js` — register the two new routes:
|
|
31
|
+
- `POST /notes/:id/archive`
|
|
32
|
+
- `POST /notes/:id/restore`
|
|
33
|
+
|
|
34
|
+
## Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- [ ] `POST /notes/1/archive` (with token) returns `{"note": {..., "archived": true}}`.
|
|
37
|
+
- [ ] `POST /notes/1/restore` (with token) returns `{"note": {..., "archived": false}}`.
|
|
38
|
+
- [ ] `GET /notes?archived=false` omits archived notes.
|
|
39
|
+
- [ ] `GET /notes?archived=true` returns only archived notes.
|
|
40
|
+
- [ ] `GET /notes` (no param) returns all notes regardless of archived status.
|
|
41
|
+
- [ ] Both new routes return `401` without a token and `404` for unknown ids.
|
|
42
|
+
- [ ] `npm run smoke` still passes.
|
|
43
|
+
|
|
44
|
+
> A multi-route feature that touches both handler and server wiring. Good for
|
|
45
|
+
> testing whether the pipeline correctly sequences route registration and
|
|
46
|
+
> confirms no regression on the existing list/create/delete flows.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "refactor(notes): extract title validation into a shared validate.js module"
|
|
3
|
+
labels: ["refactor", "priority:P3"]
|
|
4
|
+
difficulty: "easy"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`createNote()` in `src/routes/notes.js` has an inline `if (!title)` guard. When
|
|
10
|
+
the bulk-create endpoint (issue #12) is added, the same check will need to be
|
|
11
|
+
duplicated. Any future handler that creates or modifies a note title will face
|
|
12
|
+
the same duplication.
|
|
13
|
+
|
|
14
|
+
This is a pure refactor — no behavior change, no new validation rules.
|
|
15
|
+
|
|
16
|
+
## Proposed Solution
|
|
17
|
+
|
|
18
|
+
Extract the title-presence check into a new `src/validate.js` module that
|
|
19
|
+
exports a single function: `validateNote({ title })`. The function throws a
|
|
20
|
+
`400`-status error if `title` is falsy, identical to the current inline guard.
|
|
21
|
+
`createNote()` (and, if issue #12 is already merged, `bulkCreateNotes()`) should
|
|
22
|
+
call `validateNote()` instead of the inline check.
|
|
23
|
+
|
|
24
|
+
## Affected Files
|
|
25
|
+
|
|
26
|
+
1. `src/validate.js` (NEW) — exports `validateNote({ title })`.
|
|
27
|
+
2. `src/routes/notes.js` — replace the inline `if (!title)` block with
|
|
28
|
+
`validateNote({ title })` imported from `../validate`.
|
|
29
|
+
|
|
30
|
+
## Acceptance Criteria
|
|
31
|
+
|
|
32
|
+
- [ ] `POST /notes` without a `title` still returns `400` with the same error
|
|
33
|
+
message as before.
|
|
34
|
+
- [ ] `src/routes/notes.js` no longer contains a bare `if (!title)` guard —
|
|
35
|
+
that logic lives only in `src/validate.js`.
|
|
36
|
+
- [ ] `npm run smoke` still passes.
|
|
37
|
+
- [ ] No behavior change in any endpoint.
|
|
38
|
+
|
|
39
|
+
> A zero-behavior-change refactor. The review agents should confirm that the
|
|
40
|
+
> 400 path, the error message, and all other responses are byte-for-byte
|
|
41
|
+
> identical to the pre-refactor baseline.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "refactor(server): centralize auth enforcement with a protected flag in the route table"
|
|
3
|
+
labels: ["refactor", "priority:P3"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
Authorization (`requireToken`) is enforced by each route handler individually.
|
|
10
|
+
As routes grow, there is no single place to see which routes are protected — a
|
|
11
|
+
reviewer must read every handler. Missing a `requireToken` call in a new handler
|
|
12
|
+
is exactly how issue #1's bug was introduced.
|
|
13
|
+
|
|
14
|
+
## Proposed Solution
|
|
15
|
+
|
|
16
|
+
Add a boolean `protected` flag as the fourth element of each route-table entry
|
|
17
|
+
in `src/server.js`. The server's request dispatch loop should call
|
|
18
|
+
`requireToken(req)` centrally, before the handler is invoked, whenever the flag
|
|
19
|
+
is `true`. Handlers that currently call `requireToken()` themselves should have
|
|
20
|
+
that call removed (it is now redundant).
|
|
21
|
+
|
|
22
|
+
This is a pure refactor — every endpoint's authorization behavior must remain
|
|
23
|
+
identical.
|
|
24
|
+
|
|
25
|
+
## Affected Files
|
|
26
|
+
|
|
27
|
+
1. `src/server.js` — add `protected: true/false` to each TABLE entry; call
|
|
28
|
+
`requireToken(req)` in the dispatch loop when the flag is set.
|
|
29
|
+
2. `src/routes/notes.js` — remove all `requireToken(req)` calls from handlers
|
|
30
|
+
that will be protected by the table flag instead.
|
|
31
|
+
|
|
32
|
+
## Acceptance Criteria
|
|
33
|
+
|
|
34
|
+
- [ ] `POST /notes` without a token still returns `401`.
|
|
35
|
+
- [ ] `PATCH /notes/:id` without a token still returns `401`.
|
|
36
|
+
- [ ] `DELETE /notes/:id` still returns its current response (intentionally
|
|
37
|
+
unprotected in the baseline — issue #1 is not yet fixed).
|
|
38
|
+
- [ ] No `requireToken(req)` call remains inside any route handler.
|
|
39
|
+
- [ ] `npm run smoke` still passes.
|
|
40
|
+
- [ ] No behavior change in any endpoint.
|
|
41
|
+
|
|
42
|
+
> This refactor makes the authorization surface visible at a glance —
|
|
43
|
+
> a pattern that directly prevents the class of bug introduced in issue #1.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "refactor(db): extract the generic array-store machinery into store.js"
|
|
3
|
+
labels: ["refactor", "priority:P3"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`src/db.js` conflates two things: the generic in-memory array/index operations
|
|
10
|
+
(`all`, `findById`, `insert`, `remove`, `reset`) and the Notes-specific seed
|
|
11
|
+
data and field defaults. If a second resource (e.g. users) were added, all of
|
|
12
|
+
that generic machinery would need to be duplicated.
|
|
13
|
+
|
|
14
|
+
This is a pure refactor — no behavior change, no new features.
|
|
15
|
+
|
|
16
|
+
## Proposed Solution
|
|
17
|
+
|
|
18
|
+
Extract the generic store machinery into `src/store.js`, exporting a factory
|
|
19
|
+
function `createStore(seed, defaults)`. The returned object exposes:
|
|
20
|
+
`all`, `findById`, `insert`, `remove`, `reset`.
|
|
21
|
+
|
|
22
|
+
`src/db.js` becomes a thin wrapper: it calls `createStore(SEED, { secret:
|
|
23
|
+
false, tags: [], archived: false })` and re-exports the resulting methods plus
|
|
24
|
+
`query` and `bubbleSortById` (which remain in `db.js` as Notes-specific logic).
|
|
25
|
+
|
|
26
|
+
The public API visible to `routes/notes.js` must be byte-for-byte compatible.
|
|
27
|
+
|
|
28
|
+
## Affected Files
|
|
29
|
+
|
|
30
|
+
1. `src/store.js` (NEW) — generic `createStore(seed, defaults)` factory.
|
|
31
|
+
2. `src/db.js` — delegates `all`/`findById`/`insert`/`remove`/`reset` to the
|
|
32
|
+
store instance; keeps `query`/`bubbleSortById`.
|
|
33
|
+
|
|
34
|
+
## Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- [ ] `db.all()`, `db.findById()`, `db.insert()`, `db.remove()`, `db.reset()`
|
|
37
|
+
all behave identically to today.
|
|
38
|
+
- [ ] `db.query()` and `db.bubbleSortById()` are unchanged.
|
|
39
|
+
- [ ] No code in `src/routes/notes.js` or `src/server.js` changes.
|
|
40
|
+
- [ ] `npm run smoke` still passes.
|
|
41
|
+
- [ ] No behavior change in any endpoint.
|
|
42
|
+
|
|
43
|
+
> A structural refactor that introduces an abstraction boundary — the review
|
|
44
|
+
> agents should verify that the public interface is preserved exactly and that
|
|
45
|
+
> no observable behavior changes.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "perf(notes): replace O(n²) tag de-duplication in listTags with a Set"
|
|
3
|
+
labels: ["performance", "priority:P3"]
|
|
4
|
+
difficulty: "easy"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes/tags` de-duplicates tags using a nested loop: for every tag in every
|
|
10
|
+
note it scans the entire `unique` array to check for a duplicate. This is O(n²)
|
|
11
|
+
on the total tag count. With a large corpus of notes and many tags, this becomes
|
|
12
|
+
slow.
|
|
13
|
+
|
|
14
|
+
The fix is a one-line change to use a `Set` for deduplication instead.
|
|
15
|
+
|
|
16
|
+
## Investigation Hint
|
|
17
|
+
|
|
18
|
+
Before optimizing, confirm where the nested loop is and calculate the actual
|
|
19
|
+
time complexity. Verify that the `Set`-based replacement produces the same
|
|
20
|
+
output (same tags, same order — first-seen wins, matching the current
|
|
21
|
+
behavior).
|
|
22
|
+
|
|
23
|
+
## Affected Files
|
|
24
|
+
|
|
25
|
+
1. `src/routes/notes.js` — `listTags()`: replace the nested-loop uniqueness
|
|
26
|
+
check with a `Set`.
|
|
27
|
+
|
|
28
|
+
## Acceptance Criteria
|
|
29
|
+
|
|
30
|
+
- [ ] `GET /notes/tags` returns the same tags in first-seen order as before.
|
|
31
|
+
- [ ] The nested loop in `listTags()` is removed.
|
|
32
|
+
- [ ] `npm run smoke` still passes.
|
|
33
|
+
|
|
34
|
+
> A focused, single-function perf fix. The pipeline should confirm output
|
|
35
|
+
> equivalence before and after the change, not just that the test passes.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "perf(notes): remove unnecessary O(n²) sort before pagination slice"
|
|
3
|
+
labels: ["performance", "priority:P3"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes?limit=...&offset=...` runs `db.bubbleSortById(results)` before
|
|
10
|
+
slicing — an O(n²) bubble sort — even though `results` is already in id order
|
|
11
|
+
(the in-memory store is maintained in insertion order and ids are
|
|
12
|
+
monotonically increasing). The sort is both redundant and slow.
|
|
13
|
+
|
|
14
|
+
## Investigation Hint
|
|
15
|
+
|
|
16
|
+
Trace the insertion order invariant through `db.insert()` and `db.reset()` to
|
|
17
|
+
confirm that `notes` is always in id-ascending order. Then verify that
|
|
18
|
+
`db.bubbleSortById` does not change the element order for already-sorted input
|
|
19
|
+
(it should not — confirm this, don't assume it). Once confirmed, the sort call
|
|
20
|
+
is safe to remove.
|
|
21
|
+
|
|
22
|
+
## Affected Files
|
|
23
|
+
|
|
24
|
+
1. `src/routes/notes.js` — `listNotes()`: remove the `db.bubbleSortById(results)`
|
|
25
|
+
call in the pagination branch. Slice `results` directly.
|
|
26
|
+
|
|
27
|
+
## Acceptance Criteria
|
|
28
|
+
|
|
29
|
+
- [ ] `GET /notes?limit=2` returns the same two notes as before (first two by
|
|
30
|
+
id).
|
|
31
|
+
- [ ] `GET /notes?offset=1&limit=1` returns the same note as before.
|
|
32
|
+
- [ ] The `db.bubbleSortById` call is no longer present in `listNotes()`.
|
|
33
|
+
- [ ] `npm run smoke` still passes.
|
|
34
|
+
|
|
35
|
+
> This issue showcases the **investigation phase confirming an invariant** —
|
|
36
|
+
> the pipeline must verify the insertion-order guarantee before removing the
|
|
37
|
+
> sort, rather than blindly deleting it.
|