hypomnema 1.8.2 → 1.9.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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +141 -51
  4. package/README.ko.md +18 -17
  5. package/README.md +16 -15
  6. package/commands/crystallize.md +89 -24
  7. package/commands/doctor.md +3 -1
  8. package/docs/ARCHITECTURE.md +45 -14
  9. package/docs/CONTRIBUTING.md +26 -8
  10. package/hooks/atomic-write.mjs +44 -0
  11. package/hooks/base-store.mjs +409 -90
  12. package/hooks/close-gate-store.mjs +131 -16
  13. package/hooks/close-journal.mjs +250 -0
  14. package/hooks/close-receipt.mjs +559 -0
  15. package/hooks/hooks.json +3 -38
  16. package/hooks/hypo-auto-commit.mjs +80 -10
  17. package/hooks/hypo-auto-minimal-crystallize.mjs +129 -25
  18. package/hooks/hypo-auto-stage.mjs +13 -1
  19. package/hooks/hypo-close-guard.mjs +2 -1
  20. package/hooks/hypo-cwd-change.mjs +45 -20
  21. package/hooks/hypo-file-watch.mjs +44 -6
  22. package/hooks/hypo-first-prompt.mjs +15 -9
  23. package/hooks/hypo-hot-rebuild.mjs +169 -112
  24. package/hooks/hypo-personal-check.mjs +9 -1
  25. package/hooks/hypo-session-record.mjs +1 -1
  26. package/hooks/hypo-session-start.mjs +566 -20
  27. package/hooks/hypo-shared.mjs +2250 -206
  28. package/hooks/hypo-stop.mjs +207 -0
  29. package/hooks/proposal-store.mjs +201 -35
  30. package/hooks/shared.json +15 -0
  31. package/hooks/version-check.mjs +9 -7
  32. package/package.json +3 -1
  33. package/scripts/crystallize.mjs +3 -4
  34. package/scripts/doctor.mjs +549 -123
  35. package/scripts/init.mjs +86 -109
  36. package/scripts/lib/core-hooks.mjs +48 -22
  37. package/scripts/lib/crystallize-close-apply.mjs +2397 -188
  38. package/scripts/lib/crystallize-close-check.mjs +43 -23
  39. package/scripts/lib/design-history-stale.mjs +36 -9
  40. package/scripts/lib/frontmatter.mjs +39 -0
  41. package/scripts/lib/git-hooks-dir.mjs +427 -52
  42. package/scripts/lib/hook-inventory.mjs +180 -0
  43. package/scripts/lib/page-usage.mjs +1 -1
  44. package/scripts/lib/pkg-provenance.mjs +18 -1
  45. package/scripts/lib/plugin-detect.mjs +43 -9
  46. package/scripts/lib/project-create.mjs +40 -44
  47. package/scripts/lib/schema-vocab.mjs +22 -11
  48. package/scripts/lib/template-schema-version.mjs +48 -0
  49. package/scripts/lib/time.mjs +46 -0
  50. package/scripts/lint.mjs +66 -18
  51. package/scripts/proposal.mjs +423 -18
  52. package/scripts/resume.mjs +3 -2
  53. package/scripts/session-audit.mjs +2 -1
  54. package/scripts/uninstall.mjs +144 -66
  55. package/scripts/upgrade.mjs +523 -234
  56. package/scripts/verify.mjs +2 -1
  57. package/scripts/weekly-report.mjs +2 -1
  58. package/templates/SCHEMA.md +27 -7
  59. package/templates/gitignore +11 -0
  60. package/templates/hypo-automation.md +10 -13
  61. package/templates/hypo-config.md +1 -1
  62. package/templates/hypo-guide.md +53 -25
  63. package/templates/hot.md +0 -24
package/scripts/lint.mjs CHANGED
@@ -33,8 +33,13 @@ import { findDesignHistoryStale } from './lib/design-history-stale.mjs';
33
33
  import { FEEDBACK_SCOPE_RE } from './lib/feedback-scope.mjs';
34
34
  import { FAILURE_TYPE_ENUM } from './lib/failure-type.mjs';
35
35
  import { collectPagesLint, collectPagesLinkable, slugForms } from './lib/wikilink.mjs';
36
- import { parseFrontmatter, SEQUENCE_ENTRY_RE } from './lib/frontmatter.mjs';
36
+ import {
37
+ parseFrontmatter,
38
+ SEQUENCE_ENTRY_RE,
39
+ hasBlockListSourcesConsulted,
40
+ } from './lib/frontmatter.mjs';
37
41
  import { buildSlugMap } from './lib/slug-resolver.mjs';
42
+ import { parseStrictDate } from './lib/time.mjs';
38
43
 
39
44
  // ── arg parsing ──────────────────────────────────────────────────────────────
40
45
 
@@ -322,8 +327,11 @@ const issues = [];
322
327
  // Dates are compared as strings, which is only the same as comparing time
323
328
  // when every value is zero-padded YYYY-MM-DD. `2026-3-1 > 2026-12-15` is true
324
329
  // lexically and false in fact, so anything off this shape is excluded from the
325
- // comparison and reported instead of silently deciding the verdict.
326
- const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
330
+ // comparison and reported instead of silently deciding the verdict. A format
331
+ // check alone is not enough either: `new Date('2026-02-30')` normalizes to
332
+ // March 2 instead of failing, so a calendar-overflow literal used to read as
333
+ // validated. `parseStrictDate` (lib/time.mjs) is used below wherever a date
334
+ // here is judged usable, and rejects that case too.
327
335
  const bySlugForm = new Map();
328
336
  const synthesisPages = [];
329
337
 
