@catspec/core 0.1.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.
package/dist/index.d.ts CHANGED
@@ -48,7 +48,7 @@ declare const CODES: {
48
48
  };
49
49
  readonly CS0107: {
50
50
  readonly severity: "error";
51
- readonly title: "architecture spec missing or ambiguous";
51
+ readonly title: "architecture spec ambiguous or not staged";
52
52
  readonly docs: string;
53
53
  };
54
54
  readonly CS0108: {
@@ -2063,9 +2063,10 @@ interface SpecModel {
2063
2063
  config: CatspecConfig;
2064
2064
  specs: SpecSet;
2065
2065
  graph: DependencyGraph;
2066
- /** The architecture spec (`type: architecture` at `specs.architecture`), if exactly one exists. */
2066
+ /** The architecture spec (`type: architecture` at `specs.architecture`), if exactly one exists:
2067
+ * it is optional (ADR-028). */
2067
2068
  architecture?: Spec;
2068
- /** ADR ids of the architecture spec's log. */
2069
+ /** ADR ids of the architecture spec's log and of the plain ADR files at `specs.architecture`. */
2069
2070
  adrIds: string[];
2070
2071
  /** Every diagnostic of `catspec check --specs-only`, sorted. */
2071
2072
  diagnostics: Diagnostic[];
@@ -2081,6 +2082,22 @@ interface SpecModel {
2081
2082
  * `catspec check --specs-only`). Spec errors of `deprecated` specs are lowered to warnings (§D8).
2082
2083
  */
2083
2084
  declare function loadSpecModel(root: string, config: CatspecConfig, source?: SourceProvider): Promise<SpecModel>;
2085
+ /**
2086
+ * Matches the files of `specs.architecture` (ADR-028): a glob or a file path as written, or a
2087
+ * folder (no glob characters and no `.md` ending) for every Markdown file in it.
2088
+ */
2089
+ declare function architectureMatcher(pattern: string): (path: string) => boolean;
2090
+ /**
2091
+ * True when a file at `specs.architecture` is meant as CatSpec's architecture spec: its
2092
+ * frontmatter has a `catspec:` key or `type: architecture`. Other files (no frontmatter, or the
2093
+ * frontmatter of an ADR tool) are plain ADRs. A line match, so broken YAML is still reported.
2094
+ */
2095
+ declare function isArchitectureSpecText(text: string): boolean;
2096
+ /** The ADR ids of a plain ADR file: its file name (`ADR-007-….md`, `0007-….md`) and its
2097
+ * `ADR-NNN` headings. */
2098
+ declare function plainAdrIds(file: string, text: string): string[];
2099
+ /** ADR ids compare by number: `ADR-7`, `ADR-007` and `ADR-0007` are the same decision. */
2100
+ declare const sameAdrId: (a: string, b: string) => boolean;
2084
2101
 
2085
2102
  /**
2086
2103
  * `status`: enforcement by spec status (§D8): draft, ready and deprecated specs aren't checked,
@@ -2706,4 +2723,4 @@ declare function formatStageRun(run: StageRun): string;
2706
2723
  /** Package name. */
2707
2724
  declare const packageName = "@catspec/core";
2708
2725
 
2709
- export { type Ambiguity, type AttributeFact, BINDING_KINDS, type BaselineSymbol, type Binding, type BindingKind, type BindingResolution, BindingSchema, type BodyReference, type BuiltinGate, CODES, CONFIG_FILE, type CallFact, type CatspecConfig, type CheckMode, type CheckOptions, type CheckResult, type CodeInfo, type Codebase, type CommandRunner, ConfigError, type ConfigIssue, ConfigSchema, type ContractBullet, type CoverageReport, DependencyGraph, type Diagnostic, type DiagnosticCode, type DiagnosticCounts, type DiagnosticInit, type Driver, type DropUndefined, type EditClass, type Evidence, FRONTMATTER_KEYS, type Feature, type FeatureFolder, type FeaturePosition, type FileFacts, GATES, type Gate, type GateResult, Git, GitError, GitUnavailableError, type ImportFact, type ImportKind, type ImportedName, IndexProvider, type ItemPosition, type ItemResult, type LanguageAdapter, LineIndex, type Links, type LoadedConfig, type Location, type LockCheckOptions, type LockFile, type LockedSymbol, type Lost, MAX_SOURCE_BYTES, MEMBER_KINDS, MODIFIERS, ManualRuleSchema, type Mapping, type Member, MemberEntrySchema, type MemberKind, MemberSchema, type Modifier, type NextLockOptions, type NextStep, PLACEHOLDERS, PRESETS, PRESET_PROFILES, type ParamFact, type PatchOp, type PatchPath, type Phase, type Position, type PresetName, QueryRuleSchema, RULE_CLASSES, RULE_SCHEMAS, type Range, type Ratio, type ReadSpecOptions, type RenameInput, type RenameMap, type RenameReport, type ResolvedBinding, RevisionProvider, type Rule, type RuleClass, type RuleInput, RuleSchema, SDD_SKILLS, SKILL_DIRS, SPEC_ID_PATTERN, SPEC_STATUSES, SPEC_TYPES, SPEC_WRITING_STEP, STRUCTURAL_KINDS, SYMBOL_KINDS, type ScoredCandidate, type Severity, type Shape, type SourceEntry, type SourceFile, type SourceProvider, type Spec, type SpecBody, type SpecCoverage, type SpecEdit, SpecEditError, SpecFrontmatterSchema, type SpecModel, type SpecPatch, SpecSet, type SpecSource, type SpecStatus, type SpecType, type SplitSource, type Stage, type StageInfo, type StageRun, type StagedChange, type StagedScope, type StatusChange, type StatusGate, type StructuralRule, StructuralRuleSchema, type Suggestion, type SymbolEntry, type SymbolFact, type SymbolKind, SymbolTable, type SyncOptions, type SyncPlan, type Target, type TaskItem, TestRuleSchema, type TestRun, type Transition, type TreeEntry, type TypeRef, type Waiver, WaiverSchema, Workflow, WorkflowError, type WorkflowInput, type WorkflowOptions, type WorkflowProfile, WorkflowSchema, WorkingTreeProvider, agentSteps, applyLock, applyPatch, attributeMatches, baselineFromLock, baselineFromSymbol, bindingItems, bodyTokens, calleeNames, checkCode, classifyOp, classifyPatch, codeLocation, compareStrings, configJsonSchema, countDiagnostics, coverage, createDiagnostic, customWorkflowYaml, defaultBindingId, defaultConfig, describeStage, detectPreset, detectRenames, didYouMean, driverText, editPatch, editYaml, evaluateContract, evaluateRule, featureDirYaml, featureDirsText, featureFolderOf, finalGates, findConfig, findGitCommonDir, findGitRoot, findProgram, findSddSkills, fingerprintOf, formatLocation, formatNextStep, formatPath, formatStageRun, frontmatterLocation, gateName, gitBlobSha, globMatcher, hasLockFile, hookWorkflowDiagnostics, importTargets, inModule, isBatchScript, isEnforced, isListedPath, isRegisteredCode, isSegmentSuffix, itemHashes, jaccard, kebabCase, listFiles, loadConfig, loadSpecModel, lockPathOf, lockedFiles, memberNames, nameFreeFingerprint, nameMatches, nextLock, normalizeBinding, onPath, openCheckboxes, packageName, parseBody, parseConfig, parseErrorDiagnostics, parseLock, parseSpecSource, planSync, qualifiedNameMatches, queryDiagnostics, reached, readSpec, readYaml, relativePosix, renameScore, resolveBinding, resolveWorkflow, rewriteSymbol, runCommand, runTestCommand, serializeLock, shapeOf, signatureText, skillFile, sortDiagnostics, specHash, specJsonSchema, specLocation, splitFrontmatter, splitResolved, splitSymbol, stageLines, stageSnippet, stageStatus, stagedDiagnostics, stagedScope, statusGate, stripUndefined, testCommandArgs, toPosix, tokenSimilarity, typeMatches, typePatternMatches, unknownPlaceholders, wildcard, withFrontmatter, workflowDoctor, workflowProfile, writeStage, writesSpecs };
2726
+ export { type Ambiguity, type AttributeFact, BINDING_KINDS, type BaselineSymbol, type Binding, type BindingKind, type BindingResolution, BindingSchema, type BodyReference, type BuiltinGate, CODES, CONFIG_FILE, type CallFact, type CatspecConfig, type CheckMode, type CheckOptions, type CheckResult, type CodeInfo, type Codebase, type CommandRunner, ConfigError, type ConfigIssue, ConfigSchema, type ContractBullet, type CoverageReport, DependencyGraph, type Diagnostic, type DiagnosticCode, type DiagnosticCounts, type DiagnosticInit, type Driver, type DropUndefined, type EditClass, type Evidence, FRONTMATTER_KEYS, type Feature, type FeatureFolder, type FeaturePosition, type FileFacts, GATES, type Gate, type GateResult, Git, GitError, GitUnavailableError, type ImportFact, type ImportKind, type ImportedName, IndexProvider, type ItemPosition, type ItemResult, type LanguageAdapter, LineIndex, type Links, type LoadedConfig, type Location, type LockCheckOptions, type LockFile, type LockedSymbol, type Lost, MAX_SOURCE_BYTES, MEMBER_KINDS, MODIFIERS, ManualRuleSchema, type Mapping, type Member, MemberEntrySchema, type MemberKind, MemberSchema, type Modifier, type NextLockOptions, type NextStep, PLACEHOLDERS, PRESETS, PRESET_PROFILES, type ParamFact, type PatchOp, type PatchPath, type Phase, type Position, type PresetName, QueryRuleSchema, RULE_CLASSES, RULE_SCHEMAS, type Range, type Ratio, type ReadSpecOptions, type RenameInput, type RenameMap, type RenameReport, type ResolvedBinding, RevisionProvider, type Rule, type RuleClass, type RuleInput, RuleSchema, SDD_SKILLS, SKILL_DIRS, SPEC_ID_PATTERN, SPEC_STATUSES, SPEC_TYPES, SPEC_WRITING_STEP, STRUCTURAL_KINDS, SYMBOL_KINDS, type ScoredCandidate, type Severity, type Shape, type SourceEntry, type SourceFile, type SourceProvider, type Spec, type SpecBody, type SpecCoverage, type SpecEdit, SpecEditError, SpecFrontmatterSchema, type SpecModel, type SpecPatch, SpecSet, type SpecSource, type SpecStatus, type SpecType, type SplitSource, type Stage, type StageInfo, type StageRun, type StagedChange, type StagedScope, type StatusChange, type StatusGate, type StructuralRule, StructuralRuleSchema, type Suggestion, type SymbolEntry, type SymbolFact, type SymbolKind, SymbolTable, type SyncOptions, type SyncPlan, type Target, type TaskItem, TestRuleSchema, type TestRun, type Transition, type TreeEntry, type TypeRef, type Waiver, WaiverSchema, Workflow, WorkflowError, type WorkflowInput, type WorkflowOptions, type WorkflowProfile, WorkflowSchema, WorkingTreeProvider, agentSteps, applyLock, applyPatch, architectureMatcher, attributeMatches, baselineFromLock, baselineFromSymbol, bindingItems, bodyTokens, calleeNames, checkCode, classifyOp, classifyPatch, codeLocation, compareStrings, configJsonSchema, countDiagnostics, coverage, createDiagnostic, customWorkflowYaml, defaultBindingId, defaultConfig, describeStage, detectPreset, detectRenames, didYouMean, driverText, editPatch, editYaml, evaluateContract, evaluateRule, featureDirYaml, featureDirsText, featureFolderOf, finalGates, findConfig, findGitCommonDir, findGitRoot, findProgram, findSddSkills, fingerprintOf, formatLocation, formatNextStep, formatPath, formatStageRun, frontmatterLocation, gateName, gitBlobSha, globMatcher, hasLockFile, hookWorkflowDiagnostics, importTargets, inModule, isArchitectureSpecText, isBatchScript, isEnforced, isListedPath, isRegisteredCode, isSegmentSuffix, itemHashes, jaccard, kebabCase, listFiles, loadConfig, loadSpecModel, lockPathOf, lockedFiles, memberNames, nameFreeFingerprint, nameMatches, nextLock, normalizeBinding, onPath, openCheckboxes, packageName, parseBody, parseConfig, parseErrorDiagnostics, parseLock, parseSpecSource, plainAdrIds, planSync, qualifiedNameMatches, queryDiagnostics, reached, readSpec, readYaml, relativePosix, renameScore, resolveBinding, resolveWorkflow, rewriteSymbol, runCommand, runTestCommand, sameAdrId, serializeLock, shapeOf, signatureText, skillFile, sortDiagnostics, specHash, specJsonSchema, specLocation, splitFrontmatter, splitResolved, splitSymbol, stageLines, stageSnippet, stageStatus, stagedDiagnostics, stagedScope, statusGate, stripUndefined, testCommandArgs, toPosix, tokenSimilarity, typeMatches, typePatternMatches, unknownPlaceholders, wildcard, withFrontmatter, workflowDoctor, workflowProfile, writeStage, writesSpecs };
package/dist/index.js CHANGED
@@ -32,8 +32,8 @@ var CODES = {
32
32
  },
33
33
  CS0107: {
34
34
  severity: "error",
35
- title: "architecture spec missing or ambiguous",
36
- docs: "Exactly one file must match `specs.architecture` (default `docs/architecture/adr/*-adr.md`). Create it with `catspec init`, remove the extra matches, or change `specs.architecture` in `catspec.yaml`. In `check --staged`, an ADR log that exists on disk but isn't staged yet is a warning that names the file to stage, so the first commit after `catspec init` isn't blocked."
35
+ title: "architecture spec ambiguous or not staged",
36
+ docs: "The architecture spec is optional. If you have one, it is the one file at `specs.architecture` (default `docs/architecture/adr/*-adr.md`; a file, a glob or a folder) whose frontmatter has `catspec:` or `type: architecture`. More than one such file is an error: keep the repository-wide rules in one file, or narrow `specs.architecture` in `catspec.yaml`. Plain ADR files without CatSpec frontmatter are fine. In `check --staged`, an architecture spec that exists on disk but isn't staged yet is a warning that names the file to stage, so the commit isn't blocked."
37
37
  },
38
38
  CS0108: {
39
39
  severity: "warning",
@@ -53,7 +53,7 @@ var CODES = {
53
53
  CS0112: {
54
54
  severity: "error",
55
55
  title: "unknown ADR id",
56
- docs: "A rule's `adr` field names a decision that isn't in the architecture spec's ADR log (a `## ADR-NNN: <title>` heading). Fix the id or add the decision to the log."
56
+ docs: "A rule's `adr` field names a decision that isn't among your ADRs: a `## ADR-NNN: <title>` heading of the architecture spec, or a plain ADR file at `specs.architecture` (its file name, such as `0007-use-postgres.md`, or an `ADR-NNN` heading). Ids compare by number, so ADR-7 and ADR-0007 are the same. Fix the id or add the decision. Without any ADRs, `adr` isn't checked."
57
57
  },
58
58
  CS0130: {
59
59
  severity: "error",
@@ -1098,7 +1098,9 @@ var ConfigSchema = z3.strictObject({
1098
1098
  workflow: WorkflowSchema.prefault({}),
1099
1099
  specs: z3.strictObject({
1100
1100
  include: globList.default(["docs/spec/*/*.spec.md"]).describe("Machine spec files."),
1101
- architecture: z3.string().min(1).default("docs/architecture/adr/*-adr.md").describe("The architecture spec (ADR log); exactly one file must match (CS0107).")
1101
+ architecture: z3.string().min(1).default("docs/architecture/adr/*-adr.md").describe(
1102
+ "Optional. Your ADRs: a file, a glob or a folder. At most one of these files is the architecture spec (CatSpec frontmatter, repository-wide rules); the others are plain ADRs that rules can refer to (CS0107, CS0112)."
1103
+ )
1102
1104
  }).prefault({}),
1103
1105
  languages: z3.strictObject({
1104
1106
  typescript: language(["src/**/*.{ts,tsx}"], ["**/*.d.ts", "**/node_modules/**"]),
@@ -1167,6 +1169,9 @@ function parseConfig(text2, file = CONFIG_FILE) {
1167
1169
  { file, line: 1, column: 1, key: "", message: `the config can't be read: ${reason}` }
1168
1170
  ]);
1169
1171
  }
1172
+ if (value !== null && typeof value === "object" && value.specs === null) {
1173
+ delete value.specs;
1174
+ }
1170
1175
  const result = ConfigSchema.safeParse(value);
1171
1176
  if (result.success) return withWorkflowDefaults(result.data, value);
1172
1177
  throw new ConfigError(
@@ -3793,7 +3798,8 @@ var DependencyGraph = class {
3793
3798
  };
3794
3799
 
3795
3800
  // src/load.ts
3796
- import { posix as posix3 } from "path";
3801
+ import { readFile as readFile3 } from "fs/promises";
3802
+ import { join as join4, posix as posix3 } from "path";
3797
3803
  var SpecSet = class {
3798
3804
  byId = /* @__PURE__ */ new Map();
3799
3805
  byFile = /* @__PURE__ */ new Map();
@@ -3822,7 +3828,7 @@ async function loadSpecModel(root, config, source = new WorkingTreeProvider(root
3822
3828
  const specs = new SpecSet();
3823
3829
  const options = { unenforcedProse: config.enforcement.unenforced_prose };
3824
3830
  const isIncluded = globMatcher(config.specs.include);
3825
- const isArchitecture = globMatcher(config.specs.architecture);
3831
+ const isArchitecture = architectureMatcher(config.specs.architecture);
3826
3832
  const architectureFiles = files.filter((f) => isArchitecture(f));
3827
3833
  const specFiles2 = files.filter((f) => isIncluded(f) && !architectureFiles.includes(f));
3828
3834
  const involved = /* @__PURE__ */ new Map();
@@ -3833,39 +3839,37 @@ async function loadSpecModel(root, config, source = new WorkingTreeProvider(root
3833
3839
  return result.spec;
3834
3840
  };
3835
3841
  let architecture;
3836
- const unstagedLog = architectureFiles.length === 0 && source.kind === "index" ? (await listFiles(root)).find((f) => isArchitecture(f)) : void 0;
3842
+ const architectureTexts = /* @__PURE__ */ new Map();
3843
+ for (const file of architectureFiles) {
3844
+ architectureTexts.set(file, (await source.read(file))?.toString("utf8") ?? "");
3845
+ }
3846
+ const rulesFiles = architectureFiles.filter(
3847
+ (f) => isArchitectureSpecText(architectureTexts.get(f))
3848
+ );
3849
+ const unstagedLog = rulesFiles.length === 0 && source.kind === "index" ? await findUnstagedArchitectureSpec(root, isArchitecture) : void 0;
3837
3850
  if (unstagedLog) {
3838
3851
  diagnostics.push(
3839
3852
  createDiagnostic("CS0107", {
3840
3853
  severity: "warning",
3841
- message: `the ADR log ${unstagedLog} isn't staged, so this commit is checked without it`,
3854
+ message: `the architecture spec ${unstagedLog} isn't staged, so this commit is checked without its rules`,
3842
3855
  suggestions: [{ message: `stage it: git add ${unstagedLog}` }]
3843
3856
  })
3844
3857
  );
3845
- } else if (architectureFiles.length === 0) {
3858
+ } else if (rulesFiles.length > 1) {
3846
3859
  diagnostics.push(
3847
3860
  createDiagnostic("CS0107", {
3848
- message: `no architecture spec: no file matches specs.architecture "${config.specs.architecture}"`,
3861
+ specLocation: { file: rulesFiles[0], line: 1, column: 1 },
3862
+ message: `more than one architecture spec matches specs.architecture "${config.specs.architecture}": ` + listOf(rulesFiles),
3863
+ actual: rulesFiles,
3849
3864
  suggestions: [
3850
3865
  {
3851
- message: "run `catspec init` to create one, or set specs.architecture in catspec.yaml"
3866
+ message: "keep the repository-wide rules in one file, or narrow specs.architecture in catspec.yaml"
3852
3867
  }
3853
3868
  ]
3854
3869
  })
3855
3870
  );
3856
- } else if (architectureFiles.length > 1) {
3857
- diagnostics.push(
3858
- createDiagnostic("CS0107", {
3859
- specLocation: { file: architectureFiles[0], line: 1, column: 1 },
3860
- message: `more than one architecture spec matches specs.architecture "${config.specs.architecture}": ` + listOf(architectureFiles),
3861
- actual: architectureFiles,
3862
- suggestions: [
3863
- { message: "keep one ADR log, or narrow specs.architecture in catspec.yaml" }
3864
- ]
3865
- })
3866
- );
3867
- } else {
3868
- const spec = await read(architectureFiles[0]);
3871
+ } else if (rulesFiles.length === 1) {
3872
+ const spec = await read(rulesFiles[0]);
3869
3873
  if (spec) {
3870
3874
  if (spec.type === "architecture") {
3871
3875
  architecture = spec;
@@ -3937,20 +3941,28 @@ async function loadSpecModel(root, config, source = new WorkingTreeProvider(root
3937
3941
  );
3938
3942
  }
3939
3943
  }
3940
- const adrIds = architecture?.body.adrIds ?? [];
3941
- if (architecture) {
3944
+ const adrIds = [...architecture?.body.adrIds ?? []];
3945
+ for (const file of architectureFiles) {
3946
+ if (rulesFiles.includes(file)) continue;
3947
+ for (const id of plainAdrIds(file, architectureTexts.get(file))) {
3948
+ if (!adrIds.some((known) => sameAdrId(known, id))) adrIds.push(id);
3949
+ }
3950
+ }
3951
+ const adrLog = architecture?.file ?? `specs.architecture "${config.specs.architecture}"`;
3952
+ if (adrIds.length > 0) {
3942
3953
  for (const spec of specs.byFile.values()) {
3943
3954
  for (const rule of spec.rules) {
3944
- if (rule.adr === void 0 || adrIds.includes(rule.adr)) continue;
3945
- const close = didYouMean(rule.adr, adrIds);
3955
+ const ruleAdr = rule.adr;
3956
+ if (ruleAdr === void 0 || adrIds.some((id) => sameAdrId(id, ruleAdr))) continue;
3957
+ const close = didYouMean(ruleAdr, adrIds);
3946
3958
  diagnostics.push(
3947
3959
  createDiagnostic("CS0112", {
3948
3960
  spec: spec.id,
3949
3961
  ruleId: rule.id,
3950
3962
  specLocation: specLocation(spec.source, ["rules", rule.index, "adr"]),
3951
- message: `rule "${rule.id}" enforces ${rule.adr}, which is not in the ADR log ${architecture.file}`,
3963
+ message: `rule "${rule.id}" enforces ${ruleAdr}, which is not in the ADR log ${adrLog}`,
3952
3964
  expected: adrIds,
3953
- actual: rule.adr,
3965
+ actual: ruleAdr,
3954
3966
  ...close.length > 0 ? { suggestions: close.map((c) => ({ message: `did you mean ${c}?` })) } : {}
3955
3967
  })
3956
3968
  );
@@ -4021,6 +4033,33 @@ async function loadSpecModel(root, config, source = new WorkingTreeProvider(root
4021
4033
  source
4022
4034
  };
4023
4035
  }
4036
+ function architectureMatcher(pattern) {
4037
+ const clean = pattern.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
4038
+ if (/[*?[\]{}()!]/.test(clean) || /\.md$/i.test(clean)) return globMatcher(clean);
4039
+ return globMatcher(`${clean}/**/*.md`);
4040
+ }
4041
+ function isArchitectureSpecText(text2) {
4042
+ const split = splitFrontmatter(text2);
4043
+ if ("error" in split) return false;
4044
+ return /^(catspec\s*:|type\s*:\s*["']?architecture["']?\s*(#.*)?$)/m.test(split.frontmatter.text);
4045
+ }
4046
+ function plainAdrIds(file, text2) {
4047
+ const ids = [];
4048
+ const name = /^(?:adr-?)?(\d+)(?=[-_. ])/i.exec(posix3.basename(file));
4049
+ if (name) ids.push(`ADR-${name[1]}`);
4050
+ for (const match of text2.matchAll(/^#{1,6}[ \t]+\[?(ADR-\d+)\b/gm)) {
4051
+ if (!ids.some((id) => sameAdrId(id, match[1]))) ids.push(match[1]);
4052
+ }
4053
+ return ids;
4054
+ }
4055
+ var sameAdrId = (a, b) => Number(a.slice(4)) === Number(b.slice(4)) && /^ADR-\d+$/.test(a) && /^ADR-\d+$/.test(b);
4056
+ async function findUnstagedArchitectureSpec(root, isArchitecture) {
4057
+ for (const file of (await listFiles(root)).filter((f) => isArchitecture(f))) {
4058
+ const text2 = await readFile3(join4(root, file), "utf8").catch(() => "");
4059
+ if (isArchitectureSpecText(text2)) return file;
4060
+ }
4061
+ return void 0;
4062
+ }
4024
4063
 
4025
4064
  // src/spec-editor.ts
4026
4065
  import { isDeepStrictEqual } from "util";
@@ -5164,11 +5203,11 @@ var editPatch = (edit) => ({ file: edit.spec.file, ops: edit.ops });
5164
5203
  // src/workflow/engine.ts
5165
5204
  import { execFile as execFile3 } from "child_process";
5166
5205
  import { appendFile, mkdir, writeFile } from "fs/promises";
5167
- import { join as join5 } from "path";
5206
+ import { join as join6 } from "path";
5168
5207
 
5169
5208
  // src/workflow/doctor.ts
5170
5209
  import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
5171
- import { delimiter, isAbsolute, join as join4 } from "path";
5210
+ import { delimiter, isAbsolute, join as join5 } from "path";
5172
5211
  import { LineCounter as LineCounter2, parseDocument as parseDocument4 } from "yaml";
5173
5212
  function configLocator(configFile) {
5174
5213
  if (!configFile) return () => void 0;
@@ -5184,14 +5223,14 @@ function configLocator(configFile) {
5184
5223
  }
5185
5224
  function findProgram(name, root, env = process.env, platform = process.platform) {
5186
5225
  if (name.includes("/") || name.includes("\\")) {
5187
- const path = isAbsolute(name) ? name : join4(root, name);
5226
+ const path = isAbsolute(name) ? name : join5(root, name);
5188
5227
  return existsSync2(path) ? path : void 0;
5189
5228
  }
5190
5229
  const extensions = platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase())] : [""];
5191
5230
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
5192
5231
  for (const dir of dirs) {
5193
5232
  for (const ext of extensions) {
5194
- const file = join4(dir, name + ext);
5233
+ const file = join5(dir, name + ext);
5195
5234
  try {
5196
5235
  if (statSync2(file).isFile()) return file;
5197
5236
  } catch {
@@ -5204,7 +5243,7 @@ var onPath = (name, root, env = process.env) => findProgram(name, root, env) !==
5204
5243
  var isBatchScript = (path) => /\.(cmd|bat)$/i.test(path);
5205
5244
  var SKILL_DIRS = [".claude/skills", ".agents/skills"];
5206
5245
  var sddSkillsIn = (root, skillsDir) => {
5207
- const dir = join4(root, skillsDir);
5246
+ const dir = join5(root, skillsDir);
5208
5247
  return existsSync2(dir) ? readdirSync(dir).filter((n) => n.startsWith("sdd-")).sort() : [];
5209
5248
  };
5210
5249
  function findSddSkills(root) {
@@ -5215,8 +5254,8 @@ function findSddSkills(root) {
5215
5254
  return void 0;
5216
5255
  }
5217
5256
  function detectPreset(root, skillsDir) {
5218
- if (existsSync2(join4(root, "openspec"))) return "openspec";
5219
- if (existsSync2(join4(root, ".specify"))) return "spec-kit";
5257
+ if (existsSync2(join5(root, "openspec"))) return "openspec";
5258
+ if (existsSync2(join5(root, ".specify"))) return "spec-kit";
5220
5259
  const found = skillsDir ? sddSkillsIn(root, skillsDir).length > 0 : findSddSkills(root);
5221
5260
  return found ? "custom" : void 0;
5222
5261
  }
@@ -5232,7 +5271,7 @@ function workflowDoctor(root, profile, options = {}) {
5232
5271
  const driver = stage2.driver;
5233
5272
  if (driver.kind === "skill") {
5234
5273
  const file = skillFile(profile, driver.name);
5235
- if (!existsSync2(join4(root, file))) {
5274
+ if (!existsSync2(join5(root, file))) {
5236
5275
  diagnostics.push(
5237
5276
  createDiagnostic("CS0704", {
5238
5277
  ...location(["stages", index, "driver", "skill"]),
@@ -5295,7 +5334,7 @@ function workflowDoctor(root, profile, options = {}) {
5295
5334
  "spec-kit": ".specify/"
5296
5335
  };
5297
5336
  const folder = expected[profile.preset];
5298
- if (folder && !existsSync2(join4(root, folder))) {
5337
+ if (folder && !existsSync2(join5(root, folder))) {
5299
5338
  diagnostics.push(
5300
5339
  createDiagnostic("CS0700", {
5301
5340
  severity: "warning",
@@ -6015,11 +6054,11 @@ var describeStage = (stage2) => ({
6015
6054
  }
6016
6055
  });
6017
6056
  async function writeStage(root, run, now = /* @__PURE__ */ new Date()) {
6018
- for (const change of run.changes) await writeFile(join5(root, change.file), change.text, "utf8");
6057
+ for (const change of run.changes) await writeFile(join6(root, change.file), change.text, "utf8");
6019
6058
  if (!run.forced) return;
6020
6059
  const common = findGitCommonDir(root);
6021
6060
  if (!common) return;
6022
- const dir = join5(common, "catspec");
6061
+ const dir = join6(common, "catspec");
6023
6062
  await mkdir(dir, { recursive: true });
6024
6063
  const entry = {
6025
6064
  at: now.toISOString(),
@@ -6028,7 +6067,7 @@ async function writeStage(root, run, now = /* @__PURE__ */ new Date()) {
6028
6067
  target: run.target,
6029
6068
  overridden: run.diagnostics.filter((d) => FORCED.has(d.code)).map((d) => `${d.code} ${d.message}`)
6030
6069
  };
6031
- await appendFile(join5(dir, "workflow-overrides.log"), `${JSON.stringify(entry)}
6070
+ await appendFile(join6(dir, "workflow-overrides.log"), `${JSON.stringify(entry)}
6032
6071
  `, "utf8");
6033
6072
  }
6034
6073
  function runCommand(root, args, timeoutMs = 10 * 6e4, platform = process.platform, env = process.env) {
@@ -6183,6 +6222,7 @@ export {
6183
6222
  agentSteps,
6184
6223
  applyLock,
6185
6224
  applyPatch,
6225
+ architectureMatcher,
6186
6226
  attributeMatches,
6187
6227
  baselineFromLock,
6188
6228
  baselineFromSymbol,
@@ -6232,6 +6272,7 @@ export {
6232
6272
  hookWorkflowDiagnostics,
6233
6273
  importTargets,
6234
6274
  inModule,
6275
+ isArchitectureSpecText,
6235
6276
  isBatchScript,
6236
6277
  isEnforced,
6237
6278
  isListedPath,
@@ -6258,6 +6299,7 @@ export {
6258
6299
  parseErrorDiagnostics,
6259
6300
  parseLock,
6260
6301
  parseSpecSource,
6302
+ plainAdrIds,
6261
6303
  planSync,
6262
6304
  qualifiedNameMatches,
6263
6305
  queryDiagnostics,
@@ -6271,6 +6313,7 @@ export {
6271
6313
  rewriteSymbol,
6272
6314
  runCommand,
6273
6315
  runTestCommand,
6316
+ sameAdrId,
6274
6317
  serializeLock,
6275
6318
  shapeOf,
6276
6319
  signatureText,