devflow-kit 3.12.0 → 3.12.1
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/CHANGELOG.md +30 -0
- package/dist/agents/architect.md +6 -2
- package/dist/agents/debug.md +3 -3
- package/dist/cli/commands/uninstall.js +1 -1
- package/dist/commands/bug-analysis.md +4 -4
- package/dist/commands/code-review.md +4 -4
- package/dist/commands/dynamic-build.md +4 -2
- package/dist/commands/implement.md +10 -10
- package/dist/commands/plan.md +4 -4
- package/dist/commands/release.md +4 -4
- package/dist/commands/research.md +6 -6
- package/dist/commands/resolve.md +4 -4
- package/dist/commands/self-review.md +4 -4
- package/dist/compliance-off/commands/bug-analysis.md +4 -4
- package/dist/compliance-off/commands/code-review.md +4 -4
- package/dist/compliance-off/commands/dynamic-build.md +4 -2
- package/dist/compliance-off/commands/implement.md +10 -10
- package/dist/compliance-off/commands/plan.md +4 -4
- package/dist/compliance-off/commands/release.md +4 -4
- package/dist/compliance-off/commands/resolve.md +4 -4
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +2 -2
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +2 -2
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +2 -2
- package/dist/curation-compliance-off/commands/bug-analysis.md +4 -4
- package/dist/curation-compliance-off/commands/code-review.md +4 -4
- package/dist/curation-compliance-off/commands/dynamic-build.md +4 -2
- package/dist/curation-compliance-off/commands/implement.md +10 -10
- package/dist/curation-compliance-off/commands/plan.md +4 -4
- package/dist/curation-compliance-off/commands/release.md +4 -4
- package/dist/curation-compliance-off/commands/resolve.md +4 -4
- package/dist/curation-off/agents/architect.md +6 -2
- package/dist/curation-off/agents/debug.md +3 -3
- package/dist/curation-off/commands/bug-analysis.md +4 -4
- package/dist/curation-off/commands/code-review.md +4 -4
- package/dist/curation-off/commands/dynamic-build.md +4 -2
- package/dist/curation-off/commands/implement.md +10 -10
- package/dist/curation-off/commands/plan.md +4 -4
- package/dist/curation-off/commands/release.md +4 -4
- package/dist/curation-off/commands/research.md +6 -6
- package/dist/curation-off/commands/resolve.md +4 -4
- package/dist/curation-off/commands/self-review.md +4 -4
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +2 -2
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +2 -2
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +2 -2
- package/package.json +1 -1
- package/src/assets/agents/architect.mds +6 -2
- package/src/assets/agents/debug.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +4 -2
- package/src/assets/commands/_partials/_knowledge.mds +4 -4
- package/src/assets/commands/implement.mds +7 -7
- package/src/assets/commands/research.mds +2 -2
- package/src/assets/mds/git/_ops_steps.mds +2 -2
- package/src/assets/scripts/ci-wait.cjs +10 -7
- package/src/assets/scripts/resolve-root.cjs +7 -3
- package/src/assets/scripts/verify-evidence.cjs +213 -20
- package/src/assets/skills/design-review/SKILL.md +19 -8
|
@@ -21,7 +21,7 @@ Orchestrate a single task by spawning agents.
|
|
|
21
21
|
$ARGUMENTS
|
|
22
22
|
</command-input>
|
|
23
23
|
|
|
24
|
-
`COMMAND_INPUT` is the text above: a plan document path
|
|
24
|
+
`COMMAND_INPUT` is the text above: a plan document path, an issue reference, a task description, or empty (use the conversation).
|
|
25
25
|
|
|
26
26
|
{{issue_ref_grammar()}}
|
|
27
27
|
|
|
@@ -65,7 +65,7 @@ When `EVIDENCE_POLICY` is `required`, record `TRACEABILITY: DEGRADED (compliance
|
|
|
65
65
|
|
|
66
66
|
`PR_DESCRIPTION_GUIDANCE` is the full `## PR Description Guidance` section of the plan document. Any other input, empty input included, or a plan without that section, gives `(none)`.
|
|
67
67
|
|
|
68
|
-
**Empty input.** When `COMMAND_INPUT` is empty — a plan handoff arrives this way
|
|
68
|
+
**Empty input.** When `COMMAND_INPUT` is empty — a plan handoff arrives this way with the plan already in the conversation — no argument can name the branch, and the Git agent sees none of this conversation. Write the description yourself: one line, taken from the plan's title or, with no plan, from the conversation. Send it as the setup-task `TASK_DESCRIPTION` below.
|
|
69
69
|
|
|
70
70
|
Spawn Git agent:
|
|
71
71
|
|
|
@@ -93,7 +93,7 @@ The issue token is forwarded **unclassified**: routing is by SHAPE alone. Only `
|
|
|
93
93
|
<!-- compliance:end -->
|
|
94
94
|
- **Token count.** Forwarding the FIRST token alone sends `/implement fix the login bug` as `ISSUE_INPUT: fix` with no description, so the request is dropped.
|
|
95
95
|
|
|
96
|
-
**Capture from Git agent output**, each value as written. Never re-derive one issue value from another, and never infer one from a `TRACEABILITY: DEGRADED ({reason})` status line
|
|
96
|
+
**Capture from Git agent output**, each value as written. Never re-derive one issue value from another, and never infer one from a `TRACEABILITY: DEGRADED ({reason})` status line.
|
|
97
97
|
- `TASK_ID`: The branch name created by Git agent
|
|
98
98
|
- `BASE_BRANCH`: Branch this feature was created from; stop unless it passes the setup check
|
|
99
99
|
- `ISSUE_ID`: the `- **Issue ID**:` line under `### Handoff Values`, if any. `ISSUE_NUMBER` is this value.
|
|
@@ -192,7 +192,7 @@ Pass `DECISIONS_CONTEXT` to Code agent (Phase 2) and Harden agent (Phase 4).
|
|
|
192
192
|
<!-- curation:end -->
|
|
193
193
|
{{knowledge_load_after_root()}}
|
|
194
194
|
|
|
195
|
-
**Fresh base.** Before Phase 2, write `BASE_BRANCH` with the Write tool to a new file in a `mktemp -d` folder (`{name file}`). If the Write tool fails, treat the value as missing; never write it with a shell command. Then run `F="{name file}"; trap 'rm -- "$F"' EXIT; cd "{worktree}" && node "$HOME/.devflow/scripts/resolve-base.cjs" local --name-stdin < "$F"; echo "exit=$?"`. Output other than one `BASE_REF=` line (40 or 64 hex, or `none`) with `exit=0` counts as `NOTE=stop`, `REASON=resolver-unavailable`. Record `BASE_REF`. By `NOTE`: `none` continues; `info` prints one plain line; `degraded` records `TRACEABILITY: DEGRADED (base {REASON}; fix: {fix})`; `stop` (`BASE_REF=none`) stops the run, naming REASON and its fix. Fix by REASON: no-merge-base `git fetch --unshallow`; no-ref or timeout `git fetch origin <base>` (`<base>` stays as written); resolver-unavailable `re-run; if it repeats, run devflow init`; invalid-name the allowed set, never the name; any other REASON none, and `; fix: {fix}` is left out.
|
|
195
|
+
**Fresh base.** Before Phase 2, write `BASE_BRANCH` with the Write tool to a new file in a `mktemp -d` folder (`{name file}`). If the Write tool fails, treat the value as missing; never write it with a shell command. Then run `F="{name file}"; trap 'rm -- "$F"; rmdir -- "${F%/*}"' EXIT; cd "{worktree}" && node "$HOME/.devflow/scripts/resolve-base.cjs" local --name-stdin < "$F"; echo "exit=$?"`. Output other than one `BASE_REF=` line (40 or 64 hex, or `none`) with `exit=0` counts as `NOTE=stop`, `REASON=resolver-unavailable`. Record `BASE_REF`. By `NOTE`: `none` continues; `info` prints one plain line; `degraded` records `TRACEABILITY: DEGRADED (base {REASON}; fix: {fix})`; `stop` (`BASE_REF=none`) stops the run, naming REASON and its fix. Fix by REASON: no-merge-base `git fetch --unshallow`; no-ref or timeout `git fetch origin <base>` (`<base>` stays as written); resolver-unavailable `re-run; if it repeats, run devflow init`; invalid-name the allowed set, never the name; any other REASON none, and `; fix: {fix}` is left out.
|
|
196
196
|
|
|
197
197
|
### Phase 2: Implement
|
|
198
198
|
|
|
@@ -520,9 +520,9 @@ Any other or missing Status: report and halt; never PASS.
|
|
|
520
520
|
<!-- compliance:end -->
|
|
521
521
|
|
|
522
522
|
<!-- compliance:on -->
|
|
523
|
-
Run once, after the gates, under every strategy. No Code agent opens a PR: the Git agent does, and it pastes `ISSUE_PR_LINK`, `PR_EXCEPTIONS` and `PR_TEST_PLAN_BLOCK` through their paste gates and scrubs the body (D11) before `gh pr create`.
|
|
523
|
+
Run once, after the gates, under every strategy. No Code agent opens a PR: the Git agent does, and it pastes `ISSUE_PR_LINK`, `PR_EXCEPTIONS` and `PR_TEST_PLAN_BLOCK` through their paste gates and scrubs the body (D11) before `gh pr create`. This command writes no link line and creates no PR itself: it forwards Phase 1's link line verbatim.
|
|
524
524
|
<!-- compliance:off -->
|
|
525
|
-
Run once, after the gates, under every strategy. No Code agent opens a PR: the Git agent does, and it pastes `ISSUE_PR_LINK` through its paste gate and scrubs the body (D11) before `gh pr create`.
|
|
525
|
+
Run once, after the gates, under every strategy. No Code agent opens a PR: the Git agent does, and it pastes `ISSUE_PR_LINK` through its paste gate and scrubs the body (D11) before `gh pr create`. This command writes no link line and creates no PR itself: it forwards Phase 1's link line verbatim.
|
|
526
526
|
<!-- compliance:end -->
|
|
527
527
|
|
|
528
528
|
```
|
|
@@ -629,7 +629,7 @@ If any Git agent output emitted `TRACEABILITY: DEGRADED ({reason})` lines during
|
|
|
629
629
|
If any Git agent output emitted `TRACEABILITY: DEGRADED ({reason})` lines during the run, or the compliance-off line of Phase 1, the fresh base step, Phase 10's push, Phase 11's push or Phase 11's script output carried one, surface them verbatim in the report under a `### Traceability` subsection.
|
|
630
630
|
<!-- compliance:end -->
|
|
631
631
|
|
|
632
|
-
If Phase 9 listed uncommitted files, show them as a `### Uncommitted files` subsection
|
|
632
|
+
If Phase 9 listed uncommitted files, show them as a `### Uncommitted files` subsection.
|
|
633
633
|
|
|
634
634
|
If Phase 1 recorded an evidence exception, show its `## Evidence Exceptions` lines in the report.
|
|
635
635
|
|
|
@@ -19,7 +19,7 @@ output-dir: dist/commands
|
|
|
19
19
|
|
|
20
20
|
## Input
|
|
21
21
|
|
|
22
|
-
What follows `/research` is bound once, here. Every later step names it `COMMAND_INPUT
|
|
22
|
+
What follows `/research` is bound once, here. Every later step names it `COMMAND_INPUT`:
|
|
23
23
|
|
|
24
24
|
<command-input>
|
|
25
25
|
$ARGUMENTS
|
|
@@ -69,7 +69,7 @@ Analyze the research question to infer research types needed (min 2, max 5). For
|
|
|
69
69
|
- `RESEARCH_QUESTION`: Focused sub-question for this type
|
|
70
70
|
- `OUTPUT_PATH`: `{worktree}/.devflow/docs/research/{topic-slug}/{YYYY-MM-DD_HHMM}/{type}.md`
|
|
71
71
|
|
|
72
|
-
**Tool availability check**: If WebSearch/WebFetch are unavailable, restrict to `codebase` type only.
|
|
72
|
+
**Tool availability check**: If WebSearch/WebFetch are unavailable, restrict to `codebase` type only, telling the user why.
|
|
73
73
|
|
|
74
74
|
### Phase 3: Orient (Conditional)
|
|
75
75
|
|
|
@@ -93,7 +93,7 @@ For each issue reference in `SHIPPED_ISSUES` (sequentially, the first ≤50 in l
|
|
|
93
93
|
1. Verify on feature branch (not main/master/develop/integration/trunk/release/*/staging/production) - error if not
|
|
94
94
|
2. Check for uncommitted changes. `COMMIT_UNCOMMITTED` is `true` when absent: if any, create atomic commit using `devflow:git` patterns, naming `TASK_ID` when given. When it is `false`, commit nothing — a stray file is not this operation's to commit: list each uncommitted path under `### Uncommitted files` in the output, report `Committed: no` and go on; step 4a opens the PR from HEAD.
|
|
95
95
|
3. Check if branch pushed to remote - if not, push with `-u` flag. If that push is refused and the branch's open PR is cross-repository with maintainer edits off (`gh pr view --json isCrossRepository,maintainerCanModify`), emit `TRACEABILITY: DEGRADED (cannot push to fork)` and go to 4a (4a–4c edit only the PR).
|
|
96
|
-
4a. Check if PR exists - if not, create PR using guidance from (in priority order): (a) `PR_DESCRIPTION_GUIDANCE` if given and not `(none)`, (b) generated from branch context. Compose the PR body via the `devflow:git` template to `$DEVFLOW_BODY_RAW` (a D11 sink: it publishes at repo visibility), then append the caller blocks. Apply the Comment-sink scrub (D11) — a failed one posts neither block; the D11 paragraph below creates the PR. When `BASE_BRANCH` is given, check it first against the step-5a name check (judge the match by reading; never pass the value to a command). A mismatch ends with Status BLOCKED and no PR, naming only the allowed set. Then Write `BASE_BRANCH` to a new file in a `mktemp -d` folder (`{name file}`). If the Write tool fails, treat the value as missing; never write it with a shell command. Each PR create, the scrubber-failure one too, runs in the existing D11 shell: bind `F="{name file}"`, then `test -s "$F" && gh pr create … --base="$(cat -- "$F")"`, then, in that same shell and after the last create attempt, `rm -- "$F"`. That
|
|
96
|
+
4a. Check if PR exists - if not, create PR using guidance from (in priority order): (a) `PR_DESCRIPTION_GUIDANCE` if given and not `(none)`, (b) generated from branch context. Compose the PR body via the `devflow:git` template to `$DEVFLOW_BODY_RAW` (a D11 sink: it publishes at repo visibility), then append the caller blocks. Apply the Comment-sink scrub (D11) — a failed one posts neither block; the D11 paragraph below creates the PR. When `BASE_BRANCH` is given, check it first against the step-5a name check (judge the match by reading; never pass the value to a command). A mismatch ends with Status BLOCKED and no PR, naming only the allowed set. Then Write `BASE_BRANCH` to a new file in a `mktemp -d` folder (`{name file}`). If the Write tool fails, treat the value as missing; never write it with a shell command. Each PR create, the scrubber-failure one too, runs in the existing D11 shell: bind `F="{name file}"`, then `test -s "$F" && gh pr create … --base="$(cat -- "$F")"`, then, in that same shell and after the last create attempt, `rm -- "$F"; rmdir -- "${F%/*}"`. That removal runs when the shell reaches it; an INT or TERM signal skips it and leaves the file and its folder in the temp directory. The D11 trap stays unchanged: its rm list does not gain the name file, and no second EXIT trap is armed. A failed `test -s` (an empty file, or none after a failed Write) ends with Status BLOCKED and no PR.
|
|
97
97
|
<!-- compliance:on -->
|
|
98
98
|
- **Caller blocks**, in order: `PR_WAVE_BLOCK` (`check wave`), then `PR_TEST_PLAN_BLOCK` (`check block`), each when given and not `(none)`. Write it byte for byte to a new file in a `mktemp -d` folder with the Write tool, never via a shell string, and run `node "$HOME/.devflow/scripts/verify-evidence.cjs" check <wave|block> <file>; echo "exit=$?"`. Only `exit=0` admits it, verbatim; else omit it, never repaired or partly pasted, and emit `TRACEABILITY: DEGRADED (wave block does not match its grammar)` or `TRACEABILITY: DEGRADED (test-plan block does not match its grammar)` with steps 4b/4c's lines. An admitted wave block is the body's only `## Related Issues`: skip 4b.
|
|
99
99
|
<!-- compliance:off -->
|
|
@@ -148,7 +148,7 @@ For each issue reference in `SHIPPED_ISSUES` (sequentially, the first ≤50 in l
|
|
|
148
148
|
@define ensure_pr_ready_steps_from_4c():
|
|
149
149
|
4c. Retitle, only when `APPLY_CONVENTIONS` is `true`: if the PR title breaks the convention in the PR Titles section of `.devflow/conventions.md`, retitle it; skip silently when that file is absent. Two rules, because the title derives from third-party PR titles:
|
|
150
150
|
- **Validate before use.** Skip the retitle (leave the PR title as-is, no error) if the composed title contains any of `` $ ` \ " ' ; | & < > `` or a newline; judge the match by reading; never pass the value to a command.
|
|
151
|
-
- **Pass from a file, never as command text.** Write the title with the Write tool to a new file in a `mktemp -d` folder (`{title file}`). If the Write tool fails, treat the value as missing; never write it with a shell command. A missing title skips the retitle after `rm -- "{
|
|
151
|
+
- **Pass from a file, never as command text.** Write the title with the Write tool to a new file in a `mktemp -d` folder (`{title file}`). If the Write tool fails, treat the value as missing; never write it with a shell command. A missing title skips the retitle after `F="{title file}"; rm -- "$F"; rmdir -- "${F%/*}"`. Otherwise run `F="{title file}"; trap 'rm -- "$F"; rmdir -- "${F%/*}"' EXIT; test -s "$F" && gh pr edit {PR_NUMBER} --title="$(cat -- "$F")"; echo "gh=$?"`.
|
|
152
152
|
|
|
153
153
|
On any 4xx/5xx from `gh pr edit`: emit `TRACEABILITY: DEGRADED ({reason})` and continue — a failed retitle never blocks the PR.
|
|
154
154
|
5. Get the base branch
|
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
// table of /implement and /resolve as a pure function of the status, the reason, the
|
|
34
34
|
// seconds waited and the two counts, so a prompt only acts on the answer. An argv that
|
|
35
35
|
// fails its gate prints the short form
|
|
36
|
-
// `CI INDETERMINATE pr=- head=- waited=0 reason=usage
|
|
36
|
+
// `CI INDETERMINATE pr=- head=- waited=0 reason=usage`, with no NEXT line, and makes no
|
|
37
|
+
// gh call. The
|
|
37
38
|
// boundary re-checks the CI line against LINE_RE and the 400-character bound, and the
|
|
38
|
-
// NEXT line against the fixed set NEXT_LINES, before it writes them. A
|
|
39
|
-
//
|
|
39
|
+
// NEXT line against the fixed set NEXT_LINES, before it writes them. A wait that fails
|
|
40
|
+
// inside the script prints its INDETERMINATE line (`gh-failed` or `unparseable`) and,
|
|
41
|
+
// with the two flags, the NEXT line of an INDETERMINATE result that waited 0 seconds.
|
|
40
42
|
// Exit 0 whenever a line was printed; a reader treats a missing or unparseable line, an
|
|
41
43
|
// unlisted status or a non-zero exit as INDETERMINATE.
|
|
42
44
|
//
|
|
@@ -1080,10 +1082,11 @@ async function main(argv, deps) {
|
|
|
1080
1082
|
result = null;
|
|
1081
1083
|
line = indeterminate('unparseable');
|
|
1082
1084
|
}
|
|
1083
|
-
if (
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
const
|
|
1085
|
+
if (parsed.waits === undefined || parsed.fixes === undefined) return { code: 0, stdout: line + '\n' };
|
|
1086
|
+
// D-CI-NEXT: a wait that failed inside this script is INDETERMINATE that waited 0 seconds,
|
|
1087
|
+
// so the gate waits again while waits remain, as the prompts did before the NEXT line.
|
|
1088
|
+
const failed = { status: 'INDETERMINATE', reason: null, waited: 0 };
|
|
1089
|
+
const next = renderNext(nextAction(result === null ? failed : result, parsed.waits, parsed.fixes));
|
|
1087
1090
|
if (!checkNext(next)) return { code: 0, stdout: indeterminate('unparseable') + '\n' };
|
|
1088
1091
|
return { code: 0, stdout: line + '\n' + next + '\n' };
|
|
1089
1092
|
}
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
// - main() returns {code, line} and never calls process.exit or throws; the single
|
|
37
37
|
// `require.main === module` boundary is the only stdout write and the only
|
|
38
38
|
// exitCode assignment
|
|
39
|
-
// - every subprocess is spawned with an argv array (never a shell), stdin ignored
|
|
40
|
-
//
|
|
39
|
+
// - every subprocess is spawned with an argv array (never a shell), stdin ignored
|
|
40
|
+
// and a maxBuffer, with no time limit (D-ROOT-NO-TIMEOUT): main() clears the limit
|
|
41
|
+
// that resolve-settings.cjs's runGit sets, and resolve-settings.cjs keeps its own
|
|
41
42
|
// - a local read only: no network, nothing written
|
|
42
43
|
|
|
43
44
|
'use strict';
|
|
@@ -113,7 +114,10 @@ function fail(code, reason) {
|
|
|
113
114
|
*/
|
|
114
115
|
function main(argv, deps) {
|
|
115
116
|
try {
|
|
116
|
-
const
|
|
117
|
+
const base = deps && typeof deps.exec === 'function' ? deps.exec : settings.defaultExec;
|
|
118
|
+
// D-ROOT-NO-TIMEOUT: git runs with no devflow time limit, as the Bash git call of the
|
|
119
|
+
// prompts did; the Bash tool's own timeout bounds the call. runGit's limit is removed here.
|
|
120
|
+
const exec = (file, gitArgs, opts) => base(file, gitArgs, { ...opts, timeout: undefined });
|
|
117
121
|
const args = parseArgs(argv);
|
|
118
122
|
if (args.kind === 'usage') {
|
|
119
123
|
process.stderr.write(args.usage + '\n');
|
|
@@ -68,15 +68,22 @@
|
|
|
68
68
|
// nothing is written. The `exit:` part is written only for a number from 0
|
|
69
69
|
// to 255. A TP that the plan does not list gets no line. The plan is the
|
|
70
70
|
// `## Test Plan` of the `--plan` file, or of the evidence file when `--plan`
|
|
71
|
-
// is absent. The `## Claims` section is
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
71
|
+
// is absent. The `## Claims` section is created when absent, at the end
|
|
72
|
+
// of the file; when it is not the last section the lines go at its end
|
|
73
|
+
// (D-CLAIM-INSERT). A missing folder inside the working directory is
|
|
74
|
+
// created, and so is the file (D-CLAIM-FOLDER). The write is one append of
|
|
75
|
+
// every line, or, for a Claims section that is not last, one whole-file
|
|
76
|
+
// replace through a sibling temporary file that keeps the file's mode. The
|
|
77
|
+
// parser of pr-evidence.cjs accepts each line before it. It prints one
|
|
78
|
+
// line, exit 0:
|
|
75
79
|
// `CLAIM appended:<n> PASS:<n> FAIL:<n> SKIP:<n> skipped:<n>`
|
|
76
80
|
// (`skipped` counts the TPs that got no line), or exit 2 with
|
|
77
81
|
// `CLAIM refused (<reason>)`, the reason being `head not one sha`,
|
|
78
82
|
// `evidence file tracked` (git tracks it, or cannot say it does not),
|
|
79
|
-
// `evidence file unusable`
|
|
83
|
+
// `evidence file unusable` (a file that does not parse, or a folder that is
|
|
84
|
+
// missing outside the working directory) or `plan file unusable`. A refusal
|
|
85
|
+
// writes nothing. A folder the run made is removed again when the run
|
|
86
|
+
// leaves no file in it
|
|
80
87
|
// A git-tracked file counts as absent, like a tracked `.devflow/config.json`
|
|
81
88
|
// (D-TRACKED-RESULT). `verify`, `publish` and `readiness` ask git about an `--evidence`
|
|
82
89
|
// file before they read it, and about `--block-out`, `--comment-out`, `--stale-out` and
|
|
@@ -286,11 +293,12 @@ const USAGE = [
|
|
|
286
293
|
* @typedef {{ ghCalls: number, runViews: number, throttled: boolean }} Budget
|
|
287
294
|
*
|
|
288
295
|
* @typedef {{ exec: ExecFn, env: NodeJS.ProcessEnv, cwd: string, now: () => number,
|
|
289
|
-
* deadline: number, budget: Budget, notes: Set<string>,
|
|
290
|
-
* ghCap?: number }} Io
|
|
296
|
+
* deadline: number, budget: Budget, notes: Set<string>, made: string[],
|
|
297
|
+
* stderr: (text: string) => void, ghCap?: number }} Io
|
|
291
298
|
* The imperative shell's one context: every call goes through it, so every
|
|
292
299
|
* bound is counted in one place. `notes` holds closed tokens for the stderr
|
|
293
|
-
* summary, never input bytes. `
|
|
300
|
+
* summary, never input bytes. `made` holds the folders a `claim` run created, deepest
|
|
301
|
+
* first (D-CLAIM-FOLDER). `ghCap` replaces CAPS.GH_CALLS for a stage that
|
|
294
302
|
* carries a budget of its own (`publish`'s writes).
|
|
295
303
|
*
|
|
296
304
|
* @typedef {{ login: string, association: string, body: string, viewerDidAuthor: boolean, commentId: string }} PrComment
|
|
@@ -625,6 +633,59 @@ function appendTextFile(filePath, text) {
|
|
|
625
633
|
}
|
|
626
634
|
}
|
|
627
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Replace the regular file `filePath` with `text` and keep its `mode`: the text goes to a
|
|
638
|
+
* sibling temporary file, in the same folder, which a rename then puts in the file's place.
|
|
639
|
+
* It refuses first what appendTextFile refuses, so the two writes of a claim agree: a file
|
|
640
|
+
* this process may not write, a link put in its place (O_NOFOLLOW), a FIFO (O_NONBLOCK) or
|
|
641
|
+
* any other object that is not a regular file. The probe opens the file for an append and
|
|
642
|
+
* writes nothing. A write that fails leaves the file as it was and the temporary file
|
|
643
|
+
* removed, so a failed replace never truncates it. The temporary file is created new
|
|
644
|
+
* (O_EXCL) and never through a link (O_NOFOLLOW), and takes the old mode on its own
|
|
645
|
+
* descriptor, since the umask would otherwise set the new file's mode. Returns whether the
|
|
646
|
+
* file was replaced.
|
|
647
|
+
*
|
|
648
|
+
* @param {string} filePath
|
|
649
|
+
* @param {string} text
|
|
650
|
+
* @param {number} mode the permission bits of the file it replaces
|
|
651
|
+
* @returns {boolean}
|
|
652
|
+
*/
|
|
653
|
+
function replaceTextFile(filePath, text, mode) {
|
|
654
|
+
const c = fs.constants;
|
|
655
|
+
let probe;
|
|
656
|
+
try {
|
|
657
|
+
probe = fs.openSync(filePath, c.O_WRONLY | c.O_APPEND | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0));
|
|
658
|
+
if (!fs.fstatSync(probe).isFile()) return false;
|
|
659
|
+
} catch (_) {
|
|
660
|
+
return false;
|
|
661
|
+
} finally {
|
|
662
|
+
if (probe !== undefined) {
|
|
663
|
+
try { fs.closeSync(probe); } catch (_) { /* the probe wrote nothing */ }
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const temp = path.join(path.dirname(filePath), '.' + path.basename(filePath) + '.claim-' + process.pid);
|
|
667
|
+
let fd;
|
|
668
|
+
try {
|
|
669
|
+
fd = fs.openSync(temp, c.O_WRONLY | c.O_CREAT | c.O_EXCL | (c.O_NOFOLLOW || 0), 0o600);
|
|
670
|
+
} catch (_) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
fs.fchmodSync(fd, mode);
|
|
675
|
+
fs.writeFileSync(fd, text, 'utf8');
|
|
676
|
+
fs.closeSync(fd);
|
|
677
|
+
fd = undefined;
|
|
678
|
+
fs.renameSync(temp, filePath);
|
|
679
|
+
return true;
|
|
680
|
+
} catch (_) {
|
|
681
|
+
if (fd !== undefined) {
|
|
682
|
+
try { fs.closeSync(fd); } catch (_) { /* the write already failed */ }
|
|
683
|
+
}
|
|
684
|
+
try { fs.unlinkSync(temp); } catch (_) { /* the temporary file is already gone */ }
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
628
689
|
/**
|
|
629
690
|
* @param {string} dir
|
|
630
691
|
* @returns {boolean}
|
|
@@ -3098,11 +3159,84 @@ function claimRefusal(reason) {
|
|
|
3098
3159
|
return { code: EXIT_CODES.INPUT_UNUSABLE, stdout: 'CLAIM refused (' + reason + ')\n' };
|
|
3099
3160
|
}
|
|
3100
3161
|
|
|
3162
|
+
/** The most folders one path can hold: the bound of the walk up to the nearest folder that exists. */
|
|
3163
|
+
const FOLDER_DEPTH_MAX = 4096;
|
|
3164
|
+
|
|
3165
|
+
/**
|
|
3166
|
+
* Whether `child` lies below `parent`, by name alone. `parent` itself, and a path outside
|
|
3167
|
+
* it, are not below it.
|
|
3168
|
+
*
|
|
3169
|
+
* @param {string} parent absolute
|
|
3170
|
+
* @param {string} child absolute
|
|
3171
|
+
* @returns {boolean}
|
|
3172
|
+
*/
|
|
3173
|
+
function isBelow(parent, child) {
|
|
3174
|
+
const rel = path.relative(parent, child);
|
|
3175
|
+
return rel !== '' && rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
/**
|
|
3179
|
+
* D-CLAIM-FOLDER: the folder of the evidence file exists, or this run makes it. The Write
|
|
3180
|
+
* tool made the folders of a file in 3.11.0, so a first claim into a folder that is not
|
|
3181
|
+
* there yet worked. A run makes a folder only inside the working directory: the folder lies
|
|
3182
|
+
* below `io.cwd` by name, and the nearest folder that already exists resolves to `io.cwd` or
|
|
3183
|
+
* below it, so a link out of the working directory makes nothing. Each folder it makes goes
|
|
3184
|
+
* to `io.made`, deepest first, for removeMadeFolders. A path that holds a file where a folder
|
|
3185
|
+
* must be, or that the run may not make, is unusable. After each folder is made, its real path
|
|
3186
|
+
* must lie below the real working directory; if a link swapped in after the first check led the
|
|
3187
|
+
* folder out, the run is unusable and removeMadeFolders takes the folder away again. This check
|
|
3188
|
+
* narrows the window for such a swap; it does not close it, because the later open of the
|
|
3189
|
+
* evidence file resolves the path again.
|
|
3190
|
+
*
|
|
3191
|
+
* @param {Io} io
|
|
3192
|
+
* @param {string} dir absolute
|
|
3193
|
+
* @returns {boolean} whether the folder exists now
|
|
3194
|
+
*/
|
|
3195
|
+
function ensureClaimFolder(io, dir) {
|
|
3196
|
+
if (isRealDirectory(dir)) return true;
|
|
3197
|
+
try {
|
|
3198
|
+
const root = path.resolve(io.cwd);
|
|
3199
|
+
if (fs.lstatSync(dir, { throwIfNoEntry: false }) !== undefined || !isBelow(root, dir)) return false;
|
|
3200
|
+
let near = dir;
|
|
3201
|
+
for (let i = 0; i < FOLDER_DEPTH_MAX && fs.lstatSync(near, { throwIfNoEntry: false }) === undefined; i++) {
|
|
3202
|
+
near = path.dirname(near);
|
|
3203
|
+
}
|
|
3204
|
+
const real = fs.realpathSync(near);
|
|
3205
|
+
const realRoot = fs.realpathSync(root);
|
|
3206
|
+
if (real !== realRoot && !isBelow(realRoot, real)) return false;
|
|
3207
|
+
let at = near;
|
|
3208
|
+
for (const part of path.relative(near, dir).split(path.sep)) {
|
|
3209
|
+
at = path.join(at, part);
|
|
3210
|
+
fs.mkdirSync(at);
|
|
3211
|
+
io.made.unshift(at);
|
|
3212
|
+
if (!isBelow(realRoot, fs.realpathSync(at))) return false;
|
|
3213
|
+
}
|
|
3214
|
+
return true;
|
|
3215
|
+
} catch (_) {
|
|
3216
|
+
return false;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
/**
|
|
3221
|
+
* Take away the folders a `claim` run made. Each goes through rmdir, which refuses a folder
|
|
3222
|
+
* that holds anything, so a folder that holds the evidence file, or a file another process
|
|
3223
|
+
* put there, stays. It is never a recursive delete.
|
|
3224
|
+
*
|
|
3225
|
+
* @param {Io} io
|
|
3226
|
+
* @returns {void}
|
|
3227
|
+
*/
|
|
3228
|
+
function removeMadeFolders(io) {
|
|
3229
|
+
for (const dir of io.made.splice(0)) {
|
|
3230
|
+
try { fs.rmdirSync(dir); } catch (_) { /* a folder that holds a file stays */ }
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3101
3234
|
/**
|
|
3102
3235
|
* The reason a `claim` run must not start, or null. Nothing is read or written before
|
|
3103
|
-
* this
|
|
3104
|
-
*
|
|
3105
|
-
* (D-
|
|
3236
|
+
* this, but the folder of the evidence file, when it is missing: the HEAD is one 40-hex
|
|
3237
|
+
* SHA, each path is a path the commands build, the evidence file sits in a folder that
|
|
3238
|
+
* exists or that the run makes inside the working directory (D-CLAIM-FOLDER), and git says
|
|
3239
|
+
* clearly that neither file is tracked (D-TRACKED-RESULT).
|
|
3106
3240
|
*
|
|
3107
3241
|
* @param {Io} io
|
|
3108
3242
|
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
@@ -3110,7 +3244,7 @@ function claimRefusal(reason) {
|
|
|
3110
3244
|
*/
|
|
3111
3245
|
function claimRefusalReason(io, args) {
|
|
3112
3246
|
if (!SHA40_RE.test(args.head)) return 'head not one sha';
|
|
3113
|
-
if (!EVIDENCE_PATH_RE.test(args.evidence) || !
|
|
3247
|
+
if (!EVIDENCE_PATH_RE.test(args.evidence) || !ensureClaimFolder(io, path.dirname(path.resolve(args.evidence)))) return 'evidence file unusable';
|
|
3114
3248
|
if (args.plan !== null && !EVIDENCE_PATH_RE.test(args.plan)) return 'plan file unusable';
|
|
3115
3249
|
if (trackedState(io, args.evidence) !== 'untracked') return 'evidence file tracked';
|
|
3116
3250
|
if (args.plan !== null && trackedState(io, args.plan) !== 'untracked') return 'plan file unusable';
|
|
@@ -3144,8 +3278,8 @@ function claimLine(line) {
|
|
|
3144
3278
|
|
|
3145
3279
|
/**
|
|
3146
3280
|
* The text to append: the lines, behind the `## Claims` heading when the file has no such
|
|
3147
|
-
* section (a blank line first, when the file holds anything). The
|
|
3148
|
-
* the file, so the lines go at its end.
|
|
3281
|
+
* section (a blank line first, when the file holds anything). The caller appends only when
|
|
3282
|
+
* the section is the last of the file, so the lines go at its end.
|
|
3149
3283
|
*
|
|
3150
3284
|
* @param {string} text the evidence file as it is, '' when it is absent
|
|
3151
3285
|
* @param {boolean} hasSection
|
|
@@ -3159,6 +3293,51 @@ function claimAppendText(text, hasSection, lines) {
|
|
|
3159
3293
|
return hasSection ? lead + body : lead + '\n' + PE.CLAIMS_HEADING + '\n' + body;
|
|
3160
3294
|
}
|
|
3161
3295
|
|
|
3296
|
+
/**
|
|
3297
|
+
* D-CLAIM-INSERT: the whole text with the lines put at the end of a `## Claims` section that
|
|
3298
|
+
* another section follows: behind the last row of the section that is not blank, before the
|
|
3299
|
+
* blank rows that precede the next heading. A row is blank as the parser of pr-evidence.cjs
|
|
3300
|
+
* reads it, an empty row once its CR is gone. A text with no such section comes back as it was.
|
|
3301
|
+
*
|
|
3302
|
+
* @param {string} text
|
|
3303
|
+
* @param {readonly string[]} lines
|
|
3304
|
+
* @returns {string}
|
|
3305
|
+
*/
|
|
3306
|
+
function claimInsertText(text, lines) {
|
|
3307
|
+
const rows = text.split('\n');
|
|
3308
|
+
/** @param {string} row @returns {string} */
|
|
3309
|
+
const bare = row => row.replace(/\r$/, '');
|
|
3310
|
+
const start = rows.findIndex(row => bare(row) === PE.CLAIMS_HEADING);
|
|
3311
|
+
if (start === -1) return text;
|
|
3312
|
+
let end = rows.findIndex((row, i) => i > start && (bare(row) === '##' || bare(row).startsWith('## ')));
|
|
3313
|
+
if (end === -1) end = rows.length;
|
|
3314
|
+
let last = end - 1;
|
|
3315
|
+
while (last > start && bare(rows[last]) === '') last--;
|
|
3316
|
+
rows.splice(last + 1, 0, ...lines);
|
|
3317
|
+
return rows.join('\n');
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
/**
|
|
3321
|
+
* Whether `inserted` is `text` with the lines added at the end of its `## Claims` section and
|
|
3322
|
+
* nothing else changed, as the parser of pr-evidence.cjs reads both: the Claims section grew by
|
|
3323
|
+
* exactly the lines, its other sections are the same, and the result parses as an evidence file.
|
|
3324
|
+
* This is the check the replace is held to before it writes.
|
|
3325
|
+
*
|
|
3326
|
+
* @param {string} text
|
|
3327
|
+
* @param {string} inserted
|
|
3328
|
+
* @param {readonly string[]} lines
|
|
3329
|
+
* @returns {boolean}
|
|
3330
|
+
*/
|
|
3331
|
+
function claimInserted(text, inserted, lines) {
|
|
3332
|
+
const before = PE.evidenceSections(text);
|
|
3333
|
+
const after = PE.evidenceSections(inserted);
|
|
3334
|
+
if (!before.ok || !after.ok || before.value.claims === null) return false;
|
|
3335
|
+
return after.value.claims === before.value.claims + '\n' + lines.join('\n')
|
|
3336
|
+
&& after.value.testPlan === before.value.testPlan
|
|
3337
|
+
&& after.value.exceptions === before.value.exceptions
|
|
3338
|
+
&& parseEvidenceText(inserted) !== null;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3162
3341
|
/**
|
|
3163
3342
|
* The TP numbers the plan lists: the `## Test Plan` of the `--plan` file, else of the
|
|
3164
3343
|
* evidence file. A plan with no TP line, or no plan section, lists none. Null when the
|
|
@@ -3181,9 +3360,13 @@ function claimPlanIds(args, evidence) {
|
|
|
3181
3360
|
|
|
3182
3361
|
/**
|
|
3183
3362
|
* D-CLAIM-APPEND: `claim`. The checks of claimRefusalReason have passed (admitPaths).
|
|
3184
|
-
* The evidence file is read as `verify` reads it
|
|
3185
|
-
*
|
|
3186
|
-
*
|
|
3363
|
+
* The evidence file is read as `verify` reads it; a file that does not parse is refused, as
|
|
3364
|
+
* `verify` refuses it. Then every line is built and checked, and one write puts them all in.
|
|
3365
|
+
* That write is an append when the `## Claims` section is the last section or is absent. When
|
|
3366
|
+
* another section follows it, the write is a whole-file replace (D-CLAIM-INSERT) that adds the
|
|
3367
|
+
* lines at the end of the Claims section, held to claimInserted before it writes. One claim
|
|
3368
|
+
* run writes at a time, as the commands run it. A run that writes no line (every TP off the
|
|
3369
|
+
* plan) changes no file.
|
|
3187
3370
|
*
|
|
3188
3371
|
* @param {Io} io
|
|
3189
3372
|
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
@@ -3204,7 +3387,7 @@ function runClaim(io, args) {
|
|
|
3204
3387
|
if (text === null || evidence === null) return claimRefusal('evidence file unusable');
|
|
3205
3388
|
const headings = text.split('\n').map(row => row.replace(/\r$/, '')).filter(row => row === '##' || row.startsWith('## '));
|
|
3206
3389
|
const hasSection = headings.includes(PE.CLAIMS_HEADING);
|
|
3207
|
-
|
|
3390
|
+
const sectionFollowed = hasSection && headings[headings.length - 1] !== PE.CLAIMS_HEADING;
|
|
3208
3391
|
const listed = args.gate ? new Set() : claimPlanIds(args, evidence);
|
|
3209
3392
|
if (listed === null) return claimRefusal('plan file unusable');
|
|
3210
3393
|
|
|
@@ -3222,8 +3405,16 @@ function runClaim(io, args) {
|
|
|
3222
3405
|
lines.push(claimLine(claimLineOk(withExit) ? withExit : bare));
|
|
3223
3406
|
tally[tp.outcome]++;
|
|
3224
3407
|
}
|
|
3225
|
-
if (lines.length > 0
|
|
3226
|
-
|
|
3408
|
+
if (lines.length > 0) {
|
|
3409
|
+
let written;
|
|
3410
|
+
if (sectionFollowed) {
|
|
3411
|
+
const inserted = claimInsertText(text, lines);
|
|
3412
|
+
if (!claimInserted(text, inserted, lines)) return claimRefusal('evidence file unusable');
|
|
3413
|
+
written = replaceTextFile(file, inserted, st === undefined ? 0o600 : st.mode & 0o777);
|
|
3414
|
+
} else {
|
|
3415
|
+
written = appendTextFile(file, claimAppendText(text, hasSection, lines));
|
|
3416
|
+
}
|
|
3417
|
+
if (!written) return refuse(io, EXIT_CODES.WRITE_FAILED, 'an output file could not be written');
|
|
3227
3418
|
}
|
|
3228
3419
|
const skipped = args.gate ? 0 : args.tps.length - lines.length;
|
|
3229
3420
|
return {
|
|
@@ -3294,6 +3485,7 @@ function main(argv, deps) {
|
|
|
3294
3485
|
deadline: now() + DEADLINE_MS,
|
|
3295
3486
|
budget: { ghCalls: 0, runViews: 0, throttled: false },
|
|
3296
3487
|
notes: new Set(),
|
|
3488
|
+
made: [],
|
|
3297
3489
|
stderr: typeof d.stderr === 'function' ? d.stderr : (text => { process.stderr.write(text); }),
|
|
3298
3490
|
};
|
|
3299
3491
|
const parsed = parseArgs(argv);
|
|
@@ -3330,6 +3522,7 @@ function main(argv, deps) {
|
|
|
3330
3522
|
if (args.out !== null) outcome = writeReadinessOut(io, args.out, args.pr, outcome, null, planSource(args));
|
|
3331
3523
|
} else outcome = refuse(io, EXIT_CODES.REMOTE_FAILURE, label);
|
|
3332
3524
|
}
|
|
3525
|
+
removeMadeFolders(io);
|
|
3333
3526
|
if (io.notes.size > 0) io.stderr('verify-evidence: notes: ' + [...io.notes].sort().join(',') + '\n');
|
|
3334
3527
|
return outcome;
|
|
3335
3528
|
}
|
|
@@ -7,7 +7,7 @@ allowed-tools: Read, Grep, Glob
|
|
|
7
7
|
|
|
8
8
|
# Design Review
|
|
9
9
|
|
|
10
|
-
Anti-pattern detection for implementation plans.
|
|
10
|
+
Anti-pattern detection for implementation plans.
|
|
11
11
|
|
|
12
12
|
## Iron Law
|
|
13
13
|
|
|
@@ -15,8 +15,7 @@ Anti-pattern detection for implementation plans. Catches structural problems bef
|
|
|
15
15
|
>
|
|
16
16
|
> A plan that implies N+1 queries will produce N+1 queries. A plan that omits error
|
|
17
17
|
> handling will produce code with no error handling. Review the plan as rigorously as
|
|
18
|
-
>
|
|
19
|
-
> follows.
|
|
18
|
+
> the code: every implementation decision follows it.
|
|
20
19
|
|
|
21
20
|
## Anti-Pattern Detection Rules
|
|
22
21
|
|
|
@@ -34,6 +33,8 @@ Anti-pattern detection for implementation plans. Catches structural problems bef
|
|
|
34
33
|
Resolution: Replace loop with batch query: {proposed batch operation}.
|
|
35
34
|
```
|
|
36
35
|
|
|
36
|
+
**Suggest:** collect the IDs first and batch-fetch the related rows in one query, or eager-load them (JOIN) in the list query.
|
|
37
|
+
|
|
37
38
|
### 2. God Functions
|
|
38
39
|
|
|
39
40
|
**What to look for in a plan:**
|
|
@@ -48,6 +49,8 @@ Resolution: Replace loop with batch query: {proposed batch operation}.
|
|
|
48
49
|
Resolution: Split into: {responsibility 1} → {responsibility 2} → {responsibility 3}.
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
**Suggest:** one purpose per function; move notification, logging and other side effects to their own functions, and compose them at the service layer.
|
|
53
|
+
|
|
51
54
|
### 3. Missing Parallelism
|
|
52
55
|
|
|
53
56
|
**What to look for in a plan:**
|
|
@@ -62,6 +65,8 @@ Resolution: Split into: {responsibility 1} → {responsibility 2} → {responsib
|
|
|
62
65
|
Resolution: Execute {step A} and {step B} in parallel: {proposed parallel pattern}.
|
|
63
66
|
```
|
|
64
67
|
|
|
68
|
+
**Suggest:** run independent steps in parallel (`Promise.all`, goroutines, `asyncio.gather`), and name the steps that must stay in order.
|
|
69
|
+
|
|
65
70
|
### 4. Error Handling Gaps
|
|
66
71
|
|
|
67
72
|
**What to look for in a plan:**
|
|
@@ -77,6 +82,8 @@ Resolution: Execute {step A} and {step B} in parallel: {proposed parallel patter
|
|
|
77
82
|
Resolution: Add: {on failure scenario} → {specific recovery action}.
|
|
78
83
|
```
|
|
79
84
|
|
|
85
|
+
**Suggest:** for each external call, the action on timeout, 4xx and 5xx; a compensation step for each step of a multi-step operation; Result types or explicit error returns, never a silent failure.
|
|
86
|
+
|
|
80
87
|
### 5. Missing Caching
|
|
81
88
|
|
|
82
89
|
**What to look for in a plan:**
|
|
@@ -91,6 +98,8 @@ Resolution: Add: {on failure scenario} → {specific recovery action}.
|
|
|
91
98
|
Resolution: Cache {data} for {TTL}: {proposed cache strategy and invalidation}.
|
|
92
99
|
```
|
|
93
100
|
|
|
101
|
+
**Suggest:** an application cache for cross-request data, a short-TTL in-memory cache for process-local reference data, and the invalidation (on update, by TTL, or both).
|
|
102
|
+
|
|
94
103
|
### 6. Poor Decomposition
|
|
95
104
|
|
|
96
105
|
**What to look for in a plan:**
|
|
@@ -105,6 +114,8 @@ Resolution: Cache {data} for {TTL}: {proposed cache strategy and invalidation}.
|
|
|
105
114
|
Resolution: {concern A} belongs in {layer/module}; {concern B} belongs in {layer/module}.
|
|
106
115
|
```
|
|
107
116
|
|
|
117
|
+
**Suggest:** one layer per step (presentation, business logic, data access); cross-cutting concerns (logging, auth, validation) in middleware or decorators; one bounded context per module.
|
|
118
|
+
|
|
108
119
|
## Extended References
|
|
109
120
|
|
|
110
121
|
| Reference | Content |
|
|
@@ -113,9 +124,9 @@ Resolution: {concern A} belongs in {layer/module}; {concern B} belongs in {layer
|
|
|
113
124
|
|
|
114
125
|
## Severity Guidelines
|
|
115
126
|
|
|
116
|
-
| Level |
|
|
127
|
+
| Level | An anti-pattern that |
|
|
117
128
|
|-------|----------|
|
|
118
|
-
| **CRITICAL** |
|
|
119
|
-
| **HIGH** |
|
|
120
|
-
| **MEDIUM** |
|
|
121
|
-
| **LOW** |
|
|
129
|
+
| **CRITICAL** | will cause correctness failures (data loss, security breach) |
|
|
130
|
+
| **HIGH** | will cause significant performance or maintainability problems |
|
|
131
|
+
| **MEDIUM** | will require architectural rework post-implementation |
|
|
132
|
+
| **LOW** | reduces code quality but is easily refactored |
|