@macroscope/cli 0.0.0 → 0.2.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.
@@ -1,8 +1,9 @@
1
- import { H as Handler, B as Block } from '../index-BTDioymD.js';
2
- export { a as BlockManifest, b as BuildResult, R as RenderResult, T as TestResult, V as VERSION, i as isHandler } from '../index-BTDioymD.js';
1
+ import { H as Handler, B as Block } from '../version-BTDioymD.js';
2
+ export { a as BlockManifest, b as BuildResult, R as RenderResult, T as TestResult, V as VERSION, i as isHandler } from '../version-BTDioymD.js';
3
3
  import * as zod from 'zod';
4
4
  import { z } from 'zod';
5
- import { BlockManifest } from '@macroscope/contracts';
5
+ import { BlockManifest, UploadPayload } from '@macroscope/contracts';
6
+ import ts from 'typescript';
6
7
 
7
8
  declare const manifestSchema: zod.ZodObject<{
8
9
  kind: zod.ZodString;
@@ -106,9 +107,23 @@ interface BlueprintLoadResult {
106
107
  */
107
108
  declare function loadBlueprints(project: Project): Promise<BlueprintLoadResult>;
108
109
 
110
+ interface ResolvedPaths {
111
+ source?: string;
112
+ docs?: string;
113
+ preview?: string;
114
+ tests?: string[];
115
+ }
109
116
  interface ScannedBlock extends Block {
110
117
  /** SHA-256 over the manifest text. Used later by the index for change detection. */
111
118
  contentHash: string;
119
+ /**
120
+ * Absolute paths to manifest-referenced files, when present and inside the project.
121
+ * If `manifest.source` is set but the file is missing or outside the project,
122
+ * `resolvedPaths.source` is absent and a corresponding `io` or `validation`
123
+ * ScanError is in `errors[]`. T5 (extractor) should treat a missing
124
+ * `resolvedPaths.source` as a no-op, not an error.
125
+ */
126
+ resolvedPaths?: ResolvedPaths;
112
127
  }
113
128
  interface ScanError {
114
129
  file: string;
@@ -122,4 +137,76 @@ interface ScanResult {
122
137
  }
123
138
  declare function scan(project: Project): Promise<ScanResult>;
124
139
 
125
- export { Block, type BlueprintLoadCause, type BlueprintLoadError, type BlueprintLoadResult, Handler, type HandlerCapability, type LoadedBlueprint, type Manifest, type Project, type ProjectConfig, ProjectNotFoundError, type ScanError, type ScanResult, type ScannedBlock, findProject, loadBlueprints, manifestSchema, projectConfigSchema, scan };
140
+ interface ExtractorOptions {
141
+ /** Cap on README excerpt characters. Default 500. */
142
+ readmeExcerptMax?: number;
143
+ /** Test seam — inject a TS Program factory. Defaults to the real ts.createProgram. */
144
+ programFactory?: (sourceFiles: string[]) => ts.Program;
145
+ }
146
+ interface ExtractionError {
147
+ blockId: string;
148
+ kind: 'parse' | 'io' | 'symbol';
149
+ message: string;
150
+ }
151
+ interface ExtractionResult {
152
+ /** One payload per block that had a valid `kind` (the only required manifest field). */
153
+ payloads: UploadPayload[];
154
+ /** Errors collected during extraction. NEVER thrown — bad blocks are skipped. */
155
+ errors: ExtractionError[];
156
+ }
157
+ declare function extractAll(blocks: ScannedBlock[], opts?: ExtractorOptions): Promise<ExtractionResult>;
158
+ /**
159
+ * Turn raw markdown/text into a bounded excerpt safe for embedding similarity:
160
+ * collapse whitespace, strip the markdown formatting that confuses semantic search
161
+ * (`#`, `*`, backticks), and truncate at a word boundary.
162
+ */
163
+ declare function formatReadmeExcerpt(raw: string, max: number): string;
164
+
165
+ /**
166
+ * Deterministic SHA-256 over the canonical-JSON serialisation of the payload.
167
+ * Returns 64-character lowercase hex.
168
+ */
169
+ declare function hashPayload(payload: UploadPayload): string;
170
+ interface HasherOptions {
171
+ /** Override the workspace root for tests. Defaults to `process.cwd()`. */
172
+ workspaceRoot?: string;
173
+ }
174
+ interface CacheEntry {
175
+ /** SHA-256 hex of the canonical-JSON serialisation of the UploadPayload. */
176
+ hash: string;
177
+ /** Max(mtime ms) of the block's manifest + source + docs at hash time. */
178
+ mtime: number;
179
+ }
180
+ type HashCache = Record<string, CacheEntry>;
181
+ /**
182
+ * Load the cache from disk. NEVER throws: missing file or malformed JSON
183
+ * return `{}` so the watcher can recover by re-hashing everything. Transparently
184
+ * handles both the wrapped `{ schemaVersion, entries }` shape and the legacy
185
+ * flat shape, returning entries either way.
186
+ */
187
+ declare function loadHashCache(opts?: HasherOptions): Promise<HashCache>;
188
+ /**
189
+ * Atomically write the cache to disk: write to a sibling `.tmp` file then
190
+ * rename. Same-directory rename is atomic on POSIX filesystems. Throws on
191
+ * real I/O failure — callers (watcher loop) decide whether to surface or
192
+ * swallow the error.
193
+ *
194
+ * The on-disk shape is the wrapped envelope (`schemaVersion` + `entries`);
195
+ * `schemaVersion` defaults to the current `SCHEMA_VERSION` from
196
+ * `@macroscope/contracts` so T14's existing callsites transparently persist it.
197
+ */
198
+ declare function saveHashCache(cache: HashCache, opts?: HasherOptions & {
199
+ schemaVersion?: number;
200
+ }): Promise<void>;
201
+ /**
202
+ * Return the subset of blocks whose manifest, source, or docs files have an
203
+ * mtime strictly greater than the cached entry's recorded mtime. Blocks
204
+ * with no cache entry are always returned (first-time extraction needs them).
205
+ *
206
+ * Missing files (e.g. deleted between scan and stat) are treated as "changed"
207
+ * — safer to re-process than silently drop. The watcher reconciles deletions
208
+ * separately via "in cache but not in scan" diffing.
209
+ */
210
+ declare function filterChangedBlocks(blocks: ScannedBlock[], cache: HashCache): Promise<ScannedBlock[]>;
211
+
212
+ export { Block, type BlueprintLoadCause, type BlueprintLoadError, type BlueprintLoadResult, type CacheEntry, type ExtractionError, type ExtractionResult, type ExtractorOptions, Handler, type HandlerCapability, type HashCache, type HasherOptions, type LoadedBlueprint, type Manifest, type Project, type ProjectConfig, ProjectNotFoundError, type ResolvedPaths, type ScanError, type ScanResult, type ScannedBlock, extractAll, filterChangedBlocks, findProject, formatReadmeExcerpt, hashPayload, loadBlueprints, loadHashCache, manifestSchema, projectConfigSchema, saveHashCache, scan };
@@ -7,6 +7,30 @@ import { z as z2 } from "zod";
7
7
  import { z as z3 } from "zod";
8
8
  import { z as z4 } from "zod";
9
9
  import { z as z5 } from "zod";
10
+ import { z as z6 } from "zod";
11
+ var SCHEMA_VERSION = 1;
12
+ var FILESYSTEM_LAYOUT = {
13
+ user: {
14
+ /** `~/.macroscope/` */
15
+ rootDir: ".macroscope",
16
+ /** `~/.macroscope/credentials` — OAuth token store, mode 0600. */
17
+ credentialsFile: ".macroscope/credentials",
18
+ /** `~/.macroscope/machine-id` — stable per-install identifier. */
19
+ machineIdFile: ".macroscope/machine-id"
20
+ },
21
+ project: {
22
+ /** `<project>/.macroscope/` */
23
+ rootDir: ".macroscope",
24
+ /** `<project>/.macroscope/blueprints/` — one folder per kind. */
25
+ blueprintsDir: ".macroscope/blueprints",
26
+ /** `<project>/.macroscope/cache/` */
27
+ cacheDir: ".macroscope/cache",
28
+ /** `<project>/.macroscope/cache/hashes.json` — content-hash cache used by catch-up scan. */
29
+ hashesFile: ".macroscope/cache/hashes.json",
30
+ /** `<project>/.macroscope/cache/queue.json` — watcher push queue, survives restart. */
31
+ queueFile: ".macroscope/cache/queue.json"
32
+ }
33
+ };
10
34
  var BlockManifestSchema = z.object({
11
35
  /** Must reference a registered blueprint's `kind`. */
12
36
  kind: z.string().min(1),
@@ -126,6 +150,23 @@ var ErrorEnvelopeSchema = z5.object({
126
150
  requestId: z5.string().optional()
127
151
  })
128
152
  });
153
+ var ProviderSchema = z6.enum(["github", "gitlab"]);
154
+ var OAuthCredentialsSchema = z6.object({
155
+ provider: ProviderSchema,
156
+ accessToken: z6.string().min(1),
157
+ refreshToken: z6.string().min(1).optional(),
158
+ /** Unix epoch milliseconds at which `accessToken` expires. */
159
+ expiresAt: z6.number().int().nonnegative().optional(),
160
+ /** Provider-side user id (e.g. GitHub `id`, GitLab `id`). Optional — useful
161
+ * for UI display ("logged in as alexspdlr on github") and for the CLI to
162
+ * short-circuit redundant `/user` lookups. */
163
+ providerUserId: z6.string().min(1).optional()
164
+ });
165
+ var UserContextSchema = z6.object({
166
+ userId: z6.string().min(1),
167
+ indexName: z6.string().min(1),
168
+ provider: ProviderSchema
169
+ });
129
170
 
130
171
  // src/core/manifest.ts
131
172
  var manifestSchema = BlockManifestSchema;
@@ -190,10 +231,10 @@ import { existsSync } from "fs";
190
231
  import { readFile } from "fs/promises";
191
232
  import { dirname, join, resolve } from "path";
192
233
  import { parse as parseYaml } from "yaml";
193
- import { z as z6 } from "zod";
194
- var projectConfigSchema = z6.object({
195
- scanRoots: z6.array(z6.string()).default(["."]),
196
- ignore: z6.array(z6.string()).default(["node_modules", ".git", "dist", ".macroscope"])
234
+ import { z as z7 } from "zod";
235
+ var projectConfigSchema = z7.object({
236
+ scanRoots: z7.array(z7.string()).default(["."]),
237
+ ignore: z7.array(z7.string()).default(["node_modules", ".git", "dist", ".macroscope"])
197
238
  });
198
239
  var ProjectNotFoundError = class extends Error {
199
240
  constructor(startedFrom) {
@@ -361,7 +402,7 @@ function extractDefault(mod) {
361
402
  import { createHash } from "crypto";
362
403
  import { existsSync as existsSync3 } from "fs";
363
404
  import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
364
- import { dirname as dirname2, join as join3, relative, sep } from "path";
405
+ import { dirname as dirname2, join as join3, relative, resolve as resolve2, sep } from "path";
365
406
  import { parse as parseYaml2 } from "yaml";
366
407
  async function scan(project) {
367
408
  const blocks = [];
@@ -374,6 +415,7 @@ async function scan(project) {
374
415
  for await (const manifestPath of findManifests(start, ignore)) {
375
416
  const result = await parseBlock(manifestPath, project.root);
376
417
  if (result.kind === "ok") {
418
+ errors.push(...result.errors);
377
419
  if (seenIds.has(result.block.id)) {
378
420
  errors.push({
379
421
  file: manifestPath,
@@ -407,6 +449,7 @@ async function* findManifests(dir, ignore) {
407
449
  }
408
450
  for (const entry of entries) {
409
451
  if (!entry.isDirectory()) continue;
452
+ if (entry.isSymbolicLink()) continue;
410
453
  if (ignore.has(entry.name)) continue;
411
454
  yield* findManifests(join3(dir, entry.name), ignore);
412
455
  }
@@ -456,11 +499,85 @@ async function parseBlock(manifestPath, projectRoot) {
456
499
  const defaultId = toPosix(relative(projectRoot, blockDir));
457
500
  const id = manifest.id ?? defaultId;
458
501
  const contentHash = createHash("sha256").update(raw).digest("hex");
502
+ const fileErrors = [];
503
+ const resolvedPaths = resolveManifestPaths(
504
+ manifest,
505
+ blockDir,
506
+ projectRoot,
507
+ manifestPath,
508
+ fileErrors
509
+ );
459
510
  return {
460
511
  kind: "ok",
461
- block: { id, path: blockDir, manifest, contentHash }
512
+ block: { id, path: blockDir, manifest, contentHash, resolvedPaths },
513
+ errors: fileErrors
462
514
  };
463
515
  }
516
+ var FILE_FIELDS = ["source", "docs", "preview"];
517
+ function resolveManifestPaths(manifest, blockDir, projectRoot, manifestPath, errors) {
518
+ const testsRaw = manifest.tests;
519
+ const testsArray = testsRaw == null ? void 0 : typeof testsRaw === "string" ? [testsRaw] : testsRaw;
520
+ const hasAnyField = FILE_FIELDS.some((f) => manifest[f] != null) || testsArray != null;
521
+ if (!hasAnyField) return void 0;
522
+ const resolved = {};
523
+ for (const field of FILE_FIELDS) {
524
+ const value = manifest[field];
525
+ if (value == null) continue;
526
+ const abs = resolve2(blockDir, value);
527
+ if (!isInsideProject(abs, projectRoot)) {
528
+ errors.push({
529
+ file: manifestPath,
530
+ kind: "validation",
531
+ field,
532
+ message: `Field \`${field}\` in ${manifestPath} resolves to ${abs}, which is outside the project root ${projectRoot}.`
533
+ });
534
+ continue;
535
+ }
536
+ if (!existsSync3(abs)) {
537
+ errors.push({
538
+ file: manifestPath,
539
+ kind: "io",
540
+ field,
541
+ message: `Field \`${field}\` in ${manifestPath} references ${abs}, which does not exist.`
542
+ });
543
+ continue;
544
+ }
545
+ resolved[field] = abs;
546
+ }
547
+ if (testsArray) {
548
+ const resolvedTests = [];
549
+ for (const t of testsArray) {
550
+ const abs = resolve2(blockDir, t);
551
+ if (!isInsideProject(abs, projectRoot)) {
552
+ errors.push({
553
+ file: manifestPath,
554
+ kind: "validation",
555
+ field: "tests",
556
+ message: `Field \`tests\` in ${manifestPath} resolves to ${abs}, which is outside the project root ${projectRoot}.`
557
+ });
558
+ continue;
559
+ }
560
+ if (!existsSync3(abs)) {
561
+ errors.push({
562
+ file: manifestPath,
563
+ kind: "io",
564
+ field: "tests",
565
+ message: `Field \`tests\` in ${manifestPath} references ${abs}, which does not exist.`
566
+ });
567
+ continue;
568
+ }
569
+ resolvedTests.push(abs);
570
+ }
571
+ if (resolvedTests.length > 0) {
572
+ resolved.tests = resolvedTests;
573
+ }
574
+ }
575
+ return Object.keys(resolved).length > 0 ? resolved : void 0;
576
+ }
577
+ function isInsideProject(absPath, projectRoot) {
578
+ const normalized = resolve2(absPath);
579
+ return normalized === projectRoot || normalized.startsWith(projectRoot + sep);
580
+ }
464
581
  function firstZodIssue(err) {
465
582
  const issue = err.issues[0];
466
583
  if (!issue) return { fieldPath: "(root)", message: "unknown validation error" };
@@ -470,14 +587,396 @@ function firstZodIssue(err) {
470
587
  function toPosix(p) {
471
588
  return p.split(sep).join("/");
472
589
  }
590
+
591
+ // src/core/extractor.ts
592
+ import { readFile as readFile3 } from "fs/promises";
593
+ import ts from "typescript";
594
+ var DEFAULT_README_EXCERPT_MAX = 500;
595
+ var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
596
+ var DEFAULT_COMPILER_OPTIONS = {
597
+ target: ts.ScriptTarget.ES2022,
598
+ module: ts.ModuleKind.ESNext,
599
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
600
+ jsx: ts.JsxEmit.ReactJSX,
601
+ allowJs: false,
602
+ noEmit: true,
603
+ skipLibCheck: true,
604
+ esModuleInterop: true,
605
+ allowSyntheticDefaultImports: true,
606
+ strict: false,
607
+ isolatedModules: true
608
+ };
609
+ function defaultProgramFactory(rootFiles) {
610
+ return ts.createProgram(rootFiles, DEFAULT_COMPILER_OPTIONS);
611
+ }
612
+ async function extractAll(blocks, opts = {}) {
613
+ const readmeMax = opts.readmeExcerptMax ?? DEFAULT_README_EXCERPT_MAX;
614
+ const payloads = [];
615
+ const errors = [];
616
+ const sourcesToBlocks = /* @__PURE__ */ new Map();
617
+ for (const block of blocks) {
618
+ if (!block.manifest?.kind) continue;
619
+ const src = block.resolvedPaths?.source;
620
+ if (src && isTsSource(src)) {
621
+ sourcesToBlocks.set(src, block);
622
+ }
623
+ }
624
+ let program;
625
+ let checker;
626
+ if (sourcesToBlocks.size > 0) {
627
+ const rootFiles = Array.from(sourcesToBlocks.keys());
628
+ const factory = opts.programFactory ?? defaultProgramFactory;
629
+ program = factory(rootFiles);
630
+ checker = program.getTypeChecker();
631
+ }
632
+ for (const block of blocks) {
633
+ if (!block.manifest?.kind) continue;
634
+ const payload = {
635
+ blockId: block.id,
636
+ kind: block.manifest.kind,
637
+ symbols: []
638
+ };
639
+ const { name, description, tags } = block.manifest;
640
+ if (typeof name === "string") payload.name = name;
641
+ if (typeof description === "string") payload.description = description;
642
+ if (Array.isArray(tags)) payload.tags = tags;
643
+ const sourcePath = block.resolvedPaths?.source;
644
+ if (sourcePath && isTsSource(sourcePath) && program && checker) {
645
+ const sourceFile = program.getSourceFile(sourcePath);
646
+ if (!sourceFile) {
647
+ errors.push({
648
+ blockId: block.id,
649
+ kind: "parse",
650
+ message: `TS Program did not load ${sourcePath} (parse error or missing file)`
651
+ });
652
+ } else if (hasFatalSyntaxErrors(program, sourceFile)) {
653
+ errors.push({
654
+ blockId: block.id,
655
+ kind: "parse",
656
+ message: `Source file ${sourcePath} has syntax errors; skipping symbol extraction`
657
+ });
658
+ } else {
659
+ payload.symbols = extractSymbols(sourceFile, checker);
660
+ }
661
+ }
662
+ const docsPath = block.resolvedPaths?.docs;
663
+ if (docsPath) {
664
+ try {
665
+ const raw = await readFile3(docsPath, "utf8");
666
+ payload.readmeExcerpt = formatReadmeExcerpt(raw, readmeMax);
667
+ } catch (err) {
668
+ errors.push({
669
+ blockId: block.id,
670
+ kind: "io",
671
+ message: `Failed to read docs at ${docsPath}: ${err.message}`
672
+ });
673
+ }
674
+ }
675
+ payloads.push(payload);
676
+ }
677
+ return { payloads, errors };
678
+ }
679
+ function isTsSource(path) {
680
+ const dot = path.lastIndexOf(".");
681
+ if (dot < 0) return false;
682
+ return TS_EXTENSIONS.has(path.slice(dot).toLowerCase());
683
+ }
684
+ function hasFatalSyntaxErrors(program, sourceFile) {
685
+ const diagnostics = program.getSyntacticDiagnostics(sourceFile);
686
+ return diagnostics.length > 0;
687
+ }
688
+ function extractSymbols(sourceFile, checker) {
689
+ const symbols = [];
690
+ for (const statement of sourceFile.statements) {
691
+ visitTopLevel(statement, sourceFile, checker, symbols);
692
+ }
693
+ const seen = /* @__PURE__ */ new Set();
694
+ return symbols.filter((s) => {
695
+ const key = `${s.kind}\0${s.name}`;
696
+ if (seen.has(key)) return false;
697
+ seen.add(key);
698
+ return true;
699
+ });
700
+ }
701
+ function visitTopLevel(node, sourceFile, checker, out) {
702
+ if (ts.isExportAssignment(node)) {
703
+ out.push(extractDefaultExport(node, sourceFile, checker));
704
+ return;
705
+ }
706
+ if (ts.isExportDeclaration(node)) {
707
+ extractExportDeclaration(node, sourceFile, checker, out);
708
+ return;
709
+ }
710
+ if (!hasExportModifier(node)) return;
711
+ const isDefault = hasDefaultModifier(node);
712
+ if (ts.isFunctionDeclaration(node)) {
713
+ const name = node.name?.text ?? (isDefault ? "default" : void 0);
714
+ if (!name) return;
715
+ out.push({
716
+ name,
717
+ kind: "function",
718
+ signature: renderFunctionSignature(node, name, sourceFile, checker)
719
+ });
720
+ return;
721
+ }
722
+ if (ts.isClassDeclaration(node)) {
723
+ const name = node.name?.text ?? (isDefault ? "default" : void 0);
724
+ if (!name) return;
725
+ out.push({ name, kind: "class", signature: `class ${name}` });
726
+ return;
727
+ }
728
+ if (ts.isInterfaceDeclaration(node)) {
729
+ out.push({
730
+ name: node.name.text,
731
+ kind: "interface",
732
+ signature: oneLineDeclaration(node, sourceFile, `interface ${node.name.text}`)
733
+ });
734
+ return;
735
+ }
736
+ if (ts.isTypeAliasDeclaration(node)) {
737
+ out.push({
738
+ name: node.name.text,
739
+ kind: "type",
740
+ signature: oneLineDeclaration(node, sourceFile, `type ${node.name.text}`)
741
+ });
742
+ return;
743
+ }
744
+ if (ts.isEnumDeclaration(node)) {
745
+ out.push({ name: node.name.text, kind: "enum", signature: `enum ${node.name.text}` });
746
+ return;
747
+ }
748
+ if (ts.isVariableStatement(node)) {
749
+ for (const decl of node.declarationList.declarations) {
750
+ if (!ts.isIdentifier(decl.name)) continue;
751
+ const name = decl.name.text;
752
+ out.push({
753
+ name,
754
+ kind: "const",
755
+ signature: renderVariableSignature(decl, name, checker)
756
+ });
757
+ }
758
+ return;
759
+ }
760
+ }
761
+ function extractDefaultExport(node, sourceFile, checker) {
762
+ const expr = node.expression;
763
+ if (ts.isIdentifier(expr)) {
764
+ const sym = checker.getSymbolAtLocation(expr);
765
+ const type = sym ? checker.getTypeOfSymbolAtLocation(sym, expr) : void 0;
766
+ const signature = type ? checker.typeToString(type) : "default";
767
+ return { name: "default", kind: "const", signature };
768
+ }
769
+ return { name: "default", kind: "const", signature: expr.getText(sourceFile) };
770
+ }
771
+ function extractExportDeclaration(node, sourceFile, checker, out) {
772
+ if (!node.exportClause && node.moduleSpecifier) {
773
+ const moduleSymbol = checker.getSymbolAtLocation(node.moduleSpecifier);
774
+ if (moduleSymbol) {
775
+ for (const exp of checker.getExportsOfModule(moduleSymbol)) {
776
+ out.push(renderExportedSymbol(exp.name, exp, checker, sourceFile));
777
+ }
778
+ }
779
+ return;
780
+ }
781
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) {
782
+ for (const spec of node.exportClause.elements) {
783
+ const name = spec.name.text;
784
+ const sym = checker.getSymbolAtLocation(spec.name);
785
+ out.push(renderExportedSymbol(name, sym, checker, sourceFile));
786
+ }
787
+ }
788
+ }
789
+ function renderExportedSymbol(name, symbol, checker, sourceFile) {
790
+ if (!symbol) return { name, kind: "const", signature: name };
791
+ const aliased = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
792
+ const decl = aliased.declarations?.[0];
793
+ if (decl) {
794
+ if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) {
795
+ return {
796
+ name,
797
+ kind: "function",
798
+ signature: renderFunctionSignature(decl, name, sourceFile, checker)
799
+ };
800
+ }
801
+ if (ts.isClassDeclaration(decl)) return { name, kind: "class", signature: `class ${name}` };
802
+ if (ts.isInterfaceDeclaration(decl)) {
803
+ return {
804
+ name,
805
+ kind: "interface",
806
+ signature: oneLineDeclaration(decl, decl.getSourceFile(), `interface ${name}`)
807
+ };
808
+ }
809
+ if (ts.isTypeAliasDeclaration(decl)) {
810
+ return {
811
+ name,
812
+ kind: "type",
813
+ signature: oneLineDeclaration(decl, decl.getSourceFile(), `type ${name}`)
814
+ };
815
+ }
816
+ if (ts.isEnumDeclaration(decl)) return { name, kind: "enum", signature: `enum ${name}` };
817
+ }
818
+ const type = checker.getTypeOfSymbolAtLocation(aliased, sourceFile);
819
+ return { name, kind: "const", signature: checker.typeToString(type) };
820
+ }
821
+ function renderFunctionSignature(node, name, _sourceFile, checker) {
822
+ const signature = checker.getSignatureFromDeclaration(node);
823
+ if (!signature) return `function ${name}()`;
824
+ return `function ${name}${checker.signatureToString(signature)}`;
825
+ }
826
+ function renderVariableSignature(decl, name, checker) {
827
+ const type = checker.getTypeAtLocation(decl);
828
+ return `const ${name}: ${checker.typeToString(type)}`;
829
+ }
830
+ function oneLineDeclaration(node, sourceFile, fallback) {
831
+ try {
832
+ const text = node.getText(sourceFile);
833
+ const collapsed = text.replace(/\s+/g, " ").trim();
834
+ return collapsed.length > 0 ? collapsed : fallback;
835
+ } catch {
836
+ return fallback;
837
+ }
838
+ }
839
+ function hasExportModifier(node) {
840
+ return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
841
+ }
842
+ function hasDefaultModifier(node) {
843
+ return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Default) !== 0;
844
+ }
845
+ function formatReadmeExcerpt(raw, max) {
846
+ const stripped = raw.replace(/[#*`_~>]/g, "");
847
+ const collapsed = stripped.replace(/\s+/g, " ").trim();
848
+ if (collapsed.length <= max) return collapsed;
849
+ const slice = collapsed.slice(0, max);
850
+ const nextChar = collapsed.charAt(max);
851
+ if (nextChar === "" || /\s/.test(nextChar)) return slice;
852
+ const lastSpace = slice.lastIndexOf(" ");
853
+ if (lastSpace > 0) return slice.slice(0, lastSpace);
854
+ return slice;
855
+ }
856
+
857
+ // src/core/hasher.ts
858
+ import { createHash as createHash2 } from "crypto";
859
+ import { mkdir, readFile as readFile4, rename, stat, writeFile } from "fs/promises";
860
+ import { dirname as dirname3, join as join4 } from "path";
861
+ function canonicalJSON(value) {
862
+ return stringify(value);
863
+ }
864
+ function stringify(value) {
865
+ if (value === null) return "null";
866
+ if (typeof value === "string") return JSON.stringify(value.normalize("NFC"));
867
+ if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
868
+ if (Array.isArray(value)) {
869
+ const parts = value.map((v) => v === void 0 ? "null" : stringify(v));
870
+ return `[${parts.join(",")}]`;
871
+ }
872
+ if (typeof value === "object") {
873
+ const obj = value;
874
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
875
+ const parts = keys.map((k) => `${JSON.stringify(k)}:${stringify(obj[k])}`);
876
+ return `{${parts.join(",")}}`;
877
+ }
878
+ throw new TypeError(`canonicalJSON: unsupported value type ${typeof value}`);
879
+ }
880
+ function hashPayload(payload) {
881
+ const canonical = canonicalJSON(payload);
882
+ return createHash2("sha256").update(canonical, "utf8").digest("hex");
883
+ }
884
+ function resolveHashesPath(opts) {
885
+ const root = opts?.workspaceRoot ?? process.cwd();
886
+ return join4(root, FILESYSTEM_LAYOUT.project.hashesFile);
887
+ }
888
+ async function readCacheFile(opts) {
889
+ const path = resolveHashesPath(opts);
890
+ let raw;
891
+ try {
892
+ raw = await readFile4(path, "utf8");
893
+ } catch (err) {
894
+ const code = err.code;
895
+ if (code !== "ENOENT") {
896
+ console.warn(`[macroscope] failed to read hash cache at ${path}: ${err.message}`);
897
+ }
898
+ return null;
899
+ }
900
+ let parsed;
901
+ try {
902
+ parsed = JSON.parse(raw);
903
+ } catch (err) {
904
+ console.warn(`[macroscope] hash cache at ${path} is not valid JSON: ${err.message}`);
905
+ return null;
906
+ }
907
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
908
+ console.warn(`[macroscope] hash cache at ${path} has unexpected shape; ignoring`);
909
+ return null;
910
+ }
911
+ const obj = parsed;
912
+ if (typeof obj.schemaVersion === "number" && obj.entries && typeof obj.entries === "object" && !Array.isArray(obj.entries)) {
913
+ return { schemaVersion: obj.schemaVersion, entries: obj.entries };
914
+ }
915
+ return { schemaVersion: void 0, entries: obj };
916
+ }
917
+ async function loadHashCache(opts) {
918
+ const file = await readCacheFile(opts);
919
+ return file?.entries ?? {};
920
+ }
921
+ async function saveHashCache(cache, opts) {
922
+ const path = resolveHashesPath(opts);
923
+ const dir = dirname3(path);
924
+ await mkdir(dir, { recursive: true });
925
+ const tmp = `${path}.tmp`;
926
+ const envelope = {
927
+ schemaVersion: opts?.schemaVersion ?? SCHEMA_VERSION,
928
+ entries: cache
929
+ };
930
+ const body = `${JSON.stringify(envelope, null, 2)}
931
+ `;
932
+ await writeFile(tmp, body, "utf8");
933
+ await rename(tmp, path);
934
+ }
935
+ async function filterChangedBlocks(blocks, cache) {
936
+ const changed = [];
937
+ for (const block of blocks) {
938
+ const cached = cache[block.id];
939
+ if (!cached) {
940
+ changed.push(block);
941
+ continue;
942
+ }
943
+ const maxMtime = await maxFileMtime(block);
944
+ if (maxMtime === void 0 || maxMtime > cached.mtime) {
945
+ changed.push(block);
946
+ }
947
+ }
948
+ return changed;
949
+ }
950
+ async function maxFileMtime(block) {
951
+ const paths = [join4(block.path, "macroscope.yaml")];
952
+ if (block.resolvedPaths?.source) paths.push(block.resolvedPaths.source);
953
+ if (block.resolvedPaths?.docs) paths.push(block.resolvedPaths.docs);
954
+ let max;
955
+ for (const p of paths) {
956
+ try {
957
+ const s = await stat(p);
958
+ const m = s.mtimeMs;
959
+ if (max === void 0 || m > max) max = m;
960
+ } catch {
961
+ return void 0;
962
+ }
963
+ }
964
+ return max;
965
+ }
473
966
  export {
474
967
  ProjectNotFoundError,
475
968
  VERSION,
969
+ extractAll,
970
+ filterChangedBlocks,
476
971
  findProject,
972
+ formatReadmeExcerpt,
973
+ hashPayload,
477
974
  isHandler,
478
975
  loadBlueprints,
976
+ loadHashCache,
479
977
  manifestSchema,
480
978
  projectConfigSchema,
979
+ saveHashCache,
481
980
  scan
482
981
  };
483
982
  //# sourceMappingURL=index.js.map