Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@libar-dev/architect",
"version": "1.0.0-pre.6",
"version": "1.0.0-pre.7",
"description": "Context engineering platform: extract patterns from TypeScript and Gherkin into a queryable state with living docs, architecture graphs, and FSM-enforced workflows.",
"type": "module",
"sideEffects": false,
Expand Down Expand Up @@ -115,7 +115,8 @@
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"*.{json,md,yml}\"",
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"*.{json,md,yml}\"",
"preversion": "pnpm test && pnpm typecheck && pnpm lint && pnpm format:check",
"prepublishOnly": "pnpm build && pnpm test && pnpm typecheck && pnpm lint",
"verify:exports": "node scripts/verify-exports.mjs",
"prepublishOnly": "pnpm clean && pnpm build && pnpm verify:exports && pnpm test && pnpm typecheck && pnpm lint",
"postversion": "git push && git push --tags",
"release:pre": "npm version prerelease --preid=pre",
"release:patch": "npm version patch",
Expand Down
99 changes: 99 additions & 0 deletions scripts/verify-exports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env node

/**
* Verify that all declared package.json exports resolve to existing files in dist/.
*
* Runs as part of prepublishOnly to prevent publishing packages with missing
* entry points — the exact issue that caused incomplete dist/ in pre.5.
*
* Exit code 0: all exports verified.
* Exit code 1: one or more exports missing.
*/

import { readFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = resolve(__dirname, '..');

const pkg = JSON.parse(readFileSync(resolve(rootDir, 'package.json'), 'utf8'));
const exports = pkg.exports;

if (!exports || typeof exports !== 'object') {
console.error('No exports field found in package.json');
process.exit(1);
}

/**
* Verify that a resolved path stays inside the project root.
* Prevents crafted export values (absolute paths, ../ traversal) from
* probing arbitrary filesystem locations.
*/
function assertInsideRoot(absolutePath, label) {
if (!absolutePath.startsWith(rootDir + '/') && absolutePath !== rootDir) {
console.error(` SECURITY ${label} resolves outside project root: ${absolutePath}`);
process.exit(1);
}
}

let missing = 0;
let verified = 0;

for (const [entryPoint, mapping] of Object.entries(exports)) {
// Skip non-object entries (e.g., "./package.json": "./package.json")
if (typeof mapping === 'string') {
const absolutePath = resolve(rootDir, mapping);
assertInsideRoot(absolutePath, entryPoint);
if (!existsSync(absolutePath)) {
console.error(` MISSING ${entryPoint} → ${mapping}`);
missing++;
} else {
verified++;
}
continue;
}

// Check both types and import paths
for (const [condition, filePath] of Object.entries(mapping)) {
if (typeof filePath !== 'string') {
console.warn(` SKIPPED ${entryPoint} [${condition}] — nested conditional mapping (not verified)`);
continue;
}
const absolutePath = resolve(rootDir, filePath);
assertInsideRoot(absolutePath, `${entryPoint} [${condition}]`);
if (!existsSync(absolutePath)) {
console.error(` MISSING ${entryPoint} [${condition}] → ${filePath}`);
missing++;
} else {
verified++;
}
}
}

// Also verify bin entries (supports both string and object forms)
if (pkg.bin) {
const binEntries =
typeof pkg.bin === 'string'
? [[pkg.name?.split('/').pop() ?? '(default)', pkg.bin]]
: Object.entries(pkg.bin);

for (const [cmd, filePath] of binEntries) {
const absolutePath = resolve(rootDir, filePath);
assertInsideRoot(absolutePath, `bin.${cmd}`);
if (!existsSync(absolutePath)) {
console.error(` MISSING bin.${cmd} → ${filePath}`);
missing++;
} else {
verified++;
}
}
}

if (missing > 0) {
console.error(`\nExport verification failed: ${missing} missing, ${verified} verified.`);
console.error('Run "pnpm clean && pnpm build" to fix.');
process.exit(1);
}

console.log(`Exports verified: ${verified} paths exist.`);
111 changes: 109 additions & 2 deletions src/renderable/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,111 @@ function resolveCodec(type: DocumentType, options?: CodecOptions): DocumentCodec
// Generation Functions
// ═══════════════════════════════════════════════════════════════════════════

/**
* Decode a document type to RenderableDocument without rendering to markdown.
*
* This is the Live Documentation API entry point: it invokes the codec's decode()
* step and returns the structured RenderableDocument, skipping the markdown render
* step entirely. Interactive consumers (Studio desktop, MCP clients) use this to
* receive typed document blocks for native rendering.
*
* @param type - Document type to decode
* @param dataset - PatternGraph with pattern data
* @param options - Optional codec-specific options (e.g., patternName for design-review)
* @param contextEnrichment - Optional runtime context (projectMetadata, tagExampleOverrides)
* @returns Result containing RenderableDocument on success, or GenerationError on failure
*
* @example
* ```typescript
* const result = decodeDocumentSafe("architecture", patternGraph);
* if (Result.isOk(result)) {
* // result.value is RenderableDocument — structured JSON, not markdown
* const doc = result.value;
* console.log(doc.title, doc.sections.length);
* }
* ```
*/
export function decodeDocumentSafe(
type: DocumentType,
dataset: PatternGraph,
options?: CodecOptions,
contextEnrichment?: CodecContextEnrichment
): Result<RenderableDocument, GenerationError> {
const codec = resolveCodec(type, options);
if (codec === undefined) {
return Result.err({
documentType: type,
message: `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`,
phase: 'decode',
});
}

if (contextEnrichment) {
setCodecContextEnrichment(contextEnrichment);
}

try {
const doc = codec.decode(dataset) as RenderableDocument;
return Result.ok(doc);
} catch (err) {
return Result.err({
documentType: type,
message: err instanceof Error ? err.message : String(err),
cause: err instanceof Error ? err : undefined,
phase: 'decode',
});
} finally {
if (contextEnrichment) {
clearCodecContextEnrichment();
}
}
}

/**
* Decode a document type to RenderableDocument without rendering to markdown.
*
* Throwing variant of `decodeDocumentSafe()`. Use when you prefer exceptions
* over Result-based error handling.
*
* @param type - Document type to decode
* @param dataset - PatternGraph with pattern data
* @param options - Optional codec-specific options
* @param contextEnrichment - Optional runtime context
* @returns RenderableDocument — the structured intermediate format
* @throws Error if the codec is not registered or decode fails
*
* @example
* ```typescript
* const doc = decodeDocument("architecture", patternGraph);
* // doc.sections contains heading, mermaid, table, paragraph blocks
* ```
*/
export function decodeDocument(
type: DocumentType,
dataset: PatternGraph,
options?: CodecOptions,
contextEnrichment?: CodecContextEnrichment
): RenderableDocument {
const codec = resolveCodec(type, options);
if (codec === undefined) {
throw new Error(
`No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`
);
}

if (contextEnrichment) {
setCodecContextEnrichment(contextEnrichment);
}

try {
return codec.decode(dataset) as RenderableDocument;
} finally {
if (contextEnrichment) {
clearCodecContextEnrichment();
}
}
}
Comment on lines +462 to +486
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

New/updated throw paths conflict with the repository’s Result-based error-handling rule.

decodeDocument() is introduced as exception-based, and the touched generateDocument() path still throws. This diverges from the project’s required Result-monad convention.

♻️ Suggested direction
-export function decodeDocument(
+export function decodeDocument(
   type: DocumentType,
   dataset: PatternGraph,
   options?: CodecOptions,
   contextEnrichment?: CodecContextEnrichment
-): RenderableDocument {
-  const codec = resolveCodec(type, options);
-  if (codec === undefined) {
-    throw new Error(
-      `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`
-    );
-  }
-
-  if (contextEnrichment) {
-    setCodecContextEnrichment(contextEnrichment);
-  }
-
-  try {
-    return codec.decode(dataset) as RenderableDocument;
-  } finally {
-    if (contextEnrichment) {
-      clearCodecContextEnrichment();
-    }
-  }
+): Result<RenderableDocument, GenerationError> {
+  return decodeDocumentSafe(type, dataset, options, contextEnrichment);
 }

As per coding guidelines, Use Result monad for explicit error handling: functions should return Result.ok(value) or Result.error(err) instead of throwing exceptions.

Also applies to: 605-607

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderable/generate.ts` around lines 462 - 486, The decodeDocument
function currently throws on missing codec and returns decoded value directly;
change it to follow the project's Result-monad convention by returning
Result.ok(decoded) or Result.error(err) instead of throwing: use resolveCodec
and if undefined return Result.error with a message that includes
CodecRegistry.getRegisteredTypes(); wrap the codec.decode(dataset) call so its
success returns Result.ok and any runtime error returns Result.error; preserve
context enrichment behavior around setCodecContextEnrichment and
clearCodecContextEnrichment but ensure cleanup still runs and that errors are
captured and converted to Result.error rather than thrown (refer to
decodeDocument, resolveCodec, CodecRegistry.getRegisteredTypes,
setCodecContextEnrichment, clearCodecContextEnrichment, and codec.decode for
locations to change).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable — the dual-API pattern is deliberate and pre-existing.

This codebase provides paired variants throughout:

  • generateDocumentSafe()Result<OutputFile[], GenerationError>
  • generateDocument() → throws, returns OutputFile[]

The new decode functions mirror this exact convention:

  • decodeDocumentSafe()Result<RenderableDocument, GenerationError>
  • decodeDocument() → throws, returns RenderableDocument

The Safe suffix is the naming contract that distinguishes Result-based from throwing variants. The Result monad itself includes unwrap() (converts Result back to throwing), confirming both patterns are valid.

Making decodeDocument return Result would make it identical to decodeDocumentSafe, eliminating consumer choice and breaking the established Foo/FooSafe API symmetry that shipped in PR #40.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@darko-mijic, you're absolutely right — I missed the established Foo / FooSafe dual-API convention. Converting decodeDocument() to return Result would collapse it into decodeDocumentSafe(), eliminating the throwing variant that some consumers prefer. My suggestion was incorrect; the symmetry is correct as-is.

ʕ •ᴥ•ʔ (learning noted for future reviews)


✏️ Learnings added
Learnt from: darko-mijic
Repo: libar-dev/architect PR: 44
File: src/renderable/generate.ts:462-486
Timestamp: 2026-04-04T09:22:47.625Z
Learning: In libar-dev/architect, the codebase deliberately provides paired API variants throughout `src/renderable/generate.ts`:
- The throwing variant (e.g., `generateDocument()`, `decodeDocument()`) returns the value directly and throws on error.
- The `Safe` suffixed variant (e.g., `generateDocumentSafe()`, `decodeDocumentSafe()`) returns `Result<T, GenerationError>` for explicit error handling.
This `Foo` / `FooSafe` naming contract is intentional and must not be collapsed into a single Result-returning function. Do NOT flag the throwing variants as violating the Result monad convention — both patterns are valid by design.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: libar-dev/architect PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T02:23:21.646Z
Learning: Applies to src/{generators,renderable}/**/*.ts : Use Codec-Based Rendering: generators in `src/generators/` must use codecs to transform data to markdown via Zod codecs

Learnt from: CR
Repo: libar-dev/architect PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T02:23:21.646Z
Learning: Applies to src/renderable/codecs/**/*.ts : TypeScript files that implement codecs must use Zod schemas for transformation. Codecs transform data structures to markdown using schema definitions.

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/renderable/codecs/validation-rules.ts:232-235
Timestamp: 2026-04-01T14:42:40.489Z
Learning: In `src/renderable/codecs/validation-rules.ts`, `ValidationRulesCodec` intentionally ignores the `CodecContext` (passed as `_context`) because it generates documentation for static, built-in `RULE_DEFINITIONS` (Process Guard validation rules), not for user-extracted source data. The TODO comment in the file explicitly documents this design decision.

Learnt from: CR
Repo: libar-dev/architect PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T02:23:21.646Z
Learning: Applies to **/*.{ts,tsx} : Use Result monad for explicit error handling: functions should return `Result.ok(value)` or `Result.error(err)` instead of throwing exceptions

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/renderable/codecs/validation-rules.ts:232-235
Timestamp: 2026-04-01T14:42:29.625Z
Learning: When referencing a function by name in review feedback (or proposing a call site), don’t only check that the function exists—verify the proposed invocation matches the function’s actual exported signature. Confirm argument order and the expected parameter types (and any overloaded variants) against the definition. For example, in src/renderable/codecs/validation-rules.ts, composeRationaleIntoRules expects (rules: readonly RuleDefinition[], conventionContent: ReadonlyMap<string, {...}>)—so calls should pass rules first and the convention map second, not a CodecContext as the second argument.

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/renderable/codecs/types/base.ts:206-224
Timestamp: 2026-04-01T14:42:08.890Z
Learning: When reviewing JavaScript/Node.js code, do NOT suggest thread-safety or race-condition warnings for code that is explicitly synchronous and runs on the main single-threaded JS execution model. In particular, module-level or shared mutable state that is used only within a synchronous generation/decode pipeline (e.g., set codec context before a synchronous decode call, then cleared immediately after) does not imply a race condition under the stated synchronous execution contract. Avoid “speculative” warnings about hypothetical future async/worker scenarios; follow the project philosophy of not adding error handling, fallbacks, or validation for scenarios that cannot happen. If the code already documents the synchronous execution guarantee (e.g., via JSDoc), treat that as sufficient justification.

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/renderable/codecs/pr-changes.ts:6-6
Timestamp: 2026-04-01T14:42:07.229Z
Learning: In this repository, scanner opt-in and runtime dependency annotations must use the `architect` prefix (e.g., `architect`). Do NOT suggest or add deprecated/invalid tags such as `libar-docs` or `libar-docs-uses`. For TypeScript files, opt into scanning by placing `architect` in the JSDoc header; if runtime dependency annotations are present, they should follow the same `architect` prefix convention.

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/renderable/render-options.ts:21-21
Timestamp: 2026-04-01T14:42:10.736Z
Learning: In the `architect` TypeScript codebase (especially `src/renderable/**/*.ts`), do not suggest or introduce `Object.freeze` for empty default objects typed via `readonly` properties (e.g., `DEFAULT_SIZE_BUDGET: SizeBudget = {}`). If the interface/type already uses `readonly` and the object has no mutable runtime fields, freezing an empty object provides no meaningful runtime protection (effectively a no-op). Reserve `Object.freeze` for objects that contain actual mutable properties that must be protected at runtime.

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/taxonomy/arch-values.ts:1-9
Timestamp: 2026-04-01T14:42:13.881Z
Learning: In this repository (libar-dev/architect), the documentation-scanner opt-in marker is `architect` (used in a file’s JSDoc header). Do not suggest or add the old `libar-docs` prefix—this rebrand marker no longer exists in the codebase. If a file already has `architect` in its JSDoc header, it is correctly opted in for documentation scanning and should not be flagged for opt-in/marker issues.

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: tests/features/doc-generation/index-codec.feature:1-6
Timestamp: 2026-04-01T14:42:18.034Z
Learning: In the libar-dev/architect repository, the old documentation tag prefix `libar-docs-*` no longer exists (it was renamed during a rebrand). Do NOT flag missing or incorrect `libar-docs-*` tags in this repo. Instead, expect the `architect-*` tag prefix in both feature files and TypeScript sources (e.g., `architect-pattern`, `architect-status`, `architect-product-area`, `architect-unlock-reason`, etc.).

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/renderable/split.ts:72-105
Timestamp: 2026-04-01T14:42:14.584Z
Learning: When reviewing heuristic or budget-based logic (e.g., line-count/document splitting or other approximate heuristics), treat measurement imprecision within the intended heuristic bounds as expected behavior, not a bug. Only flag violations that are true hard correctness requirements (must-never conditions); distinguish them from soft heuristics that are approximate by design to avoid false positives.

Learnt from: darko-mijic
Repo: libar-dev/architect PR: 40
File: src/renderable/codecs/validation-rules.ts:232-235
Timestamp: 2026-04-01T14:42:29.625Z
Learning: When reviewing the Architect codebase and proposing changes to a codec or function, first scan nearby code for TODO comments or inline documentation that explicitly explains intentional behavior/design decisions (e.g., unused parameters preserved for interface consistency). If the ignored/unused parameter (or similar behavior) is explicitly documented as intentional, do not raise it as a problem—treat it as a likely false positive.


/**
* Generate a single document type with Result-based error handling.
*
Expand Down Expand Up @@ -416,7 +521,7 @@ export function generateDocumentSafe(
if (codec === undefined) {
return Result.err({
documentType: type,
message: `No codec registered for document type: ${type}`,
message: `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`,
phase: 'decode',
});
}
Expand Down Expand Up @@ -497,7 +602,9 @@ export function generateDocument(

const codec = resolveCodec(type, options);
if (codec === undefined) {
throw new Error(`No codec registered for document type: ${type}`);
throw new Error(
`No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`
);
}

// Set context enrichment before decode (cleared in finally)
Expand Down
4 changes: 4 additions & 0 deletions src/renderable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,18 @@ export {
// ═══════════════════════════════════════════════════════════════════════════

export {
decodeDocument,
decodeDocumentSafe,
generateDocument,
generateDocuments,
generateAllDocuments,
getAvailableDocumentTypes,
isValidDocumentType,
getDocumentTypeInfo,
DOCUMENT_TYPES,
type CodecOptions,
type DocumentType,
type GenerationError,
} from './generate.js';

// ═══════════════════════════════════════════════════════════════════════════
Expand Down
Loading