@khoavu882/doflow 2.0.1 → 2.0.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 (93) hide show
  1. package/core/.claude-plugin/plugin.json +1 -1
  2. package/core/.codex-plugin/plugin.json +1 -1
  3. package/core/.plugin/plugin.json +1 -1
  4. package/core/harnesses/claude/hooks/session-end.sh +21 -1
  5. package/core/harnesses/codex/hooks/session-end.impl.sh +21 -1
  6. package/core/harnesses/shared/hooks/policies/compact-text.sh +8 -0
  7. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +25 -1
  8. package/core/registry/contribution-points.json +1 -1
  9. package/core/registry/intake.json +3 -3
  10. package/core/registry/lifecycle.json +256 -5
  11. package/core/registry/packages/doflow/doflow-package.json +2 -2
  12. package/core/registry/settings.json +12 -12
  13. package/core/registry/storage.json +1 -2
  14. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +7 -0
  15. package/core/shared/scripts/doflow/bash/do-git-state.sh +12 -4
  16. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +86 -33
  17. package/core/shared/scripts/doflow/bash/do-review-package.sh +36 -2
  18. package/core/shared/scripts/doflow/bin/doflow-run +18 -0
  19. package/core/shared/skills/do-design/SKILL.md +6 -2
  20. package/core/shared/skills/do-design/references/data_model_example.md +167 -0
  21. package/core/shared/templates/doflow/data-model-template.md +136 -102
  22. package/package.json +1 -1
  23. package/src/adapters/antigravity/index.js +54 -12
  24. package/src/adapters/codex/agents.js +15 -1
  25. package/src/adapters/codex/config.js +5 -2
  26. package/src/adapters/copilot/index.js +11 -4
  27. package/src/adapters/copy-tree.js +7 -0
  28. package/src/adapters/created-file.js +41 -0
  29. package/src/adapters/mcp-entries.js +11 -1
  30. package/src/adapters/opencode/index.js +32 -5
  31. package/src/adapters/pi/index.js +15 -4
  32. package/src/adapters/pi/mcp.js +44 -18
  33. package/src/cli/commands/remove.js +3 -1
  34. package/src/cli/commands/update.js +7 -3
  35. package/src/cli/index.js +24 -12
  36. package/src/cli/runtime-commands.js +1 -1
  37. package/src/cli/shared.js +5 -3
  38. package/src/helper/marker-merge.js +11 -7
  39. package/src/install/backup-set.js +7 -3
  40. package/src/install/backup.js +13 -7
  41. package/src/install/context.js +2 -1
  42. package/src/install/manifest.js +35 -11
  43. package/src/install/paths.js +38 -0
  44. package/src/install/targets.js +11 -2
  45. package/src/lifecycle/index.js +80 -3
  46. package/src/lifecycle/retired-harnesses.js +16 -1
  47. package/src/lifecycle/view.js +3 -2
  48. package/src/registry/index.js +21 -4
  49. package/src/release/docs-blocks.js +5 -1
  50. package/src/runtime/advice/doctor.js +2 -2
  51. package/src/runtime/brief-tools.js +2 -1
  52. package/src/runtime/capability-router.js +26 -3
  53. package/src/runtime/context/pointer.js +17 -5
  54. package/src/runtime/context-pack.js +48 -8
  55. package/src/runtime/dispatch/integrate.js +27 -8
  56. package/src/runtime/dispatch/loop.js +7 -0
  57. package/src/runtime/dispatch/verify-run.js +9 -3
  58. package/src/runtime/feature-pointer.js +10 -1
  59. package/src/runtime/health.js +9 -4
  60. package/src/runtime/implementation-gate.js +10 -2
  61. package/src/runtime/intake/engine.js +11 -9
  62. package/src/runtime/knowledge/code-retrieval.js +11 -5
  63. package/src/runtime/knowledge/semble-provider.js +12 -2
  64. package/src/runtime/lifecycle/cli.js +7 -5
  65. package/src/runtime/lifecycle/overview.js +32 -2
  66. package/src/runtime/lifecycle/release.js +16 -3
  67. package/src/runtime/lifecycle/retention.js +12 -15
  68. package/src/runtime/lifecycle/store-upkeep.js +13 -3
  69. package/src/runtime/mask.js +3 -3
  70. package/src/runtime/mcp-consultations.js +11 -5
  71. package/src/runtime/research-request.js +3 -1
  72. package/src/runtime/review/command.js +3 -7
  73. package/src/runtime/review/start.js +24 -10
  74. package/src/runtime/roadmap/loop.js +14 -3
  75. package/src/runtime/roadmap/rows.js +2 -1
  76. package/src/runtime/settings/files.js +3 -1
  77. package/src/runtime/settings/prepared-run.js +12 -2
  78. package/src/runtime/settings/resolve.js +13 -1
  79. package/src/runtime/settings/write.js +73 -2
  80. package/src/runtime/storage/registry.js +2 -2
  81. package/src/runtime/storage/roots.js +4 -18
  82. package/src/runtime/storage/rules.js +22 -46
  83. package/src/runtime/storage/work-status.js +14 -4
  84. package/src/runtime/task-scope.js +17 -2
  85. package/src/runtime/task-state.js +11 -4
  86. package/src/runtime/trace/analyze.js +4 -2
  87. package/src/runtime/trace/indicators.js +25 -20
  88. package/src/runtime/trace/spans.js +4 -5
  89. package/src/runtime/verb-view.js +6 -5
  90. package/src/runtime/worker/command.js +4 -2
  91. package/src/runtime/worker/readiness.js +8 -4
  92. package/src/runtime/worker/supervisor.js +18 -6
  93. package/src/runtime/workflow-orchestrator.js +6 -2
