novahiz 0.9.5 → 0.9.6
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 +51 -0
- package/catalog/rules.json +10 -5
- package/dist/commands/doctor.js +59 -6
- package/dist/gate.js +9 -0
- package/install/install.mjs +8 -0
- package/package.json +1 -1
- package/src/commands/doctor.ts +51 -7
- package/src/gate.ts +11 -0
- package/src/spec.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.9.6] - 2026-10-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`PRODUCT.md` and `DESIGN.md`** - the product context and the visual-system
|
|
13
|
+
record `impeccable` asks for, both written from sources in the repository
|
|
14
|
+
(`README.md`, `docs/CONSTITUTION.md`, `src/render.ts`) with anything they do
|
|
15
|
+
not settle marked "not determined" instead of guessed. `novahiz doctor` runs
|
|
16
|
+
from a project now finds both instead of two `fail` rows; when doctor runs
|
|
17
|
+
from the Novahiz home both rows are skipped by design
|
|
18
|
+
(`impeccableChecks` returns early when `cwd === home`).
|
|
19
|
+
|
|
20
|
+
- **`when.pathExcludes`** - a rule-level *negative* selector, the counterpart of
|
|
21
|
+
`pathGlobs`. A rule carrying it never applies to a matching path, whatever the
|
|
22
|
+
other selectors say. With `match: "any"` a prompt category alone fired on any
|
|
23
|
+
file, so `design-ui` demanded the frontend design skills on unit tests, and
|
|
24
|
+
inclusion-only selectors could not express "never on tests" (`**/*.tsx` also
|
|
25
|
+
matches `tests/Foo.test.tsx`). Applied to the five design-scoped rules
|
|
26
|
+
(R11-accessibility, R13-design-craft, R14-impeccable, R16-stitch-fidelity,
|
|
27
|
+
R17-wireframe), which now skip `**/tests/**`, `**/__tests__/**`,
|
|
28
|
+
`**/*.test.*` and `**/*.spec.*`. R9-code-review and R10-security are
|
|
29
|
+
untouched — a test is still code — and R15-token-economy keeps its own
|
|
30
|
+
`**/tests/token-economy.test.ts` target.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **The installer states its own limit** - `install.mjs` now prints that
|
|
35
|
+
`deps --install` is an install-only pass: packs already present are left
|
|
36
|
+
untouched, and `/novahiz-skills-update` (`deps --install --update`) is what
|
|
37
|
+
refreshes them. Installation and update stay separate commands by choice — a
|
|
38
|
+
reinstall must not overwrite local skill edits — but the choice was previously
|
|
39
|
+
only written down in `docs/PROVIDERS.md`, not shown to the person installing.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`doctor` no longer advises a command that cannot help** - the "Referenced
|
|
44
|
+
skills" row counted every absent non-shipped skill as a pack skill and
|
|
45
|
+
answered "run /novahiz-skills-update, or novahiz deps --install". Measured on
|
|
46
|
+
the 0.9.5 install: the 7 skills it named are `eas-app-stores`, `eas-hosting`,
|
|
47
|
+
`eas-observe`, `eas-simulator`, `eas-update`, `eas-update-insights`,
|
|
48
|
+
`eas-workflows`, referenced as `defaultSkills` by the `test` and `devops`
|
|
49
|
+
categories, while `catalog/providers.json` ships four skill packs only
|
|
50
|
+
(`flutter-skills`, `dart-skills`, `expo-skills`, `impeccable`) and the expo
|
|
51
|
+
command names 20 `expo-*` skills, no `eas-*`. `deps --install` accordingly
|
|
52
|
+
returned `install: []` and exited 0 - nothing to install. The row now reads
|
|
53
|
+
each pack's `install` argv (`--skill` list, or `--skill *` resolved to the
|
|
54
|
+
repo's leading segment, so `dart-lang/skills` covers `dart-*`), and skills no
|
|
55
|
+
pack can deliver are named as such instead of being sent after a command that
|
|
56
|
+
installs nothing. New `tests/doctor-referenced-skills.test.ts` pins the
|
|
57
|
+
contract - eight cases, none existed before.
|
|
58
|
+
|
|
8
59
|
## [0.9.5] - 2026-10-15
|
|
9
60
|
|
|
10
61
|
> 0.9.4 was prepared on 2026-10-14 but never tagged nor published to npm
|
package/catalog/rules.json
CHANGED
|
@@ -94,7 +94,8 @@
|
|
|
94
94
|
"description": "Load novahiz-wcag-audit on design-ui work and on audit prompts, where accessibility is in scope.",
|
|
95
95
|
"when": {
|
|
96
96
|
"match": "any",
|
|
97
|
-
"promptCategories": ["design-ui", "audit"]
|
|
97
|
+
"promptCategories": ["design-ui", "audit"],
|
|
98
|
+
"pathExcludes": ["**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*"]
|
|
98
99
|
},
|
|
99
100
|
"require": ["novahiz-wcag-audit"]
|
|
100
101
|
},
|
|
@@ -121,7 +122,8 @@
|
|
|
121
122
|
"**/*.html",
|
|
122
123
|
"**/*.htm"
|
|
123
124
|
],
|
|
124
|
-
"minChange": 200
|
|
125
|
+
"minChange": 200,
|
|
126
|
+
"pathExcludes": ["**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*"]
|
|
125
127
|
},
|
|
126
128
|
"require": ["novahiz-humanizer", "ui-slop-remover", "ui-craft-rules"]
|
|
127
129
|
},
|
|
@@ -139,7 +141,8 @@
|
|
|
139
141
|
"**/*.html",
|
|
140
142
|
"**/*.htm"
|
|
141
143
|
],
|
|
142
|
-
"minChange": 200
|
|
144
|
+
"minChange": 200,
|
|
145
|
+
"pathExcludes": ["**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*"]
|
|
143
146
|
},
|
|
144
147
|
"require": ["impeccable"]
|
|
145
148
|
},
|
|
@@ -172,7 +175,8 @@
|
|
|
172
175
|
"**/*.scss",
|
|
173
176
|
"**/*.html"
|
|
174
177
|
],
|
|
175
|
-
"projectGlobs": ["stitch/**"]
|
|
178
|
+
"projectGlobs": ["stitch/**"],
|
|
179
|
+
"pathExcludes": ["**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*"]
|
|
176
180
|
},
|
|
177
181
|
"require": ["novahiz-stitch-fidelity"]
|
|
178
182
|
},
|
|
@@ -192,7 +196,8 @@
|
|
|
192
196
|
"**/components/**"
|
|
193
197
|
],
|
|
194
198
|
"tiers": ["lite", "full"],
|
|
195
|
-
"minChange": 800
|
|
199
|
+
"minChange": 800,
|
|
200
|
+
"pathExcludes": ["**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*"]
|
|
196
201
|
},
|
|
197
202
|
"require": ["novahiz-wireframe"]
|
|
198
203
|
},
|
package/dist/commands/doctor.js
CHANGED
|
@@ -460,19 +460,72 @@ export function referencedSkillsCheck(spec, indexSkills, packageRootDir) {
|
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
462
|
const absentShipped = absent.filter((id) => shipped.has(id));
|
|
463
|
-
const absentPack = absent.
|
|
463
|
+
const absentPack = absent.filter((id) => !shipped.has(id));
|
|
464
|
+
// Only promise the update command when a pack can actually deliver what is
|
|
465
|
+
// missing: see packDelivery below.
|
|
466
|
+
const { exact, prefixes } = packDelivery(spec);
|
|
467
|
+
const deliverable = (id) => exact.has(id) || prefixes.some((p) => id.startsWith(p));
|
|
468
|
+
const undeliverable = absentPack.filter((id) => !deliverable(id));
|
|
469
|
+
const present = referenced.length - absent.length;
|
|
470
|
+
let detail;
|
|
471
|
+
if (absent.length === 0)
|
|
472
|
+
detail = `${present} present`;
|
|
473
|
+
else if (absentShipped.length > 0)
|
|
474
|
+
detail = `missing from index: ${absent.join(", ")}`;
|
|
475
|
+
else {
|
|
476
|
+
detail = `${present} present, ${absentPack.length} pack skills not installed`;
|
|
477
|
+
// A skill the catalog references but no pack ships can never be installed —
|
|
478
|
+
// say so instead of sending the reader after a command that returns nothing.
|
|
479
|
+
detail +=
|
|
480
|
+
undeliverable.length > 0
|
|
481
|
+
? `; ${undeliverable.join(", ")} referenced by no pack (deps cannot install them)`
|
|
482
|
+
: " (run /novahiz-skills-update, or novahiz deps --install)";
|
|
483
|
+
}
|
|
464
484
|
return {
|
|
465
485
|
id: "referenced",
|
|
466
486
|
label: "Referenced skills",
|
|
467
487
|
ok: absentShipped.length === 0,
|
|
468
|
-
detail
|
|
469
|
-
? `${referenced.length} present`
|
|
470
|
-
: absentShipped.length > 0
|
|
471
|
-
? `missing from index: ${absent.join(", ")}`
|
|
472
|
-
: `${referenced.length - absent.length} present, ${absentPack} pack skills not installed (run /novahiz-skills-update, or novahiz deps --install)`,
|
|
488
|
+
detail,
|
|
473
489
|
blocking: absentShipped.length > 0
|
|
474
490
|
};
|
|
475
491
|
}
|
|
492
|
+
/** Skills a provider pack could really install, read off each pack's `install`
|
|
493
|
+
* argv: an explicit `--skill a b c` names them, and `--skill *` delivers the
|
|
494
|
+
* whole repo, whose ids start with the repo's leading segment with any hyphen
|
|
495
|
+
* suffix dropped (`flutter/skills` -> `flutter-`, `dart-lang/skills` ->
|
|
496
|
+
* `dart-`). Measured 2026-10-15: the 7 skills doctor reported as "pack skills
|
|
497
|
+
* not installed" are `eas-*`, referenced as `defaultSkills` by the `test` and
|
|
498
|
+
* `devops` categories while no provider ships them, so
|
|
499
|
+
* "run /novahiz-skills-update, or novahiz deps --install" could not have
|
|
500
|
+
* installed a single one. Advice no command can honour is a defect. */
|
|
501
|
+
function packDelivery(spec) {
|
|
502
|
+
const exact = new Set();
|
|
503
|
+
const prefixes = [];
|
|
504
|
+
const disabled = new Set(spec.config.providers.disabled);
|
|
505
|
+
for (const provider of spec.providers) {
|
|
506
|
+
if (disabled.has(provider.id))
|
|
507
|
+
continue;
|
|
508
|
+
const argv = provider.install;
|
|
509
|
+
if (!Array.isArray(argv) || argv.length === 0)
|
|
510
|
+
continue;
|
|
511
|
+
const at = argv.indexOf("--skill");
|
|
512
|
+
if (at === -1)
|
|
513
|
+
continue;
|
|
514
|
+
const args = argv.slice(at + 1).filter((arg) => !arg.startsWith("-"));
|
|
515
|
+
if (args.length === 0)
|
|
516
|
+
continue;
|
|
517
|
+
if (args.includes("*")) {
|
|
518
|
+
const repo = String(argv.find((arg) => arg.includes("/")) ?? "");
|
|
519
|
+
const head = repo.split("/")[0].split("-")[0];
|
|
520
|
+
if (head.length > 0)
|
|
521
|
+
prefixes.push(`${head}-`);
|
|
522
|
+
}
|
|
523
|
+
for (const id of args)
|
|
524
|
+
if (id !== "*")
|
|
525
|
+
exact.add(id);
|
|
526
|
+
}
|
|
527
|
+
return { exact, prefixes };
|
|
528
|
+
}
|
|
476
529
|
/** Impeccable design-context checks: PRODUCT.md and DESIGN.md resolution
|
|
477
530
|
* (root -> .agents/context -> docs/). Rows appear only when the impeccable
|
|
478
531
|
* skill is installed (opt-in) and are never blocking: a missing record is a
|
package/dist/gate.js
CHANGED
|
@@ -301,6 +301,15 @@ function selectorMatches(rule, classification, path, categories, pathless = fals
|
|
|
301
301
|
checks.push(when.promptCategories.some((category) => categories.includes(category)));
|
|
302
302
|
}
|
|
303
303
|
else {
|
|
304
|
+
// E1: pathExcludes is the negative counterpart of pathGlobs. A rule that
|
|
305
|
+
// carries it never applies to an excluded path, whatever the other
|
|
306
|
+
// selectors say — the only way to express "never on tests" when match is
|
|
307
|
+
// "any", where a prompt category alone would otherwise fire on any file.
|
|
308
|
+
if (when.pathExcludes &&
|
|
309
|
+
when.pathExcludes.length > 0 &&
|
|
310
|
+
when.pathExcludes.some((glob) => globToRegExp(glob).test(path))) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
304
313
|
if (when.fileClasses && when.fileClasses.length > 0)
|
|
305
314
|
checks.push(when.fileClasses.includes(classification));
|
|
306
315
|
if (when.pathGlobs && when.pathGlobs.length > 0)
|
package/install/install.mjs
CHANGED
|
@@ -459,6 +459,14 @@ async function main() {
|
|
|
459
459
|
});
|
|
460
460
|
if (result.stdout) ui.raw(result.stdout);
|
|
461
461
|
if (result.status !== 0 && result.stderr) process.stderr.write(result.stderr);
|
|
462
|
+
// Arbitrage assumé: installation et mise à jour restent deux commandes.
|
|
463
|
+
// `deps --install` est idempotent — un pack déjà présent n'est jamais
|
|
464
|
+
// rafraîchi ici, sinon une réinstallation écraserait les retouches
|
|
465
|
+
// locales. C'est documenté dans docs/PROVIDERS.md; on le dit aussi à
|
|
466
|
+
// l'écran plutôt que de laisser le paquet sembler à jour sans l'être.
|
|
467
|
+
if (autoInstall && result.status === 0) {
|
|
468
|
+
note("Installs only: skill packs already present are left untouched. Refresh them with /novahiz-skills-update (deps --install --update).");
|
|
469
|
+
}
|
|
462
470
|
}
|
|
463
471
|
}
|
|
464
472
|
|
package/package.json
CHANGED
package/src/commands/doctor.ts
CHANGED
|
@@ -459,21 +459,65 @@ export function referencedSkillsCheck(
|
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
const absentShipped = absent.filter((id) => shipped.has(id));
|
|
462
|
-
const absentPack = absent.
|
|
462
|
+
const absentPack = absent.filter((id) => !shipped.has(id));
|
|
463
|
+
// Only promise the update command when a pack can actually deliver what is
|
|
464
|
+
// missing: see packDelivery below.
|
|
465
|
+
const { exact, prefixes } = packDelivery(spec);
|
|
466
|
+
const deliverable = (id: string): boolean => exact.has(id) || prefixes.some((p) => id.startsWith(p));
|
|
467
|
+
const undeliverable = absentPack.filter((id) => !deliverable(id));
|
|
468
|
+
const present = referenced.length - absent.length;
|
|
469
|
+
let detail: string;
|
|
470
|
+
if (absent.length === 0) detail = `${present} present`;
|
|
471
|
+
else if (absentShipped.length > 0) detail = `missing from index: ${absent.join(", ")}`;
|
|
472
|
+
else {
|
|
473
|
+
detail = `${present} present, ${absentPack.length} pack skills not installed`;
|
|
474
|
+
// A skill the catalog references but no pack ships can never be installed —
|
|
475
|
+
// say so instead of sending the reader after a command that returns nothing.
|
|
476
|
+
detail +=
|
|
477
|
+
undeliverable.length > 0
|
|
478
|
+
? `; ${undeliverable.join(", ")} referenced by no pack (deps cannot install them)`
|
|
479
|
+
: " (run /novahiz-skills-update, or novahiz deps --install)";
|
|
480
|
+
}
|
|
463
481
|
return {
|
|
464
482
|
id: "referenced",
|
|
465
483
|
label: "Referenced skills",
|
|
466
484
|
ok: absentShipped.length === 0,
|
|
467
|
-
detail
|
|
468
|
-
absent.length === 0
|
|
469
|
-
? `${referenced.length} present`
|
|
470
|
-
: absentShipped.length > 0
|
|
471
|
-
? `missing from index: ${absent.join(", ")}`
|
|
472
|
-
: `${referenced.length - absent.length} present, ${absentPack} pack skills not installed (run /novahiz-skills-update, or novahiz deps --install)`,
|
|
485
|
+
detail,
|
|
473
486
|
blocking: absentShipped.length > 0
|
|
474
487
|
};
|
|
475
488
|
}
|
|
476
489
|
|
|
490
|
+
/** Skills a provider pack could really install, read off each pack's `install`
|
|
491
|
+
* argv: an explicit `--skill a b c` names them, and `--skill *` delivers the
|
|
492
|
+
* whole repo, whose ids start with the repo's leading segment with any hyphen
|
|
493
|
+
* suffix dropped (`flutter/skills` -> `flutter-`, `dart-lang/skills` ->
|
|
494
|
+
* `dart-`). Measured 2026-10-15: the 7 skills doctor reported as "pack skills
|
|
495
|
+
* not installed" are `eas-*`, referenced as `defaultSkills` by the `test` and
|
|
496
|
+
* `devops` categories while no provider ships them, so
|
|
497
|
+
* "run /novahiz-skills-update, or novahiz deps --install" could not have
|
|
498
|
+
* installed a single one. Advice no command can honour is a defect. */
|
|
499
|
+
function packDelivery(spec: ReturnType<typeof loadSpec>): { exact: Set<string>; prefixes: string[] } {
|
|
500
|
+
const exact = new Set<string>();
|
|
501
|
+
const prefixes: string[] = [];
|
|
502
|
+
const disabled = new Set(spec.config.providers.disabled);
|
|
503
|
+
for (const provider of spec.providers) {
|
|
504
|
+
if (disabled.has(provider.id)) continue;
|
|
505
|
+
const argv = provider.install;
|
|
506
|
+
if (!Array.isArray(argv) || argv.length === 0) continue;
|
|
507
|
+
const at = argv.indexOf("--skill");
|
|
508
|
+
if (at === -1) continue;
|
|
509
|
+
const args = argv.slice(at + 1).filter((arg) => !arg.startsWith("-"));
|
|
510
|
+
if (args.length === 0) continue;
|
|
511
|
+
if (args.includes("*")) {
|
|
512
|
+
const repo = String(argv.find((arg) => arg.includes("/")) ?? "");
|
|
513
|
+
const head = repo.split("/")[0].split("-")[0];
|
|
514
|
+
if (head.length > 0) prefixes.push(`${head}-`);
|
|
515
|
+
}
|
|
516
|
+
for (const id of args) if (id !== "*") exact.add(id);
|
|
517
|
+
}
|
|
518
|
+
return { exact, prefixes };
|
|
519
|
+
}
|
|
520
|
+
|
|
477
521
|
/** Impeccable design-context checks: PRODUCT.md and DESIGN.md resolution
|
|
478
522
|
* (root -> .agents/context -> docs/). Rows appear only when the impeccable
|
|
479
523
|
* skill is installed (opt-in) and are never blocking: a missing record is a
|
package/src/gate.ts
CHANGED
|
@@ -291,6 +291,17 @@ function selectorMatches(rule: Rule, classification: FileClass, path: string, ca
|
|
|
291
291
|
if (!when.promptCategories || when.promptCategories.length === 0) return false;
|
|
292
292
|
checks.push(when.promptCategories.some((category) => categories.includes(category)));
|
|
293
293
|
} else {
|
|
294
|
+
// E1: pathExcludes is the negative counterpart of pathGlobs. A rule that
|
|
295
|
+
// carries it never applies to an excluded path, whatever the other
|
|
296
|
+
// selectors say — the only way to express "never on tests" when match is
|
|
297
|
+
// "any", where a prompt category alone would otherwise fire on any file.
|
|
298
|
+
if (
|
|
299
|
+
when.pathExcludes &&
|
|
300
|
+
when.pathExcludes.length > 0 &&
|
|
301
|
+
when.pathExcludes.some((glob) => globToRegExp(glob).test(path))
|
|
302
|
+
) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
294
305
|
if (when.fileClasses && when.fileClasses.length > 0) checks.push(when.fileClasses.includes(classification));
|
|
295
306
|
if (when.pathGlobs && when.pathGlobs.length > 0) checks.push(when.pathGlobs.some((glob) => globToRegExp(glob).test(path)));
|
|
296
307
|
if (when.promptCategories && when.promptCategories.length > 0) {
|
package/src/spec.ts
CHANGED
|
@@ -35,6 +35,12 @@ type RuleWhen = {
|
|
|
35
35
|
match?: "any" | "all";
|
|
36
36
|
fileClasses?: string[];
|
|
37
37
|
pathGlobs?: string[];
|
|
38
|
+
/** Negative counterpart of pathGlobs: the rule never applies to a path
|
|
39
|
+
* matching one of these globs, whatever the other selectors say. This is the
|
|
40
|
+
* only way to express "never on tests" when match is "any", where a prompt
|
|
41
|
+
* category on its own would otherwise fire on any file, unit tests included.
|
|
42
|
+
* Omitted = no exclusion, exactly as before. */
|
|
43
|
+
pathExcludes?: string[];
|
|
38
44
|
/** Project-scoped selector: the rule applies only when at least one file under
|
|
39
45
|
* the project root (nearest ancestor of the edited file holding .git,
|
|
40
46
|
* package.json or pubspec.yaml) matches one of these globs, written from that
|