@skyf0xx/hedgehog 6.1.1 → 6.1.2
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/package.json
CHANGED
package/src/db/core.mjs
CHANGED
|
@@ -180,9 +180,16 @@ function indentOf(line) {
|
|
|
180
180
|
return line.length - line.trimStart().length;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// The only values `pattern` may declare — named in every rejection
|
|
184
|
+
// message below, so a typo surfaces the valid set instead of silently
|
|
185
|
+
// degrading to "unset" (which would turn conformance checking off with
|
|
186
|
+
// no signal that anything is wrong).
|
|
187
|
+
const VALID_PATTERNS = ['hexagonal', 'layered', 'vertical-slice', 'none'];
|
|
188
|
+
|
|
183
189
|
// Parses the narrow subset of YAML a core definition needs:
|
|
184
190
|
// id: <scalar>
|
|
185
191
|
// pluralizes: <bool> # optional, default true
|
|
192
|
+
// pattern: <scalar> # optional, one of hexagonal|layered|vertical-slice|none
|
|
186
193
|
// layers:
|
|
187
194
|
// - id: <scalar>
|
|
188
195
|
// depends_on: <scalar> # optional
|
|
@@ -202,7 +209,7 @@ export function parseCoreYaml(text) {
|
|
|
202
209
|
lines.push({ indent: indentOf(noComment), text: noComment.trim() });
|
|
203
210
|
}
|
|
204
211
|
|
|
205
|
-
const core = { id: undefined, pluralizes: true, layers: [] };
|
|
212
|
+
const core = { id: undefined, pluralizes: true, pattern: null, layers: [] };
|
|
206
213
|
let i = 0;
|
|
207
214
|
|
|
208
215
|
while (i < lines.length && lines[i].indent === 0) {
|
|
@@ -224,6 +231,21 @@ export function parseCoreYaml(text) {
|
|
|
224
231
|
// advisory stops firing on it for good, rather than every user of
|
|
225
232
|
// that core re-discovering the same false positive.
|
|
226
233
|
if (key === 'pluralizes') core.pluralizes = parseScalar(value) === 'true';
|
|
234
|
+
// An architecture claim, checked by validateCore below — see that
|
|
235
|
+
// function's pattern-conformance block for what each value asserts.
|
|
236
|
+
// Rejected here, at parse time, rather than left to validateCore:
|
|
237
|
+
// an unrecognized value must never silently resolve to "unset" (the
|
|
238
|
+
// one value that turns conformance checking off), so a typo has to
|
|
239
|
+
// surface as a parse error, not a quietly-skipped check.
|
|
240
|
+
if (key === 'pattern') {
|
|
241
|
+
const declared = parseScalar(value);
|
|
242
|
+
if (!VALID_PATTERNS.includes(declared)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`unknown pattern "${declared}" — must be one of: ${VALID_PATTERNS.join(', ')}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
core.pattern = declared;
|
|
248
|
+
}
|
|
227
249
|
i++;
|
|
228
250
|
}
|
|
229
251
|
|
|
@@ -498,6 +520,132 @@ export function isModuleAxis(core) {
|
|
|
498
520
|
return core.layers.some((layer) => layer.scope.join('').includes('{module}'));
|
|
499
521
|
}
|
|
500
522
|
|
|
523
|
+
// `layered`'s and `hexagonal`'s checks both anchor on "the head layer" —
|
|
524
|
+
// the first-declared layer, by the same convention `core.layers[0]`
|
|
525
|
+
// already carries informally everywhere else in this file (e.g. the
|
|
526
|
+
// once-layer checks below walk `core.layers` in declaration order too).
|
|
527
|
+
function headLayer(core) {
|
|
528
|
+
return core.layers[0];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// `pattern: layered` — a strict linear chain: every layer but the head
|
|
532
|
+
// depends on exactly one other, no two layers share a depends_on parent
|
|
533
|
+
// (that would be branching, not a chain), and every layer is reachable
|
|
534
|
+
// from the head by walking depends_on forward. Throws naming the first
|
|
535
|
+
// layer that breaks the shape, in the order the checks below run.
|
|
536
|
+
function checkLayeredPattern(core) {
|
|
537
|
+
const head = headLayer(core);
|
|
538
|
+
const rest = core.layers.filter((layer) => layer.id !== head.id);
|
|
539
|
+
|
|
540
|
+
for (const layer of rest) {
|
|
541
|
+
if (!layer.depends_on) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`core "${core.id}" declares pattern: layered, but layer "${layer.id}" has no depends_on — every layer but the head ("${head.id}") must depend on exactly one other layer`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const dependents = new Map(); // parent layer id -> the one layer that depends on it
|
|
549
|
+
for (const layer of rest) {
|
|
550
|
+
const prior = dependents.get(layer.depends_on);
|
|
551
|
+
if (prior) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`core "${core.id}" declares pattern: layered, but both "${prior}" and "${layer.id}" depend on "${layer.depends_on}" — a layered chain is linear, one dependent per layer`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
dependents.set(layer.depends_on, layer.id);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Walk forward from the head (parent -> its one dependent) and confirm
|
|
560
|
+
// every layer gets visited. This also catches a chain disconnected from
|
|
561
|
+
// the head entirely — e.g. two layers depending on each other with
|
|
562
|
+
// neither reachable from the head — which the checks above don't rule
|
|
563
|
+
// out on their own: each layer still has exactly one depends_on and no
|
|
564
|
+
// parent is shared, they just never connect back to "${head.id}".
|
|
565
|
+
const visited = new Set([head.id]);
|
|
566
|
+
let current = head;
|
|
567
|
+
while (dependents.has(current.id)) {
|
|
568
|
+
current = core.layers.find((layer) => layer.id === dependents.get(current.id));
|
|
569
|
+
visited.add(current.id);
|
|
570
|
+
}
|
|
571
|
+
for (const layer of core.layers) {
|
|
572
|
+
if (!visited.has(layer.id)) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`core "${core.id}" declares pattern: layered, but layer "${layer.id}" is not reachable from the head layer "${head.id}" by following depends_on`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// `pattern: hexagonal` — Hedgehog has no adapter marker today, so this
|
|
581
|
+
// checks direction alone rather than an actual domain/adapter boundary:
|
|
582
|
+
// the head layer (the domain, by convention) must have no depends_on, and
|
|
583
|
+
// every other layer's depends_on chain must terminate at the head with no
|
|
584
|
+
// cycle — i.e. dependencies all point one way, inward, and the head is the
|
|
585
|
+
// sink every chain ends at. Weaker than the real hexagonal rule (nothing
|
|
586
|
+
// stops an adapter depending on another adapter instead of the domain
|
|
587
|
+
// directly), and deliberately so — see #314's "Not in this issue" for why
|
|
588
|
+
// a real adapter-boundary marker is a separate design decision.
|
|
589
|
+
function checkHexagonalPattern(core) {
|
|
590
|
+
const head = headLayer(core);
|
|
591
|
+
if (head.depends_on) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`core "${core.id}" declares pattern: hexagonal, but its head layer "${head.id}" has a depends_on — the domain layer must be the sink every dependency chain points to, not itself a dependent`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const byId = new Map(core.layers.map((layer) => [layer.id, layer]));
|
|
598
|
+
for (const layer of core.layers) {
|
|
599
|
+
if (layer.id === head.id) continue;
|
|
600
|
+
if (!layer.depends_on) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
`core "${core.id}" declares pattern: hexagonal, but layer "${layer.id}" has no depends_on — only the domain layer ("${head.id}") may have none`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
const seen = new Set([layer.id]);
|
|
606
|
+
let current = layer;
|
|
607
|
+
while (current.depends_on) {
|
|
608
|
+
const next = byId.get(current.depends_on);
|
|
609
|
+
if (seen.has(next.id)) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`core "${core.id}" declares pattern: hexagonal, but layer "${layer.id}"'s depends_on chain cycles back through "${next.id}" instead of terminating at the domain layer "${head.id}"`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
seen.add(next.id);
|
|
615
|
+
current = next;
|
|
616
|
+
}
|
|
617
|
+
if (current.id !== head.id) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`core "${core.id}" declares pattern: hexagonal, but layer "${layer.id}"'s depends_on chain terminates at "${current.id}", not the domain layer "${head.id}" — every layer must point inward toward the domain`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Dispatches on `core.pattern` to the check above matching what was
|
|
626
|
+
// declared. `null` (never set) and `'none'` (set, explicitly no enforced
|
|
627
|
+
// direction — the adopted-repo default) both skip checking entirely: an
|
|
628
|
+
// absent pattern must validate exactly as it did before this field
|
|
629
|
+
// existed, and `none` recording "no direction" is a fact, not a finding.
|
|
630
|
+
function checkPatternConformance(core) {
|
|
631
|
+
if (!core.pattern || core.pattern === 'none') return;
|
|
632
|
+
if (core.pattern === 'vertical-slice') {
|
|
633
|
+
if (!isModuleAxis(core)) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`core "${core.id}" declares pattern: vertical-slice, but no layer's scope contains {module} — vertical-slice is a chain instantiated per module, so at least one layer must vary by module`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (core.pattern === 'layered') {
|
|
641
|
+
checkLayeredPattern(core);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (core.pattern === 'hexagonal') {
|
|
645
|
+
checkHexagonalPattern(core);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
501
649
|
// Enforces the interview's rule (spec: "Authored cores") — a layer without
|
|
502
650
|
// scope or without a verify command is rejected. Applied uniformly to
|
|
503
651
|
// shipped and authored cores alike; the loader has no shipped-core-only
|
|
@@ -575,6 +723,13 @@ export function validateCore(core) {
|
|
|
575
723
|
}
|
|
576
724
|
}
|
|
577
725
|
|
|
726
|
+
// An architecture claim, checked mechanically — an unchecked `pattern`
|
|
727
|
+
// is a comment, and a comment that can silently disagree with the graph
|
|
728
|
+
// is worse than no field at all. Depends on depends_on already being
|
|
729
|
+
// resolved to real layer ids (the loop just above), which every check
|
|
730
|
+
// below relies on.
|
|
731
|
+
checkPatternConformance(core);
|
|
732
|
+
|
|
578
733
|
// A `once: true` layer compiles a single task for the whole build, so
|
|
579
734
|
// there is no module to substitute into its templates. Left unchecked,
|
|
580
735
|
// a stray {module} would survive verbatim into scope_globs — a glob
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pr-writing
|
|
3
|
-
description: Use whenever writing a PR title/description, a commit message body, a code review comment, or an issue — in Hedgehog's own repo or any consuming project. Triggers on "open a PR", "write the PR description", "comment on this PR", "file an issue". Covers writing style (terse, info-dense, Simplified Technical English) and the pre-open checklist (CI status, scope, verified claims only).
|
|
3
|
+
description: Use whenever writing a PR title/description, a commit message body, a code review comment, or an issue — in Hedgehog's own repo or any consuming project. Triggers on "open a PR", "write the PR description", "comment on this PR", "file an issue". Covers writing style (terse, info-dense, Simplified Technical English), the Why/What shape, folding deep reasoning under `<details>`, and the pre-open checklist (CI status, scope, verified claims only).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# PR Writing
|
|
@@ -47,13 +47,52 @@ as a record of the work session.
|
|
|
47
47
|
|
|
48
48
|
## Shape
|
|
49
49
|
|
|
50
|
-
- **
|
|
50
|
+
- **PR title**: `<type>(<scope>): <summary>`, imperative mood, under ~70
|
|
51
51
|
chars.
|
|
52
|
-
- **
|
|
52
|
+
- **PR description**: 1-3 bullets — what changed, why. A test plan section
|
|
53
53
|
listing what you actually ran, not what should theoretically pass.
|
|
54
|
+
- **Issue title**: plain English a non-technical reader would say out
|
|
55
|
+
loud, not a commit-style `<type>(<scope>): <summary>`. Name the
|
|
56
|
+
outcome, not the mechanism — "Improve how Hedgehog tracks and enforces
|
|
57
|
+
a project's architecture", not "feat(core): add pattern field".
|
|
54
58
|
- **Comments**: lead with the concrete finding, then (if needed) the fix
|
|
55
59
|
requested. No preamble.
|
|
56
60
|
|
|
61
|
+
## Why/What for issues
|
|
62
|
+
|
|
63
|
+
An issue proposing a change — a feature, a fix worth explaining, a
|
|
64
|
+
`ROADMAP.md` item being picked up — states **Why** before **What**:
|
|
65
|
+
|
|
66
|
+
- **Why**: the problem, as a short list of plain-language facts. Each
|
|
67
|
+
bullet is one observation a reader can verify or disagree with, not a
|
|
68
|
+
justification wrapped in caveats. State the problem first, then (if
|
|
69
|
+
the fix isn't obvious from the problem) a short "to fix this" list of
|
|
70
|
+
intended outcomes.
|
|
71
|
+
- **What**: the change itself — the concrete steps, fields, or sub-issues.
|
|
72
|
+
Numbered if sequenced, bulleted if not.
|
|
73
|
+
|
|
74
|
+
Skip the Why section only when the title already states the problem in
|
|
75
|
+
full (a one-line bug report needs no restatement). Never skip What.
|
|
76
|
+
|
|
77
|
+
## Fold deep reasoning under `<details>`
|
|
78
|
+
|
|
79
|
+
An issue or PR body written for a human reader stays short. Extended
|
|
80
|
+
reasoning — architecture rationale, alternatives considered, prior
|
|
81
|
+
decisions, anything aimed at an AI agent picking up the work or a reader
|
|
82
|
+
who wants the full trail — goes under a collapsed section, not inline:
|
|
83
|
+
|
|
84
|
+
```markdown
|
|
85
|
+
<details>
|
|
86
|
+
<summary>Full reasoning (for AI agents and anyone who wants the detail)</summary>
|
|
87
|
+
|
|
88
|
+
...
|
|
89
|
+
</details>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Ask first whether that detail needs to exist in the issue at all — a
|
|
93
|
+
link to an existing doc or prior discussion is often enough. Only fold
|
|
94
|
+
in content that has no better home.
|
|
95
|
+
|
|
57
96
|
## When NOT to apply
|
|
58
97
|
|
|
59
98
|
- Internal scratch notes, planning docs, or anything not read by another
|