forgedock 1.1.1 → 1.1.2

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.
Files changed (171) hide show
  1. package/README.md +53 -8
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/demo.mjs +92 -7
  4. package/bin/engine/phases.mjs +20 -0
  5. package/bin/engine/state.mjs +8 -3
  6. package/bin/engine-cli.mjs +177 -5
  7. package/bin/forgedock.mjs +409 -26
  8. package/bin/hooks/interactive-engine.mjs +509 -0
  9. package/bin/hooks/pre-tool-use.mjs +498 -0
  10. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  11. package/bin/journey.mjs +72 -14
  12. package/bin/registry.mjs +139 -0
  13. package/bin/report.mjs +447 -0
  14. package/bin/runner.mjs +68 -19
  15. package/bin/settings-hook.mjs +235 -1
  16. package/commands/adopt.md +4 -1
  17. package/commands/analytics.md +4 -1
  18. package/commands/audit-agents.md +110 -27
  19. package/commands/audit.md +20 -1
  20. package/commands/autopilot.md +80 -8
  21. package/commands/changelog.md +2 -1
  22. package/commands/ci-audit.md +2 -1
  23. package/commands/cleanup.md +161 -1
  24. package/commands/compat-audit.md +2 -1
  25. package/commands/deploy-info.md +58 -1
  26. package/commands/design-bench.md +5 -1
  27. package/commands/design-render-critique-loop.md +1 -1
  28. package/commands/diagnose.md +2 -1
  29. package/commands/explain.md +2 -1
  30. package/commands/forge-stats.md +121 -2
  31. package/commands/forgedock-init.md +1 -1
  32. package/commands/geo-audit.md +3 -0
  33. package/commands/incident-response.md +2 -1
  34. package/commands/issue.md +23 -2
  35. package/commands/milestone.md +77 -12
  36. package/commands/optimize.md +2 -1
  37. package/commands/orchestrate/config.md +67 -0
  38. package/commands/orchestrate/phase-1-resolve.md +159 -0
  39. package/commands/orchestrate/phase-2-triage.md +126 -0
  40. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  41. package/commands/orchestrate/phase-3-dependency.md +591 -0
  42. package/commands/orchestrate/phase-4-execution.md +962 -0
  43. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  44. package/commands/orchestrate/phase-6-report.md +198 -0
  45. package/commands/orchestrate/safety.md +62 -0
  46. package/commands/orchestrate.md +25 -2045
  47. package/commands/pipeline-health/config.md +34 -0
  48. package/commands/pipeline-health/phase-1-context.md +180 -0
  49. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  50. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  51. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  52. package/commands/pipeline-health/phase-5-report.md +284 -0
  53. package/commands/pipeline-health/phase-6-summary.md +52 -0
  54. package/commands/pipeline-health.md +25 -2401
  55. package/commands/pipeline-resume.md +24 -1
  56. package/commands/pipeline-status.md +65 -0
  57. package/commands/qa-sweep.md +4 -1
  58. package/commands/quality-gate.md +248 -10
  59. package/commands/replay.md +2 -1
  60. package/commands/review-pr-agents/api.md +135 -0
  61. package/commands/review-pr-agents/auth.md +137 -0
  62. package/commands/review-pr-agents/billing.md +101 -0
  63. package/commands/review-pr-agents/concurrency.md +145 -0
  64. package/commands/review-pr-agents/database.md +103 -0
  65. package/commands/review-pr-agents/frontend.md +89 -0
  66. package/commands/review-pr-agents/infra.md +385 -0
  67. package/commands/review-pr-agents/protocols.md +185 -0
  68. package/commands/review-pr-agents/scraper.md +185 -0
  69. package/commands/review-pr-agents/security.md +406 -0
  70. package/commands/review-pr-agents.md +45 -1722
  71. package/commands/review-pr-staging.md +196 -40
  72. package/commands/review-pr.md +591 -52
  73. package/commands/rollback.md +2 -1
  74. package/commands/scope.md +2 -1
  75. package/commands/security-audit.md +2 -1
  76. package/commands/signal-planner.md +623 -0
  77. package/commands/sync-ecosystem.md +1 -0
  78. package/commands/test-gate.md +17 -1
  79. package/commands/validate.md +2 -1
  80. package/commands/work-on/build/architect.md +28 -0
  81. package/commands/work-on/build/context.md +2 -0
  82. package/commands/work-on/build/implement.md +3 -1
  83. package/commands/work-on/build/validate.md +26 -7
  84. package/commands/work-on/build.md +163 -24
  85. package/commands/work-on/close.md +130 -3
  86. package/commands/work-on/decompose.md +19 -1
  87. package/commands/work-on/investigate.md +117 -2
  88. package/commands/work-on/review.md +24 -4
  89. package/commands/work-on-monolithic.md +7 -2
  90. package/commands/work-on.md +202 -4
  91. package/docs/BRAND.md +14 -7
  92. package/docs/CI.md +2 -2
  93. package/docs/CODEX.md +36 -1
  94. package/docs/CONFIG.md +5 -8
  95. package/docs/FORGE-PROTOCOL.md +15 -1064
  96. package/docs/anthropic-devrel-outreach.md +2 -2
  97. package/docs/articles/command-decomposition-token-savings.md +214 -0
  98. package/docs/articles/model-release-playbook.md +131 -0
  99. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  100. package/docs/claude-breakpoints.json +75 -5
  101. package/docs/design/abc-benchmark.md +1 -1
  102. package/docs/design/design-spec-schema.md +1 -1
  103. package/docs/design/divergent-generation.md +1 -1
  104. package/docs/design/eval-gate-runbook.md +178 -0
  105. package/docs/design/show-hn-draft.md +26 -1
  106. package/docs/design/token-cost-baseline-1298.md +59 -0
  107. package/docs/eval/README.md +117 -0
  108. package/docs/eval/corpus/seed-v1.json +6 -0
  109. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  110. package/docs/site/annotations-explained.md +24 -0
  111. package/docs/site/command-learning-path.md +51 -14
  112. package/docs/site/command-reference.md +339 -113
  113. package/docs/site/for-companies.md +95 -0
  114. package/docs/site/forge-annotation-protocol.md +12 -256
  115. package/docs/site/getting-started.md +43 -4
  116. package/docs/site/github-is-the-memory.md +113 -0
  117. package/docs/site/how-it-works.md +3 -13
  118. package/docs/site/index.md +9 -1
  119. package/docs/spec/eval-run-result.md +147 -0
  120. package/docs/spec/forge-protocol-v1.md +441 -18
  121. package/docs/spec/label-state-machine.md +55 -0
  122. package/docs/spec/review-protocol.md +158 -0
  123. package/docs/spec-graph-schema.md +28 -0
  124. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  125. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  126. package/examples/forgedock-demo/README.md +41 -22
  127. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  128. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  129. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  130. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  131. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  132. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  133. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  134. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  135. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  136. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  137. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  138. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  139. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  140. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  141. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  142. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  143. package/examples/forgedock-demo/src/db.js +36 -11
  144. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  145. package/examples/forgedock-demo/src/server.js +4 -1
  146. package/package.json +3 -1
  147. package/packages/protocol/LICENSE +21 -0
  148. package/packages/protocol/src/cli.js +135 -0
  149. package/packages/protocol/src/emit.js +146 -0
  150. package/packages/protocol/src/index.js +43 -0
  151. package/packages/protocol/src/parse.js +229 -0
  152. package/packages/protocol/src/types.js +171 -0
  153. package/packages/protocol/src/validate.js +122 -0
  154. package/scripts/bench-topology-cost.mjs +372 -0
  155. package/scripts/check-command-docs-drift.sh +198 -0
  156. package/scripts/check-protocol-restatements.sh +176 -0
  157. package/scripts/check-registry/README.md +72 -0
  158. package/scripts/check-registry/manifest.json +6 -0
  159. package/scripts/code-index.sh +454 -0
  160. package/scripts/conformance-check.mjs +168 -0
  161. package/scripts/doctor-pipeline-state.sh +484 -0
  162. package/scripts/eval-gate-baseline.json +17 -0
  163. package/scripts/eval-gate-scorecard.mjs +242 -0
  164. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  165. package/scripts/flaky-quarantine.sh +241 -0
  166. package/scripts/forge-annotation.sh +567 -0
  167. package/scripts/issue-dedup.sh +236 -0
  168. package/scripts/validate-annotation-node.mjs +233 -0
  169. package/scripts/worktree-lifecycle.sh +3 -3
  170. package/templates/devdocs/agent/using-forgedock.md +14 -0
  171. 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** (~300 lines, zero dependencies) with five pre-written
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 couple of **intentional, clearly-labelled flaws** (a missing auth
18
- check, an unsafe query path) so that ForgeDock's `/review-pr` agents have real
19
- findings to surface — that's the part that makes the review feel impressive.
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 the intentional flaws
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/ # the 5 pre-written issue specs
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 five demo issues
56
-
57
- Each issue targets a different pipeline strength and is scoped to finish fast.
58
-
59
- | # | Type | Title | What it shows |
60
- |---|------|-------|---------------|
61
- | 1 | Bug / security | DELETE is missing an auth check | Simplest fix — fastest run |
62
- | 2 | Feature / security | Safe filtering for `GET /notes` | Investigation + architecture |
63
- | 3 | Refactor | Extract the router module | "No behavior change" review |
64
- | 4 | Performance | O(1) `findById` | Investigation phase tracing the hot path |
65
- | 5 | Docs | Add an API reference | Pipeline handles non-code work |
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–#5 in your demo repo, in this order.
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 the five issues from [`issues/`](./issues).
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 5` to see the other pipeline phases, or run
110
- `/orchestrate` to let ForgeDock work several issues in parallel.
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.