@@ -465,9 +473,41 @@ function lintPage({ path, rel }, slugMap, tagVocab, pageDirs, validTypes) {
465
473
  else bySlugForm.set(form, { updated: fm.updated || null, count: 1 });
466
474
  }
467
475
  }
468
- if (fm.type === 'synthesis' && fm.sources_consulted) {
469
- const sources = parseTagsField(fm.sources_consulted) || [];
470
- if (sources.length > 0) synthesisPages.push({ rel, updated: fm.updated, sources });
476
+ if (fm.type === 'synthesis') {
477
+ // The synthesis's own `updated` is present but fails the strict parse
478
+ // (wrong shape, or calendar overflow like `2026-02-30`). Checked here,
479
+ // per page, rather than in the sources_consulted comparison loop below:
480
+ // that loop only sees pages whose sources parsed, so a synthesis with a
481
+ // block-list sources_consulted, or with none at all, would never be
482
+ // reported. Without this, such a date drops out of W15 with no warning
483
+ // naming why. A missing `updated` is W3's alone and is not repeated here.
484
+ if (fm.updated && parseStrictDate(fm.updated) == null) {
485
+ issue(
486
+ 'warn',
487
+ rel,
488
+ `synthesis 자신의 updated: "${fm.updated}"가 유효한 YYYY-MM-DD 날짜가 아니어서 sources_consulted 비교를 건너뜁니다`,
489
+ null,
490
+ 'W16',
491
+ );
492
+ }
493
+ // sources_consulted written as a YAML block list (`- item` on its own
494
+ // line) parses to an empty value under the line-scanner above, which only
495
+ // reads unindented `key: value` lines: indistinguishable from the field
496
+ // never being set. Detect that shape from the raw block first, so "wrote
497
+ // it, wrong syntax" gets its own W16 instead of silently comparing zero
498
+ // sources and saying nothing.
499
+ if (hasBlockListSourcesConsulted(fmBlock ? fmBlock[1] : '')) {
500
+ issue(
501
+ 'warn',
502
+ rel,
503
+ 'sources_consulted가 YAML 블록 목록으로 쓰여 있어 인식되지 않습니다. 한 줄 흐름 목록으로 쓰세요: sources_consulted: [a, b]',
504
+ null,
505
+ 'W16',
506
+ );
507
+ } else if (fm.sources_consulted) {
508
+ const sources = parseTagsField(fm.sources_consulted) || [];
509
+ if (sources.length > 0) synthesisPages.push({ rel, updated: fm.updated, sources });
510
+ }
471
511
  }
472
512
 
473
513
  // type-conditional required fields
@@ -777,13 +817,14 @@ for (const { rel, updated, sources } of synthesisPages) {
777
817
  unresolved.push(`${source} (${hit.count}개 페이지가 같은 이름)`);
778
818
  continue;
779
819
  }
780
- if (!ISO_DATE_RE.test(hit.updated || '')) {
781
- unresolved.push(`${source} (updated 없음 또는 YYYY-MM-DD 아님)`);
820
+ if (parseStrictDate(hit.updated) == null) {
821
+ unresolved.push(`${source} (updated 없음, 또는 YYYY-MM-DD 형식의 실제 날짜가 아님)`);
782
822
  continue;
783
823
  }
784
824
  if (!newest || hit.updated > newest) newest = hit.updated;
785
825
  }
786
- const ownDateUsable = ISO_DATE_RE.test(updated || '');
826
+ // An unusable own `updated` was already reported per page in lintPage.
827
+ const ownDateUsable = parseStrictDate(updated) != null;
787
828
  if (newest && ownDateUsable && newest > updated) {
788
829
  issue(
789
830
  'warn',
@@ -793,15 +834,22 @@ for (const { rel, updated, sources } of synthesisPages) {
793
834
  'W15',
794
835
  );
795
836
  }
796
- // W16: sources W15 could not compare, reported once per page with the reason
797
- // for each. Without this the rule fails open, and silently: a page whose
798
- // sources mostly do not resolve still gets a W15 (or no warning at all) with
799
- // nothing saying how much of the evidence was actually read. The reasons are
800
- // kept apart because the fix differs: a name that resolves to nothing is a
801
- // typo or a value this field is not meant to hold, a name claimed by two
802
- // pages needs the longer form, and a source with no usable `updated` is W3's
803
- // problem on that page. Excluded from STRICT_PROMOTE_IDS and given its own id
804
- // for the same reasons as W15.
837
+ // W16 (this emit site): sources W15 could not compare, joined into one warn
838
+ // per page naming the reason for each. This is one of three shapes W16 now
839
+ // covers, not the only one: a block-list sources_consulted
840
+ // (hasBlockListSourcesConsulted, above) and a synthesis's own `updated`
841
+ // that fails the strict date parse (checked per page in lintPage) are separate
842
+ // emit sites, so a single page can carry more than one W16 warning at once
843
+ // (an unparseable own `updated` alongside an unresolved source, for
844
+ // example). Without this particular emit, the rule fails open, and
845
+ // silently: a page whose sources mostly do not resolve still gets a W15
846
+ // (or no warning at all) with nothing saying how much of the evidence was
847
+ // actually read. The reasons are kept apart because the fix differs: a
848
+ // name that resolves to nothing is a typo or a value this field is not
849
+ // meant to hold, a name claimed by two pages needs the longer form, and a
850
+ // source with no usable `updated` is W3's problem on that page. Excluded
851
+ // from STRICT_PROMOTE_IDS and given its own id for the same reasons as
852
+ // W15.
805
853
  if (unresolved.length > 0) {
806
854
  issue(
807
855
  'warn',