Skip to content
Open
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
6 changes: 5 additions & 1 deletion .github/scripts/change-scope.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
/* global console, module, process, require */

// eslint-disable-next-line @typescript-eslint/no-require-imports
const {isComponentSpecRecordPath} = require('./knowledge-paths.cjs');
const {
isComponentSpecRecordPath,
isPackageArchitectureRecordPath,
} = require('./knowledge-paths.cjs');

/**
* Classifies changed paths without reading PR-controlled content.
Expand Down Expand Up @@ -99,6 +102,7 @@ function isPackageReleasePath(filePath) {
function isKnowledgeRecordPath(filePath) {
return (
isSpecRecordPath(filePath) ||
isPackageArchitectureRecordPath(filePath) ||
KNOWLEDGE_RECORD_PATTERNS.some(pattern => pattern.test(filePath))
);
}
Expand Down
16 changes: 16 additions & 0 deletions .github/scripts/change-scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ describe('spec-only change scope', () => {
expect(result.specOnly).toBe(false);
});

it('treats a platform component record as a spec record', () => {
const result = classifyChanges([
{filename: 'packages/native/apple/Sources/Astryx/Button/Button.spec.md'},
]);
expect(result.touchesKnowledgeRecords).toBe(true);
expect(result.specOnly).toBe(true);
});

it('owner-gates package records like architecture records', () => {
const result = classifyChanges([
{filename: 'packages/native/specs/swift/accessibility.spec.md'},
]);
expect(result.touchesKnowledgeRecords).toBe(true);
expect(result.specOnly).toBe(false);
});

it('owner-gates normative design assets without granting the spec-only fast path', () => {
const result = classifyChanges([
{filename: 'docs/design/assets/selector/alignment.png'},
Expand Down
108 changes: 104 additions & 4 deletions .github/scripts/knowledge-paths.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
/* global module, require */

/**
* One shared path contract for component-local knowledge records. It consumes
* the component-package registry so validation, change classification, and the
* spec-owner gate cannot disagree about supported package layouts.
* One shared path contract for component-local knowledge records, platform
* component records, package records, and the spec-shaped files that must be
* records (INV27). It consumes the component-package registry so validation,
* change classification, and the spec-owner gate cannot disagree about
* supported package layouts.
*/

// eslint-disable-next-line @typescript-eslint/no-require-imports
Expand Down Expand Up @@ -93,14 +95,112 @@ function classifyComponentKnowledgePath(filePath) {
};
}

/**
* Platform implementation packages and the language their component records
* name (`component:<language>/<PublicName>`). Each component root is a
* directory of the package's component tree; its record is the direct child
* `<PublicName>.spec.md`. Swift has one package, `packages/native/apple`,
* whose `Astryx` target holds one folder per component.
*/
const PLATFORM_COMPONENT_ROOTS = Object.freeze([
Object.freeze({
language: 'swift',
pattern: /^(packages\/native\/apple\/Sources\/Astryx)\/([^/]+)\/([^/]+)$/,
}),
]);

/**
* Classify a platform implementation's component record by path alone:
* `<package component tree>/<Root>/<PublicName>.spec.md`. Its id, its
* `platform_of` link, and the core record it names are validated after
* parsing.
*/
function classifyPlatformComponentKnowledgePath(filePath) {
const normalized = normalizePath(filePath);
if (!normalized.endsWith('.spec.md')) return null;
for (const {language, pattern} of PLATFORM_COMPONENT_ROOTS) {
const match = pattern.exec(normalized);
if (!match) continue;
const [, componentTree, componentRoot, fileName] = match;
if (isIgnoredComponentKnowledgeSegment(componentRoot)) return null;
if (fileName.endsWith('.generated.spec.md')) return null;
const publicName = fileName.slice(0, -'.spec.md'.length);
if (!/^[A-Z][A-Za-z0-9]*$/.test(publicName)) return null;
return {
componentRoot,
componentTree,
fileName,
kind: 'component',
language,
layout: 'platform',
packageName: normalized.split('/')[1],
publicName,
relativePath: fileName,
};
}
return null;
}

/**
* Classify a package's cross-component record: a `<slug>.spec.md` anywhere
* under `packages/<package>/specs/`. Its kind is `architecture` and its id is
* `architecture:<package>-<directories>-<slug>`, joined with hyphens.
*/
function classifyPackageArchitecturePath(filePath) {
const match =
/^packages\/([a-z0-9][a-z0-9-]*)\/specs\/((?:[a-z0-9][a-z0-9-]*\/)*)([a-z0-9][a-z0-9-]*)\.spec\.md$/.exec(
normalizePath(filePath),
);
if (!match) return null;
const [, packageName, directories, slug] = match;
const parts = [packageName, ...directories.split('/').filter(Boolean), slug];
return {
id: `architecture:${parts.join('-')}`,
kind: 'architecture',
packageName,
};
}

