@chrisdudek/yg 5.0.0-alpha.1 → 5.0.0-alpha.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/dist/structure.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/structure/runner.ts
2
2
  import * as fs4 from "fs";
3
- import * as path6 from "path";
3
+ import * as path8 from "path";
4
4
  import { pathToFileURL as pathToFileURL2 } from "url";
5
5
 
6
6
  // src/ast/loader-hook.ts
@@ -36,12 +36,15 @@ function normalizeMappingPath(p) {
36
36
  function toPosix(p) {
37
37
  return p.replace(/\\/g, "/");
38
38
  }
39
+ function toPosixPath(p) {
40
+ return p.replace(/\\/g, "/").replace(/\/+$/, "");
41
+ }
39
42
 
40
43
  // src/structure/ctx-fs.ts
41
44
  var UndeclaredFsReadError = class extends Error {
42
- constructor(path7) {
43
- super(`structure-aspect-undeclared-fs-read: ${path7}`);
44
- this.path = path7;
45
+ constructor(path9) {
46
+ super(`structure-aspect-undeclared-fs-read: ${path9}`);
47
+ this.path = path9;
45
48
  this.name = "UndeclaredFsReadError";
46
49
  }
47
50
  };
@@ -101,8 +104,8 @@ function createCtxFs(params) {
101
104
  const p = assertAllowed(raw);
102
105
  const abs = path2.resolve(projectRoot, p);
103
106
  try {
104
- const stat = fs.statSync(abs);
105
- return stat.isDirectory() ? "dir" : stat.isFile() ? "file" : false;
107
+ const stat2 = fs.statSync(abs);
108
+ return stat2.isDirectory() ? "dir" : stat2.isFile() ? "file" : false;
106
109
  } catch {
107
110
  return false;
108
111
  }
@@ -127,6 +130,21 @@ function createCtxFs(params) {
127
130
  // src/structure/ctx-graph.ts
128
131
  import * as fs2 from "fs";
129
132
  import * as path3 from "path";
133
+
134
+ // src/structure/expand-mapping-sync.ts
135
+ function isPathInMapping(candidate, mapping) {
136
+ const c = normalizeMappingPath(candidate);
137
+ if (c === "") return false;
138
+ for (const raw of mapping) {
139
+ const n = normalizeMappingPath(raw);
140
+ if (n === "") continue;
141
+ if (c === n) return true;
142
+ if (c.startsWith(n + "/")) return true;
143
+ }
144
+ return false;
145
+ }
146
+
147
+ // src/structure/ctx-graph.ts
130
148
  var UndeclaredGraphReadError = class extends Error {
131
149
  constructor(nodePath) {
132
150
  super(`structure-aspect-undeclared-graph-read: ${nodePath}`);
@@ -175,8 +193,8 @@ function createCtxGraph(params) {
175
193
  if (!p) continue;
176
194
  const abs = path3.resolve(projectRoot, p);
177
195
  try {
178
- const stat = fs2.statSync(abs);
179
- if (stat.isFile()) {
196
+ const stat2 = fs2.statSync(abs);
197
+ if (stat2.isFile()) {
180
198
  const content = fs2.readFileSync(abs, "utf8");
181
199
  files.push({ path: p, content });
182
200
  touchedFiles.push(p);
@@ -665,6 +683,144 @@ function collectAllowedReadsForAspect(nodePath, graph) {
665
683
  return allowed;
666
684
  }
667
685
 
686
+ // src/io/hash.ts
687
+ import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
688
+ import path7 from "path";
689
+ import { createHash } from "crypto";
690
+ import { createRequire as createRequire3 } from "module";
691
+
692
+ // src/io/repo-scanner.ts
693
+ import { readFile, readdir } from "fs/promises";
694
+ import { join, relative as relative2, sep } from "path";
695
+ import { createRequire as createRequire2 } from "module";
696
+
697
+ // src/utils/debug-log.ts
698
+ import path6 from "path";
699
+
700
+ // src/io/repo-scanner.ts
701
+ var require2 = createRequire2(import.meta.url);
702
+ var ignoreFactory = require2("ignore");
703
+
704
+ // src/io/hash.ts
705
+ var require3 = createRequire3(import.meta.url);
706
+ var ignoreFactory2 = require3("ignore");
707
+ async function loadRootGitignoreStack2(projectRoot) {
708
+ if (!projectRoot) return [];
709
+ try {
710
+ const content = await readFile2(path7.join(projectRoot, ".gitignore"), "utf-8");
711
+ const matcher = ignoreFactory2();
712
+ matcher.add(content);
713
+ return [{ basePath: projectRoot, matcher }];
714
+ } catch {
715
+ return [];
716
+ }
717
+ }
718
+ function isIgnoredByStack2(candidatePath, stack) {
719
+ for (const { basePath, matcher } of stack) {
720
+ const relativePath = toPosix(path7.relative(basePath, candidatePath));
721
+ if (relativePath === "" || relativePath.startsWith("..")) continue;
722
+ if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
723
+ }
724
+ return false;
725
+ }
726
+ function hashString(content) {
727
+ return createHash("sha256").update(content).digest("hex");
728
+ }
729
+ var EMPTY_IDENTITY = { ownSubset: hashString(""), ports: {}, aspects: {} };
730
+ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
731
+ let stack = options.gitignoreStack ?? [];
732
+ try {
733
+ const localContent = await readFile2(path7.join(directoryPath, ".gitignore"), "utf-8");
734
+ const localMatcher = ignoreFactory2();
735
+ localMatcher.add(localContent);
736
+ stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
737
+ } catch {
738
+ }
739
+ const entries = await readdir2(directoryPath, { withFileTypes: true });
740
+ const dirs = [];
741
+ const files = [];
742
+ for (const entry of entries) {
743
+ const absoluteChildPath = path7.join(directoryPath, entry.name);
744
+ if (isIgnoredByStack2(absoluteChildPath, stack)) continue;
745
+ if (entry.isDirectory()) dirs.push(absoluteChildPath);
746
+ else if (entry.isFile()) files.push(absoluteChildPath);
747
+ }
748
+ const [dirResults, fileStats] = await Promise.all([
749
+ Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
750
+ projectRoot: options.projectRoot,
751
+ gitignoreStack: stack
752
+ }))),
753
+ Promise.all(files.map(async (f) => {
754
+ const fileStat = await stat(f);
755
+ return {
756
+ relPath: toPosixPath(path7.relative(rootDirectoryPath, f)),
757
+ absPath: f,
758
+ mtimeMs: fileStat.mtimeMs
759
+ };
760
+ }))
761
+ ]);
762
+ const result = [];
763
+ for (const nested of dirResults) result.push(...nested);
764
+ result.push(...fileStats);
765
+ return result;
766
+ }
767
+ async function expandMappingPaths(projectRoot, mappingPaths) {
768
+ const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
769
+ const result = [];
770
+ for (const mp of mappingPaths) {
771
+ const absPath = path7.join(projectRoot, mp);
772
+ try {
773
+ const st = await stat(absPath);
774
+ if (st.isDirectory()) {
775
+ const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
776
+ projectRoot,
777
+ gitignoreStack
778
+ });
779
+ for (const entry of dirEntries) {
780
+ result.push(toPosixPath(path7.join(mp, entry.relPath)));
781
+ }
782
+ } else {
783
+ result.push(toPosixPath(mp));
784
+ }
785
+ } catch {
786
+ continue;
787
+ }
788
+ }
789
+ return result;
790
+ }
791
+
792
+ // src/ast/suppress.ts
793
+ import { extname as extname3 } from "path";
794
+
795
+ // src/ast/find-comments.ts
796
+ import { extname as extname2 } from "path";
797
+ function findComments(target) {
798
+ const hasAst = "ast" in target;
799
+ const hasRootNode = "rootNode" in target;
800
+ if (hasAst && hasRootNode) {
801
+ throw new Error("AST_FINDCOMMENTS_AMBIGUOUS_TARGET: pass either ast or rootNode, not both");
802
+ }
803
+ let language = "language" in target ? target.language : void 0;
804
+ if (language === void 0 && "path" in target) {
805
+ language = getLanguageForExtension(extname2(target.path)) ?? void 0;
806
+ }
807
+ if (language === void 0) {
808
+ throw new Error(
809
+ "AST_FINDCOMMENTS_NO_LANGUAGE: pass a SourceFile whose path has a known extension, or an explicit { language }"
810
+ );
811
+ }
812
+ const def = LANGUAGES[language];
813
+ if (def === void 0) {
814
+ throw new Error(`AST_FINDCOMMENTS_UNKNOWN_LANGUAGE: '${language}' not in registry`);
815
+ }
816
+ const root = hasAst ? target.ast.rootNode : target.rootNode;
817
+ const out = [];
818
+ for (const type of def.commentTypes) {
819
+ out.push(...root.descendantsOfType(type));
820
+ }
821
+ return out;
822
+ }
823
+
668
824
  // src/ast/suppress.ts
669
825
  var SuppressMarkerError = class extends Error {
670
826
  constructor(message, file, line) {
@@ -711,7 +867,10 @@ function parseMarker(commentText, line, file) {
711
867
  return null;
712
868
  }
713
869
  function collectSuppressions(tree, file, totalLines) {
714
- const comments = tree.rootNode.descendantsOfType("comment");
870
+ if (getLanguageForExtension(extname3(file)) === null) {
871
+ return [];
872
+ }
873
+ const comments = findComments({ path: file, ast: tree });
715
874
  const markers = [];
716
875
  for (const c of comments) {
717
876
  const m = parseMarker(c.text, c.startPosition.row + 1, file);
@@ -827,64 +986,64 @@ ${data.next}`);
827
986
  }
828
987
  messageData;
829
988
  };
830
- function buildOwnFiles(node, projectRoot, touchedFiles) {
831
- const childMappings = /* @__PURE__ */ new Set();
989
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
990
+ ".gif",
991
+ ".png",
992
+ ".jpg",
993
+ ".jpeg",
994
+ ".webp",
995
+ ".bmp",
996
+ ".ico",
997
+ ".svgz",
998
+ ".woff",
999
+ ".woff2",
1000
+ ".ttf",
1001
+ ".otf",
1002
+ ".eot",
1003
+ ".zip",
1004
+ ".gz",
1005
+ ".tgz",
1006
+ ".tar",
1007
+ ".bz2",
1008
+ ".7z",
1009
+ ".pdf",
1010
+ ".mp4",
1011
+ ".mov",
1012
+ ".webm",
1013
+ ".mp3",
1014
+ ".wav",
1015
+ ".wasm",
1016
+ ".bin"
1017
+ ]);
1018
+ async function buildOwnFiles(node, projectRoot, touchedFiles) {
1019
+ const childMappingEntries = [];
832
1020
  for (const child of node.children) {
833
1021
  for (const raw of child.meta.mapping ?? []) {
834
1022
  const p = normalizeMappingPath(raw);
835
- if (p) childMappings.add(p);
1023
+ if (p) childMappingEntries.push(p);
836
1024
  }
837
1025
  }
1026
+ const rawMapping = (node.meta.mapping ?? []).map(normalizeMappingPath).filter((p) => p !== "");
1027
+ const expanded = await expandMappingPaths(projectRoot, rawMapping);
838
1028
  const result = [];
839
- for (const raw of node.meta.mapping ?? []) {
840
- const p = normalizeMappingPath(raw);
841
- if (!p || childMappings.has(p)) continue;
842
- const abs = path6.resolve(projectRoot, p);
1029
+ for (const p of expanded) {
1030
+ if (childMappingEntries.length > 0 && isPathInMapping(p, childMappingEntries)) continue;
1031
+ if (BINARY_EXTENSIONS.has(path8.extname(p).toLowerCase())) continue;
1032
+ const abs = path8.resolve(projectRoot, p);
1033
+ let content;
843
1034
  try {
844
- const stat = fs4.statSync(abs);
845
- if (stat.isFile()) {
846
- const content = fs4.readFileSync(abs, "utf8");
847
- result.push({ path: p, content });
848
- touchedFiles.push(p);
849
- }
1035
+ content = fs4.readFileSync(abs, "utf8");
850
1036
  } catch {
1037
+ continue;
851
1038
  }
1039
+ result.push({ path: p, content });
1040
+ touchedFiles.push(p);
852
1041
  }
853
1042
  return result;
854
1043
  }
855
- function enumerateMappedFilesSync(mappingPaths, projectRoot) {
856
- const out = [];
857
- for (const raw of mappingPaths) {
858
- const rel = normalizeMappingPath(raw);
859
- if (!rel) continue;
860
- const abs = path6.resolve(projectRoot, rel);
861
- try {
862
- const stat = fs4.statSync(abs);
863
- if (stat.isFile()) {
864
- out.push(rel);
865
- } else if (stat.isDirectory()) {
866
- for (const sub of walkDirSync(abs)) {
867
- const relSub = path6.relative(projectRoot, sub).split(/[\\/]/).join("/");
868
- out.push(relSub);
869
- }
870
- }
871
- } catch {
872
- }
873
- }
874
- return out;
875
- }
876
- function* walkDirSync(dir) {
877
- let entries;
878
- try {
879
- entries = fs4.readdirSync(dir, { withFileTypes: true });
880
- } catch {
881
- return;
882
- }
883
- for (const e of entries) {
884
- const child = path6.join(dir, e.name);
885
- if (e.isDirectory()) yield* walkDirSync(child);
886
- else if (e.isFile()) yield child;
887
- }
1044
+ async function enumerateMappedFilesAsync(mappingPaths, projectRoot) {
1045
+ const normalized = mappingPaths.map(normalizeMappingPath).filter((p) => p !== "");
1046
+ return expandMappingPaths(projectRoot, normalized);
888
1047
  }
889
1048
  async function runStructureAspect(params) {
890
1049
  ensureLoaderRegistered();
@@ -899,8 +1058,8 @@ async function runStructureAspect(params) {
899
1058
  next: `Pass an existing node path, or add the node to the graph.`
900
1059
  });
901
1060
  }
902
- const aspectDirAbs = path6.isAbsolute(aspectDir) ? aspectDir : path6.resolve(projectRoot, aspectDir);
903
- const checkPath = path6.join(aspectDirAbs, "check.mjs");
1061
+ const aspectDirAbs = path8.isAbsolute(aspectDir) ? aspectDir : path8.resolve(projectRoot, aspectDir);
1062
+ const checkPath = path8.join(aspectDirAbs, "check.mjs");
904
1063
  let mod;
905
1064
  try {
906
1065
  mod = await import(pathToFileURL2(checkPath).href);
@@ -923,7 +1082,7 @@ async function runStructureAspect(params) {
923
1082
  const ctxFs = createCtxFs({ allowedSet, projectRoot, touchedFiles });
924
1083
  const ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles });
925
1084
  const parsers = createCtxParsers({ allowedSet, projectRoot, touchedFiles, astCache });
926
- const ownFiles = buildOwnFiles(node, projectRoot, touchedFiles);
1085
+ const ownFiles = await buildOwnFiles(node, projectRoot, touchedFiles);
927
1086
  await prewarmupAstCache({ astCache, projectRoot, files: ownFiles });
928
1087
  const ownFilesEnriched = enrichFilesWithAst(ownFiles, astCache);
929
1088
  const ctx = {
@@ -946,8 +1105,8 @@ async function runStructureAspect(params) {
946
1105
  for (const rel of node.meta.relations ?? []) {
947
1106
  const target = graph.nodes.get(rel.target);
948
1107
  if (!target) continue;
949
- for (const p of enumerateMappedFilesSync(target.meta.mapping ?? [], projectRoot)) {
950
- const abs = path6.resolve(projectRoot, p);
1108
+ for (const p of await enumerateMappedFilesAsync(target.meta.mapping ?? [], projectRoot)) {
1109
+ const abs = path8.resolve(projectRoot, p);
951
1110
  try {
952
1111
  const content = fs4.readFileSync(abs, "utf8");
953
1112
  astInputSet.push({ path: p, content });
@@ -1090,35 +1249,6 @@ function inFile(file, pattern) {
1090
1249
  if ("contains" in pattern) return file.path.includes(pattern.contains);
1091
1250
  return false;
1092
1251
  }
1093
-
1094
- // src/ast/find-comments.ts
1095
- import { extname as extname2 } from "path";
1096
- function findComments(target) {
1097
- const hasAst = "ast" in target;
1098
- const hasRootNode = "rootNode" in target;
1099
- if (hasAst && hasRootNode) {
1100
- throw new Error("AST_FINDCOMMENTS_AMBIGUOUS_TARGET: pass either ast or rootNode, not both");
1101
- }
1102
- let language = "language" in target ? target.language : void 0;
1103
- if (language === void 0 && "path" in target) {
1104
- language = getLanguageForExtension(extname2(target.path)) ?? void 0;
1105
- }
1106
- if (language === void 0) {
1107
- throw new Error(
1108
- "AST_FINDCOMMENTS_NO_LANGUAGE: pass a SourceFile whose path has a known extension, or an explicit { language }"
1109
- );
1110
- }
1111
- const def = LANGUAGES[language];
1112
- if (def === void 0) {
1113
- throw new Error(`AST_FINDCOMMENTS_UNKNOWN_LANGUAGE: '${language}' not in registry`);
1114
- }
1115
- const root = hasAst ? target.ast.rootNode : target.rootNode;
1116
- const out = [];
1117
- for (const type of def.commentTypes) {
1118
- out.push(...root.descendantsOfType(type));
1119
- }
1120
- return out;
1121
- }
1122
1252
  export {
1123
1253
  StructureRunnerError,
1124
1254
  closest,
@@ -15,16 +15,35 @@ name: CrossCuttingRequirementName # required — display name
15
15
  description: "Short description" # required — shown in yg aspects output and context packages.
16
16
  # Validator emits description-missing if absent.
17
17
 
18
- reviewer: # requiredwhich reviewer verifies this aspect
19
- type: llm # required: 'llm' or 'deterministic'
20
- # llm — aspect ships content.md; an LLM reads it and judges the code.
21
- # deterministic — aspect ships check.mjs; the structure runner executes it
22
- # locally with graph-aware ctx (files, fs, graph, parsers).
23
- # Language-agnostic. No LLM call, zero token cost.
24
- # tier: deep # optional, only when type: llm.
18
+ # reviewer: # OPTIONAL — reviewer kind is inferred from rule-file presence:
19
+ #
20
+ # content.md present → llm
21
+ # check.mjs present → deterministic
22
+ # neither + implies declared → aggregate
23
+ #
24
+ # The reviewer: block is only required when you need to declare
25
+ # reviewer.tier: for an LLM aspect. When present, an explicit
26
+ # reviewer.type must agree with the inferred kind (validator
27
+ # error otherwise).
28
+ #
29
+ # Three kinds:
30
+ # llm — aspect ships content.md; an LLM reads it and
31
+ # judges the code against the rule.
32
+ # deterministic — aspect ships check.mjs; the runner executes it
33
+ # locally with graph-aware ctx (files, fs, graph,
34
+ # parsers). Language-agnostic. No LLM call, zero
35
+ # token cost.
36
+ # aggregate — aspect ships neither rule source but declares
37
+ # implies:. A named bundle — expands its implied
38
+ # aspects onto every node where effective. Has no
39
+ # own reviewer and produces no own verdict. An
40
+ # aspect with neither rule source and no implies:
41
+ # is rejected (aspect-empty).
42
+ # type: llm # optional; must be 'llm', 'deterministic', or 'aggregate' if set.
43
+ # tier: deep # optional, only when type: llm (or inferred llm).
25
44
  # If omitted, the aspect uses reviewer.default from yg-config.yaml.
26
45
  # If present, must reference a key under reviewer.tiers in the config.
27
- # Forbidden when type is 'deterministic'.
46
+ # Forbidden when type is 'deterministic' or 'aggregate'.
28
47
 
29
48
  status: enforced # optional — aspect-level default. enum: draft | advisory | enforced.
30
49
  # Absent → 'enforced'.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrisdudek/yg",
3
- "version": "5.0.0-alpha.1",
3
+ "version": "5.0.0-alpha.2",
4
4
  "description": "Architecture rules your coding agent can't ignore. Written in Markdown, verified on every change, enforced in the agent's loop — not after on a PR. Works with Claude Code, Cursor, Copilot, Codex, Cline.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "dist/**/*.js",
11
11
  "dist/**/*.d.ts",
12
12
  "dist/**/*.wasm",
13
+ "dist/**/*.node-types.json",
13
14
  "graph-schemas/"
14
15
  ],
15
16
  "engines": {
@@ -87,7 +88,7 @@
87
88
  "@tree-sitter-grammars/tree-sitter-yaml": "^0.7.1",
88
89
  "@types/node": "^25.9.1",
89
90
  "@types/semver": "^7.7.1",
90
- "@vitest/coverage-v8": "^4.1.7",
91
+ "@vitest/coverage-v8": "^4.1.8",
91
92
  "eslint": "^10.4.1",
92
93
  "prettier": "^3.8.3",
93
94
  "tree-sitter-c": "^0.24.1",
@@ -105,6 +106,6 @@
105
106
  "tsup": "^8.5.1",
106
107
  "typescript": "^6.0.3",
107
108
  "typescript-eslint": "^8.60.0",
108
- "vitest": "^4.1.7"
109
+ "vitest": "^4.1.8"
109
110
  }
110
111
  }