@shadowclone/cli 0.0.17 → 0.0.18
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/dist/_.._/web/client/index.html +13 -12
- package/dist/shadowclone.js +524 -503
- package/dist/web/index-25g5ahxa.css +1 -0
- package/dist/web/index-s4mjwgw0.js +134 -0
- package/package.json +3 -9
- package/plugins/shadowclone/.claude-plugin/plugin.json +1 -1
- package/plugins/shadowclone/.codex-plugin/plugin.json +1 -1
- package/plugins/shadowclone/plugin.json +1 -1
- package/skills/choose-by-consequence/SKILL.md +66 -0
- package/skills/design-deep-modules/SKILL.md +32 -15
- package/skills/diagnose-before-editing/SKILL.md +38 -17
- package/skills/plan-with-review-page/SKILL.md +73 -0
- package/skills/plan-with-review-page/assets/review-page.html +160 -0
- package/skills/plan-with-review-page/scripts/validate-page.mjs +158 -0
- package/skills/research-primary-sources/SKILL.md +32 -14
- package/skills/resolve-conflicts-by-intent/SKILL.md +34 -15
- package/skills/scope-confirmed-changes/SKILL.md +44 -14
- package/skills/shadowclone-work/SKILL.md +5 -2
- package/skills/tests-that-catch-bugs/SKILL.md +73 -0
- package/skills/typescript-type-safety/SKILL.md +32 -16
- package/skills/verify-and-review/SKILL.md +40 -16
- package/skills/verify-review-findings/SKILL.md +59 -0
- package/skills/write-plain-english/SKILL.md +58 -0
- package/skills/write-plain-english/scripts/check-ste.mjs +200 -0
- package/dist/web/index-ntyv9x8a.css +0 -1
- package/dist/web/index-twq4tm3n.js +0 -135
- package/skills/prove-regression-tests/SKILL.md +0 -33
- package/skills/testing-first/SKILL.md +0 -35
- package/skills/testing-risk-based/SKILL.md +0 -34
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: choose-by-consequence
|
|
3
|
+
description: 'Use when code must decide what happens on an unusual or failing path. Also use it when the user says "just handle that case", "skip it if it fails", or "is this the best way?". Compares the easy route with each alternative by what it does to the user''s files, data, and output. Picks the option that fails loudly and keeps every count and exit code true. Never writes through a file that the change does not own. Not for checking finished work against the real command output (use `verify-and-review`).'
|
|
4
|
+
metadata:
|
|
5
|
+
shadowclone-category: change-control
|
|
6
|
+
shadowclone-section: workflow
|
|
7
|
+
shadowclone-applies-when: when a change adds a skip, block, fallback, default, or retry
|
|
8
|
+
---
|
|
9
|
+
# Choose by Consequence
|
|
10
|
+
|
|
11
|
+
## Use when
|
|
12
|
+
|
|
13
|
+
The code must decide what happens when something is unusual or fails. Examples: a file is a symbolic link, a tool is missing, or a step times out. Each of these is a behavior choice. A choice is risky when it touches owned or shared files, data, security, or public behavior such as output and exit codes.
|
|
14
|
+
|
|
15
|
+
## Gates
|
|
16
|
+
|
|
17
|
+
1. For each option, can you say what the user sees and what happens to their files and data? If not, check before you choose.
|
|
18
|
+
2. While you plan, did you give each risky choice to the user as an open decision? Put the option that fails loudly first.
|
|
19
|
+
3. While you execute, did you pick the option that fails loudly? A skip is loud only when the summary counts it, the final output names it, and the exit code shows it.
|
|
20
|
+
4. Do all counts, totals, and statuses in the output still match what happened?
|
|
21
|
+
5. Does any write go through a file that the change does not own, such as a symbolic link target? If so, block, and name both paths.
|
|
22
|
+
6. Does the handoff list each choice with the real output line that shows it?
|
|
23
|
+
|
|
24
|
+
## Process
|
|
25
|
+
|
|
26
|
+
### While you plan
|
|
27
|
+
|
|
28
|
+
1. List the options. Usual options: do the full job safely, block with an error, skip with a visible line, ask, or fall back.
|
|
29
|
+
2. For each option, write its hidden cost: who finds the problem, when, and what the recovery costs.
|
|
30
|
+
3. Put each risky choice to the user as one question with 2 to 4 options. Put the option that fails loudly first, and give one reason for it.
|
|
31
|
+
|
|
32
|
+
### While you execute
|
|
33
|
+
|
|
34
|
+
1. Do not ask again about a choice that the user made while planning. Follow it.
|
|
35
|
+
2. For a new choice, act without asking. Pick the first option that applies:
|
|
36
|
+
- Do the full job safely.
|
|
37
|
+
- Block with a message that names the file, the cause, and the next action.
|
|
38
|
+
- Skip with a counted line at the end of the output, and a nonzero exit code.
|
|
39
|
+
3. Never use a silent fallback. A fallback is acceptable only when the output names it and the result is still correct.
|
|
40
|
+
4. Write the message that the user reads before you write the code that prints it.
|
|
41
|
+
5. Run the real command, and read the summary line, the final lines, and the exit code.
|
|
42
|
+
6. Write down each choice and its output line for the handoff.
|
|
43
|
+
|
|
44
|
+
## Example
|
|
45
|
+
|
|
46
|
+
**Situation:** A setup command installs guidance for several agents. The instruction file of one agent is a symbolic link to a file that other tools share.
|
|
47
|
+
**Easy route:** Skip that agent, print the skip in the middle of the output, and report success.
|
|
48
|
+
**Hidden cost:** The summary still said "Installed for 2 agents". The user believed that the setup installed the agent, and nothing told them that the shared file was left alone.
|
|
49
|
+
**Best route:** Do not write through the link. Count the agent as not installed. End the output with a line that names the agent and the link target. Exit with a nonzero code.
|
|
50
|
+
**Evidence:** The last line is "Not installed for codex: its AGENTS.md is a symbolic link to a shared file". The summary says "Installed for 1 agent", and the exit code is 1.
|
|
51
|
+
|
|
52
|
+
## Guardrails
|
|
53
|
+
|
|
54
|
+
- Never write through a symbolic link, a shared file, or a file that another tool owns. Block, and name both paths.
|
|
55
|
+
- Do not catch an error only to continue. Catch it to add context, then report it.
|
|
56
|
+
- Do not change a default that people rely on without a line in the output and in the handoff.
|
|
57
|
+
- Make each error message specific: the path, the cause, and one next action. "Something went wrong" is not a message.
|
|
58
|
+
- Keep the decision in one place. A second copy of the same check drifts, and then the two paths disagree.
|
|
59
|
+
|
|
60
|
+
## Completion
|
|
61
|
+
|
|
62
|
+
- Each risky choice in one line: the option that you picked and the hidden cost that it avoids.
|
|
63
|
+
- The command that you ran and the output line that shows each skip, block, or fallback.
|
|
64
|
+
- The exit code of that command.
|
|
65
|
+
- The summary or count line, checked against what happened.
|
|
66
|
+
- The `file:line` of each new block, skip, or fallback.
|
|
@@ -1,34 +1,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: design-deep-modules
|
|
3
|
-
description:
|
|
3
|
+
description: 'Use when designing or reshaping a module, or when callers coordinate too many steps. Also use it when the user says "this is hard to test", "callers know too much", or "simplify this interface". Puts related state and decisions behind a small interface that states what the caller wants. Tests then use that interface instead of the internal steps. Not for typing values at a boundary (use `typescript-type-safety`).'
|
|
4
4
|
metadata:
|
|
5
5
|
shadowclone-category: architecture
|
|
6
6
|
shadowclone-section: engineering
|
|
7
|
-
shadowclone-applies-when: designing or reshaping a module or subsystem
|
|
7
|
+
shadowclone-applies-when: when designing or reshaping a module or subsystem
|
|
8
8
|
---
|
|
9
9
|
# Design Deep Modules
|
|
10
10
|
|
|
11
11
|
## Use when
|
|
12
12
|
|
|
13
|
-
Callers
|
|
13
|
+
Callers repeat the same sequence of calls, a layer only forwards arguments, or tests must reach through layers to see behavior. A deep module gives callers a lot of behavior through a small interface.
|
|
14
|
+
|
|
15
|
+
## Gates
|
|
16
|
+
|
|
17
|
+
1. Can each caller state its intent in one call, without knowing the internal order of steps?
|
|
18
|
+
2. Does one owner hold each piece of related state and each decision about it?
|
|
19
|
+
3. Does each new layer serve a real variation or isolation need, not only forwarding?
|
|
20
|
+
4. Do the tests use the public interface, and replace dependencies only at outside boundaries?
|
|
21
|
+
5. Are the invariants and the error behavior part of the interface contract?
|
|
14
22
|
|
|
15
23
|
## Process
|
|
16
24
|
|
|
17
|
-
1. Read the callers
|
|
18
|
-
2. Put
|
|
19
|
-
3.
|
|
20
|
-
4. Move internal
|
|
21
|
-
5.
|
|
22
|
-
6.
|
|
25
|
+
1. Read the callers. List the state, the order of steps, and the rules that they coordinate.
|
|
26
|
+
2. Put that state and those decisions under one owner. Name an operation for what the caller wants.
|
|
27
|
+
3. If the boundary is not clear, compare two placements by caller effort, error handling, and test effort.
|
|
28
|
+
4. Move the internal order of steps behind the interface.
|
|
29
|
+
5. Remove each layer that only forwards calls.
|
|
30
|
+
6. Write tests against the new interface, then remove the knowledge that callers no longer need.
|
|
23
31
|
|
|
24
|
-
##
|
|
32
|
+
## Example
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
**Situation:** Three commands each read a config file, merge it with defaults, check a policy, and take a lock before they write.
|
|
35
|
+
**Easy route:** Add a helper for each of the four steps, and keep the order in each command.
|
|
36
|
+
**Hidden cost:** Each command still owns the order. A new command can skip the policy check, and each test must repeat the four steps.
|
|
37
|
+
**Best route:** Add one operation that takes the intended change and does the four steps inside. The commands call that operation.
|
|
38
|
+
**Evidence:** Each command became one call. A test through the new operation failed without the policy check, and the step tests in each command were no longer needed.
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
## Guardrails
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
- Use an options object when callers would otherwise pass related values in a fixed order.
|
|
43
|
+
- Return the outcomes that callers need, and keep side effects inside their owner.
|
|
44
|
+
- Do not add an abstraction without a concrete variation or isolation need.
|
|
45
|
+
- A short interface still costs effort if callers must call it in a hidden order.
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
## Completion
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
- The new interface: its exported names and one call example.
|
|
50
|
+
- The caller steps that the module now owns.
|
|
51
|
+
- The test command for the new interface, with its result line.
|
|
@@ -1,37 +1,58 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: diagnose-before-editing
|
|
3
|
-
description:
|
|
3
|
+
description: 'Use when behavior fails, flakes, or slows down, and nobody knows the cause yet. Also use it when the user says "why does this fail?", "it worked yesterday", or "this got slow". Reproduces the failure, makes it smaller, and tests one cause at a time until one cause predicts the symptom. Then fixes the cause at its origin, not the symptom. Not for scoping a fix whose cause you already know (use `scope-confirmed-changes`).'
|
|
4
4
|
metadata:
|
|
5
5
|
shadowclone-category: debugging
|
|
6
6
|
shadowclone-section: workflow
|
|
7
|
-
shadowclone-applies-when:
|
|
7
|
+
shadowclone-applies-when: when behavior is wrong or slow and the cause is not known
|
|
8
8
|
---
|
|
9
9
|
# Diagnose Before Editing
|
|
10
10
|
|
|
11
11
|
## Use when
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Something fails, throws, flakes, or slows down, and nobody has shown the cause yet. A change before the cause is known can hide the bug instead of fixing it.
|
|
14
|
+
|
|
15
|
+
## Gates
|
|
16
|
+
|
|
17
|
+
1. Can you make the failure happen on demand with one command or test?
|
|
18
|
+
2. Did you write down the input, the actual result, and the expected result?
|
|
19
|
+
3. For each suspected cause, did you name an observation that would prove it wrong?
|
|
20
|
+
4. Did you change one variable in each probe?
|
|
21
|
+
5. Does the cause that you found predict the symptom, and does a controlled change remove it?
|
|
22
|
+
6. Did you remove each temporary log line and probe before the handoff?
|
|
14
23
|
|
|
15
24
|
## Process
|
|
16
25
|
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
3. Trace
|
|
20
|
-
4.
|
|
21
|
-
5.
|
|
22
|
-
6.
|
|
23
|
-
7. Run the
|
|
26
|
+
1. Find the fastest command that reaches the failing path. Run it until you see the failure.
|
|
27
|
+
2. Remove inputs and steps while the failure stays. Stop when no further removal keeps it.
|
|
28
|
+
3. Trace the state from the input. Find the first place where a correct value becomes wrong.
|
|
29
|
+
4. List the possible causes. For each one, write the observation that would rule it out.
|
|
30
|
+
5. Test one cause at a time with a debugger, a log line, a timer, or a controlled input.
|
|
31
|
+
6. When one cause predicts the symptom, fix it at its origin.
|
|
32
|
+
7. Run the small and the original reproductions. Keep one as a regression test where practical.
|
|
24
33
|
|
|
25
|
-
For flaky
|
|
34
|
+
For a flaky failure, measure how often it happens. Control the clock, the random values, the concurrency, and the outside responses. For a slow path, measure a baseline first, and measure the suspected cost before you change it.
|
|
26
35
|
|
|
27
|
-
##
|
|
36
|
+
## Example
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
**Situation:** A test fails one run in ten on a busy machine. A teammate suggests a longer timeout.
|
|
39
|
+
**Easy route:** Raise the timeout from 5 to 30 seconds. The test passes.
|
|
40
|
+
**Hidden cost:** The cause stays. The real command can still hang for users, and the slow test now hides other delays.
|
|
41
|
+
**Best route:** Measure the failure rate, then control the clock in the test. The failure now shows on each run, when two writes wait for one lock.
|
|
42
|
+
**Evidence:** With the clock fixed, the test failed 10 of 10 runs before the fix and passed 10 of 10 after it. The timeout stayed at 5 seconds.
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
## Guardrails
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
- Do not turn a theory that you did not test into a production change.
|
|
47
|
+
- Do not add a fallback that only hides invalid state.
|
|
48
|
+
- Remove secrets and private paths from traces before you report them.
|
|
49
|
+
- Do not fix other defects in this change. List them in the handoff.
|
|
50
|
+
- If you cannot reproduce the failure, report what you tried and what evidence is missing.
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
## Completion
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
- The reproduction command, with its failing line before the fix.
|
|
55
|
+
- The same command after the fix, with its passing line.
|
|
56
|
+
- The cause in one sentence, with the `file:line` where the state first goes wrong.
|
|
57
|
+
- Each cause that you ruled out, with the observation that ruled it out.
|
|
58
|
+
- Anything that you could not reproduce.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-with-review-page
|
|
3
|
+
description: 'Use before a change that touches many files, needs many steps, or has open design choices. Also use it when the user says "plan this first", "what is your approach?", or "do not code yet". Explores the code read-only, asks one round of decisions with the recommended option first, and writes one plan. Builds a self-contained HTML review page from a template, and checks the page with a validator. Not for checking finished work (use `verify-and-review`).'
|
|
4
|
+
metadata:
|
|
5
|
+
shadowclone-category: workflow
|
|
6
|
+
shadowclone-section: workflow
|
|
7
|
+
shadowclone-applies-when: before a change that has many steps or open design choices
|
|
8
|
+
---
|
|
9
|
+
# Plan With a Review Page
|
|
10
|
+
|
|
11
|
+
## Use when
|
|
12
|
+
|
|
13
|
+
A change touches many files, needs many steps, or has a design choice that belongs to the user. An approved plan stops work in the wrong direction. A small and clear change does not need this skill.
|
|
14
|
+
|
|
15
|
+
## Gates
|
|
16
|
+
|
|
17
|
+
1. Are you read-only until the user approves? Before approval, write only the plan file and the review page.
|
|
18
|
+
2. Did you read the code, the documents, and the history before you asked? Never ask what the code can answer.
|
|
19
|
+
3. Did you ask one round of at most 4 questions, each with 2 to 4 options and the recommended option first?
|
|
20
|
+
4. Is each risky behavior choice in that round, such as a skip, a block, a fallback, or a default?
|
|
21
|
+
5. Is there one plan file for this conversation, with each skipped step still visible and its reason?
|
|
22
|
+
6. Does the review page pass `scripts/validate-page.mjs` with 0 errors?
|
|
23
|
+
|
|
24
|
+
## Process
|
|
25
|
+
|
|
26
|
+
### Explore
|
|
27
|
+
|
|
28
|
+
1. Read the code that the request touches, and the instructions of the repository for that code.
|
|
29
|
+
2. Find the functions and patterns that the change can use again.
|
|
30
|
+
|
|
31
|
+
### Ask
|
|
32
|
+
|
|
33
|
+
1. List each open decision about scope, behavior, edge cases, data, release, and tests.
|
|
34
|
+
2. Ask the decisions in one round of at most 4 questions. Put the recommended option first, with one reason.
|
|
35
|
+
3. If an answer is not clear, ask again. Do not guess.
|
|
36
|
+
|
|
37
|
+
### Write
|
|
38
|
+
|
|
39
|
+
1. Put the plan at `/tmp/agent-artifacts/<repo>/<date>-<slug>/plan.md`. If the sandbox blocks `/tmp`, use the temporary folder of the agent.
|
|
40
|
+
2. Write the context, the decisions with their source, the approach, the files, the steps, and the verification commands.
|
|
41
|
+
3. Copy `assets/review-page.html` to the same folder as `index.html`. Edit only the JSON block with the id `plan`, and replace each value that starts with `TEMPLATE:`.
|
|
42
|
+
4. Add Mermaid source in `diagram` when the change has more than two parts. The page shows the source as text, and draws it when the pinned Mermaid file loads.
|
|
43
|
+
5. Run `node scripts/validate-page.mjs <page>`. If Node is not available, run the script with `bun`. Fix each error.
|
|
44
|
+
6. Give the user both paths, the approach, and the main risk. Wait for approval.
|
|
45
|
+
|
|
46
|
+
### Execute
|
|
47
|
+
|
|
48
|
+
1. Do the steps in order. After each step, update its status in the page and in the plan.
|
|
49
|
+
2. If a fact changes a decision, stop. Update the plan, and ask again.
|
|
50
|
+
3. Keep a step that you skip in the list, with the status `skipped` and the reason in its note.
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
**Situation:** The user asks for a sync command that updates installed skills. Some installed copies contain edits that the user made.
|
|
55
|
+
**Easy route:** Plan to replace each copy with the new version, and do not ask.
|
|
56
|
+
**Hidden cost:** The plan hides a choice that deletes the work of the user. The user finds the loss after the release.
|
|
57
|
+
**Best route:** Put the choice in the question round: keep each edited copy and list it (recommended), or replace it and save a backup. Record the answer in the plan.
|
|
58
|
+
**Evidence:** The decision table of the page shows "Edited copies: keep and list" with the source "user". The validator printed "validate-page: 0 errors in 1 file".
|
|
59
|
+
|
|
60
|
+
## Guardrails
|
|
61
|
+
|
|
62
|
+
- Approval covers only the steps in the plan. Ask before a step that the plan does not list.
|
|
63
|
+
- Never commit the plan or the review page to a repository.
|
|
64
|
+
- Keep the review page self-contained. Do not add scripts, styles, or fonts from the network.
|
|
65
|
+
- Do not open a decision again unless a new fact changes it.
|
|
66
|
+
- Keep the plan short enough to scan. Write the recommended approach, not each option that you considered.
|
|
67
|
+
|
|
68
|
+
## Completion
|
|
69
|
+
|
|
70
|
+
- The plan path and the review page path.
|
|
71
|
+
- The validator command and its result line.
|
|
72
|
+
- The question round, with the recommended option first in each question.
|
|
73
|
+
- Each step with its status, and each skipped step with its reason.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Plan review</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--page: #fbfaf7; --text: #1d1d1b; --muted: #66645f; --card: #ffffff; --line: #e2dfd8;
|
|
10
|
+
--done: #2f7d4f; --doing: #a8701c; --todo: #77756f; --blocked: #b5443b; --skipped: #6d5fa8;
|
|
11
|
+
}
|
|
12
|
+
@media (prefers-color-scheme: dark) {
|
|
13
|
+
:root {
|
|
14
|
+
--page: #161615; --text: #ecebe7; --muted: #a09e98; --card: #1f1f1d; --line: #34332f;
|
|
15
|
+
--done: #5fbf83; --doing: #e0a84a; --todo: #8f8c85; --blocked: #e57b72; --skipped: #a99be8;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
body { margin: 0; background: var(--page); color: var(--text); font: 15px/1.55 system-ui, sans-serif; }
|
|
20
|
+
main { max-width: 960px; margin: 0 auto; padding: 32px 16px 64px; }
|
|
21
|
+
h1 { font-size: 24px; margin: 0 0 6px; }
|
|
22
|
+
h2 { font-size: 17px; margin: 28px 0 10px; }
|
|
23
|
+
.meta { color: var(--muted); margin: 0 0 18px; }
|
|
24
|
+
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; }
|
|
25
|
+
.counts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
|
|
26
|
+
.counts b { display: block; font-size: 22px; }
|
|
27
|
+
.counts span { color: var(--muted); font-size: 13px; }
|
|
28
|
+
ol.steps { list-style: none; margin: 0; padding: 0; }
|
|
29
|
+
ol.steps li { display: grid; grid-template-columns: 84px 1fr; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line); }
|
|
30
|
+
ol.steps li:last-child { border-bottom: 0; }
|
|
31
|
+
.status { font-size: 12px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
32
|
+
.done { color: var(--done); } .doing { color: var(--doing); } .todo { color: var(--todo); }
|
|
33
|
+
.blocked { color: var(--blocked); } .skipped { color: var(--skipped); }
|
|
34
|
+
.note { display: block; color: var(--muted); font-size: 13px; }
|
|
35
|
+
.scroll { overflow-x: auto; }
|
|
36
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
37
|
+
th, td { text-align: left; vertical-align: top; padding: 7px 8px; border-bottom: 1px solid var(--line); }
|
|
38
|
+
th { color: var(--muted); font-weight: 600; }
|
|
39
|
+
code, pre { font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
40
|
+
pre { white-space: pre-wrap; margin: 0; }
|
|
41
|
+
#diagram img { display: block; max-width: 100%; height: auto; margin-bottom: 10px; }
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<main>
|
|
46
|
+
<h1 id="title"></h1>
|
|
47
|
+
<p id="goal"></p>
|
|
48
|
+
<p class="meta">Plan file: <code id="plan-path"></code>. Updated: <span id="updated"></span>.</p>
|
|
49
|
+
<div class="counts">
|
|
50
|
+
<div class="card"><b id="count-done">0</b><span>steps done</span></div>
|
|
51
|
+
<div class="card"><b id="count-left">0</b><span>steps left</span></div>
|
|
52
|
+
<div class="card"><b id="count-open">0</b><span>open questions</span></div>
|
|
53
|
+
</div>
|
|
54
|
+
<section id="diagram-section" hidden>
|
|
55
|
+
<h2>Diagram</h2>
|
|
56
|
+
<div class="card scroll">
|
|
57
|
+
<div id="diagram"></div>
|
|
58
|
+
<p class="note" id="diagram-note">The diagram shows as source text until the pinned Mermaid file loads.</p>
|
|
59
|
+
<pre id="diagram-source"></pre>
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
62
|
+
<h2>Steps</h2>
|
|
63
|
+
<div class="card"><ol class="steps" id="steps"></ol></div>
|
|
64
|
+
<h2>Decisions</h2>
|
|
65
|
+
<div class="card scroll">
|
|
66
|
+
<table>
|
|
67
|
+
<thead><tr><th>Question</th><th>Answer</th><th>Source</th></tr></thead>
|
|
68
|
+
<tbody id="decisions"></tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
<h2>Open questions</h2>
|
|
72
|
+
<div class="card"><ul id="open"></ul></div>
|
|
73
|
+
</main>
|
|
74
|
+
<script type="application/json" id="plan">
|
|
75
|
+
{
|
|
76
|
+
"title": "TEMPLATE: A short name for the change",
|
|
77
|
+
"goal": "TEMPLATE: One sentence about the result that the user wants.",
|
|
78
|
+
"planPath": "TEMPLATE: The absolute path of the plan file",
|
|
79
|
+
"updated": "TEMPLATE: The date and time of this update",
|
|
80
|
+
"steps": [
|
|
81
|
+
{ "status": "done", "text": "TEMPLATE: Read the code that the request touches.", "note": "" },
|
|
82
|
+
{ "status": "doing", "text": "TEMPLATE: Ask one round of decisions.", "note": "" },
|
|
83
|
+
{ "status": "todo", "text": "TEMPLATE: Write the plan and this page.", "note": "" },
|
|
84
|
+
{ "status": "skipped", "text": "TEMPLATE: A step that you skipped.", "note": "TEMPLATE: The reason for the skip." }
|
|
85
|
+
],
|
|
86
|
+
"decisions": [
|
|
87
|
+
{ "question": "TEMPLATE: A decision that the user made.", "answer": "TEMPLATE: The answer.", "source": "user" }
|
|
88
|
+
],
|
|
89
|
+
"open": ["TEMPLATE: A question that is still open."],
|
|
90
|
+
"diagram": "flowchart LR\n Request --> Plan --> Approval --> Steps"
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
<script>
|
|
94
|
+
const plan = JSON.parse(document.getElementById("plan").textContent);
|
|
95
|
+
const element = (name, text, className) => {
|
|
96
|
+
const node = document.createElement(name);
|
|
97
|
+
node.textContent = text;
|
|
98
|
+
if (className) node.className = className;
|
|
99
|
+
return node;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
document.title = plan.title;
|
|
103
|
+
document.getElementById("title").textContent = plan.title;
|
|
104
|
+
document.getElementById("goal").textContent = plan.goal;
|
|
105
|
+
document.getElementById("plan-path").textContent = plan.planPath;
|
|
106
|
+
document.getElementById("updated").textContent = plan.updated;
|
|
107
|
+
document.getElementById("count-done").textContent = plan.steps.filter((step) => step.status === "done").length;
|
|
108
|
+
document.getElementById("count-left").textContent = plan.steps.filter((step) => step.status !== "done" && step.status !== "skipped").length;
|
|
109
|
+
document.getElementById("count-open").textContent = plan.open.length;
|
|
110
|
+
|
|
111
|
+
for (const step of plan.steps) {
|
|
112
|
+
const item = document.createElement("li");
|
|
113
|
+
const body = element("div", step.text);
|
|
114
|
+
if (step.note) body.append(element("span", step.note, "note"));
|
|
115
|
+
item.append(element("span", step.status, `status ${step.status}`), body);
|
|
116
|
+
document.getElementById("steps").append(item);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const decision of plan.decisions) {
|
|
120
|
+
const row = document.createElement("tr");
|
|
121
|
+
row.append(element("td", decision.question), element("td", decision.answer), element("td", decision.source));
|
|
122
|
+
document.getElementById("decisions").append(row);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const question of plan.open) {
|
|
126
|
+
document.getElementById("open").append(element("li", question));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (plan.diagram) {
|
|
130
|
+
document.getElementById("diagram-section").hidden = false;
|
|
131
|
+
document.getElementById("diagram-source").textContent = plan.diagram;
|
|
132
|
+
|
|
133
|
+
const script = document.createElement("script");
|
|
134
|
+
script.src = "https://cdn.jsdelivr.net/npm/mermaid@12.1.0/dist/mermaid.min.js";
|
|
135
|
+
script.integrity = "sha384-EbBpjO7rlR6eqZEcG7GaPpyk9H9WrMyPWX4d3KvPYltgt8Z8l0z6R56B1qP40pR4";
|
|
136
|
+
script.crossOrigin = "anonymous";
|
|
137
|
+
script.onload = async () => {
|
|
138
|
+
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
139
|
+
window.mermaid.initialize({
|
|
140
|
+
startOnLoad: false,
|
|
141
|
+
securityLevel: "strict",
|
|
142
|
+
htmlLabels: false,
|
|
143
|
+
flowchart: { htmlLabels: false },
|
|
144
|
+
theme: dark ? "dark" : "default",
|
|
145
|
+
});
|
|
146
|
+
const { svg } = await window.mermaid.render("plan-diagram", plan.diagram);
|
|
147
|
+
const image = document.createElement("img");
|
|
148
|
+
image.alt = "Plan diagram";
|
|
149
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
150
|
+
document.getElementById("diagram").replaceChildren(image);
|
|
151
|
+
document.getElementById("diagram-note").textContent = "Diagram source:";
|
|
152
|
+
};
|
|
153
|
+
script.onerror = () => {
|
|
154
|
+
document.getElementById("diagram-note").textContent = "Mermaid did not load, so the diagram shows as source text.";
|
|
155
|
+
};
|
|
156
|
+
document.head.append(script);
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const statuses = ["todo", "doing", "done", "blocked", "skipped"];
|
|
5
|
+
const dashes = new RegExp(`[${String.fromCharCode(0x2013, 0x2014)}]`);
|
|
6
|
+
const mermaidUrl = "https://cdn.jsdelivr.net/npm/mermaid@12.1.0/dist/mermaid.min.js";
|
|
7
|
+
const mermaidIntegrity = "sha384-EbBpjO7rlR6eqZEcG7GaPpyk9H9WrMyPWX4d3KvPYltgt8Z8l0z6R56B1qP40pR4";
|
|
8
|
+
|
|
9
|
+
function readPlan(html) {
|
|
10
|
+
const blocks = [
|
|
11
|
+
...html.matchAll(/<script type="application\/json" id="plan">([\s\S]*?)<\/script>/g),
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
if (blocks.length !== 1) {
|
|
15
|
+
return {
|
|
16
|
+
error: `found ${blocks.length} plan blocks, keep exactly one JSON block with the id plan`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
return { plan: JSON.parse(blocks[0][1]) };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return { error: `the plan block is not valid JSON: ${error.message}` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textValues(value, at) {
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
return [[at, value]];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.flatMap((entry, index) => textValues(entry, `${at}[${index}]`));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (value !== null && typeof value === "object") {
|
|
37
|
+
return Object.entries(value).flatMap(([key, entry]) =>
|
|
38
|
+
textValues(entry, at ? `${at}.${key}` : key),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const filled = (value) => typeof value === "string" && value.trim().length > 0;
|
|
46
|
+
|
|
47
|
+
function validate(html) {
|
|
48
|
+
const { plan, error } = readPlan(html);
|
|
49
|
+
|
|
50
|
+
if (error) {
|
|
51
|
+
return [error];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const errors = [];
|
|
55
|
+
const steps = Array.isArray(plan.steps) ? plan.steps : [];
|
|
56
|
+
const decisions = Array.isArray(plan.decisions) ? plan.decisions : [];
|
|
57
|
+
|
|
58
|
+
for (const key of ["title", "goal", "planPath", "updated"]) {
|
|
59
|
+
if (!filled(plan[key])) {
|
|
60
|
+
errors.push(`${key} must be filled in`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (steps.length === 0) {
|
|
65
|
+
errors.push("steps must list at least one step");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [index, step] of steps.entries()) {
|
|
69
|
+
if (!statuses.includes(step?.status)) {
|
|
70
|
+
errors.push(`steps[${index}].status must be one of ${statuses.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!filled(step?.text)) {
|
|
74
|
+
errors.push(`steps[${index}].text must be filled in`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ((step?.status === "skipped" || step?.status === "blocked") && !filled(step.note)) {
|
|
78
|
+
errors.push(`steps[${index}] is ${step.status}, so its note must give the reason`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(plan.decisions)) {
|
|
83
|
+
errors.push("decisions must be a list");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const [index, decision] of decisions.entries()) {
|
|
87
|
+
for (const key of ["question", "answer", "source"]) {
|
|
88
|
+
if (!filled(decision?.[key])) {
|
|
89
|
+
errors.push(`decisions[${index}].${key} must be filled in`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!Array.isArray(plan.open) || plan.open.some((entry) => typeof entry !== "string")) {
|
|
95
|
+
errors.push("open must be a list of questions");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof plan.diagram !== "string") {
|
|
99
|
+
errors.push("diagram must be Mermaid source, or empty when there is no diagram");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const [at, text] of textValues(plan, "")) {
|
|
103
|
+
if (text.startsWith("TEMPLATE:")) {
|
|
104
|
+
errors.push(`${at} still has template text, replace it`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (dashes.test(text)) {
|
|
108
|
+
errors.push(`${at} has an em or en dash, use a comma, a period, or parentheses`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const [url] of html.matchAll(/https?:\/\/[^\s"'<>)]+/g)) {
|
|
113
|
+
if (url !== mermaidUrl) {
|
|
114
|
+
errors.push(`${url} loads from the network, keep the page self-contained`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (html.includes(mermaidUrl) && !html.includes(mermaidIntegrity)) {
|
|
119
|
+
errors.push("the Mermaid script must keep its pinned integrity hash");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return errors;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(files) {
|
|
126
|
+
if (files.length === 0) {
|
|
127
|
+
console.error("Usage: validate-page.mjs <page.html>...");
|
|
128
|
+
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let total = 0;
|
|
133
|
+
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
let html;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
html = await readFile(file, "utf8");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(`${file}: cannot read the file (${error.code ?? error.message})`);
|
|
141
|
+
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const message of validate(html)) {
|
|
146
|
+
console.log(`${file}: error ${message}`);
|
|
147
|
+
total += 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
152
|
+
|
|
153
|
+
console.log(`validate-page: ${plural(total, "error")} in ${plural(files.length, "file")}`);
|
|
154
|
+
|
|
155
|
+
return total > 0 ? 1 : 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.exitCode = await main(process.argv.slice(2));
|