/**
* A spec-shaped file that must be a valid knowledge record (INV27): a tracked
* Markdown file named `*.spec.md`, or any Markdown file under a directory
* named `specs`. README.md indexes, plan.md implementation plans, and paths
* discovery ignores (fixtures, tests, generated output, hidden directories)
* are not candidates.
*/
function isKnowledgeRecordCandidatePath(filePath) {
const normalized = normalizePath(filePath);
if (!normalized.endsWith('.md')) return false;
const segments = normalized.split('/');
const fileName = segments.at(-1);
if (fileName === 'README.md' || fileName === 'plan.md') return false;
if (fileName.startsWith('.')) return false;
if (fileName.endsWith('.generated.spec.md')) return false;
const directories = segments.slice(0, -1);
if (directories.some(isIgnoredComponentKnowledgeSegment)) return false;
return fileName.endsWith('.spec.md') || directories.includes('specs');
}

function isComponentSpecRecordPath(filePath) {
return classifyComponentKnowledgePath(filePath) !== null;
return (
classifyComponentKnowledgePath(filePath) !== null ||
classifyPlatformComponentKnowledgePath(filePath) !== null
);
}

function isPackageArchitectureRecordPath(filePath) {
return classifyPackageArchitecturePath(filePath) !== null;
}

module.exports = {
COMPONENT_PACKAGE_NAMES,
IGNORED_COMPONENT_KNOWLEDGE_SEGMENTS,
PLATFORM_COMPONENT_ROOTS,
classifyComponentKnowledgePath,
classifyPackageArchitecturePath,
classifyPlatformComponentKnowledgePath,
isComponentSpecRecordPath,
isIgnoredComponentKnowledgeSegment,
isKnowledgeRecordCandidatePath,
isPackageArchitectureRecordPath,
};
85 changes: 85 additions & 0 deletions .github/scripts/knowledge-paths.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {describe, expect, it} from 'vitest';
const require = createRequire(import.meta.url);
const {
classifyComponentKnowledgePath,
classifyPackageArchitecturePath,
classifyPlatformComponentKnowledgePath,
isComponentSpecRecordPath,
isKnowledgeRecordCandidatePath,
} = require('./knowledge-paths.cjs');

describe('component knowledge paths', () => {
Expand Down Expand Up @@ -74,3 +77,85 @@ describe('component knowledge paths', () => {
expect(classifyComponentKnowledgePath(filePath)).toBeNull();
});
});

describe('platform component knowledge paths', () => {
it('classifies a Swift component record beside its code', () => {
const filePath =
'packages/native/apple/Sources/Astryx/Button/Button.spec.md';
expect(classifyPlatformComponentKnowledgePath(filePath)).toMatchObject({
componentRoot: 'Button',
componentTree: 'packages/native/apple/Sources/Astryx',
kind: 'component',
language: 'swift',
publicName: 'Button',
});
expect(isComponentSpecRecordPath(filePath)).toBe(true);
});

it.each([
'packages/native/apple/Sources/Astryx/Button/plugins/Thing.spec.md',
'packages/native/apple/Sources/Astryx/Button.spec.md',
'packages/native/apple/Sources/AstryxNative/Button/Button.spec.md',
'packages/native/apple/Sources/Astryx/__fixtures__/Button.spec.md',
'packages/native/apple/Sources/Astryx/Button/button.spec.md',
'packages/native/swift-ios-1/core/Button/Button.spec.md',
])('rejects %s', filePath => {
expect(classifyPlatformComponentKnowledgePath(filePath)).toBeNull();
});
});

describe('package record paths', () => {
it.each([
[
'packages/native/specs/swift/accessibility.spec.md',
'architecture:native-swift-accessibility',
],
[
'packages/native/specs/swift/library/theme-generator.spec.md',
'architecture:native-swift-library-theme-generator',
],
[
'packages/native/specs/boundaries.spec.md',
'architecture:native-boundaries',
],
])('derives the id of %s', (filePath, id) => {
expect(classifyPackageArchitecturePath(filePath)).toEqual({
id,
kind: 'architecture',
packageName: 'native',
});
});

it.each([
'packages/native/specs/swift/accessibility.md',
'packages/native/specs/swift/Accessibility.spec.md',
'packages/native/docs/swift/accessibility.spec.md',
])('rejects %s', filePath => {
expect(classifyPackageArchitecturePath(filePath)).toBeNull();
});
});

