@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "6.1.1",
3
+ "version": "6.1.2",
4
4
  "description": "Install the Hedgehog build discipline (agents + skills) into a repo, for Claude Code, Cursor, or Gemini CLI.",
5
5
  "type": "module",
6
6
  "repository": {
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": "hedgehog",
3
- "version": "6.1.1",
3
+ "version": "6.1.2",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -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
- - **Title**: `<type>(<scope>): <summary>`, imperative mood, under ~70
50
+ - **PR title**: `<type>(<scope>): <summary>`, imperative mood, under ~70
51
51
  chars.
52
- - **Description**: 1-3 bullets — what changed, why. A test plan section
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