jig-ui 0.14.1 → 0.15.0
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 +42 -0
- package/README.md +13 -4
- package/dist/index.js +258 -157
- package/package.json +1 -1
- package/templates/COMMAND.md.tmpl +19 -9
- package/templates/SKILL.md.tmpl +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
The gate holds an agent on warnings, waits while it asks the owner a question,
|
|
6
|
+
and reaches projects where the skill is installed globally.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- **`gate` blocks on warnings in the files the agent changed**, not only on
|
|
11
|
+
mechanical errors. One em dash survived two runs of fresh agents on a real
|
|
12
|
+
site: the first saw the warning, called it pre-existing, and finished. The
|
|
13
|
+
gate still reads only changed files, so old warnings elsewhere never trap an
|
|
14
|
+
agent, and it now also runs when a changed file carries interface text.
|
|
15
|
+
- **The skill asks the user to run each command.** `decide`, `spec`, `mockup`,
|
|
16
|
+
`make` and `critique` hold their procedures in the command, not in the
|
|
17
|
+
skill. An agent told in prose to "run decide" made up five questions of its
|
|
18
|
+
own and wrote a `DECISIONS.md` with no direction and no `## Unresolved`
|
|
19
|
+
section.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`jig-allow <ID>: <why>`** waives one warning, on its own line or the line
|
|
24
|
+
above, in any comment syntax. Some warnings are a detector's guess: `A-01`
|
|
25
|
+
cannot tell a chosen violet from a default one. Warnings only, a reason is
|
|
26
|
+
required, and `check` lists every waiver it honoured, with its reason, on
|
|
27
|
+
every run. Waived findings are left out of `warnings=`.
|
|
28
|
+
- **`install --agent claude --hook` on a global install** writes only this
|
|
29
|
+
project's Stop hook. The hook was written only by a project install, which is
|
|
30
|
+
refused while the skill is global, so most projects never had a gate.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **A question to the owner is not a finish.** After `decide`, `spec` or
|
|
35
|
+
`mockup`, a stop whose last message asks a question is allowed, and the
|
|
36
|
+
command's output is checked on the stop after the answer. Before, the gate
|
|
37
|
+
refused `decide`'s first question because `DECISIONS.md` did not exist yet,
|
|
38
|
+
and on the second refusal the agent wrote the file itself with no answers.
|
|
39
|
+
- **The gate finds `DECISIONS.md` beside the token layer** that `brand` in
|
|
40
|
+
`jig.config.json` names. It looked in three fixed places, rejected a
|
|
41
|
+
correctly placed file, and the agent moved the file to satisfy it.
|
|
42
|
+
- **`jig seo` says an endpoint is not a page**, so `pages=` can be checked
|
|
43
|
+
against a site's route files.
|
|
44
|
+
|
|
3
45
|
## 0.14.1
|
|
4
46
|
|
|
5
47
|
`jig seo` cites only what a rule says.
|
package/README.md
CHANGED
|
@@ -53,10 +53,19 @@ Every agent supports both scopes.
|
|
|
53
53
|
answer yes when an interactive install offers it. It adds one entry to
|
|
54
54
|
`.claude/settings.json`, keeping everything already in that file, and runs `jig gate`
|
|
55
55
|
when the agent tries to finish: it holds the agent there while the files it changed
|
|
56
|
-
|
|
57
|
-
spec that is prose, a critique with no verdict files, a review
|
|
58
|
-
the page. After three attempts it lets the agent stop and says
|
|
59
|
-
unfinished.
|
|
56
|
+
have a `check` error or warning, or while the `/jig` command it just ran left its
|
|
57
|
+
work incomplete — a spec that is prose, a critique with no verdict files, a review
|
|
58
|
+
that never rendered the page. After three attempts it lets the agent stop and says
|
|
59
|
+
the work is unfinished.
|
|
60
|
+
|
|
61
|
+
A warning that is right as it stands is waived on its own line, in a comment
|
|
62
|
+
reading `jig-allow <ID>: <why>` (`/* jig-allow A-01: the brand is violet */`). The
|
|
63
|
+
reason is required, an error cannot be waived, and `check` lists every waiver it
|
|
64
|
+
honoured, with its reason, on every run.
|
|
65
|
+
|
|
66
|
+
The skill and the hook are installed separately. With Jig installed globally, run
|
|
67
|
+
`install --agent claude --hook` in a project to add only that project's hook: a
|
|
68
|
+
global hook would run on every stop in every project on the machine.
|
|
60
69
|
|
|
61
70
|
It is off by default, and `--yes` never adds it, because that is the path an agent
|
|
62
71
|
takes and nobody is there to consent. It exists because weak models skip any step
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { readFileSync as
|
|
5
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
6
6
|
import { homedir as homedir2 } from "os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join38 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/paths.ts
|
|
10
10
|
import { existsSync } from "fs";
|
|
@@ -568,11 +568,19 @@ function install(opts) {
|
|
|
568
568
|
} catch {
|
|
569
569
|
globalManifest = null;
|
|
570
570
|
}
|
|
571
|
+
if (globalManifest && opts.hook && opts.agent === "claude") {
|
|
572
|
+
return {
|
|
573
|
+
written: [],
|
|
574
|
+
skipped: [],
|
|
575
|
+
stopHook: installStopHook(opts.projectRoot, opts.version),
|
|
576
|
+
hookOnly: `Jig's skill is installed globally for 'claude' (${globalReferenceDir} under your home directory), so it stays there. Added only this project's Stop hook.`
|
|
577
|
+
};
|
|
578
|
+
}
|
|
571
579
|
if (globalManifest) {
|
|
572
580
|
return {
|
|
573
581
|
written: [],
|
|
574
582
|
skipped: [],
|
|
575
|
-
warning: `Jig is already installed globally for '${opts.agent}' (${globalReferenceDir} under your home directory). Installing again at project scope would leave two contradicting '${opts.agent}' skills. Run 'jig update' to refresh the global install instead, or choose a different --agent for this project.`
|
|
583
|
+
warning: `Jig is already installed globally for '${opts.agent}' (${globalReferenceDir} under your home directory). Installing again at project scope would leave two contradicting '${opts.agent}' skills. Run 'jig update' to refresh the global install instead, or choose a different --agent for this project.` + (opts.agent === "claude" ? ` To add only this project's Stop hook, run install again with --hook.` : "")
|
|
576
584
|
};
|
|
577
585
|
}
|
|
578
586
|
}
|
|
@@ -1455,8 +1463,8 @@ function sectionsOf(entries) {
|
|
|
1455
1463
|
}
|
|
1456
1464
|
|
|
1457
1465
|
// src/commands/check.ts
|
|
1458
|
-
import { existsSync as existsSync14, readFileSync as
|
|
1459
|
-
import { join as
|
|
1466
|
+
import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
|
|
1467
|
+
import { join as join24 } from "path";
|
|
1460
1468
|
|
|
1461
1469
|
// src/check/files.ts
|
|
1462
1470
|
import { execFileSync } from "child_process";
|
|
@@ -1689,6 +1697,11 @@ function formatReport(findings, meta) {
|
|
|
1689
1697
|
lines.push(` ${meta.exempt.slice(0, 8).join(", ")}${n > 8 ? `, and ${n - 8} more` : ""}`);
|
|
1690
1698
|
}
|
|
1691
1699
|
}
|
|
1700
|
+
if (meta.waived && meta.waived.length > 0) {
|
|
1701
|
+
lines.push("");
|
|
1702
|
+
lines.push(` ${plural(meta.waived.length, "warning")} waived in the source with jig-allow:`);
|
|
1703
|
+
for (const w of meta.waived) lines.push(` ${w.ruleId.padEnd(6)}${w.file}:${w.line} "${w.reason}"`);
|
|
1704
|
+
}
|
|
1692
1705
|
if (meta.unscanned && meta.unscanned.count > 0) {
|
|
1693
1706
|
const exts = meta.unscanned.extensions.join(", ");
|
|
1694
1707
|
lines.push("");
|
|
@@ -3779,6 +3792,46 @@ function applyExemptions(files, patterns) {
|
|
|
3779
3792
|
};
|
|
3780
3793
|
}
|
|
3781
3794
|
|
|
3795
|
+
// src/check/waiver.ts
|
|
3796
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
3797
|
+
import { join as join21 } from "path";
|
|
3798
|
+
var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
|
|
3799
|
+
function readWaiver(line) {
|
|
3800
|
+
const m = WAIVER.exec(line);
|
|
3801
|
+
if (!m) return void 0;
|
|
3802
|
+
const reason = m[2].replace(/\s*(\*\/\s*\}?|-->|#\})\s*$/, "").trim();
|
|
3803
|
+
if (!/\w/.test(reason)) return void 0;
|
|
3804
|
+
return { ids: m[1].split(/\s*,\s*/), reason };
|
|
3805
|
+
}
|
|
3806
|
+
function applyWaivers(root, findings) {
|
|
3807
|
+
const lines = /* @__PURE__ */ new Map();
|
|
3808
|
+
const linesOf = (file) => {
|
|
3809
|
+
let cached = lines.get(file);
|
|
3810
|
+
if (!cached) {
|
|
3811
|
+
try {
|
|
3812
|
+
cached = readFileSync16(join21(root, file), "utf8").split("\n");
|
|
3813
|
+
} catch {
|
|
3814
|
+
cached = [];
|
|
3815
|
+
}
|
|
3816
|
+
lines.set(file, cached);
|
|
3817
|
+
}
|
|
3818
|
+
return cached;
|
|
3819
|
+
};
|
|
3820
|
+
const kept = [];
|
|
3821
|
+
const waived = [];
|
|
3822
|
+
for (const f of findings) {
|
|
3823
|
+
if (f.severity !== "warning") {
|
|
3824
|
+
kept.push(f);
|
|
3825
|
+
continue;
|
|
3826
|
+
}
|
|
3827
|
+
const source = linesOf(f.file);
|
|
3828
|
+
const waiver = [source[f.line - 1], source[f.line - 2]].map((l) => l === void 0 ? void 0 : readWaiver(l)).find((w) => w?.ids.includes(f.ruleId));
|
|
3829
|
+
if (waiver) waived.push({ ...f, reason: waiver.reason });
|
|
3830
|
+
else kept.push(f);
|
|
3831
|
+
}
|
|
3832
|
+
return { findings: kept, waived };
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3782
3835
|
// src/check/responsive.ts
|
|
3783
3836
|
var MEDIA_RE = /@media\b([^{]*)\{/gi;
|
|
3784
3837
|
var VIEWPORT_FEATURE_RE = /\b(?:min-|max-)?(?:width|height|aspect-ratio|orientation|resolution)\b|\b(?:any-)?(?:pointer|hover)\b/i;
|
|
@@ -3821,8 +3874,8 @@ function hasMenuToggle(raw) {
|
|
|
3821
3874
|
}
|
|
3822
3875
|
|
|
3823
3876
|
// src/check/mode-wiring.ts
|
|
3824
|
-
import { existsSync as existsSync12, readFileSync as
|
|
3825
|
-
import { dirname as dirname6, join as
|
|
3877
|
+
import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
|
|
3878
|
+
import { dirname as dirname6, join as join22 } from "path";
|
|
3826
3879
|
var MODES = ["editorial", "product", "operator"];
|
|
3827
3880
|
function configProblems(config) {
|
|
3828
3881
|
const problems = [];
|
|
@@ -3844,10 +3897,10 @@ function configProblems(config) {
|
|
|
3844
3897
|
}
|
|
3845
3898
|
function modeWiringProblems(projectRoot) {
|
|
3846
3899
|
let config;
|
|
3847
|
-
const path =
|
|
3900
|
+
const path = join22(projectRoot, "jig.config.json");
|
|
3848
3901
|
if (!existsSync12(path)) return [];
|
|
3849
3902
|
try {
|
|
3850
|
-
config = JSON.parse(
|
|
3903
|
+
config = JSON.parse(readFileSync17(path, "utf8"));
|
|
3851
3904
|
} catch {
|
|
3852
3905
|
return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
|
|
3853
3906
|
}
|
|
@@ -3858,14 +3911,14 @@ function modeWiringProblems(projectRoot) {
|
|
|
3858
3911
|
if (modes.length === 0) return [];
|
|
3859
3912
|
const dir = dirname6(config.brand);
|
|
3860
3913
|
const rel = (f) => dir === "." ? f : `${dir}/${f}`;
|
|
3861
|
-
if (!existsSync12(
|
|
3914
|
+
if (!existsSync12(join22(projectRoot, rel("theme.css")))) return [];
|
|
3862
3915
|
const problems = [];
|
|
3863
3916
|
modes.forEach((mode, i) => {
|
|
3864
3917
|
const barrel = rel(i === 0 ? "theme.css" : `theme.${mode}.css`);
|
|
3865
3918
|
const modeFile = `mode.${mode}.css`;
|
|
3866
3919
|
let body;
|
|
3867
3920
|
try {
|
|
3868
|
-
body =
|
|
3921
|
+
body = readFileSync17(join22(projectRoot, barrel), "utf8");
|
|
3869
3922
|
} catch {
|
|
3870
3923
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
|
|
3871
3924
|
return;
|
|
@@ -3877,7 +3930,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
3877
3930
|
barrel,
|
|
3878
3931
|
message: `jig.config.json declares ${mode}, but ${barrel} imports ${imported ? `mode.${imported}.css` : "no mode file"} \u2014 the page is styled at ${imported ?? "no"} density`
|
|
3879
3932
|
});
|
|
3880
|
-
} else if (!existsSync12(
|
|
3933
|
+
} else if (!existsSync12(join22(projectRoot, rel(modeFile)))) {
|
|
3881
3934
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
|
|
3882
3935
|
}
|
|
3883
3936
|
});
|
|
@@ -3885,8 +3938,8 @@ function modeWiringProblems(projectRoot) {
|
|
|
3885
3938
|
}
|
|
3886
3939
|
|
|
3887
3940
|
// src/check/token-audit.ts
|
|
3888
|
-
import { existsSync as existsSync13, readdirSync as readdirSync9, readFileSync as
|
|
3889
|
-
import { join as
|
|
3941
|
+
import { existsSync as existsSync13, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
3942
|
+
import { join as join23 } from "path";
|
|
3890
3943
|
var TEXT_FLOOR = 4.5;
|
|
3891
3944
|
var UI_FLOOR = 3;
|
|
3892
3945
|
var FOREGROUNDS = [
|
|
@@ -3942,24 +3995,24 @@ function floorFor(token) {
|
|
|
3942
3995
|
function tokenFiles(projectRoot) {
|
|
3943
3996
|
const read = (rel) => {
|
|
3944
3997
|
try {
|
|
3945
|
-
return
|
|
3998
|
+
return readFileSync18(join23(projectRoot, ...rel.split("/")), "utf8");
|
|
3946
3999
|
} catch {
|
|
3947
4000
|
return null;
|
|
3948
4001
|
}
|
|
3949
4002
|
};
|
|
3950
|
-
const statePath =
|
|
4003
|
+
const statePath = join23(projectRoot, ".jig", "state.json");
|
|
3951
4004
|
if (existsSync13(statePath)) {
|
|
3952
4005
|
try {
|
|
3953
|
-
const state = JSON.parse(
|
|
4006
|
+
const state = JSON.parse(readFileSync18(statePath, "utf8"));
|
|
3954
4007
|
const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
|
|
3955
4008
|
const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
|
|
3956
4009
|
if (out.length > 0) return out;
|
|
3957
4010
|
} catch {
|
|
3958
4011
|
}
|
|
3959
4012
|
}
|
|
3960
|
-
const legacy =
|
|
4013
|
+
const legacy = join23(projectRoot, ".jig", "tokens");
|
|
3961
4014
|
if (!existsSync13(legacy)) return [];
|
|
3962
|
-
return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source:
|
|
4015
|
+
return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync18(join23(legacy, f), "utf8") }));
|
|
3963
4016
|
}
|
|
3964
4017
|
function auditTokenLayer(projectRoot) {
|
|
3965
4018
|
const files = tokenFiles(projectRoot);
|
|
@@ -4027,15 +4080,15 @@ function auditTokenLayer(projectRoot) {
|
|
|
4027
4080
|
}
|
|
4028
4081
|
|
|
4029
4082
|
// src/commands/check.ts
|
|
4030
|
-
var LEGACY_INDEX_REL =
|
|
4083
|
+
var LEGACY_INDEX_REL = join24(".jig", "rules.index.json");
|
|
4031
4084
|
function resolveIndexPath(projectRoot) {
|
|
4032
|
-
const legacy =
|
|
4085
|
+
const legacy = join24(projectRoot, LEGACY_INDEX_REL);
|
|
4033
4086
|
if (existsSync14(legacy)) return legacy;
|
|
4034
|
-
return
|
|
4087
|
+
return join24(assetRoot(), "rules.index.json");
|
|
4035
4088
|
}
|
|
4036
4089
|
function resolveMode(projectRoot) {
|
|
4037
4090
|
try {
|
|
4038
|
-
const config = JSON.parse(
|
|
4091
|
+
const config = JSON.parse(readFileSync19(join24(projectRoot, "jig.config.json"), "utf8"));
|
|
4039
4092
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
4040
4093
|
if (modes.length === 1) return modes[0];
|
|
4041
4094
|
return modes.length > 1 ? "mixed" : "unknown";
|
|
@@ -4052,14 +4105,14 @@ function summariseUnscanned(files) {
|
|
|
4052
4105
|
}
|
|
4053
4106
|
function countSpecs() {
|
|
4054
4107
|
try {
|
|
4055
|
-
return loadSpecs(
|
|
4108
|
+
return loadSpecs(join24(assetRoot(), "rules")).length;
|
|
4056
4109
|
} catch {
|
|
4057
4110
|
return 0;
|
|
4058
4111
|
}
|
|
4059
4112
|
}
|
|
4060
4113
|
function check(opts) {
|
|
4061
4114
|
const indexPath = resolveIndexPath(opts.projectRoot);
|
|
4062
|
-
const index = validateIndex(JSON.parse(
|
|
4115
|
+
const index = validateIndex(JSON.parse(readFileSync19(indexPath, "utf8")));
|
|
4063
4116
|
const selection = selectFiles(opts.projectRoot, opts.all);
|
|
4064
4117
|
const exemptions = readExemptions(opts.projectRoot);
|
|
4065
4118
|
const { scanned: files, exempt, byPattern } = applyExemptions(selection.files, exemptions);
|
|
@@ -4069,7 +4122,7 @@ function check(opts) {
|
|
|
4069
4122
|
const projectParticipates = stylesheets.some((f) => {
|
|
4070
4123
|
if (!hasExtension(f, CSS_EXTENSIONS)) return false;
|
|
4071
4124
|
try {
|
|
4072
|
-
return participatesInTokenLayer(
|
|
4125
|
+
return participatesInTokenLayer(readFileSync19(join24(opts.projectRoot, f), "utf8"), tokens);
|
|
4073
4126
|
} catch {
|
|
4074
4127
|
return false;
|
|
4075
4128
|
}
|
|
@@ -4077,7 +4130,7 @@ function check(opts) {
|
|
|
4077
4130
|
const projectResponsive = stylesheets.some((f) => {
|
|
4078
4131
|
if (!isStyleBearing(f)) return false;
|
|
4079
4132
|
try {
|
|
4080
|
-
const raw =
|
|
4133
|
+
const raw = readFileSync19(join24(opts.projectRoot, f), "utf8");
|
|
4081
4134
|
return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
|
|
4082
4135
|
} catch {
|
|
4083
4136
|
return false;
|
|
@@ -4086,14 +4139,14 @@ function check(opts) {
|
|
|
4086
4139
|
const viewportFitCover = stylesheets.some((f) => {
|
|
4087
4140
|
if (!isStyleBearing(f)) return false;
|
|
4088
4141
|
try {
|
|
4089
|
-
return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(
|
|
4142
|
+
return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync19(join24(opts.projectRoot, f), "utf8"));
|
|
4090
4143
|
} catch {
|
|
4091
4144
|
return false;
|
|
4092
4145
|
}
|
|
4093
4146
|
});
|
|
4094
4147
|
const projectMenuToggle = stylesheets.some((f) => {
|
|
4095
4148
|
try {
|
|
4096
|
-
return hasMenuToggle(
|
|
4149
|
+
return hasMenuToggle(readFileSync19(join24(opts.projectRoot, f), "utf8"));
|
|
4097
4150
|
} catch {
|
|
4098
4151
|
return false;
|
|
4099
4152
|
}
|
|
@@ -4101,14 +4154,14 @@ function check(opts) {
|
|
|
4101
4154
|
const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
|
|
4102
4155
|
const withStyles = files.filter((f) => {
|
|
4103
4156
|
try {
|
|
4104
|
-
const src =
|
|
4157
|
+
const src = readFileSync19(join24(opts.projectRoot, f), "utf8");
|
|
4105
4158
|
return hasExtension(f, CSS_EXTENSIONS) ? src.trim().length > 0 : maskNonStyleRegions(src, f).trim().length > 0;
|
|
4106
4159
|
} catch {
|
|
4107
4160
|
return false;
|
|
4108
4161
|
}
|
|
4109
4162
|
}).length;
|
|
4110
4163
|
const resolvedMode = resolveMode(opts.projectRoot);
|
|
4111
|
-
const
|
|
4164
|
+
const detected = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties);
|
|
4112
4165
|
const TOKEN_RULE = {
|
|
4113
4166
|
"--text-prose": "B-75",
|
|
4114
4167
|
"--size-touch-target": "E-51"
|
|
@@ -4116,7 +4169,7 @@ function check(opts) {
|
|
|
4116
4169
|
for (const problem of auditTokenLayer(opts.projectRoot)) {
|
|
4117
4170
|
const ruleId = TOKEN_RULE[problem.token] ?? "C-19";
|
|
4118
4171
|
const entry = index.find((e) => e.id === ruleId);
|
|
4119
|
-
|
|
4172
|
+
detected.push({
|
|
4120
4173
|
ruleId,
|
|
4121
4174
|
detector: "token-layer",
|
|
4122
4175
|
bucket: entry?.bucket ?? "mechanical",
|
|
@@ -4126,6 +4179,7 @@ function check(opts) {
|
|
|
4126
4179
|
message: problem.message
|
|
4127
4180
|
});
|
|
4128
4181
|
}
|
|
4182
|
+
const { findings, waived } = applyWaivers(opts.projectRoot, detected);
|
|
4129
4183
|
const hasError = findings.some((f) => f.bucket === "mechanical" && f.severity === "error");
|
|
4130
4184
|
const cssFiles = files.filter((f) => /\.(css|scss|less)$/i.test(f));
|
|
4131
4185
|
const noTokenLayer = (
|
|
@@ -4133,7 +4187,7 @@ function check(opts) {
|
|
|
4133
4187
|
// told to add a token @import it has nowhere to put.
|
|
4134
4188
|
cssFiles.length > 0 && cssFiles.every((f) => {
|
|
4135
4189
|
try {
|
|
4136
|
-
return !participatesInTokenLayer(
|
|
4190
|
+
return !participatesInTokenLayer(readFileSync19(join24(opts.projectRoot, f), "utf8"), tokens);
|
|
4137
4191
|
} catch {
|
|
4138
4192
|
return true;
|
|
4139
4193
|
}
|
|
@@ -4154,18 +4208,19 @@ function check(opts) {
|
|
|
4154
4208
|
exempt,
|
|
4155
4209
|
exemptPatterns: byPattern,
|
|
4156
4210
|
scope: selection.mode,
|
|
4157
|
-
modeUnwired: modeWiringProblems(opts.projectRoot)
|
|
4211
|
+
modeUnwired: modeWiringProblems(opts.projectRoot),
|
|
4212
|
+
waived
|
|
4158
4213
|
});
|
|
4159
|
-
return { findings, report, hasError };
|
|
4214
|
+
return { findings, report, hasError, waived };
|
|
4160
4215
|
}
|
|
4161
4216
|
|
|
4162
4217
|
// src/commands/init.ts
|
|
4163
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as
|
|
4164
|
-
import { dirname as dirname7, isAbsolute, join as
|
|
4218
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync23, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
4219
|
+
import { dirname as dirname7, isAbsolute, join as join28, resolve as resolve2, sep as sep3 } from "path";
|
|
4165
4220
|
|
|
4166
4221
|
// src/init/detect.ts
|
|
4167
|
-
import { readFileSync as
|
|
4168
|
-
import { join as
|
|
4222
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
4223
|
+
import { join as join25 } from "path";
|
|
4169
4224
|
var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
|
|
4170
4225
|
var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
|
|
4171
4226
|
var TAILWIND_V4_THEME_RE = /@theme\b/;
|
|
@@ -4182,7 +4237,7 @@ var FRAMEWORK_DEPS = [
|
|
|
4182
4237
|
function detectFramework(projectRoot) {
|
|
4183
4238
|
let pkg;
|
|
4184
4239
|
try {
|
|
4185
|
-
pkg = JSON.parse(
|
|
4240
|
+
pkg = JSON.parse(readFileSync20(join25(projectRoot, "package.json"), "utf8"));
|
|
4186
4241
|
} catch {
|
|
4187
4242
|
return void 0;
|
|
4188
4243
|
}
|
|
@@ -4200,7 +4255,7 @@ function detect(projectRoot) {
|
|
|
4200
4255
|
for (const f of cssFiles) {
|
|
4201
4256
|
let source;
|
|
4202
4257
|
try {
|
|
4203
|
-
source =
|
|
4258
|
+
source = readFileSync20(join25(projectRoot, f), "utf8");
|
|
4204
4259
|
} catch {
|
|
4205
4260
|
continue;
|
|
4206
4261
|
}
|
|
@@ -4235,8 +4290,8 @@ function isIgnored(projectRoot, relPath) {
|
|
|
4235
4290
|
}
|
|
4236
4291
|
|
|
4237
4292
|
// src/init/derive.ts
|
|
4238
|
-
import { readFileSync as
|
|
4239
|
-
import { join as
|
|
4293
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
4294
|
+
import { join as join26 } from "path";
|
|
4240
4295
|
var DEFAULT_PROPOSAL = {
|
|
4241
4296
|
h: 264,
|
|
4242
4297
|
s: 0,
|
|
@@ -4269,7 +4324,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
|
|
|
4269
4324
|
for (const file of cssFiles) {
|
|
4270
4325
|
let source;
|
|
4271
4326
|
try {
|
|
4272
|
-
source =
|
|
4327
|
+
source = readFileSync21(join26(projectRoot, file), "utf8");
|
|
4273
4328
|
} catch {
|
|
4274
4329
|
continue;
|
|
4275
4330
|
}
|
|
@@ -4318,7 +4373,7 @@ function balancedBraceBody(source, openIdx) {
|
|
|
4318
4373
|
function fromTailwindConfig(projectRoot, tailwindConfigFile) {
|
|
4319
4374
|
let source;
|
|
4320
4375
|
try {
|
|
4321
|
-
source =
|
|
4376
|
+
source = readFileSync21(join26(projectRoot, tailwindConfigFile), "utf8");
|
|
4322
4377
|
} catch {
|
|
4323
4378
|
return null;
|
|
4324
4379
|
}
|
|
@@ -4374,7 +4429,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
|
|
|
4374
4429
|
for (const file of cssFiles) {
|
|
4375
4430
|
let source;
|
|
4376
4431
|
try {
|
|
4377
|
-
source =
|
|
4432
|
+
source = readFileSync21(join26(projectRoot, file), "utf8");
|
|
4378
4433
|
} catch {
|
|
4379
4434
|
continue;
|
|
4380
4435
|
}
|
|
@@ -4534,8 +4589,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
|
|
|
4534
4589
|
}
|
|
4535
4590
|
|
|
4536
4591
|
// src/init/project-name.ts
|
|
4537
|
-
import { readFileSync as
|
|
4538
|
-
import { basename, join as
|
|
4592
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
4593
|
+
import { basename, join as join27 } from "path";
|
|
4539
4594
|
function slugify(name) {
|
|
4540
4595
|
const withoutScope = name.replace(/^@[^/]+\//, "");
|
|
4541
4596
|
const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -4543,7 +4598,7 @@ function slugify(name) {
|
|
|
4543
4598
|
}
|
|
4544
4599
|
function deriveProjectSlug(projectRoot) {
|
|
4545
4600
|
try {
|
|
4546
|
-
const pkg = JSON.parse(
|
|
4601
|
+
const pkg = JSON.parse(readFileSync22(join27(projectRoot, "package.json"), "utf8"));
|
|
4547
4602
|
if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
|
|
4548
4603
|
} catch {
|
|
4549
4604
|
}
|
|
@@ -4697,9 +4752,9 @@ function isValidSurfaceArray(v) {
|
|
|
4697
4752
|
function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
|
|
4698
4753
|
if (!existsSync15(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
4699
4754
|
try {
|
|
4700
|
-
const parsed = JSON.parse(
|
|
4755
|
+
const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
|
|
4701
4756
|
const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
|
|
4702
|
-
const brandResolves = brandCandidate ? existsSync15(
|
|
4757
|
+
const brandResolves = brandCandidate ? existsSync15(join28(projectRoot, ...brandCandidate.split("/"))) : false;
|
|
4703
4758
|
return {
|
|
4704
4759
|
brand: brandResolves ? brandCandidate : fallbackBrand,
|
|
4705
4760
|
surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
|
|
@@ -4712,7 +4767,7 @@ function configuredBrandPath(projectRoot, configAbsPath) {
|
|
|
4712
4767
|
if (!existsSync15(configAbsPath)) return { path: null };
|
|
4713
4768
|
let declared;
|
|
4714
4769
|
try {
|
|
4715
|
-
declared = JSON.parse(
|
|
4770
|
+
declared = JSON.parse(readFileSync23(configAbsPath, "utf8")).brand;
|
|
4716
4771
|
} catch {
|
|
4717
4772
|
return { path: null };
|
|
4718
4773
|
}
|
|
@@ -4742,7 +4797,7 @@ function defaultTokenDir(detection) {
|
|
|
4742
4797
|
function readConfiguredSurfaces(configAbsPath) {
|
|
4743
4798
|
if (!existsSync15(configAbsPath)) return null;
|
|
4744
4799
|
try {
|
|
4745
|
-
const parsed = JSON.parse(
|
|
4800
|
+
const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
|
|
4746
4801
|
return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
|
|
4747
4802
|
} catch {
|
|
4748
4803
|
return null;
|
|
@@ -4752,7 +4807,7 @@ function isCssModule(path) {
|
|
|
4752
4807
|
return /\.module\.(css|scss)$/i.test(path);
|
|
4753
4808
|
}
|
|
4754
4809
|
function legacyBrandFile(projectRoot) {
|
|
4755
|
-
const dir =
|
|
4810
|
+
const dir = join28(projectRoot, ".jig", "tokens");
|
|
4756
4811
|
if (!existsSync15(dir)) return void 0;
|
|
4757
4812
|
const found = readdirSync10(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
|
|
4758
4813
|
return found ? relKey(".jig", "tokens", found) : void 0;
|
|
@@ -4784,7 +4839,7 @@ var LEGACY_PAIR_RE = new RegExp(
|
|
|
4784
4839
|
`@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
|
|
4785
4840
|
);
|
|
4786
4841
|
function wireBarrel(absPath, barrelImport) {
|
|
4787
|
-
const content = existsSync15(absPath) ?
|
|
4842
|
+
const content = existsSync15(absPath) ? readFileSync23(absPath, "utf8") : "";
|
|
4788
4843
|
const line = `@import "${barrelImport}";`;
|
|
4789
4844
|
if (content.includes(line)) return { status: "already-present" };
|
|
4790
4845
|
const legacy = LEGACY_PAIR_RE.exec(content);
|
|
@@ -4872,7 +4927,7 @@ async function init(opts) {
|
|
|
4872
4927
|
}
|
|
4873
4928
|
}
|
|
4874
4929
|
if (opts.yes) {
|
|
4875
|
-
const declared = readConfiguredSurfaces(
|
|
4930
|
+
const declared = readConfiguredSurfaces(join28(opts.projectRoot, "jig.config.json"));
|
|
4876
4931
|
const inEffect = declared ?? surfaces;
|
|
4877
4932
|
const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
|
|
4878
4933
|
log(
|
|
@@ -4891,7 +4946,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
|
|
|
4891
4946
|
const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
|
|
4892
4947
|
const projectSlug = deriveProjectSlug(opts.projectRoot);
|
|
4893
4948
|
const configRelPath = "jig.config.json";
|
|
4894
|
-
const configAbsPath =
|
|
4949
|
+
const configAbsPath = join28(opts.projectRoot, configRelPath);
|
|
4895
4950
|
const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
|
|
4896
4951
|
if (configuredBrand.refused) {
|
|
4897
4952
|
log(
|
|
@@ -4925,7 +4980,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
|
|
|
4925
4980
|
}
|
|
4926
4981
|
}
|
|
4927
4982
|
const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
|
|
4928
|
-
const brandAbsPath =
|
|
4983
|
+
const brandAbsPath = join28(opts.projectRoot, ...brandRelPath.split("/"));
|
|
4929
4984
|
const tokensRelDir = brandRelPath.split("/").slice(0, -1);
|
|
4930
4985
|
log(
|
|
4931
4986
|
`Token layer: ${tokensRelDir.join("/") || "."}/ \u2014 ` + (configuredBrand.path ? "from jig.config.json." : priorBrand ? "the layout this project already had. Set `brand` in jig.config.json to move it." : findWireTarget(detection) ? "beside the stylesheet being wired. Set `brand` in jig.config.json to put it elsewhere." : "no stylesheet found to follow, so the project root. Set `brand` in jig.config.json to move it.")
|
|
@@ -4958,7 +5013,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
4958
5013
|
if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
|
|
4959
5014
|
}
|
|
4960
5015
|
if (brandAction === "written") {
|
|
4961
|
-
const defaultCssRaw =
|
|
5016
|
+
const defaultCssRaw = readFileSync23(join28(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
|
|
4962
5017
|
const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
|
|
4963
5018
|
mkdirSync6(dirname7(brandAbsPath), { recursive: true });
|
|
4964
5019
|
writeFileSync5(brandAbsPath, content, "utf8");
|
|
@@ -4966,7 +5021,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
4966
5021
|
}
|
|
4967
5022
|
if (relocateTo && configAction !== "written") {
|
|
4968
5023
|
try {
|
|
4969
|
-
const existing = JSON.parse(
|
|
5024
|
+
const existing = JSON.parse(readFileSync23(configAbsPath, "utf8"));
|
|
4970
5025
|
const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
|
|
4971
5026
|
`;
|
|
4972
5027
|
writeFileSync5(configAbsPath, content, "utf8");
|
|
@@ -4985,12 +5040,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
4985
5040
|
} else if (configAction === "merged") {
|
|
4986
5041
|
let existing = {};
|
|
4987
5042
|
try {
|
|
4988
|
-
existing = JSON.parse(
|
|
5043
|
+
existing = JSON.parse(readFileSync23(configAbsPath, "utf8"));
|
|
4989
5044
|
} catch {
|
|
4990
5045
|
existing = {};
|
|
4991
5046
|
}
|
|
4992
5047
|
const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
|
|
4993
|
-
const brandStillResolves = existingBrand ? existsSync15(
|
|
5048
|
+
const brandStillResolves = existingBrand ? existsSync15(join28(opts.projectRoot, ...existingBrand.split("/"))) : false;
|
|
4994
5049
|
const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
|
|
4995
5050
|
const merged = {
|
|
4996
5051
|
brand: brandStillResolves ? existingBrand : brandRelPath,
|
|
@@ -5005,12 +5060,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5005
5060
|
}
|
|
5006
5061
|
const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
|
|
5007
5062
|
const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
|
|
5008
|
-
const wiringBrandAbsPath =
|
|
5063
|
+
const wiringBrandAbsPath = join28(opts.projectRoot, ...effectiveConfig.brand.split("/"));
|
|
5009
5064
|
const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
|
|
5010
5065
|
const modeAbsPaths = {};
|
|
5011
5066
|
for (const mode of declaredModes) {
|
|
5012
5067
|
const modeFileName = `mode.${mode}.css`;
|
|
5013
|
-
const modeAbsPath2 =
|
|
5068
|
+
const modeAbsPath2 = join28(opts.projectRoot, ...tokensRelDir, modeFileName);
|
|
5014
5069
|
modeAbsPaths[mode] = modeAbsPath2;
|
|
5015
5070
|
const modeRelPath = relKey(...tokensRelDir, modeFileName);
|
|
5016
5071
|
const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
|
|
@@ -5018,7 +5073,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5018
5073
|
if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
|
|
5019
5074
|
else if (modeState.modified) modeAction = "skipped-edited";
|
|
5020
5075
|
if (modeAction === "written") {
|
|
5021
|
-
const modeSourceRaw =
|
|
5076
|
+
const modeSourceRaw = readFileSync23(join28(opts.packageRoot, "tokens", modeFileName), "utf8");
|
|
5022
5077
|
const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
|
|
5023
5078
|
mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
|
|
5024
5079
|
writeFileSync5(modeAbsPath2, content, "utf8");
|
|
@@ -5032,7 +5087,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5032
5087
|
const barrelFor = (mode) => mode === primaryMode ? "theme.css" : `theme.${mode}.css`;
|
|
5033
5088
|
for (const mode of declaredModes) {
|
|
5034
5089
|
const rel = relKey(...tokensRelDir, barrelFor(mode));
|
|
5035
|
-
const abs =
|
|
5090
|
+
const abs = join28(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
|
|
5036
5091
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5037
5092
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
5038
5093
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
@@ -5047,7 +5102,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5047
5102
|
const declared = declaredTokenNames(
|
|
5048
5103
|
[brandAbsPath, modeAbsPath].map((f) => {
|
|
5049
5104
|
try {
|
|
5050
|
-
return
|
|
5105
|
+
return readFileSync23(f, "utf8");
|
|
5051
5106
|
} catch {
|
|
5052
5107
|
return "";
|
|
5053
5108
|
}
|
|
@@ -5072,7 +5127,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5072
5127
|
log(` This changes how components are written. Both styles are correct.`);
|
|
5073
5128
|
const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
|
|
5074
5129
|
if (answer === "y" || answer === "yes") {
|
|
5075
|
-
const abs =
|
|
5130
|
+
const abs = join28(opts.projectRoot, ...tokensRelDir, "utilities.css");
|
|
5076
5131
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5077
5132
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
5078
5133
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
@@ -5083,8 +5138,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5083
5138
|
files[rel] = checksum(content);
|
|
5084
5139
|
const target = findWireTarget(detection);
|
|
5085
5140
|
const importPath = target ? relativeImportPath(
|
|
5086
|
-
dirname7(
|
|
5087
|
-
|
|
5141
|
+
dirname7(join28(opts.projectRoot, target)),
|
|
5142
|
+
join28(opts.projectRoot, ...tokensRelDir, "utilities.css")
|
|
5088
5143
|
) : `./${rel}`;
|
|
5089
5144
|
log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
|
|
5090
5145
|
log(` @import "tailwindcss";`);
|
|
@@ -5102,11 +5157,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5102
5157
|
writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
|
|
5103
5158
|
if (relocateTo && currentBrand) {
|
|
5104
5159
|
const oldDir = currentBrand.split("/").slice(0, -1);
|
|
5105
|
-
const oldAbsDir =
|
|
5160
|
+
const oldAbsDir = join28(opts.projectRoot, ...oldDir);
|
|
5106
5161
|
if (existsSync15(oldAbsDir)) {
|
|
5107
5162
|
for (const name of readdirSync10(oldAbsDir).filter((f) => f.endsWith(".css"))) {
|
|
5108
5163
|
const rel = relKey(...oldDir, name);
|
|
5109
|
-
const abs =
|
|
5164
|
+
const abs = join28(oldAbsDir, name);
|
|
5110
5165
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5111
5166
|
if (!state.tracked || state.modified) {
|
|
5112
5167
|
log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
|
|
@@ -5120,21 +5175,21 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5120
5175
|
const wireTarget = findWireTarget(detection);
|
|
5121
5176
|
let wiring;
|
|
5122
5177
|
if (wireTarget) {
|
|
5123
|
-
const targetAbsDir = dirname7(
|
|
5178
|
+
const targetAbsDir = dirname7(join28(opts.projectRoot, wireTarget));
|
|
5124
5179
|
const barrelImport = relativeImportPath(
|
|
5125
5180
|
targetAbsDir,
|
|
5126
|
-
|
|
5181
|
+
join28(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
5127
5182
|
);
|
|
5128
5183
|
const snippet = `@import "${barrelImport}";`;
|
|
5129
5184
|
try {
|
|
5130
5185
|
if (relocateTo && currentBrand) {
|
|
5131
|
-
const abs =
|
|
5186
|
+
const abs = join28(opts.projectRoot, wireTarget);
|
|
5132
5187
|
const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
|
|
5133
|
-
const before = existsSync15(abs) ?
|
|
5188
|
+
const before = existsSync15(abs) ? readFileSync23(abs, "utf8") : "";
|
|
5134
5189
|
const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
|
|
5135
5190
|
if (after !== before) writeFileSync5(abs, after, "utf8");
|
|
5136
5191
|
}
|
|
5137
|
-
const outcome = wireBarrel(
|
|
5192
|
+
const outcome = wireBarrel(join28(opts.projectRoot, wireTarget), barrelImport);
|
|
5138
5193
|
wiring = { target: wireTarget, status: outcome.status, snippet };
|
|
5139
5194
|
const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
|
|
5140
5195
|
log(`
|
|
@@ -5151,7 +5206,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5151
5206
|
} else {
|
|
5152
5207
|
const barrelImport = relativeImportPath(
|
|
5153
5208
|
opts.projectRoot,
|
|
5154
|
-
|
|
5209
|
+
join28(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
5155
5210
|
);
|
|
5156
5211
|
const snippet = `@import "${barrelImport}";`;
|
|
5157
5212
|
wiring = { target: null, status: "print-only", snippet };
|
|
@@ -5160,7 +5215,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5160
5215
|
if (pasteTargets.length > 0) {
|
|
5161
5216
|
log("Paste the block for whichever of these is your global stylesheet:");
|
|
5162
5217
|
for (const target of pasteTargets) {
|
|
5163
|
-
const dir = dirname7(
|
|
5218
|
+
const dir = dirname7(join28(opts.projectRoot, target));
|
|
5164
5219
|
const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
|
|
5165
5220
|
@import "${relativeImportPath(dir, modeAbsPath)}";`;
|
|
5166
5221
|
log(`
|
|
@@ -5207,28 +5262,28 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5207
5262
|
}
|
|
5208
5263
|
|
|
5209
5264
|
// src/commands/verdicts.ts
|
|
5210
|
-
import { existsSync as existsSync20, readFileSync as
|
|
5211
|
-
import { join as
|
|
5265
|
+
import { existsSync as existsSync20, readFileSync as readFileSync27 } from "fs";
|
|
5266
|
+
import { join as join34, resolve as resolve4 } from "path";
|
|
5212
5267
|
|
|
5213
5268
|
// src/rules/citations.ts
|
|
5214
|
-
import { join as
|
|
5269
|
+
import { join as join29 } from "path";
|
|
5215
5270
|
function citableIds(packageRoot2) {
|
|
5216
|
-
const rulesDir =
|
|
5271
|
+
const rulesDir = join29(packageRoot2, "rules");
|
|
5217
5272
|
const ids = [
|
|
5218
|
-
...loadRules(rulesDir,
|
|
5273
|
+
...loadRules(rulesDir, join29(packageRoot2, "rules.index.json")).map((r) => r.id),
|
|
5219
5274
|
...loadSpecs(rulesDir).map((s) => s.id)
|
|
5220
5275
|
];
|
|
5221
5276
|
return [...new Set(ids)].sort();
|
|
5222
5277
|
}
|
|
5223
5278
|
|
|
5224
5279
|
// src/probe/check.ts
|
|
5225
|
-
import { existsSync as existsSync18, readdirSync as readdirSync13, readFileSync as
|
|
5226
|
-
import { join as
|
|
5280
|
+
import { existsSync as existsSync18, readdirSync as readdirSync13, readFileSync as readFileSync25 } from "fs";
|
|
5281
|
+
import { join as join32 } from "path";
|
|
5227
5282
|
|
|
5228
5283
|
// src/probe/save.ts
|
|
5229
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as
|
|
5284
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync24, writeFileSync as writeFileSync6 } from "fs";
|
|
5230
5285
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5231
|
-
import { join as
|
|
5286
|
+
import { join as join31, relative as relative3, resolve as resolve3 } from "path";
|
|
5232
5287
|
|
|
5233
5288
|
// src/probe/script.ts
|
|
5234
5289
|
var PROBE_VERSION = 5;
|
|
@@ -5375,7 +5430,7 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
5375
5430
|
import { spawn } from "child_process";
|
|
5376
5431
|
import { existsSync as existsSync16, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync3 } from "fs";
|
|
5377
5432
|
import { homedir, tmpdir } from "os";
|
|
5378
|
-
import { join as
|
|
5433
|
+
import { join as join30 } from "path";
|
|
5379
5434
|
var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
|
|
5380
5435
|
var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"];
|
|
5381
5436
|
var CHROME_APPS = [
|
|
@@ -5387,10 +5442,10 @@ var CHROME_APPS = [
|
|
|
5387
5442
|
];
|
|
5388
5443
|
function cachedBrowsers() {
|
|
5389
5444
|
const roots = [
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5445
|
+
join30(homedir(), ".cache", "ms-playwright"),
|
|
5446
|
+
join30(homedir(), "Library", "Caches", "ms-playwright"),
|
|
5447
|
+
join30(homedir(), "AppData", "Local", "ms-playwright"),
|
|
5448
|
+
join30(homedir(), ".cache", "puppeteer", "chrome")
|
|
5394
5449
|
];
|
|
5395
5450
|
const found = [];
|
|
5396
5451
|
for (const root of roots) {
|
|
@@ -5410,7 +5465,7 @@ function cachedBrowsers() {
|
|
|
5410
5465
|
["chrome-win", "chrome.exe"],
|
|
5411
5466
|
["chrome-win64", "chrome.exe"]
|
|
5412
5467
|
]) {
|
|
5413
|
-
const candidate =
|
|
5468
|
+
const candidate = join30(root, entry, ...rel);
|
|
5414
5469
|
if (existsSync16(candidate)) found.push(candidate);
|
|
5415
5470
|
}
|
|
5416
5471
|
}
|
|
@@ -5433,7 +5488,7 @@ function which(command) {
|
|
|
5433
5488
|
const dirs = (process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
|
|
5434
5489
|
for (const dir of dirs) {
|
|
5435
5490
|
if (!dir) continue;
|
|
5436
|
-
const candidate =
|
|
5491
|
+
const candidate = join30(dir, command);
|
|
5437
5492
|
if (existsSync16(candidate)) return candidate;
|
|
5438
5493
|
}
|
|
5439
5494
|
return void 0;
|
|
@@ -5445,7 +5500,7 @@ async function runProbe(opts) {
|
|
|
5445
5500
|
"No browser found to run the probe. Install one, or set JIG_CHROME to a Chrome, Chromium or Edge binary. A project with Playwright or Puppeteer already has one, and Jig will find it."
|
|
5446
5501
|
);
|
|
5447
5502
|
}
|
|
5448
|
-
const profile = mkdtempSync(
|
|
5503
|
+
const profile = mkdtempSync(join30(tmpdir(), "jig-probe-"));
|
|
5449
5504
|
const child = spawn(chrome, [
|
|
5450
5505
|
"--headless=new",
|
|
5451
5506
|
"--remote-debugging-port=0",
|
|
@@ -5558,11 +5613,11 @@ function saveProbe(opts) {
|
|
|
5558
5613
|
throw new Error(`The probe ran at ${url || "no url"}, which is not a file in this project. Open the page you built \u2014 a file:// URL under the project \u2014 and probe that.`);
|
|
5559
5614
|
}
|
|
5560
5615
|
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
5561
|
-
probe.pageChecksum = checksum(
|
|
5616
|
+
probe.pageChecksum = checksum(readFileSync24(page, "utf8"));
|
|
5562
5617
|
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5563
|
-
const dir =
|
|
5618
|
+
const dir = join31(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
5564
5619
|
mkdirSync7(dir, { recursive: true });
|
|
5565
|
-
const path =
|
|
5620
|
+
const path = join31(dir, `probe-${width}.json`);
|
|
5566
5621
|
writeFileSync6(path, JSON.stringify(probe), "utf8");
|
|
5567
5622
|
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
5568
5623
|
}
|
|
@@ -5591,17 +5646,17 @@ async function runAndSaveProbes(opts) {
|
|
|
5591
5646
|
return saved;
|
|
5592
5647
|
}
|
|
5593
5648
|
async function ensureProbes(opts) {
|
|
5594
|
-
const dir =
|
|
5649
|
+
const dir = join31(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
5595
5650
|
const abs = resolve3(opts.projectRoot, opts.page);
|
|
5596
5651
|
let current;
|
|
5597
5652
|
try {
|
|
5598
|
-
current = checksum(
|
|
5653
|
+
current = checksum(readFileSync24(abs, "utf8"));
|
|
5599
5654
|
} catch {
|
|
5600
5655
|
return { recorded: [], reason: `${opts.page} could not be read` };
|
|
5601
5656
|
}
|
|
5602
5657
|
const missing = PROBE_WIDTHS.filter((width) => {
|
|
5603
5658
|
try {
|
|
5604
|
-
const probe = JSON.parse(
|
|
5659
|
+
const probe = JSON.parse(readFileSync24(join31(dir, `probe-${width}.json`), "utf8"));
|
|
5605
5660
|
return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
|
|
5606
5661
|
} catch {
|
|
5607
5662
|
return true;
|
|
@@ -5616,9 +5671,9 @@ async function ensureProbes(opts) {
|
|
|
5616
5671
|
return { recorded: missing };
|
|
5617
5672
|
}
|
|
5618
5673
|
function critiquedSurfaces(projectRoot) {
|
|
5619
|
-
const root =
|
|
5674
|
+
const root = join31(projectRoot, ".jig", "critique");
|
|
5620
5675
|
if (!existsSync17(root)) return [];
|
|
5621
|
-
return readdirSync12(root).filter((surface) => existsSync17(
|
|
5676
|
+
return readdirSync12(root).filter((surface) => existsSync17(join31(root, surface, "screen.json")) || existsSync17(join31(root, surface, "code.json")));
|
|
5622
5677
|
}
|
|
5623
5678
|
|
|
5624
5679
|
// src/probe/check.ts
|
|
@@ -5630,7 +5685,7 @@ function stampProblem(projectRoot, file, p) {
|
|
|
5630
5685
|
if (!page) return `${file} names a page outside this project (${p.url ?? "no url"}).`;
|
|
5631
5686
|
let current;
|
|
5632
5687
|
try {
|
|
5633
|
-
current =
|
|
5688
|
+
current = readFileSync25(page, "utf8");
|
|
5634
5689
|
} catch {
|
|
5635
5690
|
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
5636
5691
|
}
|
|
@@ -5644,7 +5699,7 @@ function readProbes(projectRoot, dir, errors) {
|
|
|
5644
5699
|
const probes = [];
|
|
5645
5700
|
for (const f of readdirSync13(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
5646
5701
|
try {
|
|
5647
|
-
const p = JSON.parse(
|
|
5702
|
+
const p = JSON.parse(readFileSync25(join32(dir, f), "utf8"));
|
|
5648
5703
|
if (p.jigProbe !== PROBE_VERSION || typeof p.width !== "number") {
|
|
5649
5704
|
errors.push(`${f} is not output of \`jig probe\` (version ${PROBE_VERSION}). Re-run the probe; do not write it by hand.`);
|
|
5650
5705
|
continue;
|
|
@@ -5736,13 +5791,19 @@ function probeContradictions(probes, verdictOf, indexable = true) {
|
|
|
5736
5791
|
}
|
|
5737
5792
|
|
|
5738
5793
|
// src/check/decisions.ts
|
|
5739
|
-
import { existsSync as existsSync19, readFileSync as
|
|
5740
|
-
import { join as
|
|
5794
|
+
import { existsSync as existsSync19, readFileSync as readFileSync26 } from "fs";
|
|
5795
|
+
import { join as join33, posix as posix3 } from "path";
|
|
5741
5796
|
var HEADING3 = /^(#{2,3})\s+(.+?)\s*$/gm;
|
|
5742
5797
|
var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
|
|
5743
5798
|
function decisionsFile(projectRoot) {
|
|
5744
|
-
|
|
5745
|
-
|
|
5799
|
+
const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
|
|
5800
|
+
try {
|
|
5801
|
+
const brand = JSON.parse(readFileSync26(join33(projectRoot, "jig.config.json"), "utf8")).brand;
|
|
5802
|
+
if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
|
|
5803
|
+
} catch {
|
|
5804
|
+
}
|
|
5805
|
+
for (const candidate of candidates) {
|
|
5806
|
+
if (existsSync19(join33(projectRoot, candidate))) return candidate;
|
|
5746
5807
|
}
|
|
5747
5808
|
return void 0;
|
|
5748
5809
|
}
|
|
@@ -5751,7 +5812,7 @@ function decisionNames(projectRoot) {
|
|
|
5751
5812
|
if (!path) return [];
|
|
5752
5813
|
let body;
|
|
5753
5814
|
try {
|
|
5754
|
-
body =
|
|
5815
|
+
body = readFileSync26(join33(projectRoot, path), "utf8");
|
|
5755
5816
|
} catch {
|
|
5756
5817
|
return [];
|
|
5757
5818
|
}
|
|
@@ -5770,17 +5831,17 @@ var ABSENCE = /\b(rule (not found|does not exist)|context unavailable|cannot (fi
|
|
|
5770
5831
|
function readJson(path, errors) {
|
|
5771
5832
|
if (!existsSync20(path)) return null;
|
|
5772
5833
|
try {
|
|
5773
|
-
return JSON.parse(
|
|
5834
|
+
return JSON.parse(readFileSync27(path, "utf8"));
|
|
5774
5835
|
} catch (e) {
|
|
5775
5836
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
5776
5837
|
return {};
|
|
5777
5838
|
}
|
|
5778
5839
|
}
|
|
5779
5840
|
function specIndexable(projectRoot, surface) {
|
|
5780
|
-
const path =
|
|
5841
|
+
const path = join34(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
5781
5842
|
let front = "";
|
|
5782
5843
|
try {
|
|
5783
|
-
front =
|
|
5844
|
+
front = readFileSync27(path, "utf8").split(/^---\s*$/m)[1] ?? "";
|
|
5784
5845
|
} catch {
|
|
5785
5846
|
}
|
|
5786
5847
|
const declared = /^\s*indexable\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase();
|
|
@@ -5790,9 +5851,9 @@ function specIndexable(projectRoot, surface) {
|
|
|
5790
5851
|
return mode !== "product" && mode !== "operator";
|
|
5791
5852
|
}
|
|
5792
5853
|
function specNeedsNav(projectRoot, surface) {
|
|
5793
|
-
const path =
|
|
5854
|
+
const path = join34(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
5794
5855
|
if (!existsSync20(path)) return false;
|
|
5795
|
-
const front =
|
|
5856
|
+
const front = readFileSync27(path, "utf8").split(/^---\s*$/m)[1] ?? "";
|
|
5796
5857
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
5797
5858
|
const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
|
|
5798
5859
|
return navField || navRegion;
|
|
@@ -5843,7 +5904,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
5843
5904
|
function checkDecisions(projectRoot, dir, errors) {
|
|
5844
5905
|
const required = decisionNames(projectRoot);
|
|
5845
5906
|
if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
|
|
5846
|
-
const file = readJson(
|
|
5907
|
+
const file = readJson(join34(dir, "decisions.json"), errors);
|
|
5847
5908
|
if (!file) {
|
|
5848
5909
|
errors.push(
|
|
5849
5910
|
`decisions.json is missing. Every decision in DECISIONS.md is judged against the built page, one verdict each: ${required.slice(0, 4).join(", ")}${required.length > 4 ? `, and ${required.length - 4} more` : ""}. A page can satisfy every rule and still break what this project decided.`
|
|
@@ -5888,7 +5949,7 @@ function checkDecisions(projectRoot, dir, errors) {
|
|
|
5888
5949
|
function verifyVerdicts(opts) {
|
|
5889
5950
|
const root = opts.packageRoot ?? assetRoot();
|
|
5890
5951
|
const errors = [];
|
|
5891
|
-
const index = JSON.parse(
|
|
5952
|
+
const index = JSON.parse(readFileSync27(join34(root, "rules.index.json"), "utf8"));
|
|
5892
5953
|
const judgment = index.filter((r) => r.bucket === "judgment");
|
|
5893
5954
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
5894
5955
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -5897,9 +5958,9 @@ function verifyVerdicts(opts) {
|
|
|
5897
5958
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
5898
5959
|
]);
|
|
5899
5960
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
5900
|
-
const dir =
|
|
5901
|
-
const screenFile = readJson(
|
|
5902
|
-
const codeFile = readJson(
|
|
5961
|
+
const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
5962
|
+
const screenFile = readJson(join34(dir, "screen.json"), errors);
|
|
5963
|
+
const codeFile = readJson(join34(dir, "code.json"), errors);
|
|
5903
5964
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
5904
5965
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors);
|
|
5905
5966
|
const code = checkArm("code", codeFile, codeIds, passOf, specIds, [], errors);
|
|
@@ -5934,20 +5995,20 @@ function verifyVerdicts(opts) {
|
|
|
5934
5995
|
}
|
|
5935
5996
|
|
|
5936
5997
|
// src/commands/gate.ts
|
|
5937
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as
|
|
5998
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync29, writeFileSync as writeFileSync7 } from "fs";
|
|
5938
5999
|
import { createHash as createHash2 } from "crypto";
|
|
5939
|
-
import { join as
|
|
6000
|
+
import { join as join36 } from "path";
|
|
5940
6001
|
|
|
5941
6002
|
// src/check/spec-shape.ts
|
|
5942
|
-
import { existsSync as existsSync21, readFileSync as
|
|
5943
|
-
import { join as
|
|
6003
|
+
import { existsSync as existsSync21, readFileSync as readFileSync28, readdirSync as readdirSync14, statSync as statSync2 } from "fs";
|
|
6004
|
+
import { join as join35 } from "path";
|
|
5944
6005
|
function newestSpec(projectRoot) {
|
|
5945
|
-
const dir =
|
|
6006
|
+
const dir = join35(projectRoot, ".jig", "specs");
|
|
5946
6007
|
if (!existsSync21(dir)) return void 0;
|
|
5947
6008
|
const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
|
|
5948
6009
|
if (files.length === 0) return void 0;
|
|
5949
|
-
const newest = files.map((f) => ({ f, at: statSync2(
|
|
5950
|
-
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body:
|
|
6010
|
+
const newest = files.map((f) => ({ f, at: statSync2(join35(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
|
|
6011
|
+
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readFileSync28(join35(dir, newest), "utf8") };
|
|
5951
6012
|
}
|
|
5952
6013
|
function specProblems(spec) {
|
|
5953
6014
|
const parts = spec.body.split(/^---\s*$/m);
|
|
@@ -6019,7 +6080,7 @@ function lastJigCommand(transcriptPath) {
|
|
|
6019
6080
|
if (!transcriptPath || !existsSync22(transcriptPath)) return void 0;
|
|
6020
6081
|
let text;
|
|
6021
6082
|
try {
|
|
6022
|
-
text =
|
|
6083
|
+
text = readFileSync29(transcriptPath, "utf8");
|
|
6023
6084
|
} catch {
|
|
6024
6085
|
return void 0;
|
|
6025
6086
|
}
|
|
@@ -6033,14 +6094,41 @@ function lastJigCommand(transcriptPath) {
|
|
|
6033
6094
|
}
|
|
6034
6095
|
return found;
|
|
6035
6096
|
}
|
|
6097
|
+
function lastAssistantText(transcriptPath) {
|
|
6098
|
+
if (!transcriptPath || !existsSync22(transcriptPath)) return void 0;
|
|
6099
|
+
let found;
|
|
6100
|
+
try {
|
|
6101
|
+
for (const line of readFileSync29(transcriptPath, "utf8").split("\n")) {
|
|
6102
|
+
if (!line.includes('"assistant"')) continue;
|
|
6103
|
+
let entry;
|
|
6104
|
+
try {
|
|
6105
|
+
entry = JSON.parse(line);
|
|
6106
|
+
} catch {
|
|
6107
|
+
continue;
|
|
6108
|
+
}
|
|
6109
|
+
if (entry.type !== "assistant" || !Array.isArray(entry.message?.content)) continue;
|
|
6110
|
+
const text = entry.message.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("");
|
|
6111
|
+
if (text.trim()) found = text;
|
|
6112
|
+
}
|
|
6113
|
+
} catch {
|
|
6114
|
+
return void 0;
|
|
6115
|
+
}
|
|
6116
|
+
return found;
|
|
6117
|
+
}
|
|
6118
|
+
function asksOwner(text) {
|
|
6119
|
+
if (!text) return false;
|
|
6120
|
+
const last = text.trim().split("\n").filter((l) => l.trim()).pop() ?? "";
|
|
6121
|
+
return /\?\s*$/.test(last.replace(/[*_`"')\]\s]+$/, ""));
|
|
6122
|
+
}
|
|
6123
|
+
var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup"]);
|
|
6036
6124
|
function commandProblems(root, command) {
|
|
6037
6125
|
const problems = [];
|
|
6038
6126
|
const spec = newestSpec(root);
|
|
6039
6127
|
if (command === "decide") {
|
|
6040
|
-
const
|
|
6041
|
-
if (!
|
|
6128
|
+
const found = decisionsFile(root);
|
|
6129
|
+
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
6042
6130
|
else {
|
|
6043
|
-
const body =
|
|
6131
|
+
const body = readFileSync29(join36(root, found), "utf8");
|
|
6044
6132
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
6045
6133
|
problems.push("DECISIONS.md has no `## Unresolved` section. Round 3 asks by name what is still undecided; write what the owner named, or `None named by the owner.`");
|
|
6046
6134
|
}
|
|
@@ -6061,13 +6149,13 @@ function commandProblems(root, command) {
|
|
|
6061
6149
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
6062
6150
|
if (/^approved/i.test(mockup)) {
|
|
6063
6151
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
6064
|
-
else if (!/^https?:/i.test(at) && !existsSync22(
|
|
6152
|
+
else if (!/^https?:/i.test(at) && !existsSync22(join36(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
|
|
6065
6153
|
else if (!/^https?:/i.test(at) && !at.startsWith(".jig/mockups/")) problems.push(`The drawing is at ${at}. A mockup lives in .jig/mockups/, outside what check scans and outside what ships.`);
|
|
6066
6154
|
}
|
|
6067
6155
|
}
|
|
6068
6156
|
if (command === "critique") {
|
|
6069
|
-
const dir =
|
|
6070
|
-
const surfaces = existsSync22(dir) ? readdirSync15(dir).filter((s) => existsSync22(
|
|
6157
|
+
const dir = join36(root, ".jig", "critique");
|
|
6158
|
+
const surfaces = existsSync22(dir) ? readdirSync15(dir).filter((s) => existsSync22(join36(dir, s, "screen.json")) || existsSync22(join36(dir, s, "code.json"))) : [];
|
|
6071
6159
|
for (const surface of surfaces) {
|
|
6072
6160
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
6073
6161
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -6089,36 +6177,43 @@ function surfacePage(projectRoot, surface) {
|
|
|
6089
6177
|
const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
6090
6178
|
const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
|
|
6091
6179
|
for (const candidate of candidates) {
|
|
6092
|
-
if (candidate && existsSync22(
|
|
6180
|
+
if (candidate && existsSync22(join36(projectRoot, candidate))) return candidate;
|
|
6093
6181
|
}
|
|
6094
6182
|
return void 0;
|
|
6095
6183
|
}
|
|
6096
6184
|
function gate(opts) {
|
|
6097
6185
|
const root = opts.projectRoot;
|
|
6098
|
-
if (!existsSync22(
|
|
6186
|
+
if (!existsSync22(join36(root, "jig.config.json")) && !existsSync22(join36(root, ".jig"))) {
|
|
6099
6187
|
return { block: false, reason: "" };
|
|
6100
6188
|
}
|
|
6101
6189
|
const command = lastJigCommand(opts.input.transcript_path);
|
|
6102
|
-
const
|
|
6190
|
+
const waiting = command !== void 0 && ASKS_THE_OWNER.has(command) && asksOwner(lastAssistantText(opts.input.transcript_path));
|
|
6191
|
+
const problems = command && !waiting ? commandProblems(root, command).map((p) => `/jig ${command}: ${p}`) : [];
|
|
6103
6192
|
const selection = selectFiles(root, false);
|
|
6104
|
-
const
|
|
6105
|
-
if (
|
|
6193
|
+
const changedUi = selection.mode === "changed" && selection.files.some((f) => isStyleBearing(f) || isReaderText(f));
|
|
6194
|
+
if (changedUi) {
|
|
6106
6195
|
const result = check({ projectRoot: root, homeDir: "", version: opts.version, all: false, ci: false });
|
|
6107
6196
|
const errors = result.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error");
|
|
6108
|
-
|
|
6109
|
-
|
|
6197
|
+
const warnings = result.findings.filter((f) => f.severity === "warning");
|
|
6198
|
+
const blocking = [...errors, ...warnings];
|
|
6199
|
+
if (blocking.length > 0) {
|
|
6200
|
+
const shown = blocking.slice(0, 8).map((f) => ` ${f.severity === "error" ? "error " : "warning"} ${f.ruleId} ${f.file}:${f.line} ${f.message}`);
|
|
6201
|
+
const counted = [
|
|
6202
|
+
errors.length > 0 ? `${errors.length} mechanical error(s)` : "",
|
|
6203
|
+
warnings.length > 0 ? `${warnings.length} warning(s)` : ""
|
|
6204
|
+
].filter(Boolean).join(" and ");
|
|
6110
6205
|
problems.push(
|
|
6111
|
-
`jig check: ${
|
|
6112
|
-
` + shown.join("\n") + (
|
|
6113
|
-
\u2026 and ${
|
|
6206
|
+
`jig check: ${counted} in the files you changed. Fix every one. A warning that is right as it stands can be waived on its own line with a comment reading \`jig-allow <ID>: <why>\`; an error cannot. Then run \`jig check\` again.
|
|
6207
|
+
` + shown.join("\n") + (blocking.length > shown.length ? `
|
|
6208
|
+
\u2026 and ${blocking.length - shown.length} more` : "")
|
|
6114
6209
|
);
|
|
6115
6210
|
}
|
|
6116
6211
|
}
|
|
6117
|
-
const critiqueDir =
|
|
6212
|
+
const critiqueDir = join36(root, ".jig", "critique");
|
|
6118
6213
|
if (existsSync22(critiqueDir)) {
|
|
6119
6214
|
for (const surface of readdirSync15(critiqueDir)) {
|
|
6120
|
-
const dir =
|
|
6121
|
-
if (!existsSync22(
|
|
6215
|
+
const dir = join36(critiqueDir, surface);
|
|
6216
|
+
if (!existsSync22(join36(dir, "screen.json")) && !existsSync22(join36(dir, "code.json"))) continue;
|
|
6122
6217
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
6123
6218
|
if (!v.ok) {
|
|
6124
6219
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -6132,10 +6227,10 @@ function gate(opts) {
|
|
|
6132
6227
|
}
|
|
6133
6228
|
const session = opts.input.session_id ?? "unknown";
|
|
6134
6229
|
const key = `${session}:${createHash2("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
6135
|
-
const stateFile =
|
|
6230
|
+
const stateFile = join36(root, ".jig", "gate.json");
|
|
6136
6231
|
let state = {};
|
|
6137
6232
|
try {
|
|
6138
|
-
state = JSON.parse(
|
|
6233
|
+
state = JSON.parse(readFileSync29(stateFile, "utf8"));
|
|
6139
6234
|
} catch {
|
|
6140
6235
|
}
|
|
6141
6236
|
if (problems.length === 0) {
|
|
@@ -6165,15 +6260,15 @@ ${problems.join("\n\n")}`
|
|
|
6165
6260
|
}
|
|
6166
6261
|
function save(file, state) {
|
|
6167
6262
|
try {
|
|
6168
|
-
mkdirSync8(
|
|
6263
|
+
mkdirSync8(join36(file, ".."), { recursive: true });
|
|
6169
6264
|
writeFileSync7(file, JSON.stringify(state), "utf8");
|
|
6170
6265
|
} catch {
|
|
6171
6266
|
}
|
|
6172
6267
|
}
|
|
6173
6268
|
|
|
6174
6269
|
// src/commands/seo.ts
|
|
6175
|
-
import { readFileSync as
|
|
6176
|
-
import { join as
|
|
6270
|
+
import { readFileSync as readFileSync30 } from "fs";
|
|
6271
|
+
import { join as join37 } from "path";
|
|
6177
6272
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
6178
6273
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
6179
6274
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -6214,7 +6309,7 @@ function seo(opts) {
|
|
|
6214
6309
|
for (const file of files) {
|
|
6215
6310
|
let raw;
|
|
6216
6311
|
try {
|
|
6217
|
-
raw =
|
|
6312
|
+
raw = readFileSync30(join37(root, file), "utf8");
|
|
6218
6313
|
} catch {
|
|
6219
6314
|
continue;
|
|
6220
6315
|
}
|
|
@@ -6270,7 +6365,7 @@ function seo(opts) {
|
|
|
6270
6365
|
|
|
6271
6366
|
// src/index.ts
|
|
6272
6367
|
var packageRoot = getPackageRoot();
|
|
6273
|
-
var { version } = JSON.parse(
|
|
6368
|
+
var { version } = JSON.parse(readFileSync31(join38(packageRoot, "package.json"), "utf8"));
|
|
6274
6369
|
function warnIfUnpublishedPin() {
|
|
6275
6370
|
if (isPublishedBuild(packageRoot)) return;
|
|
6276
6371
|
console.warn(
|
|
@@ -6315,6 +6410,12 @@ program.command("install").description("Install Jig rules and the agent skill fi
|
|
|
6315
6410
|
console.warn(result.warning);
|
|
6316
6411
|
return;
|
|
6317
6412
|
}
|
|
6413
|
+
if (result.hookOnly) {
|
|
6414
|
+
console.log(result.hookOnly);
|
|
6415
|
+
if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
|
|
6416
|
+
if (result.stopHook === false) console.log(" ! .claude/settings.json is not valid JSON \u2014 the Stop hook was not added. Fix the file and run install again.");
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6318
6419
|
console.log(`Installed Jig v${version} for ${opts.agent} (${opts.scope} scope)`);
|
|
6319
6420
|
warnIfUnpublishedPin();
|
|
6320
6421
|
for (const f of result.written) console.log(` + ${f}`);
|
|
@@ -6456,7 +6557,7 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
6456
6557
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
6457
6558
|
let input = {};
|
|
6458
6559
|
try {
|
|
6459
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
6560
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync31(0, "utf8") || "{}");
|
|
6460
6561
|
} catch {
|
|
6461
6562
|
}
|
|
6462
6563
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -6495,7 +6596,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
6495
6596
|
});
|
|
6496
6597
|
function resolveSurfaceMode(projectRoot) {
|
|
6497
6598
|
try {
|
|
6498
|
-
const config = JSON.parse(
|
|
6599
|
+
const config = JSON.parse(readFileSync31(join38(projectRoot, "jig.config.json"), "utf8"));
|
|
6499
6600
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
6500
6601
|
return modes.length === 1 ? modes[0] : void 0;
|
|
6501
6602
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A design system for coding agents. 130 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -759,8 +759,9 @@ menu and reported "matched exactly".
|
|
|
759
759
|
**1. `check` passes.** Run `{{scripts_path}} check --all` and paste its
|
|
760
760
|
`JIG_CHECK:` line into your report, as printed. `mechanical=fail:<n>` means not done:
|
|
761
761
|
fix every error and run it again. No mechanical finding is non-blocking, and you do
|
|
762
|
-
not get to decide one is.
|
|
763
|
-
|
|
762
|
+
not get to decide one is. Every warning is fixed, or waived on its own line with
|
|
763
|
+
`jig-allow <ID>: <why>` (see `gate`); an explanation in your report is not a waiver.
|
|
764
|
+
A report with no `JIG_CHECK:` line, or one you typed yourself, is not a finished
|
|
764
765
|
build.
|
|
765
766
|
|
|
766
767
|
**2. The build matches the approved mockup at each size.** Skip only when
|
|
@@ -1162,9 +1163,10 @@ safe to run on the first day and on somebody else's codebase. It audits the whol
|
|
|
1162
1163
|
which is what one file cannot: a route whose metadata says `noindex` sitting in
|
|
1163
1164
|
the sitemap, two pages claiming the same title, a sitemap that lists nothing or
|
|
1164
1165
|
lists paths instead of full URLs. It counts the pages it found, a dynamic route
|
|
1165
|
-
once
|
|
1166
|
-
no
|
|
1167
|
-
|
|
1166
|
+
once and an endpoint (`rss.xml.ts`, `[id].txt.ts`, `api/`) not at all, since it
|
|
1167
|
+
serves no page. It also counts whether a sitemap and a robots file exist;
|
|
1168
|
+
neither is a finding, since no rule asks for them and a project with no domain
|
|
1169
|
+
yet cannot write an honest sitemap. Do not invent one to clear a count.
|
|
1168
1170
|
|
|
1169
1171
|
It is not a phase. The per-page half belongs where the copy is decided and
|
|
1170
1172
|
written — `spec` records whether a page is indexable and what it claims, `make`
|
|
@@ -1176,10 +1178,18 @@ positioning it had already dropped.
|
|
|
1176
1178
|
## gate
|
|
1177
1179
|
|
|
1178
1180
|
Not a command to run. In Claude Code, `install` adds a Stop hook that runs
|
|
1179
|
-
`{{scripts_path}} gate` whenever you try to finish. If files you changed
|
|
1180
|
-
or a critique's verdict files fail `verdicts`, it refuses
|
|
1181
|
-
you the failures as the reason. **Fix what it names.** Do
|
|
1182
|
-
satisfy it, and do not report the work as done while it is
|
|
1181
|
+
`{{scripts_path}} gate` whenever you try to finish. If files you changed have a
|
|
1182
|
+
`check` error or warning, or a critique's verdict files fail `verdicts`, it refuses
|
|
1183
|
+
to let you stop and hands you the failures as the reason. **Fix what it names.** Do
|
|
1184
|
+
not edit verdict files to satisfy it, and do not report the work as done while it is
|
|
1185
|
+
blocking.
|
|
1186
|
+
|
|
1187
|
+
A warning you are sure is right as it stands, because the detector guessed and the
|
|
1188
|
+
guess is wrong here, is waived on its own line or the line above, in the file's
|
|
1189
|
+
comment syntax: `jig-allow <ID>: <why>`. The reason is what the user reads, so write
|
|
1190
|
+
the case, not "intended". An error cannot be waived, a waiver with no reason waives
|
|
1191
|
+
nothing, and `check` prints every waiver it honoured. Waiving a warning to get past
|
|
1192
|
+
the gate, when the fix was available, is the failure the gate exists to catch. After three
|
|
1183
1193
|
refusals in one session it lets you stop; then tell the user plainly that the work is
|
|
1184
1194
|
not finished and what is still failing.
|
|
1185
1195
|
|
package/templates/SKILL.md.tmpl
CHANGED
|
@@ -14,18 +14,21 @@ cite the number when you follow or deliberately break one.
|
|
|
14
14
|
b. `DECISIONS.md` exists beside the token files — `jig/DECISIONS.md` by
|
|
15
15
|
default, or wherever `brand` in `{{config_file}}` puts them — and has
|
|
16
16
|
substance: not empty, no `[TODO]` markers left in it, more than a few
|
|
17
|
-
lines. If it does not,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
lines. If it does not, stop and ask the user to run
|
|
18
|
+
`{{command_prefix}}decide`. The interview lives in that command, not in
|
|
19
|
+
this file: questions you make up from here are not `decide`, and neither
|
|
20
|
+
is a file written from their answers. Do not write it yourself from this
|
|
21
|
+
task's prompt — it records decisions the project has made, and a file you
|
|
22
|
+
author alone records decisions *you* have made, which is the opposite
|
|
23
|
+
thing wearing the same name.
|
|
22
24
|
1. Load `{{rules_path}}/00-anti-patterns.md` and `{{rules_path}}/01-modes.md`.
|
|
23
25
|
2. Determine the mode from `{{config_file}}`, or infer it using the procedure in
|
|
24
26
|
`{{rules_path}}/01-modes.md` and state the inference in one line before building.
|
|
25
|
-
3. **Building a page, feature or functionality?**
|
|
27
|
+
3. **Building a page, feature or functionality?** It goes through
|
|
26
28
|
`{{command_prefix}}spec` → `{{command_prefix}}mockup` (low fidelity) →
|
|
27
29
|
`{{command_prefix}}make` (high fidelity) → `{{command_prefix}}critique`, one at a
|
|
28
|
-
time
|
|
30
|
+
time, each one run by the user. The procedure for each step is in the command,
|
|
31
|
+
so ask for the step you need rather than doing it from this summary. Never design the whole product up front. Critique findings go back to
|
|
29
32
|
`make`, then `critique` again, until clean or accepted by the user — then the
|
|
30
33
|
next feature.
|
|
31
34
|
**Building a screen rather than a single component?** Read `L-01 · Layout
|