entropy-machines 0.1.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/LICENSE +93 -0
- package/README.md +68 -0
- package/agents/isolated-worker.md +128 -0
- package/agents/verifier.md +158 -0
- package/bin/dispatch +700 -0
- package/bin/doclint +460 -0
- package/bin/drain +507 -0
- package/bin/drain-pick.py +168 -0
- package/bin/drain-prompt.md +67 -0
- package/bin/drain-run.sh +342 -0
- package/bin/entropy-machines-init +285 -0
- package/bin/handoff +1151 -0
- package/bin/init +232 -0
- package/bin/post-fold-audit +377 -0
- package/bin/serve +724 -0
- package/bin/status +208 -0
- package/bin/tracker +153 -0
- package/docs/AGENT-QUICKSTART.md +86 -0
- package/docs/CONFIG.md +68 -0
- package/docs/NPM.md +91 -0
- package/docs/SERVE.md +74 -0
- package/docs/TRACKER-ADAPTER.md +66 -0
- package/doctrine/HANDOFF-PROMPT.md +63 -0
- package/doctrine/README.md +62 -0
- package/doctrine/ROLES.md +27 -0
- package/doctrine/WORKFLOW.md +87 -0
- package/hooks/commit-msg +24 -0
- package/hooks/post-checkout +354 -0
- package/hooks/pre-commit +33 -0
- package/lib/PRD-001-orientation.html +1180 -0
- package/lib/REPORT-TEMPLATE.html +413 -0
- package/lib/changelog-collate.mjs +328 -0
- package/lib/changelog-guard.sh +157 -0
- package/lib/changelog-new.mjs +70 -0
- package/lib/config.mjs +283 -0
- package/lib/config.py +317 -0
- package/lib/doc-template.html +807 -0
- package/lib/entropy-drain.plist.in +59 -0
- package/lib/entropy-drain.service.in +53 -0
- package/lib/entropy-drain.timer.in +36 -0
- package/lib/fail-first.mjs +901 -0
- package/lib/handoff-guard.sh +623 -0
- package/lib/install-hooks.sh +169 -0
- package/lib/notes.py +675 -0
- package/lib/preflight-tree.mjs +82 -0
- package/lib/roots.sh +212 -0
- package/lib/themes/daylight.css +84 -0
- package/lib/themes/high-contrast.css +36 -0
- package/lib/tracker-file +333 -0
- package/lib/tracker-view.py +784 -0
- package/package.json +38 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Refuse to run a suite from a tree that cannot resolve its own dependencies.
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* THE FAILURE THIS EXISTS FOR. A git worktree has no `node_modules`. Node then
|
|
7
|
+
* walks UP from the worktree looking for one — and on 2026-08-17 a dispatched
|
|
8
|
+
* agent's `npm run e2e` resolved `playwright` out of a SIBLING agent's worktree
|
|
9
|
+
* and ran with that worktree as cwd. It tested someone else's code and reported
|
|
10
|
+
* the result as its own. Green, confident, wrong. The agent caught it only by
|
|
11
|
+
* watching the output; nothing in the tooling would have told it, or the
|
|
12
|
+
* session landing its work.
|
|
13
|
+
*
|
|
14
|
+
* That is worse than the already-known symptom (an empty ENOENT from a missing
|
|
15
|
+
* .bin/tsc), because a spurious red gets investigated and a spurious green does
|
|
16
|
+
* not. A worktree isolates the filesystem; it does not isolate node resolution.
|
|
17
|
+
*
|
|
18
|
+
* THE CHECK. Every package a suite spawns must be present under THIS tree's own
|
|
19
|
+
* node_modules. A symlink to the main checkout satisfies that and is the fix we
|
|
20
|
+
* recommend — what must never happen is no entry at all, because that is the
|
|
21
|
+
* only case where resolution leaves the tree.
|
|
22
|
+
*
|
|
23
|
+
* Silent and fast on the happy path: one `git rev-parse` and four `stat`s.
|
|
24
|
+
*/
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
// Anything a suite spawns or imports from the tree root. Keep this list to
|
|
30
|
+
// things whose absence produces a WRONG ANSWER rather than a clean crash.
|
|
31
|
+
const NEEDED = ['playwright', 'vitest', 'typescript', 'esbuild'];
|
|
32
|
+
|
|
33
|
+
function git(...args) {
|
|
34
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let root;
|
|
38
|
+
try {
|
|
39
|
+
root = git('rev-parse', '--show-toplevel');
|
|
40
|
+
} catch {
|
|
41
|
+
process.exit(0); // not a git checkout — nothing to be confused about
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nodeModules = join(root, 'node_modules');
|
|
45
|
+
const missing = existsSync(nodeModules)
|
|
46
|
+
? NEEDED.filter((p) => !existsSync(join(nodeModules, p)))
|
|
47
|
+
: NEEDED;
|
|
48
|
+
|
|
49
|
+
if (missing.length === 0) process.exit(0);
|
|
50
|
+
|
|
51
|
+
// Where a worktree should point. --git-common-dir is the MAIN checkout's .git
|
|
52
|
+
// even when called from a worktree, so its parent is the tree that has the
|
|
53
|
+
// real node_modules.
|
|
54
|
+
let mainTree = null;
|
|
55
|
+
try {
|
|
56
|
+
const commonDir = git('rev-parse', '--path-format=absolute', '--git-common-dir');
|
|
57
|
+
const candidate = dirname(commonDir);
|
|
58
|
+
if (candidate !== root && existsSync(join(candidate, 'node_modules'))) mainTree = candidate;
|
|
59
|
+
} catch {
|
|
60
|
+
/* older git without --path-format; the message still works without it */
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = [
|
|
64
|
+
'',
|
|
65
|
+
'preflight: this tree cannot resolve its own dependencies — refusing to run.',
|
|
66
|
+
'',
|
|
67
|
+
` tree: ${root}`,
|
|
68
|
+
` missing: ${missing.join(', ')}${existsSync(nodeModules) ? ' (node_modules exists but is incomplete)' : ' (no node_modules at all)'}`,
|
|
69
|
+
'',
|
|
70
|
+
' Node would search UPWARD from here and can land in another worktree, which',
|
|
71
|
+
' means testing someone else\'s code and reporting it as yours. That has',
|
|
72
|
+
' happened once already.',
|
|
73
|
+
'',
|
|
74
|
+
];
|
|
75
|
+
lines.push(
|
|
76
|
+
mainTree
|
|
77
|
+
? ` Fix: ln -s ${JSON.stringify(join(mainTree, 'node_modules'))} ${JSON.stringify(nodeModules)}`
|
|
78
|
+
: ' Fix: symlink or install node_modules in this tree (npm ci).'
|
|
79
|
+
);
|
|
80
|
+
lines.push('');
|
|
81
|
+
console.error(lines.join('\n'));
|
|
82
|
+
process.exit(1);
|
package/lib/roots.sh
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# roots.sh — resolve the ONE root every entry point needs. SOURCE it, do not
|
|
2
|
+
# run it.
|
|
3
|
+
#
|
|
4
|
+
# . "$(dirname "$0")/../lib/roots.sh"
|
|
5
|
+
# ENTROPY_MACHINES_HOME=$(entropy_machines_home "$0")
|
|
6
|
+
# entropy_machines_require_root <tool-name> # sets+exports ENTROPY_MACHINES_ROOT,
|
|
7
|
+
# # refusing a nested clone first
|
|
8
|
+
#
|
|
9
|
+
# THERE IS ONE ROOT: THE GIT REPOSITORY.
|
|
10
|
+
#
|
|
11
|
+
# The harness is VENDORED AS PLAIN TRACKED FILES inside the project it works
|
|
12
|
+
# on and committed alongside the project's code — exactly as a `scripts/`
|
|
13
|
+
# directory is. There is no nested `.git`, so `git` from anywhere inside the
|
|
14
|
+
# project always answers with the project. config.json, .entropy-machines/, the
|
|
15
|
+
# harness's own bin/ and lib/ are all inside one repository.
|
|
16
|
+
#
|
|
17
|
+
# ENTROPY_MACHINES_ROOT the repository's MAIN checkout. .entropy-machines/
|
|
18
|
+
# lives here. This is THE root.
|
|
19
|
+
# ENTROPY_MACHINES_HOME the directory the harness's own files sit in (bin/, lib/,
|
|
20
|
+
# hooks/, doctrine/, config.json). NOT a root and not a repo —
|
|
21
|
+
# just a path, derived from the calling script's own $0, so a
|
|
22
|
+
# script can find its sibling lib/ files whether the harness is
|
|
23
|
+
# vendored at the repo root or in a subdirectory. It is always
|
|
24
|
+
# inside ENTROPY_MACHINES_ROOT, or equal to it when the harness
|
|
25
|
+
# is its own project.
|
|
26
|
+
#
|
|
27
|
+
# WHAT USED TO BE HERE AND WHY IT IS GONE. This file used to resolve TWO
|
|
28
|
+
# roots, because the install layout was a NESTED GIT CLONE (`cd my-project &&
|
|
29
|
+
# git clone <harness> entropy-machines`). A nested repo shadows its parent for
|
|
30
|
+
# every git query, so "which repo am I in" had two answers depending on cwd.
|
|
31
|
+
# That one fact produced all of: a second root, drift detection
|
|
32
|
+
# (`entropy_harness_drift`), drift refusals in bin/init, gitignoring the
|
|
33
|
+
# harness directory, and a worktree-resolution stanza to tell the two apart.
|
|
34
|
+
# The vendored layout deletes the premise, so all of it is deleted. If you
|
|
35
|
+
# find yourself re-adding a "which repo is this really" check, the layout has
|
|
36
|
+
# regressed, not the code.
|
|
37
|
+
#
|
|
38
|
+
# THE NESTED CLONE IS REFUSED, NOT RESOLVED. Deleting the two-root machinery
|
|
39
|
+
# removed the harness's ability to COPE with a nested clone; it did not stop
|
|
40
|
+
# anyone creating one (`cd my-project && git clone <harness> tools/` is the
|
|
41
|
+
# instinct, and is what the README said to do until recently). A nested repo
|
|
42
|
+
# shadows its parent for every git query, so every command run from inside it
|
|
43
|
+
# resolves to the HARNESS as the project: the tracker writes .entropy-machines/ into
|
|
44
|
+
# the harness, dispatch and handoff operate on the wrong repo, and nothing
|
|
45
|
+
# says a word. Three agents in a sibling project were lost to exactly that on
|
|
46
|
+
# 2026-08-17. entropy_machines_refuse_nested_clone() says so and exits. It is a
|
|
47
|
+
# REFUSAL, not a resolution scheme — if it ever grows a way to keep working
|
|
48
|
+
# in that layout, the expensive mistake has been re-made.
|
|
49
|
+
#
|
|
50
|
+
# ENTROPY_MACHINES_ROOT USES --git-common-dir, NOT --show-toplevel. THIS IS
|
|
51
|
+
# LOAD-BEARING. From inside a LINKED WORKTREE --show-toplevel prints the
|
|
52
|
+
# WORKTREE's own path. Tracker state lives under .entropy-machines/, which is
|
|
53
|
+
# gitignored and therefore absent from every worktree — resolving it that way
|
|
54
|
+
# hands a dispatched worker an EMPTY tracker instead of the project's, which
|
|
55
|
+
# reads as "no issues" rather than as an error. --git-common-dir names the
|
|
56
|
+
# main checkout's .git from anywhere, including from inside a worktree, which
|
|
57
|
+
# is the question actually being asked. A previous bug here sent state to the
|
|
58
|
+
# wrong place; do not "simplify" this to --show-toplevel.
|
|
59
|
+
|
|
60
|
+
# entropy_machines_home <path-to-calling-script> — echoes the harness directory.
|
|
61
|
+
entropy_machines_home() {
|
|
62
|
+
CDPATH= cd -- "$(dirname -- "$1")/.." 2>/dev/null && pwd -P
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# entropy_machines_git_root [dir] — echoes the main checkout of the git repository
|
|
66
|
+
# containing <dir> (default: the current directory), or nothing (exit 1).
|
|
67
|
+
#
|
|
68
|
+
# GIT ONLY: no config.json walk-up. entropy_machines_root() adds that fallback; the
|
|
69
|
+
# nested-clone check must not have it, because it asks specifically "WHICH GIT
|
|
70
|
+
# REPOSITORY owns this directory" and a walk-up would answer a different
|
|
71
|
+
# question with a path that looks like an answer.
|
|
72
|
+
entropy_machines_git_root() {
|
|
73
|
+
_egr_dir=${1:-}
|
|
74
|
+
if [ -n "$_egr_dir" ]; then
|
|
75
|
+
_egr_common=$(CDPATH= cd -- "$_egr_dir" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null) || _egr_common=""
|
|
76
|
+
else
|
|
77
|
+
# No argument: run git in the AMBIENT cwd rather than cd-ing to "$PWD",
|
|
78
|
+
# so entropy_machines_root() below keeps the exact behaviour it had before this
|
|
79
|
+
# helper was extracted out of it.
|
|
80
|
+
_egr_dir=$PWD
|
|
81
|
+
_egr_common=$(git rev-parse --git-common-dir 2>/dev/null) || _egr_common=""
|
|
82
|
+
fi
|
|
83
|
+
if [ -n "$_egr_common" ]; then
|
|
84
|
+
# --git-common-dir may print a path relative to the directory git ran in.
|
|
85
|
+
case "$_egr_common" in
|
|
86
|
+
/*) ;;
|
|
87
|
+
*) _egr_common="$_egr_dir/$_egr_common" ;;
|
|
88
|
+
esac
|
|
89
|
+
if _egr_top=$(CDPATH= cd -- "$_egr_common/.." 2>/dev/null && pwd -P); then
|
|
90
|
+
printf '%s\n' "$_egr_top"
|
|
91
|
+
unset _egr_dir _egr_common _egr_top
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
unset _egr_dir _egr_common
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# entropy_machines_root — echoes the repository's main checkout, or nothing (exit 1).
|
|
100
|
+
#
|
|
101
|
+
# No environment override. Nothing in this harness needs one any more: every
|
|
102
|
+
# caller either runs with a cwd inside the project (git answers), or is a git
|
|
103
|
+
# hook (git guarantees the cwd) or a scheduled unit that cd's into the harness
|
|
104
|
+
# directory first — which is inside the project. The old ENTROPY_PROJECT
|
|
105
|
+
# override existed to paper over the nested clone, and papering is exactly
|
|
106
|
+
# what made the wrong answer survivable.
|
|
107
|
+
entropy_machines_root() {
|
|
108
|
+
if _er_top=$(entropy_machines_git_root); then
|
|
109
|
+
printf '%s\n' "$_er_top"
|
|
110
|
+
unset _er_top
|
|
111
|
+
return 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# No git. Walk up for config.json so the harness still works in a plain
|
|
115
|
+
# directory — the config loader's own tests do exactly this.
|
|
116
|
+
_er_d=$PWD
|
|
117
|
+
while [ -n "$_er_d" ] && [ "$_er_d" != "/" ]; do
|
|
118
|
+
if [ -f "$_er_d/config.json" ]; then
|
|
119
|
+
printf '%s\n' "$_er_d"
|
|
120
|
+
unset _er_d
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
_er_d=$(dirname -- "$_er_d")
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
unset _er_d
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# entropy_machines_refuse_nested_clone <tool-name> — exits 2 if the harness is a
|
|
131
|
+
# NESTED GIT REPOSITORY inside the project. A REFUSAL, not a resolution: it
|
|
132
|
+
# never tries to make that layout work (see the header).
|
|
133
|
+
#
|
|
134
|
+
# The harness is vendored — plain files committed into the project — so
|
|
135
|
+
# $ENTROPY_MACHINES_HOME/.git normally does not exist at all and this returns
|
|
136
|
+
# immediately. When it DOES exist there is exactly one innocent explanation:
|
|
137
|
+
# the harness IS the project, either developed standalone or vendored at the
|
|
138
|
+
# repo root. That still holds from inside a linked worktree, where the .git
|
|
139
|
+
# is a FILE pointing at the main checkout.
|
|
140
|
+
#
|
|
141
|
+
# So: nested iff a DIFFERENT repository encloses the harness's parent
|
|
142
|
+
# directory than the one that owns the harness directory itself. Deliberately
|
|
143
|
+
# phrased without ENTROPY_MACHINES_ROOT, which is resolved from the CWD and is
|
|
144
|
+
# therefore the shadowed, wrong answer in the very case being detected.
|
|
145
|
+
entropy_machines_refuse_nested_clone() {
|
|
146
|
+
_rnc_tool=${1:-entropy-machines}
|
|
147
|
+
_rnc_home=${ENTROPY_MACHINES_HOME:-}
|
|
148
|
+
if [ -z "$_rnc_home" ] || [ ! -e "$_rnc_home/.git" ]; then
|
|
149
|
+
unset _rnc_tool _rnc_home
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
_rnc_owner=$(entropy_machines_git_root "$_rnc_home") || _rnc_owner=""
|
|
154
|
+
_rnc_outer=$(entropy_machines_git_root "$_rnc_home/..") || _rnc_outer=""
|
|
155
|
+
|
|
156
|
+
if [ -z "$_rnc_outer" ] || [ "$_rnc_outer" = "$_rnc_owner" ]; then
|
|
157
|
+
unset _rnc_tool _rnc_home _rnc_owner _rnc_outer
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
echo "$_rnc_tool: REFUSED — the harness is a nested git repository." >&2
|
|
162
|
+
echo " $_rnc_home has its own .git, and it sits inside another" >&2
|
|
163
|
+
echo " repository ($_rnc_outer)." >&2
|
|
164
|
+
echo "" >&2
|
|
165
|
+
echo " A nested repo shadows its parent for every git query, so every" >&2
|
|
166
|
+
echo " command run from inside it resolves to the HARNESS as the project:" >&2
|
|
167
|
+
echo " the tracker writes .entropy-machines/ into the harness, and dispatch and" >&2
|
|
168
|
+
echo " handoff operate on the wrong repository. Nothing has been written." >&2
|
|
169
|
+
echo "" >&2
|
|
170
|
+
echo " This harness is VENDORED, not cloned — plain files committed into" >&2
|
|
171
|
+
echo " the project alongside its code, exactly as a scripts/ directory is." >&2
|
|
172
|
+
echo " Fix it either way:" >&2
|
|
173
|
+
echo " rm -rf $_rnc_home/.git" >&2
|
|
174
|
+
echo " git -C $_rnc_outer add $_rnc_home && git -C $_rnc_outer commit" >&2
|
|
175
|
+
echo " or re-vendor: delete $_rnc_home and copy the harness's files in" >&2
|
|
176
|
+
echo " (no .git), then commit them into $_rnc_outer." >&2
|
|
177
|
+
unset _rnc_tool _rnc_home _rnc_owner _rnc_outer
|
|
178
|
+
exit 2
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# entropy_machines_require_root <tool-name> — sets and exports ENTROPY_MACHINES_ROOT, or exits 2
|
|
182
|
+
# with a message that names the directory actually looked in.
|
|
183
|
+
#
|
|
184
|
+
# config.json is still the project's contract with the harness, and its
|
|
185
|
+
# absence is still a refusal: every other entry point would otherwise have to
|
|
186
|
+
# guess at paths, suite commands and protected paths it has not been told.
|
|
187
|
+
# bin/init is the one command that runs without it, because it is the thing
|
|
188
|
+
# that writes it.
|
|
189
|
+
entropy_machines_require_root() {
|
|
190
|
+
_err_tool=${1:-entropy-machines}
|
|
191
|
+
# Before anything is resolved: in a nested clone every answer below is the
|
|
192
|
+
# harness's, not the project's, and they all look plausible.
|
|
193
|
+
entropy_machines_refuse_nested_clone "$_err_tool"
|
|
194
|
+
if ! ENTROPY_MACHINES_ROOT=$(entropy_machines_root); then
|
|
195
|
+
echo "$_err_tool: REFUSED — not inside a git repository, and no config.json" >&2
|
|
196
|
+
echo " found by walking up from $PWD." >&2
|
|
197
|
+
echo " cd into the project you are running the factory on." >&2
|
|
198
|
+
unset _err_tool
|
|
199
|
+
exit 2
|
|
200
|
+
fi
|
|
201
|
+
if [ ! -f "${ENTROPY_MACHINES_HOME:-$ENTROPY_MACHINES_ROOT}/config.json" ]; then
|
|
202
|
+
echo "$_err_tool: REFUSED — no config.json at ${ENTROPY_MACHINES_HOME:-$ENTROPY_MACHINES_ROOT}." >&2
|
|
203
|
+
echo " That is this project's contract with the harness: every path," >&2
|
|
204
|
+
echo " command and suite the harness would otherwise hardcode lives in" >&2
|
|
205
|
+
echo " it. See ${ENTROPY_MACHINES_HOME:-<harness>}/docs/CONFIG.md." >&2
|
|
206
|
+
echo " To create a starter one: ${ENTROPY_MACHINES_HOME:-<harness>}/bin/init" >&2
|
|
207
|
+
unset _err_tool
|
|
208
|
+
exit 2
|
|
209
|
+
fi
|
|
210
|
+
export ENTROPY_MACHINES_ROOT
|
|
211
|
+
unset _err_tool
|
|
212
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* daylight — the long-reading theme. LIGHT FIRST.
|
|
2
|
+
The bare :root is the light palette and [data-theme=dark] is the override,
|
|
3
|
+
which is the mirror image of high-contrast.css. Both directions of the
|
|
4
|
+
toggle therefore work under either theme: whichever state a theme does not
|
|
5
|
+
name falls through to its own bare :root.
|
|
6
|
+
|
|
7
|
+
EVERY TOKEN HAS A VALUE IN THE BARE :root. A token defined only under
|
|
8
|
+
[data-theme=dark] renders unstyled in the default state — that is the
|
|
9
|
+
classic bug in a theme file and the reason the test compares the two files'
|
|
10
|
+
token NAME sets rather than eyeballing them.
|
|
11
|
+
|
|
12
|
+
WHAT IT IS, AND WHY IT IS NOT A RECOLOURED hc-light. hc-light maximises
|
|
13
|
+
legibility: pure white ground, saturated 4.5:1-and-up ink on every element,
|
|
14
|
+
a border hue loud enough to be a signal on its own. daylight is for reading
|
|
15
|
+
a long report end to end, so it trades some of that for comfort:
|
|
16
|
+
|
|
17
|
+
* The ground is warm paper (#FAF8F4), not #fff, and the ink is a
|
|
18
|
+
blue-leaning near-black (#23262E), not #292929 grey — a slight hue bias
|
|
19
|
+
in both directions rather than a pure neutral, which is what stops a
|
|
20
|
+
full page of prose reading as printer output.
|
|
21
|
+
* The rules are quiet. --line sits at 3.25:1 on the ground: a real
|
|
22
|
+
boundary you can see, not a stripe that competes with the text. The
|
|
23
|
+
structural CSS still separates with borders and never with tints, so
|
|
24
|
+
quieting the line is the whole difference between the two looks.
|
|
25
|
+
* The type scale is bigger and further apart — 13/16/20/28 against
|
|
26
|
+
high-contrast's 12/14/16/20. Body sits at the browser default rather
|
|
27
|
+
than below it, and the title is more than twice the small step, so a
|
|
28
|
+
heading reads as a heading in a page of running text.
|
|
29
|
+
* --accent (#2A5DB0 light / #7FB2F0 dark) is one blue that works on both
|
|
30
|
+
grounds, so a link does not change character with the toggle.
|
|
31
|
+
* --focus is amber (#B45309 / #F0A868) — the one hue no other token
|
|
32
|
+
occupies in either state, because focus has to be unmistakable, and the
|
|
33
|
+
structural CSS draws it as a border and never as a fill.
|
|
34
|
+
|
|
35
|
+
SEMANTICS DO NOT RELY ON HUE. ok is a teal-green, warn an amber, bad a red;
|
|
36
|
+
under deuteranopia or protanopia the teal stays visibly cool while the other
|
|
37
|
+
two collapse together, so the three are also separated by LIGHTNESS — in the
|
|
38
|
+
light state ok/warn/bad differ by 1.40:1, 1.47:1 and 2.05:1 in luminance,
|
|
39
|
+
and in the dark state by 1.85:1, 1.29:1 and 1.43:1. Every one of them clears
|
|
40
|
+
4.5:1 against its own ground. The templates pair each of these colours with
|
|
41
|
+
a word ("open", "ok"); keep that true — the colour is the second signal, not
|
|
42
|
+
the only one.
|
|
43
|
+
|
|
44
|
+
Body text on ground: 14.26:1 light, 14.13:1 dark. Muted text (--dim):
|
|
45
|
+
6.29:1 light, 7.43:1 dark.
|
|
46
|
+
|
|
47
|
+
A THEME FILE IS TOKENS ONLY — no layout, no component rules, no @font-face,
|
|
48
|
+
no remote URL. Selected by docs.theme in config.json; bin/serve inlines it
|
|
49
|
+
between a doc's entropy-machines-theme:begin and entropy-machines-theme:end markers and never
|
|
50
|
+
links to it. See docs/CONFIG.md. */
|
|
51
|
+
:root{
|
|
52
|
+
--bg:#FAF8F4; /* warm paper, not #fff */
|
|
53
|
+
--panel:#FFFFFF; /* one step up from the ground, for insets */
|
|
54
|
+
--ink:#23262E; /* blue-leaning near-black — 14.26:1 on --bg */
|
|
55
|
+
--dim:#5A5C63; /* 6.29:1 — a real value, never opacity */
|
|
56
|
+
--line:#8E8A7E; /* 3.25:1 — visible, quiet */
|
|
57
|
+
--focus:#B45309; /* amber; no other token uses this hue */
|
|
58
|
+
--accent:#2A5DB0; /* one link blue for both grounds */
|
|
59
|
+
--ok:#0B6E5A; /* teal-green: stays cool for a red-green reader */
|
|
60
|
+
--warn:#A06E12;
|
|
61
|
+
--bad:#8C1D18; /* darkest of the three here, so it shouts */
|
|
62
|
+
--alt:#6D4AA8;
|
|
63
|
+
--fs-sm:13px; --fs-base:16px; --fs-head:20px; --fs-title:28px;
|
|
64
|
+
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
65
|
+
--positive:var(--ok); --good:var(--ok); --muted:var(--dim); --border:var(--line);
|
|
66
|
+
--caution:var(--warn); --warn-deep:var(--warn); --code-bg:var(--panel);
|
|
67
|
+
}
|
|
68
|
+
/* The dark state. Ink-on-slate rather than white-on-black: the same trade as
|
|
69
|
+
the light state, one stop softer than high contrast at both ends. The type
|
|
70
|
+
scale does not change with the state, so it is not restated. The aliases are
|
|
71
|
+
var() references and re-resolve here on their own. */
|
|
72
|
+
:root[data-theme=dark]{
|
|
73
|
+
--bg:#14161A; /* slate, not #000 */
|
|
74
|
+
--panel:#1B1E24;
|
|
75
|
+
--ink:#E6E3DC; /* warm off-white — 14.13:1 on --bg */
|
|
76
|
+
--dim:#A8A69F; /* 7.43:1 */
|
|
77
|
+
--line:#626973; /* 3.27:1 */
|
|
78
|
+
--focus:#F0A868;
|
|
79
|
+
--accent:#7FB2F0;
|
|
80
|
+
--ok:#34A98D;
|
|
81
|
+
--warn:#F2C87B;
|
|
82
|
+
--bad:#FF8C82; /* lightest of the three here, so it shouts */
|
|
83
|
+
--alt:#C79BEE;
|
|
84
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* high-contrast — the house style, and the default.
|
|
2
|
+
VS Code High Contrast: hc-black as the bare :root, hc-light on the toggle.
|
|
3
|
+
Lifted verbatim from what lib/REPORT-TEMPLATE.html has always shipped; the
|
|
4
|
+
values are established house style, not up for improvement here.
|
|
5
|
+
|
|
6
|
+
A THEME FILE IS TOKENS ONLY. No layout, no component rules, no @font-face
|
|
7
|
+
and no remote URL of any kind — the structural CSS stays in the templates
|
|
8
|
+
so both themes get every structural fix for free, and bin/doclint refuses
|
|
9
|
+
any doc that references something off this machine.
|
|
10
|
+
|
|
11
|
+
Selected by docs.theme in config.json (see docs/CONFIG.md). bin/serve
|
|
12
|
+
INLINES this file between a doc's entropy-machines-theme:begin and entropy-machines-theme:end
|
|
13
|
+
markers, and into its own dashboard. It is never linked to as a stylesheet:
|
|
14
|
+
a doc is a standalone local file that must still look right when it is
|
|
15
|
+
opened straight off disk or moved somewhere else. */
|
|
16
|
+
:root{
|
|
17
|
+
--bg:#000; --panel:#000;
|
|
18
|
+
--ink:#fff; --dim:rgba(255,255,255,.7);
|
|
19
|
+
--line:#6FC3DF; /* contrastBorder */
|
|
20
|
+
--focus:#F38518; /* focusBorder / activeContrastBorder */
|
|
21
|
+
--accent:#21A6FF; /* textLink.foreground */
|
|
22
|
+
--ok:#23D18B; --warn:#F5F543; --bad:#F48771; --alt:#D670D6;
|
|
23
|
+
--fs-sm:12px; --fs-base:14px; --fs-head:16px; --fs-title:20px;
|
|
24
|
+
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
25
|
+
--positive:var(--ok); --good:var(--ok); --muted:var(--dim); --border:var(--line);
|
|
26
|
+
--caution:var(--warn); --warn-deep:var(--warn); --code-bg:var(--panel);
|
|
27
|
+
}
|
|
28
|
+
/* The toggle stamps data-theme on the root element. hc-black is the bare
|
|
29
|
+
:root, so only the light state needs restating; [data-theme=dark] matches
|
|
30
|
+
nothing here and correctly falls through to the default. */
|
|
31
|
+
:root[data-theme=light]{
|
|
32
|
+
--bg:#fff; --panel:#fff;
|
|
33
|
+
--ink:#292929; --dim:rgba(41,41,41,.75);
|
|
34
|
+
--line:#0F4A85; --focus:#006BBD; --accent:#0F4A85;
|
|
35
|
+
--ok:#0A5C21; --warn:#7A4A00; --bad:#A81C0B; --alt:#6B21A8;
|
|
36
|
+
}
|