describe('knowledge record candidates (INV27)', () => {
it.each([
'packages/native/specs/TEMPLATE.md',
'packages/native/specs/swift/library/accessibility.md',
'packages/widgets/docs/Widget.spec.md',
'docs/specs/AST-082-native-first-surfaces/spec.md',
'packages/core/src/Button/Button.spec.md',
])('treats %s as a candidate', filePath => {
expect(isKnowledgeRecordCandidatePath(filePath)).toBe(true);
});

it.each([
'packages/native/specs/README.md',
'docs/specs/AST-082-native-first-surfaces/plan.md',
'packages/native/specs/__fixtures__/fixture.md',
'packages/core/src/Button/node_modules/dep/Dep.spec.md',
'packages/core/src/Button/.Hidden.spec.md',
'packages/core/src/Button/Thing.generated.spec.md',
'packages/native/specs/swift/notes.txt',
'docs/contributing/spec-writing.md',
])('does not treat %s as a candidate', filePath => {
expect(isKnowledgeRecordCandidatePath(filePath)).toBe(false);
});
});
7 changes: 0 additions & 7 deletions docs/architecture/knowledge-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -1012,13 +1012,6 @@ Evidence placement is enforced by contributors and reviewers. The local
`docs/pr-assets/` ignore prevents accidental staging; it is not a blocking CI
check and does not prevent force-added evidence elsewhere.

Current enforcement gap: discovery does not yet read record candidates outside
registered component packages, themes, and `docs/`; component records have no
`platform_of` field until the next schema version; and the component id
pattern does not yet accept a language-qualified id such as
`component:swift/Button`. Until all three land, records under
`packages/native/` are checked only by review.

Current enforcement gap: no checked-in gate yet proves the open-pull-request
search. Until one exists, the pull-request summary records the search terms,
canonical owner/path, and overlapping open work inspected.
57 changes: 57 additions & 0 deletions docs/schemas/knowledge/v5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"schemaVersion": 5,
"extends": 4,
"kinds": {
"component": {
"template": "docs/templates/knowledge/component-spec.md",
"requiredFrontmatter": [
"schema_version",
"template_version",
"kind",
"id",
"platform_of",
"authority",
"archive_reason",
"superseded_by",
"approved_by",
"approved_at",
"owners",
"review_triggers",
"verified_by",
"modules",
"families",
"design_specs",
"architecture",
"contributing",
"system_specs"
],
"requiredSections": [
"Intent",
"Compatibility and migration",
"Ownership boundary",
"Public concepts",
"Behavioral and layout contract",
"Accessibility contract",
"Design relationships",
"Family and system relationships",
"Verification map",
"Decision log",
"Open questions",
"Content boundary"
],
"currentNonEmptyFields": ["owners", "review_triggers", "verified_by"],
"listFields": [
"owners",
"review_triggers",
"verified_by",
"modules",
"families",
"design_specs",
"architecture",
"contributing",
"system_specs"
],
"idPattern": "^component:(?:[a-z][a-z0-9]*/)?[A-Z][A-Za-z0-9]*$"
}
}
}
8 changes: 3 additions & 5 deletions docs/specs/AST-093-native-component-guidelines/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,9 @@ validating, and reviewing individual native components while

- `@astryxdesign/native` gains `packages/native/specs/`, holding one directory
per language, starting with `swift/`. The directory is not published.
- The Swift specs under `packages/native/specs/swift/` and the package template
`packages/native/specs/TEMPLATE.md` predate FR1's record form. They convert
to knowledge records in one change, which removes the template. Until then,
the package READMEs still name the template, and a new component spec follows
FR1 instead.
- The Swift rules under `packages/native/specs/swift/` are architecture
records (`architecture:native-swift-<slug>`), and the package keeps no
template of its own.
- No architecture, family, contributing, or consumer-doc record changes.
- No native component spec exists yet.
- Component folders live inside a language's implementation package. For
Expand Down
7 changes: 5 additions & 2 deletions docs/templates/knowledge/component-spec.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
schema_version: 3
template_version: 7
schema_version: 5
template_version: 8
kind: component
id: component:<Name>
platform_of: null
authority: draft
archive_reason: null
superseded_by: null
Expand All @@ -23,6 +24,8 @@ system_specs: [spec:AST-000/DEC-0]

<!-- Describe the system, not the project: present tense, what it does. No proposals, history, pull requests, or research in the record; see docs/contributing/spec-writing.md and report its rubric results in the pull request. -->

<!-- `platform_of` is null for a Core or Lab record. A platform implementation's record (id `component:<language>/<Name>`, beside the platform's code) names its core component in `platform_of` (`component:<Name>`), states only what the platform adds or must do differently, and has no anatomy-theming block; the core record, or until it exists the core component's source and docs, keeps the shared contract and anatomy (architecture:knowledge-contracts DEC-13). -->

## Contract at a glance

| Area | Contract |
Expand Down
2 changes: 1 addition & 1 deletion docs/templates/knowledge/versions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"architecture": 3,
"component": 7,
"component": 8,
"design": 2,
"family": 2,
"implementation-plan": 1,
Expand Down
3 changes: 2 additions & 1 deletion packages/charts/src/Chart.spec.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
schema_version: 3
schema_version: 5
template_version: 4
kind: component
id: component:Chart
platform_of: null
authority: draft
archive_reason: null
superseded_by: null
Expand Down
Loading
Loading