@@ -111,10 +111,14 @@ is_dirty() {
111
111
  # means anything. The local branch was used unconditionally, so a develop that had not been pulled
112
112
  # made every distance wrong by however stale it was — observed live at 22 reported against 32.
113
113
  #
114
- # The remote-tracking ref is preferred where it exists; the local branch is the fallback for a
115
- # repository with no remote or one never fetched. Preferring it does not make the answer *fresh* —
116
- # origin/develop is only as current as the last fetch — so the third field reports how far the
117
- # local branch trails, which is non-zero exactly when a `git fetch` would change the answer.
114
+ # The remote-tracking ref is preferred where it exists, unless the local branch is strictly ahead
115
+ # of it (origin/<branch> is an ancestor of the branch and they differ): then the local branch holds
116
+ # integrated work the remote has not seen yet, and measuring from origin would count that work as
117
+ # the caller's own. An equal, behind or diverged local branch keeps the remote preference; the local
118
+ # branch is the fallback for a repository with no remote or one never fetched. Preferring the remote
119
+ # does not make the answer *fresh* — origin/develop is only as current as the last fetch — so the
120
+ # third field reports how far the local branch trails, which is non-zero exactly when a `git fetch`
121
+ # would change the answer.
118
122
  #
119
123
  # Echoes: "<ref> <kind> <local-behind-remote>"
120
124
  resolve_integration_ref() {
@@ -125,6 +129,10 @@ resolve_integration_ref() {
125
129
  kind="remote-tracking"
126
130
  if git rev-parse --verify --quiet "refs/heads/${branch}" >/dev/null 2>&1; then
127
131
  behind="$(git rev-list --count "${branch}..origin/${branch}" 2>/dev/null || echo 0)"
132
+ if [ "$behind" = "0" ] && [ "$(git rev-list --count "origin/${branch}..${branch}" 2>/dev/null || echo 0)" -gt 0 ]; then
133
+ ref="$branch"
134
+ kind="local"
135
+ fi
128
136
  fi
129
137
  fi
130
138
  printf '%s %s %s\n' "$ref" "$kind" "$behind"
@@ -12,6 +12,10 @@ case "${OSTYPE:-}" in msys*|cygwin*|win32*) if command -v jq >/dev/null 2>&1; th
12
12
  # reducing dispatch overhead. The script forms groups from `owner:` fields, computes union
13
13
  # file sets per group, and reports overlaps across groups (`group_overlaps[]`).
14
14
  #
15
+ # Owner and files are read from the whole task block: the task line and a `Verify:` line under it. A task
16
+ # that declares no `files:` (`files: none` is a declaration), or declares different sets, is named in
17
+ # `files_missing[]` / `files_conflict[]`, and while one exists `parallel_safe` is `null`, never `true`.
18
+ #
15
19
  # Advisory by design: it reports overlaps and grouping decisions; the calling skill decides
16
20
  # to serialize or group dispatch. It never edits the plan and never dispatches anything.
17
21
  #
@@ -67,7 +71,70 @@ plan_abs="$repo_root/$plan_rel"
67
71
 
68
72
  # Pipeline: awk outputs tab-separated values (one per line), jq reads raw lines as strings,
69
73
  # aggregates them into an array, then processes each row.
74
+ #
75
+ # A task is its whole block: the `- [ ] <id>` line and the indented lines under it, up to the next task or
76
+ # heading. `owner:` and `files:` are read from the task line and from a `Verify:` line (the layout plans
77
+ # written with a verification line use); each is the text after the line's first em dash. A task that
78
+ # declares files twice must declare the same set twice: a different set is `conflict`, and a task that
79
+ # declares none is `missing`. `files: none` is a declaration of an empty set, not a missing one.
70
80
  awk -v want="$phase" '
81
+ # Sorted, de-duplicated, comma-joined form of a comma list, so two spellings of one set compare equal.
82
+ function normalize(list, n, parts, i, j, t, out, last) {
83
+ n = split(list, parts, ",")
84
+ for (i = 1; i <= n; i++) { gsub(/^[ \t]+|[ \t]+$/, "", parts[i]) }
85
+ for (i = 2; i <= n; i++) {
86
+ t = parts[i]
87
+ for (j = i - 1; j >= 1 && parts[j] > t; j--) parts[j + 1] = parts[j]
88
+ parts[j + 1] = t
89
+ }
90
+ out = ""; last = "\001"
91
+ for (i = 1; i <= n; i++) {
92
+ if (parts[i] == "" || parts[i] ~ /^none/ || parts[i] == last) continue
93
+ out = out (out == "" ? "" : ",") parts[i]
94
+ last = parts[i]
95
+ }
96
+ return out
97
+ }
98
+ # Reads one line'"'"'s owner:/files: fields into the current task.
99
+ function declare(line, dash_pos, section, o, f) {
100
+ dash_pos = index(line, "—")
101
+ if (dash_pos == 0) return
102
+ section = substr(line, dash_pos + length("—"))
103
+ if (match(section, /owner:[^;]*/)) {
104
+ o = substr(section, RSTART + 7, RLENGTH - 7)
105
+ gsub(/^[ \t]+|[ \t]+$/, "", o)
106
+ if (o != "") {
107
+ if (owner == "") owner = o
108
+ else if (owner != o) conflict = 1
109
+ }
110
+ }
111
+ if (match(section, /files:[^;]*/)) {
112
+ f = substr(section, RSTART + 6, RLENGTH - 6)
113
+ gsub(/^[ \t]+|[ \t]+$/, "", f)
114
+ if (f == "") return
115
+ declared++
116
+ set = normalize(f)
117
+ if (declared == 1) first = set
118
+ else if (set != first) conflict = 1
119
+ m = split(set, items, ",")
120
+ for (k = 1; k <= m; k++) if (items[k] != "" && !(items[k] in seen)) { seen[items[k]] = 1; order[++norder] = items[k] }
121
+ }
122
+ }
123
+ function flush( status, k) {
124
+ if (id == "") return
125
+ status = conflict ? "conflict" : (declared == 0 ? "missing" : "ok")
126
+ if (norder == 0) print id "\t" par "\t" owner "\t\t" status
127
+ for (k = 1; k <= norder; k++) print id "\t" par "\t" owner "\t" order[k] "\t" status
128
+ id = ""
129
+ }
130
+ function start(line) {
131
+ id = substr(line, RSTART + 6, RLENGTH - 6)
132
+ par = (line ~ /^- \[[ x]\] [^ ]+ \[P\]/) ? "P" : "S"
133
+ owner = ""; declared = 0; conflict = 0; first = ""; norder = 0
134
+ delete seen; delete order
135
+ declare(line)
136
+ }
137
+ /^#+ / { flush() }
71
138
  /^### Phase / {
72
139
  inphase = ($3 == want)
73
140
  next
@@ -75,43 +142,22 @@ awk -v want="$phase" '
75
142
  /^### / { inphase = 0; next }
76
143
  !inphase { next }
77
144
  /^- \[[ x]\] / {
78
- line = $0
79
- if (match(line, /^- \[[ x]\] [^ ]+/) == 0) next
80
- id = substr(line, RSTART + 6, RLENGTH - 6)
81
- par = (line ~ /^- \[[ x]\] [^ ]+ \[P\]/) ? "P" : "S"
82
- # Extract only the portion after em-dash for owner:/files: parsing
83
- dash_pos = index(line, "—")
84
- if (dash_pos == 0) {
85
- print id "\t" par "\t\t"
86
- next
87
- }
88
- field_section = substr(line, dash_pos + length("—"))
89
- owner = ""
90
- if (match(field_section, /owner:[^;]*/)) {
91
- owner = substr(field_section, RSTART + 7, RLENGTH - 7)
92
- gsub(/^[ \t]+|[ \t]+$/, "", owner)
93
- }
94
- if (match(field_section, /files:[^;]*/)) {
95
- files = substr(field_section, RSTART + 6, RLENGTH - 6)
96
- n = split(files, parts, ",")
97
- emitted = 0
98
- for (i = 1; i <= n; i++) {
99
- f = parts[i]
100
- gsub(/^[ \t]+|[ \t]+$/, "", f)
101
- if (f == "" || f ~ /^none/) continue
102
- print id "\t" par "\t" owner "\t" f
103
- emitted = 1
104
- }
105
- if (!emitted) print id "\t" par "\t" owner "\t"
106
- } else {
107
- print id "\t" par "\t" owner "\t"
108
- }
145
+ flush()
146
+ if (match($0, /^- \[[ x]\] [^ ]+/) == 0) next
147
+ start($0)
148
+ next
109
149
  }
150
+ id != "" && /^[ \t]*[-*]?[ \t]*\**Verify\**:/ { declare($0) }
151
+ END { flush() }
110
152
  ' "$plan_abs" | jq -R . | jq -s --arg phase "$phase" '
111
153
  # Convert array of tab-separated strings to array of objects
112
154
  map(split("\t")) |
113
- map({id: .[0], par: .[1], owner: (.[2] // ""), file: (.[3] // "")}) |
155
+ map({id: .[0], par: .[1], owner: (.[2] // ""), file: (.[3] // ""), meta: (.[4] // "missing")}) |
114
156
  . as $rows |
157
+
158
+ # Tasks whose file metadata cannot establish anything: none declared, or declarations that disagree
159
+ ([.[] | select(.meta == "missing") | .id] | unique) as $files_missing |
160
+ ([.[] | select(.meta == "conflict") | .id] | unique) as $files_conflict |
115
161
 
116
162
  # Separate by parallel/sequential
117
163
  ([.[] | select(.par == "P") | .id] | unique) as $par |
@@ -200,8 +246,15 @@ awk -v want="$phase" '
200
246
  # a non-empty `group_overlaps` naming the very file two groups both write. `do-execute-plan`
201
247
  # tells the reader to branch on this field and never on the exit code, so either shape
202
248
  # authorises concurrent writes to one file.
249
+ #
250
+ # A task with no `files:` (or two that disagree) has an unknown write set, so a phase containing one
251
+ # is never reported safe: `null` (unknown, run serially) unless a real overlap already makes it `false`.
203
252
  parallel_safe: (if (($par | length) == 0 and ($seq | length) == 0) then null
204
- else (($overlaps | length) == 0 and ($final_group_overlaps | length) == 0) end),
253
+ elif (($overlaps | length) > 0 or ($final_group_overlaps | length) > 0) then false
254
+ elif (($files_missing | length) > 0 or ($files_conflict | length) > 0) then null
255
+ else true end),
256
+ files_missing: $files_missing,
257
+ files_conflict: $files_conflict,
205
258
  serialize: ([$overlaps[] | .tasks[]] | unique),
206
259
  groups: $final_groups,
207
260
  group_overlaps: $final_group_overlaps,
@@ -21,7 +21,10 @@ case "${OSTYPE:-}" in msys*|cygwin*|win32*) if command -v jq >/dev/null 2>&1; th
21
21
  # Usage:
22
22
  # do-review-package.sh --task=<id> --base=<sha> --head=<sha> [--slug=<slug>] [--json]
23
23
  #
24
- # Emits: {path, commits, bytes, base, head}
24
+ # Emits: {path, commits, bytes, base, head}, plus missing[] when the rules block could not be built.
25
+ #
26
+ # Between the title and the commit list the package carries the "Rules that apply to you" block: the
27
+ # mustCarry lines of the rule ledger (core/registry/rules.json), as the task and group briefs print them.
25
28
 
26
29
  set -uo pipefail
27
30
 
@@ -96,10 +99,40 @@ if [ "${commits:-0}" -eq 0 ]; then
96
99
  exit 2
97
100
  fi
98
101
 
102
+ # The registry file `core/registry/<name>` of the runtime this script belongs to: next to the checkout or
103
+ # projected runtime found above this script, else the global projected runtime (as do-task-brief.sh).
104
+ registry_file() { # registry_file <name>
105
+ local d="$script_dir" c
106
+ while [ -n "$d" ] && [ "$d" != "/" ]; do
107
+ for c in "$d/core/registry/$1" "$d/runtime/core/registry/$1"; do
108
+ if [ -f "$c" ]; then printf '%s' "$c"; return 0; fi
109
+ done
110
+ d="$(dirname "$d")"
111
+ done
112
+ c="${HOME:-}/.doflow/runtime/core/registry/$1"
113
+ if [ -f "$c" ]; then printf '%s' "$c"; fi
114
+ return 0
115
+ }
116
+
117
+ missing='[]'
118
+ rules_block=""
119
+ rules_file="$(registry_file rules.json)"
120
+ if [ -z "$rules_file" ]; then
121
+ missing='["brief-rules: core/registry/rules.json is missing"]'
122
+ else
123
+ rule_lines="$(jq -r 'if (.mustCarry | type) == "array" and (.mustCarry | length) > 0 and (.mustCarry | all(type == "string")) then .mustCarry[] | "- " + . else empty end' "$rules_file" 2>/dev/null)"
124
+ if [ -z "$rule_lines" ]; then
125
+ missing='["brief-rules: core/registry/rules.json has no mustCarry array"]'
126
+ else
127
+ rules_block="$(printf '## Rules that apply to you\n\n%s\n' "$rule_lines")"
128
+ fi
129
+ fi
130
+
99
131
  write_ok=1
100
132
  {
101
133
  echo "# Review package: $base7..$head7"
102
134
  echo
135
+ if [ -n "$rules_block" ]; then printf '%s\n\n' "$rules_block"; fi
103
136
  echo "## Commits"
104
137
  git log --oneline "$base..$head_ref"
105
138
  echo
@@ -127,5 +160,6 @@ jq -n \
127
160
  --argjson commits "${commits:-0}" \
128
161
  --argjson bytes "${bytes:-0}" \
129
162
  --arg base "$base7" --arg head "$head7" \
130
- '{path:$path, commits:$commits, bytes:$bytes, base:$base, head:$head}'
163
+ --argjson missing "$missing" \
164
+ '{path:$path, commits:$commits, bytes:$bytes, base:$base, head:$head} + (if ($missing | length) > 0 then {missing:$missing} else {} end)'
131
165
  exit 0
@@ -339,6 +339,23 @@ resolve_node_cli() {
339
339
  return 1
340
340
  }
341
341
 
342
+ # A checkout found from the caller's cwd may be older than its local develop branch. Keep this
343
+ # advisory on stderr so the selected CLI's stdout and exit status remain untouched.
344
+ warn_checkout_pwd_behind_develop() {
345
+ local cwd head counts head_only develop_only
346
+ [ "$NODE_CLI_SOURCE" = "checkout-pwd" ] || return 0
347
+ cwd="$(pwd -P 2>/dev/null || pwd)"
348
+ head="$(git -C "$cwd" rev-parse --verify HEAD 2>/dev/null)" || return 0
349
+ git -C "$cwd" show-ref --verify --quiet refs/heads/develop || return 0
350
+ counts="$(git -C "$cwd" rev-list --left-right --count HEAD...refs/heads/develop 2>/dev/null)" || return 0
351
+ read -r head_only develop_only <<EOF
352
+ $counts
353
+ EOF
354
+ case "$head_only:$develop_only" in
355
+ 0:[1-9]* ) printf 'doflow-run: warning: selected runtime %s at %s is behind local develop\n' "$NODE_CLI_PATH" "$head" >&2 ;;
356
+ esac
357
+ }
358
+
342
359
  # ---------------------------------------------------------------------------------- reporting
343
360
 
344
361
  wants_json=false
@@ -585,6 +602,7 @@ dispatch_node() {
585
602
  fail 2 "cli-not-found" \
586
603
  "verb '$verb' is served by the Node CLI, which could not be located; set DOFLOW_CLI to a doflow.js path, install the package (npm i -g @khoavu882/doflow), or run from a checkout"
587
604
  fi
605
+ warn_checkout_pwd_behind_develop
588
606
  if [ "$NODE_CLI_KIND" = "script" ]; then
589
607
  command -v node >/dev/null 2>&1 || fail 2 "node-not-found" \
590
608
  "verb '$verb' needs node on PATH to run $NODE_CLI_PATH"
@@ -136,6 +136,10 @@ a `design.md` for it invents an artifact its workflow never reads.
136
136
  create one: fill Data Model and Sequence / Data Flow in `design.md` — the content
137
137
  `specs.md`'s former §2 and the template's old §6 carried — exactly as the old two-file
138
138
  convention did, and leave that dir's layout alone — this feature migrates nothing.
139
+ Otherwise fill `data-model.md` §1 to §9. Read `references/data_model_example.md` only while filling it.
140
+ Each table row ends in a Source tag; copy every `proposed` row to `design.md` Open Points for
141
+ Review. `design.md` Journeys keep user-visible states only and link to `./data-model.md` §3 for
142
+ the entity states.
139
143
  The template is `templates/doflow/design-template.md` in the install step 1 resolved: take `constitution_base` from that JSON and swap its trailing `guidance/references/CONSTITUTION_BASE.md` for that path.
140
144
  Fill `[REQUIREMENT_PATH]` with step 1's own resolved `requirement` field, verbatim
141
145
  (repo-root-relative, exactly as the resolver returns it) — never hand-compute it, which is only
@@ -167,8 +171,8 @@ Filling the C4 levels, read the guidance tree's `references/DIAGRAM_GUIDE.md` fi
167
171
  ```
168
172
  Run it a second time against the specs path when step 6 wrote one — `specs.md`'s §1 Interface
169
173
  Contracts is an indexed section, so it is checked the same way. Run it a third time against the
170
- data-model path when step 6 wrote one — `data-model.md`'s §3 History is its one indexed section,
171
- checked the same way, even though its §1/§2 stay free-form. Surface findings verbatim; a
174
+ data-model path when step 6 wrote one — `data-model.md`'s §9 History is its one indexed section,
175
+ checked the same way, even though its §1-§8 stay free-form. Surface findings verbatim; a
172
176
  non-zero exit is advisory and does not halt the chain.
173
177
  8. **Batch this stage's evidence** — one pass here at the stage boundary, never one call per fact.
174
178
  `<task id>` is the unit these stores key on: the plan task id once `plan.md` exists, otherwise
@@ -0,0 +1,167 @@
1
+ # Data Model: Order checkout (worked example)
2
+
3
+ **Feature:** 000-order-checkout · **Design:** ./design.md · **Maturity:** Draft · **Created:** 2025-01-01
4
+
5
+ > Worked example of a filled `data-model.md`. Read it only while filling the template; the domain
6
+ > is invented. FR-001, FR-002, NFR-001 and IC-001 stand for requirement and contract IDs of that feature.
7
+
8
+ ## 1. Data Model
9
+
10
+ ### Domain inventory
11
+
12
+ | Domain | Entities | Stateful | Owner | Source |
13
+ |---|---|---|---|---|
14
+ | Ordering | Order | yes | order-service | FR-001 |
15
+ | Ordering | OrderLine | no | order-service | FR-001 |
16
+
17
+ ### Conceptual domain map
18
+
19
+ ```mermaid
20
+ flowchart LR
21
+ O["Order"] -->|"contains"| L["OrderLine"]
22
+ ```
23
+
24
+ #### ER view: ordering
25
+
26
+ ```mermaid
27
+ erDiagram
28
+ ORDER ||--|{ ORDERLINE : "contains"
29
+ ORDER {
30
+ uuid id PK
31
+ string status
32
+ timestamp placed_at
33
+ }
34
+ ORDERLINE {
35
+ uuid id PK
36
+ uuid order_id FK
37
+ int quantity
38
+ }
39
+ ```
40
+
41
+ ### Data dictionary
42
+
43
+ #### Dictionary: Order
44
+
45
+ | Field | Type | Null | Rule | PII class | Source |
46
+ |---|---|---|---|---|---|
47
+ | id | uuid | no | generated once | none | code:src/orders/order.ts:12 |
48
+ | status | string | no | one of New, Paid, Shipped, Closed | none | FR-001 |
49
+ | placed_at | timestamp | no | set at creation | none | proposed |
50
+
51
+ #### Dictionary: OrderLine
52
+
53
+ | Field | Type | Null | Rule | PII class | Source |
54
+ |---|---|---|---|---|---|
55
+ | id | uuid | no | generated once | none | code:src/orders/line.ts:8 |
56
+ | order_id | uuid | no | references Order.id | none | FR-001 |
57
+ | quantity | int | no | at least 1 | none | FR-002 |
58
+
59
+ ### Database Schemas (ORM / DDL)
60
+
61
+ | Table | Purpose | Primary Key | Foreign Keys & Relations | Key Indexes | Schema / ORM File |
62
+ |---|---|---|---|---|---|
63
+ | `orders` | One customer order | `id` | none | `status` | src/orders/order.ts |
64
+ | `order_lines` | Items of an order | `id` | `order_id` → `orders(id)` | `order_id` | src/orders/line.ts |
65
+
66
+ ### UX / UI Specifications
67
+ - **Design Tokens & Cues:** status badge per state (New, Paid, Shipped, Closed)
68
+ - **Component States:** loading, empty cart, populated, payment error
69
+
70
+ ## 2. Sequence / Data Flow
71
+
72
+ ### Interaction: place and pay
73
+
74
+ A shopper submits a cart; the flow ends with a paid order or a refused payment.
75
+
76
+ ```mermaid
77
+ sequenceDiagram
78
+ actor Shopper
79
+ participant SVC as order-service
80
+ participant DB as orders-db
81
+
82
+ Shopper->>SVC: submit cart
83
+ SVC->>DB: insert Order (New)
84
+ alt payment refused
85
+ SVC-->>Shopper: payment error
86
+ else payment accepted
87
+ SVC->>DB: update Order (Paid)
88
+ SVC-->>Shopper: confirmation
89
+ end
90
+ ```
91
+
92
+ ### Swimlane flows
93
+
94
+ #### Flow: place-order
95
+
96
+ ![End-to-end flow: place-order](design/c4/flow-place-order.png)
97
+
98
+ A shopper submits a cart; the flow ends with a paid order handed to fulfilment.
99
+
100
+ ## 3. Lifecycles
101
+
102
+ > **Authoritative:** this section governs the states of Order and the moves between them.
103
+
104
+ #### Lifecycle: Order
105
+
106
+ ```mermaid
107
+ stateDiagram-v2
108
+ [*] --> New
109
+ New --> Paid: pay
110
+ New --> Closed: cancel
111
+ Paid --> Shipped: ship
112
+ Shipped --> Closed: confirm delivery
113
+ ```
114
+
115
+ **Transitions**
116
+
117
+ | From | Event | Guard | Actor | Side effect | To | Emitted event | Source |
118
+ |---|---|---|---|---|---|---|---|
119
+ | New | pay | payment accepted | Shopper | stock reserved | Paid | OrderPaid | FR-001 |
120
+ | New | cancel | none | Shopper | none | Closed | OrderClosed | proposed |
121
+ | Paid | ship | stock picked | Warehouse | tracking number set | Shipped | OrderShipped | IC-001 |
122
+ | Shipped | confirm delivery | carrier scan | System | none | Closed | OrderClosed | UNKNOWN -> Open Point |
123
+
124
+ **Illegal transitions**
125
+
126
+ - Closed → any state: a closed order is final.
127
+ - Shipped → Paid: payment cannot be undone after dispatch.
128
+
129
+ **Terminal states:** Closed
130
+
131
+ #### Lifecycle: OrderLine
132
+
133
+ N/A: stateless, a line is fixed when the order is placed and follows its Order.
134
+
135
+ ## 4. Cross-entity coupling
136
+
137
+ N/A: one actor-owned entity changes state; OrderLine has no state to couple.
138
+
139
+ ## 5. Invariants
140
+
141
+ | Invariant | Holds over | Enforced by | Source |
142
+ |---|---|---|---|
143
+ | An Order has at least one OrderLine | Order, OrderLine | check at placement | FR-002 |
144
+ | A Closed order never changes | Order | code path rejects updates | NFR-001 |
145
+
146
+ ## 6. Ownership / CRUD matrix
147
+
148
+ | Entity | Owner | Create | Read | Update | Delete | Source |
149
+ |---|---|---|---|---|---|---|
150
+ | Order | order-service | Shopper | Shopper, Warehouse | order-service | none | IC-001 |
151
+ | OrderLine | order-service | Shopper | Shopper, Warehouse | none | none | proposed |
152
+
153
+ ## 7. Event catalogue
154
+
155
+ | Event | Emitted by | Payload | Consumers | Source |
156
+ |---|---|---|---|---|
157
+ | OrderPaid | order-service | order id, total | warehouse | IC-001 |
158
+ | OrderShipped | order-service | order id, tracking number | notifier | proposed |
159
+ | OrderClosed | order-service | order id, reason | reporting | proposed |
160
+
161
+ ## 8. Retention, PII and migration
162
+
163
+ N/A: no personal data is stored in these entities and the tables are new.
164
+
165
+ ## 9. History
166
+
167
+ None — initial version.