hypomnema 1.3.2 → 1.3.4

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/scripts/graph.mjs CHANGED
@@ -17,7 +17,7 @@
17
17
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
18
18
  import { join, relative, extname, basename } from 'path';
19
19
  import { resolveHypoRoot, expandHome } from './lib/hypo-root.mjs';
20
- import { loadHypoIgnore, isIgnored } from './lib/hypo-ignore.mjs';
20
+ import { loadHypoIgnore, isScanIgnored } from './lib/hypo-ignore.mjs';
21
21
 
22
22
  // ── arg parsing ───────────────────────────────────────────────────────────────
23
23
 
@@ -38,7 +38,7 @@ function collectPages(dir, root, pages = [], ignorePatterns = []) {
38
38
  if (!existsSync(dir)) return pages;
39
39
  for (const entry of readdirSync(dir)) {
40
40
  const full = join(dir, entry);
41
- if (isIgnored(full, root, ignorePatterns)) continue;
41
+ if (isScanIgnored(full, root, ignorePatterns)) continue;
42
42
  const st = statSync(full);
43
43
  if (st.isDirectory()) {
44
44
  collectPages(full, root, pages, ignorePatterns);
package/scripts/init.mjs CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  renameSync,
33
33
  unlinkSync,
34
34
  } from 'fs';
35
- import { join, basename } from 'path';
35
+ import { join, basename, dirname } from 'path';
36
36
  import { homedir } from 'os';
37
37
  import { execSync, spawnSync } from 'child_process';
38
38
  import { fileURLToPath } from 'url';
@@ -171,7 +171,19 @@ docstring at the top of scripts/<command>.mjs.`);
171
171
 
172
172
  // ── result tracking ──────────────────────────────────────────────────────────
173
173
 
174
- const results = { created: [], skipped: [], merged: [], errors: [] };
174
+ const results = { created: [], skipped: [], merged: [], deduped: [], errors: [] };
175
+
176
+ // Stock template basename → legacy hand-authored equivalents to honor instead of
177
+ // injecting a duplicate. The hypo-/wiki- namespace split (the rename that
178
+ // produced hypo-guide from wiki-guide, etc.) means an exact-filename check misses
179
+ // a user's existing page and drops a 0-reference duplicate orphan beside it.
180
+ // Mirrors the explicit HOOK_RENAMES idiom in upgrade.mjs. hypo-guide.md is
181
+ // intentionally absent: the runtime reads it by name, so a mid-migration vault
182
+ // must still receive it even when wiki-guide.md is present.
183
+ const PAGE_EQUIVALENTS = {
184
+ 'hypo-automation.md': ['wiki-automation.md'],
185
+ 'hypo-help.md': ['wiki-help.md'],
186
+ };
175
187
 
176
188
  function log(action, path) {
177
189
  results[action].push(path);
@@ -213,6 +225,19 @@ function copyTemplate(srcName, destPath, dryRun, transform) {
213
225
  log('skipped', destPath);
214
226
  return;
215
227
  }
228
+ // If a legacy hand-authored equivalent already exists, do NOT inject the stock
229
+ // page — that is exactly what produced a duplicate orphan. Skip LOUDLY (own
230
+ // bucket, ⚠ in the summary) so the user can merge manually. Fires in --dry-run
231
+ // too, so a dry run previews the suppression.
232
+ const equivalents = PAGE_EQUIVALENTS[basename(srcName)] || [];
233
+ const existingEquivalent = equivalents.find((name) => existsSync(join(dirname(destPath), name)));
234
+ if (existingEquivalent) {
235
+ log(
236
+ 'deduped',
237
+ `${destPath} — kept existing ${existingEquivalent}; stock ${basename(srcName)} not installed (merge manually if you want the template content)`,
238
+ );
239
+ return;
240
+ }
216
241
  if (!dryRun) {
217
242
  let content = readFileSync(src, 'utf-8');
218
243
  content = content.replace(/YYYY-MM-DD/g, new Date().toISOString().slice(0, 10));
@@ -1017,6 +1042,10 @@ if (results.skipped.length)
1017
1042
  );
1018
1043
  if (results.merged.length)
1019
1044
  lines.push(`↪ Merged into settings.json:\n${results.merged.map((p) => ` ${p}`).join('\n')}`);
1045
+ if (results.deduped.length)
1046
+ lines.push(
1047
+ `⚠ Skipped to avoid a duplicate — an equivalent page already exists (${results.deduped.length}):\n${results.deduped.map((p) => ` ${p}`).join('\n')}`,
1048
+ );
1020
1049
  if (results.errors.length)
1021
1050
  lines.push(`✗ Errors:\n${results.errors.map((p) => ` ${p}`).join('\n')}`);
1022
1051
 
@@ -23,6 +23,30 @@ function globToRegex(glob) {
23
23
  );
24
24
  }
25
25
 
26
+ // Generated, regenerable tool artifacts that live at the WIKI ROOT and must not
27
+ // pollute the knowledge catalog. These are intentionally NOT added to
28
+ // .hypoignore: that list also drives the pre-commit secret gate
29
+ // (hooks/hypo-pre-commit.mjs), so listing them there would block the report's
30
+ // commit and freeze every auto-commit while it sits at root. Instead they are
31
+ // excluded from catalog/scan only, via isScanIgnored(). The patterns are
32
+ // anchored to the root-relative path (no '/'), so an equivalent name nested
33
+ // under pages/, projects/, or sources/ is left untouched.
34
+ const GENERATED_ARTIFACT_RES = [/^MIGRATION-v[^/]*\.md$/, /^GRAPH_REPORT\.md$/];
35
+
36
+ export function isGeneratedArtifact(filePath, hypoDir) {
37
+ const rel = relative(hypoDir, filePath).replace(/\\/g, '/');
38
+ return GENERATED_ARTIFACT_RES.some((re) => re.test(rel));
39
+ }
40
+
41
+ // Catalog/scan exclusion = privacy/.hypoignore patterns PLUS generated root
42
+ // artifacts. Use this in tools that build the knowledge catalog (lint, graph,
43
+ // stats, query, verify, crystallize, rename, doctor broken-links). Do NOT use it
44
+ // in the pre-commit gate or ingest --check, which are privacy boundaries that
45
+ // must stay on isIgnored()/.hypoignore alone.
46
+ export function isScanIgnored(filePath, hypoDir, patterns) {
47
+ return isIgnored(filePath, hypoDir, patterns) || isGeneratedArtifact(filePath, hypoDir);
48
+ }
49
+
26
50
  export function isIgnored(filePath, hypoDir, patterns) {
27
51
  const rel = relative(hypoDir, filePath).replace(/\\/g, '/');
28
52
  const base = basename(filePath);
package/scripts/lint.mjs CHANGED
@@ -20,7 +20,7 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from '
20
20
  import { join, relative, extname, basename } from 'path';
21
21
  import { resolveHypoRoot, expandHome } from './lib/hypo-root.mjs';
22
22
  import { SESSION_STATE_NEXT_HEADINGS } from '../hooks/hypo-shared.mjs';
23
- import { loadHypoIgnore, isIgnored } from './lib/hypo-ignore.mjs';
23
+ import { loadHypoIgnore, isScanIgnored } from './lib/hypo-ignore.mjs';
24
24
  import { parseSchemaVocab, checkForbidden, parseSchemaPageDirs } from './lib/schema-vocab.mjs';
25
25
  import { findDesignHistoryStale } from './lib/design-history-stale.mjs';
26
26
  import { FEEDBACK_SCOPE_RE } from './lib/feedback-scope.mjs';
@@ -117,7 +117,7 @@ function collectPages(dir, root, pages = [], ignorePatterns = []) {
117
117
  if (!existsSync(dir)) return pages;
118
118
  for (const entry of readdirSync(dir)) {
119
119
  const full = join(dir, entry);
120
- if (isIgnored(full, root, ignorePatterns)) continue;
120
+ if (isScanIgnored(full, root, ignorePatterns)) continue;
121
121
  const st = statSync(full);
122
122
  if (st.isDirectory()) {
123
123
  // `_`-prefixed dirs (e.g. pages/feedback/_drafts) hold scaffolds / scratch
@@ -164,13 +164,14 @@ function collectLinkTargets(hypoDir, ignorePatterns = []) {
164
164
  if (existsSync(hypoDir)) {
165
165
  for (const entry of readdirSync(hypoDir)) {
166
166
  const full = join(hypoDir, entry);
167
- // root-level *.md FILES only (no recursion), honoring .hypoignore exactly
168
- // like collectPages otherwise an ignored root file (e.g. a secret) would
169
- // resolve [[its-slug]] as valid, a false negative.
167
+ // root-level *.md FILES only (no recursion), honoring .hypoignore plus the
168
+ // scan-only generated-artifact exclusions (isScanIgnored) like collectPages
169
+ // otherwise an ignored root file (e.g. a secret) or a regenerable report
170
+ // would resolve [[its-slug]] as valid, a false negative.
170
171
  if (
171
172
  extname(entry) === '.md' &&
172
173
  !entry.startsWith('.') &&
173
- !isIgnored(full, hypoDir, ignorePatterns) &&
174
+ !isScanIgnored(full, hypoDir, ignorePatterns) &&
174
175
  statSync(full).isFile()
175
176
  ) {
176
177
  targets.push(entry.replace(/\.md$/, ''));
package/scripts/query.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
20
20
  import { join, relative, extname } from 'path';
21
21
  import { resolveHypoRoot, expandHome } from './lib/hypo-root.mjs';
22
- import { loadHypoIgnore, isIgnored } from './lib/hypo-ignore.mjs';
22
+ import { loadHypoIgnore, isScanIgnored } from './lib/hypo-ignore.mjs';
23
23
 
24
24
  // ── arg parsing ──────────────────────────────────────────────────────────────
25
25
 
@@ -42,7 +42,7 @@ function collectMdFiles(dir, root, acc = [], ignorePatterns = []) {
42
42
  for (const entry of readdirSync(dir)) {
43
43
  if (entry.startsWith('.')) continue;
44
44
  const full = join(dir, entry);
45
- if (isIgnored(full, root, ignorePatterns)) continue;
45
+ if (isScanIgnored(full, root, ignorePatterns)) continue;
46
46
  const st = statSync(full);
47
47
  if (st.isDirectory()) collectMdFiles(full, root, acc, ignorePatterns);
48
48
  else if (extname(entry) === '.md') acc.push({ path: full, rel: relative(root, full) });